feat: route incoming messages through interaction kernel

This commit is contained in:
2026-08-03 13:00:52 +08:00
parent d2646f534e
commit 883767c72e
4 changed files with 167 additions and 3 deletions
+30 -3
View File
@@ -2,12 +2,13 @@ import DOMPurify from "dompurify";
import { marked } from "marked";
import "./style.css";
import {
decodeConversationItem,
type Actor,
type ConversationItem,
type Envelope,
type JsonObject,
} from "./protocol";
import { InteractionKernel } from "./runtime/interaction-kernel";
import { RendererRegistry } from "./runtime/renderer-registry";
type Session = {
api: string;
@@ -32,6 +33,7 @@ const DEFAULT_API = "http://127.0.0.1:8090";
let session: Session = { api: localStorage.getItem("lineup.api") || DEFAULT_API, uid: "", agentUID: "agent_hermes_main", channelID: "agent_default_channel", channelType: 2, lastSeq: 0, active: false };
let polling = false;
let thinking: HTMLElement | undefined;
const interactionKernel = new InteractionKernel();
const app = document.querySelector<HTMLDivElement>("#app");
if (!app) throw new Error("LineUp host root is missing");
@@ -132,6 +134,7 @@ function hideThinking(): void { thinking?.remove(); thinking = undefined; }
function render(item: ConversationItem): void {
switch (item.kind) {
case "user-message": appendBubble("human", item.text); break;
case "markdown": hideThinking(); appendBubble("agent", item.markdown, true); break;
case "agent-status": item.status === "thinking" ? showThinking(item.detail || "正在思考…") : (hideThinking(), setPresence(item.detail || item.status || "在线")); break;
case "progress": { const percent = Math.max(0, Math.min(100, item.percent)); addSystem(`${item.title || "Agent 任务"}${percent}%${item.status ? ` · ${item.status}` : ""}`); break; }
@@ -142,6 +145,22 @@ function render(item: ConversationItem): void {
}
}
// M0-03 establishes the dispatch boundary. M0-06 will move these individual
// DOM renderers out of App Shell; until then the registry keeps the same UI
// behavior while ensuring all incoming content traverses the Kernel first.
const rendererRegistry = new RendererRegistry<void>();
rendererRegistry.register("markdown", render);
rendererRegistry.register("agent-status", render);
rendererRegistry.register("progress", render);
rendererRegistry.register("error", render);
rendererRegistry.register("surface", render);
rendererRegistry.register("app-call", render);
rendererRegistry.register("fallback", render);
function renderIncoming(item: ConversationItem): void {
if (!rendererRegistry.render(item, undefined)) addSystem("收到当前客户端没有可信 Renderer 的内容。");
}
function decodeBase64(value: string): string {
try { return decodeURIComponent([...atob(value)].map(c => `%${c.charCodeAt(0).toString(16).padStart(2, "0")}`).join("")); } catch { return value; }
}
@@ -179,7 +198,14 @@ async function syncLoop(): Promise<void> {
const previousSeq = session.lastSeq;
session.lastSeq = Math.max(session.lastSeq, message.message_seq);
advanced ||= session.lastSeq > previousSeq;
if (message.from_uid !== session.uid) render(decodeConversationItem(decodeBase64(message.payload)));
if (message.from_uid !== session.uid) {
const result = interactionKernel.ingest({
raw: decodeBase64(message.payload),
source: "sync",
transport_id: String(message.message_seq),
});
for (const item of result.items) renderIncoming(item);
}
}
// Avoid a hot loop if an upstream response repeats a sequence that
@@ -219,6 +245,7 @@ $<HTMLFormElement>("#login-form").addEventListener("submit", async event => {
const response = await fetchLogin(session.api, phone, code);
const body = await response.json().catch(() => ({})) as LoginResponse & { error?: string };
if (!response.ok) throw new Error(body.error || "登录失败");
interactionKernel.reset();
session = { ...session, uid: body.uid, agentUID: body.agent_uid || session.agentUID, channelID: body.channel_id || session.channelID, channelType: body.channel_type || session.channelType, lastSeq: 0, active: true };
loginView.hidden = true; chatView.hidden = false; addSystem("已进入与 AI Agent 的对话"); setPresence("正在同步消息…"); void syncLoop();
} catch (reason) { error.textContent = loginFailureMessage(reason); }
@@ -236,7 +263,7 @@ $<HTMLFormElement>("#message-form").addEventListener("submit", async event => {
finally { send.disabled = false; input.focus(); }
});
$<HTMLButtonElement>("#logout").addEventListener("click", () => { session.active = false; messages.replaceChildren(); chatView.hidden = true; loginView.hidden = false; });
$<HTMLButtonElement>("#logout").addEventListener("click", () => { session.active = false; interactionKernel.reset(); messages.replaceChildren(); chatView.hidden = true; loginView.hidden = false; });
export function makeProtocolEvent(
type: string,