From 4a6fb4cd372d45e26ffd483d75cc6c70cea2a328 Mon Sep 17 00:00:00 2001 From: claude_dev Date: Sat, 15 Aug 2026 21:08:36 +0800 Subject: [PATCH] =?UTF-8?q?fix(editor):=20=E8=AF=AD=E6=B3=95=E6=A3=80?= =?UTF-8?q?=E6=9F=A5=E6=8C=89=E9=92=AE=E6=8E=A5=E7=9C=9F=E2=80=94=E2=80=94?= =?UTF-8?q?=E5=8E=9F=E4=B8=BA=E5=8E=9F=E5=9E=8B=E6=9C=9F=E5=81=87=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0(sleep=20600ms=E6=81=92=E6=8A=A5=E9=80=9A=E8=BF=87,?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E5=88=A0=E4=B8=AD=E6=8B=AC=E5=8F=B7=E4=BB=8D?= =?UTF-8?q?=E6=8F=90=E7=A4=BA=E6=88=90=E5=8A=9F);=E5=90=8E=E7=AB=AFPOST=20?= =?UTF-8?q?/strategy/file/{name}/check(compile()=E4=B8=8D=E8=90=BD?= =?UTF-8?q?=E7=9B=98,SyntaxError=E8=BF=94=E8=A1=8C=E5=8F=B7+=E6=B6=88?= =?UTF-8?q?=E6=81=AF,=E4=B8=8E=E4=BF=9D=E5=AD=98=E7=9A=84py=5Fcompile?= =?UTF-8?q?=E9=97=A8=E7=A6=81=E5=90=8C=E6=BA=90);=E5=89=8D=E7=AB=AF?= =?UTF-8?q?=E8=B0=83=E7=9C=9F=E6=8E=A5=E5=8F=A3,=E9=94=99=E8=AF=AF?= =?UTF-8?q?=E6=98=BE=E7=A4=BA=E3=80=8C=E7=AC=ACN=E8=A1=8C:=E6=B6=88?= =?UTF-8?q?=E6=81=AF=E3=80=8D=E7=BA=A2tag+toast,=E7=BC=96=E8=BE=91?= =?UTF-8?q?=E5=8D=B3=E6=B8=85;3=E6=96=B0=E5=90=8E=E7=AB=AF=E6=B5=8B?= =?UTF-8?q?=E8=AF=95(=E5=88=A0=E6=8B=AC=E5=8F=B7=E5=BF=85=E6=8A=A5?= =?UTF-8?q?=E9=94=99)=20[vps]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/mock/index.ts | 1 + frontend/src/views/strategy/Code.vue | 27 +++++++++++++++++++++---- sanguo_api/routes_strategy.py | 17 ++++++++++++++++ tests/api/test_routes_strategy.py | 30 ++++++++++++++++++++++++++++ 4 files changed, 71 insertions(+), 4 deletions(-) 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