feat(iteration): implement sdk v1 miniapp runtime loop

This commit is contained in:
2026-08-05 18:30:29 +08:00
parent 486280bec0
commit d2a2fd0aff
37 changed files with 2135 additions and 60 deletions
+119 -1
View File
@@ -27,6 +27,8 @@ import { CapabilityAuditLog } from "@/runtime/capabilities/capability-audit";
import { CapabilityExecutor, type PickedFile } from "@/runtime/capabilities/capability-executor";
import { LineUpRuntime } from "@/runtime/coordination/lineup-runtime";
import { RuntimeAppHost } from "@/runtime/app-management/runtime-app-host";
import { TaskDashboardMiniApp } from "@/core-apps/task-dashboard/task-dashboard-miniapp";
import { WhiteboardMiniApp } from "@/core-apps/whiteboard/whiteboard-miniapp";
// Vite 的开发标记和 M4 测试控制器需要扩展浏览器的全局类型。
declare global {
@@ -123,13 +125,84 @@ const mountedApp = new RuntimeAppHost(runtime, app, {
}).mountDefaultApp();
// 从通用挂载结果中取得当前应用的 SDK 和页面投影对象。
chatSDK = mountedApp.sdk;
const { messages, loginView, chatView, presence, rendererContext, rendererRegistry } = mountedApp;
const { messages, appSessions, loginView, chatView, presence, rendererContext, rendererRegistry } = mountedApp;
// Surface Host 只负责承载已经通过 Runtime/Surface 策略检查的隔离界面。
// Surface 的 ready/event 回调仍回到状态管理和 SDK Action,不直接执行 Agent 指令。
const surfaceHost = new IsolatedSurfaceHost(messages, {
ready(instanceID) { surfaceInstances.ready(instanceID, new Date().toISOString()); },
event(instanceID) { queueSurfaceCancel(instanceID); },
});
// These are the two shipped reference MiniApps. They are mounted through the
// same restricted SDK as any future bundled MiniApp; this adapter only wires
// lifecycle to the SDK object and never hands them Runtime internals.
const bundledMiniApps = new Map<string, { stop: () => void }>();
function reconcileBundledMiniApps(): void {
const active = new Set<string>();
for (const instance of runtime.lifecycle.instances.list(runtime.currentConversationID())) {
if (instance.app_scope !== "task-dashboard" && instance.app_scope !== "whiteboard") continue;
if (!["starting", "foreground", "background", "suspended"].includes(instance.state)) continue;
active.add(instance.instance_id);
if (bundledMiniApps.has(instance.instance_id)) continue;
try {
const sdk = runtime.openBundledMiniApp(instance.app_scope, instance.instance_id);
if (instance.app_scope === "task-dashboard") {
const miniapp = new TaskDashboardMiniApp(sdk);
miniapp.start();
bundledMiniApps.set(instance.instance_id, { stop: () => miniapp.stop() });
} else {
const miniapp = new WhiteboardMiniApp(sdk);
miniapp.start();
bundledMiniApps.set(instance.instance_id, { stop: () => { void miniapp.close(); } });
}
} catch (reason) {
console.warn("[LineUp Host] bundled MiniApp mount rejected", instance.instance_id, reason);
}
}
for (const [instanceID, mounted] of bundledMiniApps) {
if (active.has(instanceID)) continue;
mounted.stop();
bundledMiniApps.delete(instanceID);
}
}
runtime.onEvent(event => {
if (event.type === "app-lifecycle") reconcileBundledMiniApps();
});
/**
* Bundled MiniApps request local Surfaces through Runtime. The Host adapter
* is the only place that turns the already-scoped request into a DOM iframe;
* no request is sent back through the Agent protocol.
*/
runtime.onEvent(event => {
if (event.type !== "surface-request") return;
const now = new Date().toISOString();
const appID = event.app_scope === "task-dashboard" ? "lineup.task-dashboard" : event.app_scope === "whiteboard" ? "lineup.whiteboard" : undefined;
if (!appID) return;
const version = "0.1.0";
if (event.event === "open") {
const result = surfaceInstances.open({ instance_id: event.instance_id, app: { app_id: appID, version }, state: event.data.state ?? event.data }, currentConversationKey(), now);
if (result.instance && (result.disposition === "accepted" || result.disposition === "duplicate")) surfaceHost.mount(result.instance);
if (result.disposition === "accepted") runtime.replaceSurfaces(surfaceInstances.snapshot());
return;
}
if (event.event === "patch") {
const result = surfaceInstances.patch(event.instance_id, event.data.state ?? event.data, now);
if (result.instance && result.disposition === "accepted") surfaceHost.update(result.instance);
if (result.disposition === "accepted") runtime.replaceSurfaces(surfaceInstances.snapshot());
return;
}
surfaceInstances.close(event.instance_id);
surfaceHost.unmount(event.instance_id);
runtime.replaceSurfaces(surfaceInstances.snapshot());
});
// These two Surfaces are shipped with the Host for SDK v1. Enabling them here
// does not create a market or download path; it only admits the exact local
// manifests used by the bundled reference MiniApps.
for (const [appID, version] of [["lineup.task-dashboard", "0.1.0"], ["lineup.whiteboard", "0.1.0"]] as const) {
surfaceRegistry.enable(appID, version, new Date().toISOString());
}
$<HTMLInputElement>("#api").value = localStorage.getItem("lineup.api") || DEFAULT_API;
/**
@@ -220,6 +293,51 @@ chatSDK.subscribeAgentMessages(message => {
chatSDK.acknowledgeAgentMessage(message.message_id);
});
chatSDK.subscribe(view => renderAppSessions(view.app_sessions));
function renderAppSessions(sessions: readonly {
app_session_id: string;
app_scope: string;
instance_id: string;
state: string;
interaction_count: number;
pending_interaction_count: number;
history: readonly { id: string; role: "agent" | "user"; text: string }[];
continued_from_app_session_id?: string;
}[]): void {
appSessions.replaceChildren();
for (const session of sessions.filter(candidate => candidate.app_scope !== "chat")) {
const group = document.createElement("details");
group.className = "app-session-group";
const summary = document.createElement("summary");
const name = session.app_scope === "task-dashboard" ? "Task Dashboard" : session.app_scope === "whiteboard" ? "Whiteboard" : session.app_scope;
const pending = session.pending_interaction_count > 0 ? ` · 等待回答 ${session.pending_interaction_count}` : "";
summary.textContent = `${name} · ${session.interaction_count} 条交互${pending}`;
const detail = document.createElement("small");
detail.textContent = session.state === "stopped" || session.state === "failed" ? "已结束,只读历史" : session.state === "background" ? "后台运行" : "当前应用会话";
group.append(summary, detail);
const history = document.createElement("div");
history.className = "app-session-history";
for (const entry of session.history) {
const line = document.createElement("p");
line.className = `app-session-history-${entry.role}`;
line.textContent = `${entry.role === "agent" ? "Agent" : "你"}${entry.text}`;
history.append(line);
}
group.append(history);
if (session.state === "stopped" || session.state === "failed") {
const continueButton = document.createElement("button");
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 });
});
group.append(continueButton);
}
appSessions.append(group);
}
}
chatSDK.subscribeEvents(event => {
// 这些是 Runtime 给 Chat 的安全事件投影,不包含原始 Agent delivery。
if (event.type === "local-item") {