From 9a5ab26014638a72cadfd22e7b3b2d0cb3696cd7 Mon Sep 17 00:00:00 2001 From: Jayway Liu <14022498+bluejayway@user.noreply.gitee.com> Date: Sat, 26 Sep 2026 01:30:34 +0800 Subject: [PATCH] =?UTF-8?q?fix(dialogs):=20F04=20=E8=BD=BD=E5=85=A5?= =?UTF-8?q?=E5=B7=B2=E4=BF=9D=E5=AD=98=E5=AE=9A=E4=B9=89=E5=90=8E=E6=8A=8A?= =?UTF-8?q?=E5=8E=9F=E5=AE=9A=E4=B9=89=E5=B8=A6=E5=85=A5=E5=8F=AF=E8=A7=81?= =?UTF-8?q?=E5=AF=B9=E8=AF=9D=E4=B8=8A=E4=B8=8B=E6=96=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 载入定义时以“用户消息+助手确认”形式把名称、含义、范围、缺失处理和表达式 写入对话,后续“把刚才公式改成乘以 100”等指代可关联到已载入定义; 生成前仍需显式确认,不静默沿用旧定义 - 选择空选项时同步清理当前定义与上下文,选择状态与预览保持一致 - 服务端单条消息长度上限 1000→2000 以容纳完整定义上下文,超长仍拒绝 - E2E 覆盖:载入后请求包含原定义、修改后表达式为 (收入−成本)×100、 空选项清理;单测覆盖 2000/2001 边界 --- finance/formula_views.py | 3 +- frontend/src/dialogs.tsx | 33 +++++++++++++++++++-- tests/e2e/formula-export.spec.ts | 51 ++++++++++++++++++++++++++------ tests/test_formula_columns.py | 48 ++++++++++++++++++++++++++++++ 4 files changed, 123 insertions(+), 12 deletions(-) diff --git a/finance/formula_views.py b/finance/formula_views.py index 075e22e..afcf4f8 100644 --- a/finance/formula_views.py +++ b/finance/formula_views.py @@ -22,7 +22,8 @@ def design_column(messages, ds, ctx): or set(item) != {"role", "content"} or item["role"] not in ("user", "assistant") or not isinstance(item["content"], str) - or len(item["content"]) > 1000 + # 2000 字符容纳载入已保存定义后的完整对话上下文 + or len(item["content"]) > 2000 for item in messages ): raise APIError("对话消息内容无效") diff --git a/frontend/src/dialogs.tsx b/frontend/src/dialogs.tsx index 24828be..21329b1 100644 --- a/frontend/src/dialogs.tsx +++ b/frontend/src/dialogs.tsx @@ -41,6 +41,19 @@ function describeExpression(expression: FormulaExpression): string { return `(${describeExpression(expression.left)} ${operators[expression.operator]} ${describeExpression(expression.right)})`; } +function definitionSummary(definition: Record): string { + const text = [ + `已载入已保存定义「${String(definition.name)}」`, + `含义:${String(definition.meaning)}`, + `适用范围:${String(definition.scope)}`, + `缺失/零分母处理:${String(definition.missing)}`, + `计算表达式:${JSON.stringify(definition.expression)}`, + ].join(";"); + return text.length <= 2000 + ? text + : `${text.slice(0, 2000)}…(内容过长已截断)`; +} + export function Modal({ title, onClose, @@ -812,9 +825,25 @@ export function FormulaExportDialog({ function loadDefinition(id: string) { setSelectedDefinition(id); const saved = definitions.find((item) => item.id === id); - if (!saved) return; + if (!saved) { + // 选择空选项时同步清理当前定义,避免下拉选择与预览不一致 + setColumn(null); + setMessages([]); + setQuestions([]); + return; + } setColumn(saved.definition); - setMessages([]); + // 载入定义后把原定义显式放进可见、经同意的对话上下文, + // 使“把刚才公式改成乘以 100”这类指代能关联到已载入表达式; + // 修改结果仍需重新确认,不会静默沿用旧定义。 + setMessages([ + { role: "user", content: definitionSummary(saved.definition) }, + { + role: "assistant", + content: + "已载入该定义作为当前基准。请描述要调整的内容,我会在该定义基础上生成新的列定义,仍需你确认后再生成。", + }, + ]); setQuestions([]); setDraft(""); } diff --git a/tests/e2e/formula-export.spec.ts b/tests/e2e/formula-export.spec.ts index ede33c5..9fa4ae3 100644 --- a/tests/e2e/formula-export.spec.ts +++ b/tests/e2e/formula-export.spec.ts @@ -59,11 +59,22 @@ test("新增列:补充信息触发追问并重新确认后生成公式工作 meaning: "实际收入减成本", scope: "所选分组与期间", missing: "结果不可用(#N/A);零分母不可用", - expression: { - operator: "subtract", - left: { metric: "revenue" }, - right: { metric: "cost" }, - }, + expression: + designCalls === 5 + ? { + operator: "multiply", + left: { + operator: "subtract", + left: { metric: "revenue" }, + right: { metric: "cost" }, + }, + right: { number: 100 }, + } + : { + operator: "subtract", + left: { metric: "revenue" }, + right: { metric: "cost" }, + }, }, }, }); @@ -123,11 +134,33 @@ test("新增列:补充信息触发追问并重新确认后生成公式工作 const reopened = page.getByRole("dialog", { name: "生成公式工作簿" }); await reopened.getByLabel("已保存定义").selectOption({ label: "收入成本差" }); await expect(reopened).toContainText("计算表达式:(实际收入 − 成本)"); - await reopened - .getByRole("button", { name: "确认并生成公式工作簿" }) - .click(); + // F04 回归:载入定义后,原定义作为可见上下文进入对话 + await expect(reopened).toContainText("已载入已保存定义「收入成本差」"); + await expect(reopened).toContainText("我:已载入已保存定义"); + await expect(reopened).toContainText("助手:已载入该定义作为当前基准"); + // F04 回归:选择空选项时同步清理当前定义与上下文,选择状态与预览一致 + await reopened.getByLabel("已保存定义").selectOption({ label: "选择定义" }); + await expect(reopened.getByText("计算表达式:(实际收入 − 成本)")).toHaveCount( + 0, + ); + await expect(reopened.getByText("已载入已保存定义")).toHaveCount(0); + + await reopened.getByLabel("已保存定义").selectOption({ label: "收入成本差" }); + await reopened.getByRole("checkbox").check(); + await reopened.getByLabel("列计算描述").fill("把刚才公式改成乘以 100"); + await reopened.getByRole("button", { name: "发送描述" }).click(); + await expect(reopened).toContainText( + "计算表达式:((实际收入 − 成本) × 100)", + ); + // 模型请求必须包含载入的原定义上下文和新的修改描述 + expect(sentMessages.at(0)?.content).toContain( + "已载入已保存定义「收入成本差」", + ); + expect(sentMessages.at(0)?.content).toContain('"operator":"subtract"'); + expect(sentMessages.at(-1)?.content).toBe("把刚才公式改成乘以 100"); + await reopened.getByRole("button", { name: "确认并生成公式工作簿" }).click(); await expect(reopened.getByRole("status")).toContainText("生成完成", { timeout: 30000, }); - expect(designCalls).toBe(4); + expect(designCalls).toBe(5); }); diff --git a/tests/test_formula_columns.py b/tests/test_formula_columns.py index d9a9f8b..c07fbaf 100644 --- a/tests/test_formula_columns.py +++ b/tests/test_formula_columns.py @@ -200,6 +200,54 @@ def test_design_column_preserves_clarification_branch_and_full_conversation(form assert payload["input"][1:] == messages +def test_design_column_accepts_loaded_definition_context(formula_data, settings): + ds, _ = formula_data + settings.AI_ENABLED = True + settings.OPENAI_API_KEY = "test-key" + settings.OPENAI_MODEL = "test-model" + context = "已载入已保存定义「收入成本差」;计算表达式:" + '{"operator":"subtract",' * 45 + assert 1000 < len(context) <= 2000 + messages = [ + {"role": "user", "content": context}, + {"role": "assistant", "content": "已载入该定义作为当前基准。"}, + {"role": "user", "content": "把刚才公式改成乘以 100"}, + ] + clarification = { + "status": "completed", + "output": [ + { + "content": [ + { + "type": "output_text", + "text": '{"complete":false,"questions":["要乘在整个表达式上吗?"],"column":null}', + } + ] + } + ], + } + with patch("finance.formula_views.httpx.Client") as http: + http.return_value.__enter__.return_value.post.return_value = type( + "Response", + (), + {"raise_for_status": lambda self: None, "json": lambda self: clarification}, + )() + result = design_column( + messages, + ds, + {"current": ["2026-08"], "baseline": ["2025-08"], "filters": {}, "group": "department"}, + ) + assert result["complete"] is False + payload = http.return_value.__enter__.return_value.post.call_args.kwargs["json"] + assert payload["input"][1:] == messages + + with pytest.raises(Exception, match="对话消息内容无效"): + design_column( + [{"role": "user", "content": "y" * 2001}], + ds, + {"current": ["2026-08"], "baseline": ["2025-08"], "filters": {}, "group": "department"}, + ) + + def test_design_column_returns_only_validated_formula_definition(formula_data, settings): ds, _ = formula_data settings.AI_ENABLED = True