From 0b7257f3d34c7f2e3bd442b7c883cd81e4cf1ae0 Mon Sep 17 00:00:00 2001 From: kyugao Date: Thu, 6 Aug 2026 00:51:52 +0800 Subject: [PATCH] fix(runtime): persist miniapp tool progress --- .../coordination/lineup-runtime.test.ts | 22 +++++++++++++++++++ .../runtime/coordination/lineup-runtime.ts | 2 +- .../coordination/miniapp-tool-state.test.ts | 10 +++++++++ .../coordination/miniapp-tool-state.ts | 10 ++++++++- .../03.sdk_and_coreapp/04.acceptance_review.md | 9 ++++---- 5 files changed, 47 insertions(+), 6 deletions(-) diff --git a/tauri/src/runtime/coordination/lineup-runtime.test.ts b/tauri/src/runtime/coordination/lineup-runtime.test.ts index 6de1013..714ce5a 100644 --- a/tauri/src/runtime/coordination/lineup-runtime.test.ts +++ b/tauri/src/runtime/coordination/lineup-runtime.test.ts @@ -416,6 +416,28 @@ describe("LineUpRuntime", () => { expect(secondTransport.sent.filter(entry => entry.payload.includes("restart-tool-call"))).toHaveLength(1); }); + it("restores the latest MiniApp progress after Runtime restart", async () => { + const conversationID = "user_1:2:channel_1"; + const call = JSON.stringify({ + v: 1, id: "progress-restart-call", type: "lineup.v1.miniapp.tool.call", conversation_id: conversationID, + sender: { kind: "agent", id: "agent_1" }, + payload: { call_id: "progress-restart-call", tool_id: "task-dashboard.open", app_scope: "task-dashboard", inventory_revision: "catalog-1", input: { title: "进度恢复" } }, + }); + const storage = new MemoryStorage(); + const firstTransport = new FakeTransport([[{ message_seq: 71, from_uid: "agent_1", payload: call }], []]); + const first = new LineUpRuntime({ storage, createTransport: () => firstTransport }); + await first.login(login); + await first.syncOnce(); + const instance = first.lifecycle.instances.activeFor("task-dashboard", conversationID)!; + const sdk = first.openBundledMiniApp("task-dashboard", instance.instance_id); + await sdk.tools.reportProgress("progress-restart-call", { percent: 42, status: "working", detail: "正在处理" }); + expect(first.miniAppToolCalls()).toEqual([expect.objectContaining({ call_id: "progress-restart-call", status: "running", progress: { percent: 42, status: "working", detail: "正在处理" } })]); + + const second = new LineUpRuntime({ storage, createTransport: () => new FakeTransport() }); + await second.login(login); + expect(second.miniAppToolCalls()).toEqual([expect.objectContaining({ call_id: "progress-restart-call", status: "waiting_for_app", progress: { percent: 42, status: "working", detail: "正在处理" } })]); + }); + it("projects Agent state and Chat-safe Runtime status through the SDK", async () => { const conversationID = "user_1:2:channel_1"; const transport = new FakeTransport([[ diff --git a/tauri/src/runtime/coordination/lineup-runtime.ts b/tauri/src/runtime/coordination/lineup-runtime.ts index 4388351..2e5feb1 100644 --- a/tauri/src/runtime/coordination/lineup-runtime.ts +++ b/tauri/src/runtime/coordination/lineup-runtime.ts @@ -763,7 +763,7 @@ export class LineUpRuntime { public reportMiniAppToolProgress(instanceID: string, callID: string, progress: MiniAppToolProgress): Promise { if (!validProgress(progress)) return Promise.resolve({ disposition: "rejected", code: "invalid_action" }); - const transition = this.miniappTools.progress(callID, instanceID, this.now()); + const transition = this.miniappTools.progress(callID, instanceID, progress, this.now()); if (transition.disposition !== "accepted") return Promise.resolve({ disposition: "rejected", code: "invalid_action" }); this.store.replaceMiniAppToolCalls(this.miniappTools.list()); return this.queueProtocolAction("app-result", "lineup.v1.miniapp.tool.progress", { diff --git a/tauri/src/runtime/coordination/miniapp-tool-state.test.ts b/tauri/src/runtime/coordination/miniapp-tool-state.test.ts index 7f00215..e9c41ad 100644 --- a/tauri/src/runtime/coordination/miniapp-tool-state.test.ts +++ b/tauri/src/runtime/coordination/miniapp-tool-state.test.ts @@ -19,4 +19,14 @@ describe("MiniAppToolStateMachine", () => { expect(machine.cancel("call-2", "task:2", "app_closed", "2026-08-04T00:00:02.000Z")).toEqual(expect.objectContaining({ disposition: "invalid_scope" })); expect(machine.cancel("call-2", "task:1", "app_closed", "2026-08-04T00:00:02.000Z")).toMatchObject({ disposition: "accepted", record: { status: "cancelled", cancel_reason: "app_closed" } }); }); + + it("keeps the latest progress in the durable call record", () => { + const machine = new MiniAppToolStateMachine(); + machine.receive({ call_id: "call-progress", tool_id: "task-dashboard.open", inventory_revision: "catalog-1", app_scope: "task-dashboard", instance_id: "task:1", conversation_id: "c", input: {}, created_at: "2026-08-04T00:00:00.000Z" }); + machine.route("call-progress", "task:1", "2026-08-04T00:00:01.000Z"); + expect(machine.progress("call-progress", "task:1", { percent: 42, status: "working", detail: "正在处理" }, "2026-08-04T00:00:02.000Z")).toMatchObject({ disposition: "accepted", record: { progress: { percent: 42, status: "working", detail: "正在处理" } } }); + const restored = new MiniAppToolStateMachine(); + restored.restore(machine.list(), "2026-08-04T00:00:03.000Z"); + expect(restored.get("call-progress")).toMatchObject({ status: "waiting_for_app", progress: { percent: 42, status: "working", detail: "正在处理" } }); + }); }); diff --git a/tauri/src/runtime/coordination/miniapp-tool-state.ts b/tauri/src/runtime/coordination/miniapp-tool-state.ts index e70ab9e..702db39 100644 --- a/tauri/src/runtime/coordination/miniapp-tool-state.ts +++ b/tauri/src/runtime/coordination/miniapp-tool-state.ts @@ -14,6 +14,12 @@ export type MiniAppToolStatus = export type MiniAppToolCancelReason = "app_closed" | "agent_cancelled" | "runtime_cancelled"; +export type MiniAppToolProgress = { + percent?: number; + status?: string; + detail?: string; +}; + export type MiniAppToolCallRecord = { call_id: string; tool_id: string; @@ -23,6 +29,7 @@ export type MiniAppToolCallRecord = { conversation_id: string; status: MiniAppToolStatus; input: JsonObject; + progress?: MiniAppToolProgress; result?: JsonObject; error_code?: string; cancel_reason?: MiniAppToolCancelReason; @@ -66,10 +73,11 @@ export class MiniAppToolStateMachine { }); } - public progress(callID: string, instanceID: string, now: string): MiniAppToolTransition { + public progress(callID: string, instanceID: string, progress: MiniAppToolProgress, now: string): MiniAppToolTransition { return this.transition(callID, now, ["waiting_for_app", "running"], record => { if (record.instance_id !== instanceID) throw new Error("instance mismatch"); record.status = "running"; + record.progress = clone(progress); }); } diff --git a/迭代/03.sdk_and_coreapp/04.acceptance_review.md b/迭代/03.sdk_and_coreapp/04.acceptance_review.md index 5668270..fc39b74 100644 --- a/迭代/03.sdk_and_coreapp/04.acceptance_review.md +++ b/迭代/03.sdk_and_coreapp/04.acceptance_review.md @@ -24,7 +24,7 @@ | ✅ | P1 | A6 | 标准交互的超时、dismiss、提交和答案 schema 没有由 Runtime 统一裁决 | `lineup-runtime.ts`、`agent-interaction-service.ts`、`standard-interaction-contract.ts` | 已让本地提交、取消、过期和 Agent dismiss 同步推进 Interaction 与 Kernel;notice 也会完成 Kernel;四种答案在 Runtime 按固定 schema 校验,默认有效期和重启过期均只写一条 outbox。 | | ✅ | P2 | A7 | 迭代文档中的 Tool 名称和代码 Manifest 不一致 | 主文档 §5.2/§5.3;`reference-miniapps.ts` | 已统一为 `task-dashboard.open`、`task-dashboard.update`、`whiteboard.open`、`whiteboard.submit`;Manifest schema、MiniApp 实现、sandbox iframe、fixture 和测试均已同步。 | | 🔴 | P2 | A8 | 当前 Task Dashboard/Whiteboard Surface 主要是 Host 硬编码的静态页面,无法证明真实业务 UI 在各自受限 Surface 内运行 | `main.ts:180-198`、`isolated-surface-host.ts` | 至少提供能代表业务边界的受限 Surface fixture,并验证 MiniApp 业务逻辑不能取得 Host DOM 或 Runtime 内部。 | -| 🔴 | P2 | A9 | MiniApp progress 没有进入可恢复的 Tool 状态记录 | `miniapp-tool-state.ts` | 如果契约要求进度可恢复,需持久化最后进度并覆盖 Runtime 重启;否则修改文档,明确 progress 只是一种可丢失事件。 | +| ✅ | P2 | A9 | MiniApp progress 没有进入可恢复的 Tool 状态记录 | `miniapp-tool-state.ts`、`lineup-runtime.test.ts` | 已在 Tool 记录保存最后一次 `percent/status/detail`,ConversationStore 随记录持久化;Runtime 重启后可恢复,且补充状态机和重启回归测试。 | | 🔴 | P3 | A10 | App session 记录和 opened/closed 事件没有显式保存 `agent_id` / `conversation_id` 字段 | `app-instance-manager.ts`、`lineup-runtime.ts:203-209,1052-1058` | P3 也必须在本迭代处理:App session opened/closed 事件和持久化记录补齐两个上下文字段。 | | 🔴 | P3 | A11 | 旧的 `openExtensionApp` 公开入口仍提供较宽的旁路能力 | `lineup-runtime.ts:286-300` | P3 也必须在本迭代处理:删除公开旁路,或让它只返回受统一 SDK v1 约束的适配器。 | @@ -33,6 +33,7 @@ - `npm run build` 通过。 - `npm test -- --run` 通过:29 个测试文件、131 个测试(含 Tool 契约、默认有效期、重启过期和答案 schema 回归)。 - `git diff --check` 通过。 +- MiniApp progress 回归通过:状态机保存最新进度,Runtime 重启后恢复 `percent/status/detail`。 - 使用独立 `agent-browser` 会话登录本地测试账号成功。 - 主 IM 页面、同步状态、应用子会话区域和“启用任务面板”入口可见。 - 用户消息可以发送并显示“已发送”。 @@ -95,10 +96,10 @@ Interact 仍然可以使用可信 DOM,但它的 SDK 已明确拆成两层:`s 基础构建与自动化测试:通过 真实登录和消息发送:通过 App 架构边界:部分通过(A1/A2/A5/A6 已实现,A2 待浏览器复验) -MiniApp 隔离与 SDK 规范:A1~A7 已通过,仍需处理后续 P2/P3 +MiniApp 隔离与 SDK 规范:A1~A9 已通过,仍需处理后续 P3 标准交互终态契约:通过 -迭代整体验收:不通过,继续处理 A8~A11;A2 需在最终浏览器复验中确认 +迭代整体验收:不通过,继续处理 A8、A10、A11;A2 需在最终浏览器复验中确认 ``` -本轮独立验收由子 agent 完成。A6 已在本轮修复并通过自动化验证,A7 已完成契约收敛并通过全量测试;下一轮继续处理 A8~A11,并重新执行 +本轮独立验收由子 agent 完成。A6 已在本轮修复并通过自动化验证,A7 已完成契约收敛并通过全量测试,A9 已完成进度持久化并通过重启回归;下一轮继续处理 A8、A10、A11,并重新执行 独立验收。P2/P3 不能被静默删除,必须在本迭代结束前解决或保留明确的延期记录。