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 set(item) != {"role", "content"}
|
||||||
or item["role"] not in ("user", "assistant")
|
or item["role"] not in ("user", "assistant")
|
||||||
or not isinstance(item["content"], str)
|
or not isinstance(item["content"], str)
|
||||||
or len(item["content"]) > 1000
|
# 2000 字符容纳载入已保存定义后的完整对话上下文
|
||||||
|
or len(item["content"]) > 2000
|
||||||
for item in messages
|
for item in messages
|
||||||
):
|
):
|
||||||
raise APIError("对话消息内容无效")
|
raise APIError("对话消息内容无效")
|
||||||
|
|||||||
@@ -41,6 +41,19 @@ function describeExpression(expression: FormulaExpression): string {
|
|||||||
return `(${describeExpression(expression.left)} ${operators[expression.operator]} ${describeExpression(expression.right)})`;
|
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({
|
export function Modal({
|
||||||
title,
|
title,
|
||||||
onClose,
|
onClose,
|
||||||
@@ -812,10 +825,26 @@ export function FormulaExportDialog({
|
|||||||
function loadDefinition(id: string) {
|
function loadDefinition(id: string) {
|
||||||
setSelectedDefinition(id);
|
setSelectedDefinition(id);
|
||||||
const saved = definitions.find((item) => item.id === id);
|
const saved = definitions.find((item) => item.id === id);
|
||||||
if (!saved) return;
|
if (!saved) {
|
||||||
setColumn(saved.definition);
|
// 选择空选项时同步清理当前定义,避免下拉选择与预览不一致
|
||||||
|
setColumn(null);
|
||||||
setMessages([]);
|
setMessages([]);
|
||||||
setQuestions([]);
|
setQuestions([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setColumn(saved.definition);
|
||||||
|
// 载入定义后把原定义显式放进可见、经同意的对话上下文,
|
||||||
|
// 使“把刚才公式改成乘以 100”这类指代能关联到已载入表达式;
|
||||||
|
// 修改结果仍需重新确认,不会静默沿用旧定义。
|
||||||
|
setMessages([
|
||||||
|
{ role: "user", content: definitionSummary(saved.definition) },
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
content:
|
||||||
|
"已载入该定义作为当前基准。请描述要调整的内容,我会在该定义基础上生成新的列定义,仍需你确认后再生成。",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
setQuestions([]);
|
||||||
setDraft("");
|
setDraft("");
|
||||||
}
|
}
|
||||||
async function deleteDefinition() {
|
async function deleteDefinition() {
|
||||||
|
|||||||
@@ -59,7 +59,18 @@ test("新增列:补充信息触发追问并重新确认后生成公式工作
|
|||||||
meaning: "实际收入减成本",
|
meaning: "实际收入减成本",
|
||||||
scope: "所选分组与期间",
|
scope: "所选分组与期间",
|
||||||
missing: "结果不可用(#N/A);零分母不可用",
|
missing: "结果不可用(#N/A);零分母不可用",
|
||||||
expression: {
|
expression:
|
||||||
|
designCalls === 5
|
||||||
|
? {
|
||||||
|
operator: "multiply",
|
||||||
|
left: {
|
||||||
|
operator: "subtract",
|
||||||
|
left: { metric: "revenue" },
|
||||||
|
right: { metric: "cost" },
|
||||||
|
},
|
||||||
|
right: { number: 100 },
|
||||||
|
}
|
||||||
|
: {
|
||||||
operator: "subtract",
|
operator: "subtract",
|
||||||
left: { metric: "revenue" },
|
left: { metric: "revenue" },
|
||||||
right: { metric: "cost" },
|
right: { metric: "cost" },
|
||||||
@@ -123,11 +134,33 @@ test("新增列:补充信息触发追问并重新确认后生成公式工作
|
|||||||
const reopened = page.getByRole("dialog", { name: "生成公式工作簿" });
|
const reopened = page.getByRole("dialog", { name: "生成公式工作簿" });
|
||||||
await reopened.getByLabel("已保存定义").selectOption({ label: "收入成本差" });
|
await reopened.getByLabel("已保存定义").selectOption({ label: "收入成本差" });
|
||||||
await expect(reopened).toContainText("计算表达式:(实际收入 − 成本)");
|
await expect(reopened).toContainText("计算表达式:(实际收入 − 成本)");
|
||||||
await reopened
|
// F04 回归:载入定义后,原定义作为可见上下文进入对话
|
||||||
.getByRole("button", { name: "确认并生成公式工作簿" })
|
await expect(reopened).toContainText("已载入已保存定义「收入成本差」");
|
||||||
.click();
|
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("生成完成", {
|
await expect(reopened.getByRole("status")).toContainText("生成完成", {
|
||||||
timeout: 30000,
|
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
|
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):
|
def test_design_column_returns_only_validated_formula_definition(formula_data, settings):
|
||||||
ds, _ = formula_data
|
ds, _ = formula_data
|
||||||
settings.AI_ENABLED = True
|
settings.AI_ENABLED = True
|
||||||
|
|||||||
Reference in New Issue
Block a user