fix(dialogs): F04 载入已保存定义后把原定义带入可见对话上下文
- 载入定义时以“用户消息+助手确认”形式把名称、含义、范围、缺失处理和表达式 写入对话,后续“把刚才公式改成乘以 100”等指代可关联到已载入定义; 生成前仍需显式确认,不静默沿用旧定义 - 选择空选项时同步清理当前定义与上下文,选择状态与预览保持一致 - 服务端单条消息长度上限 1000→2000 以容纳完整定义上下文,超长仍拒绝 - E2E 覆盖:载入后请求包含原定义、修改后表达式为 (收入−成本)×100、 空选项清理;单测覆盖 2000/2001 边界
This commit is contained in:
@@ -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("对话消息内容无效")
|
||||
|
||||
@@ -41,6 +41,19 @@ function describeExpression(expression: FormulaExpression): string {
|
||||
return `(${describeExpression(expression.left)} ${operators[expression.operator]} ${describeExpression(expression.right)})`;
|
||||
}
|
||||
|
||||
function definitionSummary(definition: Record<string, unknown>): 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("");
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user