fix(editor): 语法检查按钮接真——原为原型期假实现(sleep 600ms恒报通过,用户删中括号仍提示成功);后端POST /strategy/file/{name}/check(compile()不落盘,SyntaxError返行号+消息,与保存的py_compile门禁同源);前端调真接口,错误显示「第N行:消息」红tag+toast,编辑即清;3新后端测试(删括号必报错) [vps]
CI/CD / test (push) Failing after 11m17s
CI/CD / nas-deploy (push) Has been skipped
CI/CD / nas-verify (push) Has been skipped

This commit is contained in:
2026-08-15 21:08:36 +08:00
parent 94507f2ad7
commit 4a6fb4cd37
4 changed files with 71 additions and 4 deletions
+1
View File
@@ -25,6 +25,7 @@ const routes: Route[] = [
{ m: 'delete', re: /^\/strategy\/instances\/[^/]+$/, build: () => ({ ok: true }) },
// strategy code files(在线编辑)
{ m: 'get', re: /^\/strategy\/files$/, build: () => D.strategyFilesMock },
{ m: 'post', re: /^\/strategy\/file\/[^/]+\/check$/, build: () => ({ ok: true }) },
{ m: 'post', re: /^\/strategy\/file\/[^/]+$/, build: () => ({ ok: true }) },
// strategy configsCRUD
{ m: 'get', re: /^\/strategy\/configs$/, build: () => D.strategyConfigsMock },
+23 -4
View File
@@ -23,6 +23,7 @@ const dirty = ref(false)
const saving = ref(false)
const checking = ref(false)
const syntaxOk = ref<boolean | null>(null)
const syntaxError = ref<string | null>(null)
const active = computed(() => files.value.find((f) => f.name === activeName.value))
@@ -60,6 +61,7 @@ async function select(f: StrategyFile): Promise<void> {
activeName.value = f.name
dirty.value = false
syntaxOk.value = null
syntaxError.value = null
loadingCode.value = true
try {
const { data } = await apiClient.get<{ code: string }>(`/strategy/file/${f.name}`)
@@ -78,14 +80,29 @@ watch(code, () => {
if (echoGrace) return
dirty.value = true
syntaxOk.value = null
syntaxError.value = null
})
async function onCheck(): Promise<void> {
if (!active.value) return
checking.value = true
await new Promise((r) => setTimeout(r, 600)) // 模拟 py_compile
syntaxOk.value = true
checking.value = false
ElMessage.success('语法检查通过')
syntaxError.value = null
try {
const { data } = await apiClient.post<{ ok: boolean; error?: string; line?: number | null }>(
`/strategy/file/${activeName.value}/check`, { code: code.value })
if (data.ok) {
syntaxOk.value = true
ElMessage.success('语法检查通过')
} else {
syntaxOk.value = false
syntaxError.value = data.line != null ? `${data.line} 行:${data.error ?? '语法错误'}` : (data.error ?? '语法错误')
ElMessage.error(syntaxError.value)
}
} catch (e) {
ElMessage.error(e instanceof Error ? e.message : '语法检查请求失败')
} finally {
checking.value = false
}
}
async function onSave(): Promise<void> {
@@ -162,6 +179,7 @@ function runBacktest(): void {
<span class="path mono">{{ active?.dir }}{{ activeName }}</span>
<span v-if="dirty" class="tag tag-dirty">未保存</span>
<span v-if="syntaxOk" class="tag tag-ok mono"> 语法正确</span>
<span v-else-if="syntaxError" class="tag tag-err mono" :title="syntaxError"> {{ syntaxError }}</span>
</div>
<div class="bar-right">
<span class="modified mono">{{ active?.modified }}</span>
@@ -298,6 +316,7 @@ function runBacktest(): void {
font-family: var(--mono);
}
.tag-dirty { color: var(--lamp-warn); background: var(--amber-soft); border: 1px solid rgba(255, 176, 0, 0.3); }
.tag-err { color: var(--lamp-err, #ff5470); background: rgba(255, 84, 112, 0.1); border: 1px solid rgba(255, 84, 112, 0.3); max-width: 46ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tag-ok { color: var(--lamp-ok); background: rgba(46, 230, 138, 0.1); border: 1px solid rgba(46, 230, 138, 0.3); }
.monaco-wrap {
+17
View File
@@ -53,6 +53,23 @@ def post_file(name: str, req: FileWriteRequest):
return {"ok": True}
@router.post("/strategy/file/{name}/check")
def check_file(name: str, req: FileWriteRequest):
"""语法检查(不落盘):编辑器「语法检查」按钮真接线。
原前端按钮是原型期假实现(sleep 600ms 恒报通过,2026-08-15 用户实况
删括号仍报成功)。compile() 不执行代码,只做语法编译;SyntaxError
返回行号+消息供编辑器定位。
"""
try:
compile(req.code, name, "exec")
except SyntaxError as e:
return {"ok": False, "error": str(e.msg or e), "line": e.lineno}
except ValueError as e: # null bytes 等 compile 级错误
return {"ok": False, "error": str(e), "line": None}
return {"ok": True}
@router.get("/strategy/instances")
def get_instances():
return instance_store.list_instances()
+30
View File
@@ -67,3 +67,33 @@ def test_file_read_returns_code(monkeypatch, tmp_path):
assert isinstance(body["code"], str) and body["code"] # 编辑器空白 bug 的回归测试
assert body["name"] == name
assert c.get("/api/v1/strategy/file/__no_such__.py").status_code == 404
# ===== 语法检查端点(编辑器「语法检查」按钮真接线)=====
def test_check_syntax_ok(monkeypatch, tmp_path):
c = _client(monkeypatch, tmp_path)
r = c.post("/api/v1/strategy/file/foo.py/check",
json={"code": "x = [1, 2, 3]\nprint(x)\n"})
assert r.status_code == 200
assert r.json() == {"ok": True}
def test_check_syntax_catches_missing_bracket(monkeypatch, tmp_path):
"""删掉一个中括号必须报错并给行号(2026-08-15 用户实况:删括号仍报通过)。"""
c = _client(monkeypatch, tmp_path)
r = c.post("/api/v1/strategy/file/foo.py/check",
json={"code": "x = [1, 2, 3\nprint(x)\n"})
assert r.status_code == 200
body = r.json()
assert body["ok"] is False
assert body["line"] is not None and body["line"] >= 1
assert body["error"]
def test_check_syntax_empty_code_ok(monkeypatch, tmp_path):
"""空代码/纯注释可编译 → ok(编辑器允许半成品里写注释)。"""
c = _client(monkeypatch, tmp_path)
r = c.post("/api/v1/strategy/file/foo.py/check", json={"code": "# 只有注释\n"})
assert r.status_code == 200
assert r.json()["ok"] is True