feat(agent): render local interactive answers and fold completed process

This commit is contained in:
2026-10-12 18:54:18 +08:00
parent e0eb6a721e
commit 2c5bf15cce
30 changed files with 1610 additions and 75 deletions
@@ -0,0 +1,81 @@
import { describe, expect, it } from "vitest";
import { parseAnswerUiBlock } from "./contract";
const block = {
version: "answer-ui@1",
block_id: "reading",
fallback_text: "仍待核对",
spec: {
root: "note",
elements: { note: { type: "Text", props: { text: "仍待核对", tone: "neutral" }, children: [] } }
}
};
describe("untrusted answer components", () => {
it("accepts registered text and rejects arbitrary code, URLs and unknown components", () => {
expect(parseAnswerUiBlock(block)).not.toBeNull();
for (const node of [
{ type: "iframe", props: { src: "https://example.com" }, children: [] },
{
...block.spec.elements.note,
props: { ...block.spec.elements.note.props, script: "alert(1)" }
},
{ ...block.spec.elements.note, on: { click: { action: "fetch" } } }
])
expect(
parseAnswerUiBlock({ ...block, spec: { root: "note", elements: { note: node } } })
).toBeNull();
});
it("rejects missing, duplicate, cyclic, unreachable and excessive trees", () => {
const stack = { type: "Stack", props: { columns: "one", gap: "normal" }, children: ["note"] };
for (const children of [["missing"], ["note", "note"], ["root"]])
expect(
parseAnswerUiBlock({
...block,
spec: { root: "root", elements: { root: { ...stack, children }, ...block.spec.elements } }
})
).toBeNull();
expect(
parseAnswerUiBlock({
...block,
spec: {
...block.spec,
elements: { ...block.spec.elements, unused: block.spec.elements.note }
}
})
).toBeNull();
});
it("requires aligned, finite chart values and unambiguous form fields", () => {
const chart = {
type: "Chart",
props: {
title: "演练压力",
chart_type: "line",
source: "演练数据",
x_data: ["08:00"],
series: [{ name: "压力", data: [0.3] }]
},
children: []
};
const withNode = (node: unknown) => ({
...block,
spec: { root: "note", elements: { note: node } }
});
expect(parseAnswerUiBlock(withNode(chart))).not.toBeNull();
for (const data of [[0.3, 0.2], [Infinity], [NaN]])
expect(
parseAnswerUiBlock(
withNode({ ...chart, props: { ...chart.props, series: [{ name: "压力", data }] } })
)
).toBeNull();
const field = { name: "location", label: "位置", kind: "text", required: true };
expect(
parseAnswerUiBlock(
withNode({
type: "FollowUpForm",
props: { title: "核验", description: "用户补充", fields: [field, field] },
children: []
})
)
).toBeNull();
});
});