feat(iteration): implement sdk v1 miniapp runtime loop
This commit is contained in:
+119
-1
@@ -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") {
|
||||
|
||||
Reference in New Issue
Block a user