refactor(interact): separate runtime actions from ui projection

This commit is contained in:
2026-08-06 00:32:59 +08:00
parent c77a0cf69b
commit 823e71a162
8 changed files with 125 additions and 57 deletions
+20 -20
View File
@@ -102,17 +102,17 @@ let chatSDK: ReturnType<LineUpRuntime["openApp"]>;
// Chat Renderer 不会获得 Transport、Store 或 Tauri 特权对象。
const mountedApp = new RuntimeAppHost(runtime, app, {
toolInteractions: {
submit(callID, submission) { return chatSDK.interactions.submit(callID, submission); },
cancel(callID, reason) { return chatSDK.interactions.cancel(callID, reason); },
expire(callID) { return chatSDK.interactions.expire(callID); },
read(callID) { return chatSDK.interactions.read(callID); },
loadDraft(callID) { return chatSDK.interactions.loadDraft(callID); },
saveDraft(callID, values) { chatSDK.interactions.saveDraft(callID, values); },
clearDraft(callID) { chatSDK.interactions.clearDraft(callID); },
submit(callID, submission) { return chatSDK.runtime.interactions.submit(callID, submission); },
cancel(callID, reason) { return chatSDK.runtime.interactions.cancel(callID, reason); },
expire(callID) { return chatSDK.runtime.interactions.expire(callID); },
read(callID) { return chatSDK.runtime.interactions.read(callID); },
loadDraft(callID) { return chatSDK.runtime.interactions.loadDraft(callID); },
saveDraft(callID, values) { chatSDK.runtime.interactions.saveDraft(callID, values); },
clearDraft(callID) { chatSDK.runtime.interactions.clearDraft(callID); },
},
taskInteractions: {
requestCancel(operationID) { return chatSDK.tasks.requestCancel(operationID); },
read(operationID) { return chatSDK.tasks.read(operationID); },
requestCancel(operationID) { return chatSDK.runtime.tasks.requestCancel(operationID); },
read(operationID) { return chatSDK.runtime.tasks.read(operationID); },
},
capabilityInteractions: {
async approve(callID) { return approveCapabilityCall(callID); },
@@ -273,15 +273,15 @@ function loginFailureMessage(reason: unknown, endpoint?: string): string {
// Chat 是 Runtime 上的 Core App。它不接收 Transport 原始 payloadRuntime 会先完成
// 协议解析、作用域筛选、持久化和 App Inbox 投递,再通过 SDK 交给这里。
chatSDK.subscribeAgentMessages(message => {
chatSDK.ui.subscribeAgentMessages(message => {
// 收到 Agent 消息后,先把执行状态投影出来,再进行具体内容渲染,最后 ACK。
// ACK 表示 Chat 已经接管这条消息,Runtime 才可以从可靠 Inbox 中移除它。
projectExecutionProgress(message.item, message.received_at);
renderIncoming(message.item);
chatSDK.acknowledgeAgentMessage(message.message_id);
chatSDK.ui.acknowledgeAgentMessage(message.message_id);
});
chatSDK.subscribe(view => renderAppSessions(view.app_sessions));
chatSDK.ui.subscribe(view => renderAppSessions(view.app_sessions));
function renderAppSessions(sessions: readonly {
app_session_id: string;
@@ -318,7 +318,7 @@ function renderAppSessions(sessions: readonly {
continueButton.type = "button";
continueButton.textContent = "继续处理";
continueButton.addEventListener("click", () => {
void chatSDK.dispatch({ type: "chat.continue_app_session", conversation_id: chatSDK.snapshot().conversation_id ?? "", app_session_id: session.app_session_id });
void chatSDK.runtime.dispatch({ type: "chat.continue_app_session", conversation_id: chatSDK.ui.snapshot().conversation_id ?? "", app_session_id: session.app_session_id });
});
group.append(continueButton);
}
@@ -667,10 +667,10 @@ async function approveCapabilityCall(callID: string): Promise<CapabilityCallReco
function queueAppResult(record: CapabilityCallRecord): void {
// 回传给 Agent 的只是有限结果(call_id、能力名、状态和有限错误码),不包含 URL、
// 剪贴板正文、本地路径、异常堆栈或 Artifact 引用等敏感输入。
const conversationID = chatSDK.snapshot().conversation_id;
const conversationID = chatSDK.ui.snapshot().conversation_id;
if (!conversationID) return;
const payload = safeCapabilityResult(record);
void chatSDK.dispatch({ type: "chat.report_app_result", conversation_id: conversationID, result: payload })
void chatSDK.runtime.dispatch({ type: "chat.report_app_result", conversation_id: conversationID, result: payload })
.catch(reason => rendererContext.renderSystemMessage(storageFailureMessage(reason)));
}
@@ -725,9 +725,9 @@ function pickFileFromTrustedHost(): Promise<PickedFile | undefined> {
function queueSurfaceCancel(instanceID: string): void {
// Surface 内部事件只转换为 Runtime Action;它不能直接发送网络消息或调用系统能力。
const conversationID = chatSDK.snapshot().conversation_id;
const conversationID = chatSDK.ui.snapshot().conversation_id;
if (!conversationID) return;
void chatSDK.dispatch({ type: "chat.cancel_surface", conversation_id: conversationID, instance_id: instanceID })
void chatSDK.runtime.dispatch({ type: "chat.cancel_surface", conversation_id: conversationID, instance_id: instanceID })
.then(receipt => {
if (receipt.disposition === "accepted") rendererContext.renderSystemMessage("已请求取消任务。");
})
@@ -783,7 +783,7 @@ $<HTMLFormElement>("#login-form").addEventListener("submit", async event => {
// DOM nodes or re-executing a user action.
// 这些 Inbox 消息已经通过持久化 transcript 成功渲染,因此这里只 ACK,不再次渲染,
// 避免刷新后出现重复消息或重新执行用户动作。
for (const pending of chatSDK.listAgentMessages()) chatSDK.acknowledgeAgentMessage(pending.message_id);
for (const pending of chatSDK.ui.listAgentMessages()) chatSDK.ui.acknowledgeAgentMessage(pending.message_id);
for (const instance of surfaceInstances.snapshot().instances) surfaceHost.mount(instance);
rendererContext.restoreExecutionSummaries(snapshot.execution_summaries);
await mountM4SignedFixture();
@@ -801,7 +801,7 @@ $<HTMLFormElement>("#message-form").addEventListener("submit", async event => {
event.preventDefault();
const input = $<HTMLTextAreaElement>("#message-input"); const send = $<HTMLButtonElement>("#send"); const text = input.value.trim();
if (!text) return;
const conversationID = chatSDK.snapshot().conversation_id;
const conversationID = chatSDK.ui.snapshot().conversation_id;
if (!conversationID) {
rendererContext.renderSystemMessage("当前会话尚未初始化,无法发送消息;请退出后重新登录。");
input.focus();
@@ -809,7 +809,7 @@ $<HTMLFormElement>("#message-form").addEventListener("submit", async event => {
}
input.value = ""; send.disabled = true;
try {
const receipt = await chatSDK.dispatch({ type: "chat.send_text", conversation_id: conversationID, text });
const receipt = await chatSDK.runtime.dispatch({ type: "chat.send_text", conversation_id: conversationID, text });
if (receipt.disposition === "rejected") rendererContext.renderSystemMessage("当前会话无法发送消息,请退出后重新登录。");
} catch (reason) {
rendererContext.renderSystemMessage(storageFailureMessage(reason));