Skip to content

AgentInterface:会话宿主与事件分发

源码版本v0.73.1

AgentInterfacepi-web-ui 里夹在 pi-agent-coreAgent 和具体消息渲染组件之间的一层 Lit customElement。它持有一个 Agent 实例作为 session 属性,负责装配 streamFngetApiKey,订阅 Agent 事件,把用户在 MessageEditor 里敲的文本转成 prompt() 调用,再把回流的事件分发给 MessageListStreamingMessageContainer。本身不碰 LLM,也不存历史,只是一个有状态的事件路由器。

职责

  1. 持有 session:@property session 指向一个 Agent 实例,session 变更时重新订阅事件,见 packages/web-ui/src/components/AgentInterface.ts:20-47
  2. 装配 streamFn 与 getApiKey:connectedCallback 时若 session.streamFn 还是默认的 streamSimple,就换成带代理支持的 createStreamFn,同时注入默认 getApiKeyAppStorage 读 key,见 packages/web-ui/src/components/AgentInterface.ts:130-152
  3. 订阅事件:session.subscribe 注册回调,把 message_start/message_update/message_end/agent_end 等事件映射到 StreamingMessageContainer.setMessagerequestUpdate,见 packages/web-ui/src/components/AgentInterface.ts:153-187
  4. 发送消息:sendMessage(input, attachments) 校验 model 与 API key,缺 key 时回调 onApiKeyRequired,然后调 session.prompt(input) 或带附件的 prompt(message),见 packages/web-ui/src/components/AgentInterface.ts:215-262
  5. 自动滚动:ResizeObserver 观察内容高度变化,配合 scroll 事件判断用户是否主动向上滚,决定是否自动贴底,见 packages/web-ui/src/components/AgentInterface.ts:89-105

设计动机

为什么不直接让 MessageEditorAgent.prompt?因为浏览器侧还缺几件杂事:代理 URL 要按 provider/key 动态决定、API key 要从 IndexedDB 异步取、缺 key 时要弹对话框、流式期间要管 streaming 容器与稳定列表的去重。这些逻辑如果塞进每个 host 应用都会重复,放进消息组件又会让渲染层耦合存储层。AgentInterface 抽出来当唯一入口,ChatPanel 只管布局,MessageEditor 只管输入,MessageList 只管渲染,三者通过它对接 Agent

另一个动机是 session 可替换:用户切换会话时不重建整个组件,只换 session 属性,willUpdate 检测到变更就 setupSessionSubscription() 重新订阅,见 packages/web-ui/src/components/AgentInterface.ts:68-75

关键文件

setupSessionSubscription 是核心装配点,streamFn 和 getApiKey 都在这里替换默认实现:

typescript
// packages/web-ui/src/components/AgentInterface.ts:138-151
if (this.session.streamFn === streamSimple) {
    this.session.streamFn = createStreamFn(async () => {
        const enabled = await getAppStorage().settings.get<boolean>("proxy.enabled");
        return enabled ? (await getAppStorage().settings.get<string>("proxy.url")) || undefined : undefined;
    });
}

if (!this.session.getApiKey) {
    this.session.getApiKey = async (provider: string) => {
        const key = await getAppStorage().providerKeys.get(provider);
        return key ?? undefined;
    };
}

sendMessage 把 key 校验和 onApiKeyRequired 回调串起来,缺 key 时让 host 决定怎么弹:

typescript
// packages/web-ui/src/components/AgentInterface.ts:222-238
const provider = session.state.model.provider;
const apiKey = await getAppStorage().providerKeys.get(provider);

if (!apiKey) {
    if (!this.onApiKeyRequired) {
        console.error("No API key configured and no onApiKeyRequired handler set");
        return;
    }
    const success = await this.onApiKeyRequired(provider);
    if (!success) {
        return;
    }
}

数据流

用户在 MessageEditor 按回车,经过 key 校验、代理装配,最终触发 Agent 循环并回流到渲染:

边界与失败

小结

AgentInterface 是浏览器侧的「会话宿主」:装配 streamFngetApiKey、订阅事件、分发到渲染组件。往上由 ChatPanel 装入布局,往下驱动 pi-agent-coreAgent。代理决策细节看 CORS 代理与 createStreamFn,渲染层组件看 消息渲染组件