diff --git a/frontend/src/mock/index.ts b/frontend/src/mock/index.ts index e6f3f1a..487a129 100644 --- a/frontend/src/mock/index.ts +++ b/frontend/src/mock/index.ts @@ -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 configs(CRUD) { m: 'get', re: /^\/strategy\/configs$/, build: () => D.strategyConfigsMock }, diff --git a/frontend/src/views/strategy/Code.vue b/frontend/src/views/strategy/Code.vue index 5a92476..80c50a9 100644 --- a/frontend/src/views/strategy/Code.vue +++ b/frontend/src/views/strategy/Code.vue @@ -23,6 +23,7 @@ const dirty = ref(false) const saving = ref(false) const checking = ref(false) const syntaxOk = ref(null) +const syntaxError = ref(null) const active = computed(() => files.value.find((f) => f.name === activeName.value)) @@ -60,6 +61,7 @@ async function select(f: StrategyFile): Promise { 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 { + 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 { @@ -162,6 +179,7 @@ function runBacktest(): void { {{ active?.dir }}{{ activeName }} 未保存 ✓ 语法正确 + ✗ {{ syntaxError }}
{{ active?.modified }} @@ -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 { diff --git a/sanguo_api/routes_strategy.py b/sanguo_api/routes_strategy.py index 21175fa..3846f47 100644 --- a/sanguo_api/routes_strategy.py +++ b/sanguo_api/routes_strategy.py @@ -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() diff --git a/tests/api/test_routes_strategy.py b/tests/api/test_routes_strategy.py index 4eb1508..e123208 100644 --- a/tests/api/test_routes_strategy.py +++ b/tests/api/test_routes_strategy.py @@ -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