mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-10-15 12:15:16 +00:00
fix(clients): 三方客户端与内核的 SSE 协议一致性 —— 幽灵类型、发了没人收、客户端漏接
一次三方(CLI/GUI/鸿蒙)与内核的协议审计,发现四处**全部静默**的不一致
(无任何测试或日志会报),本次一并修复并把它们变成可执行判据。
## 四处不一致
1. `agent_error` 是**幽灵类型**:SSE 白名单订阅了它、鸿蒙还写了处理分支
(显示「本轮处理失败」),但全仓**无任何发布者** —— events 包与 SDK 都
没有这个常量。于是那段分支永远不可达,而真正的失败帧叫 `error`
(webui/cli 都发 `{type:"error"}`),鸿蒙从未接住。
2. `system` **发了没人收**:internal/plugin/dynamic_proc.go 一直在发插件崩溃
(注释还写着「webui/诊断插件可订阅」),却不在 SSE 白名单里 ⇒ 事件发出来
就掉地上,插件崩溃对用户完全不可见。
3. CLI 缺 `stage`/`system`:CLI 是唯一看不到阶段进展的客户端,agent 跑长任务
时终端长时间静默,用户分不清「在干活」还是「卡住了」。
4. 鸿蒙设备桥**静默丢弃**:只处理 bind_ack 与 cmd,服务端下行的
push / cmd_speech_start / cmd_speech_end / hello_ack 直接 return ——
「收到了但不支持」与「根本没收到」在日志上完全一样,无从排查。
## 修法(注意:三方客户端的通信模型不同,清单不可一刀切)
CLI 走 InjectTextSync **同步拿最终回复**(response 帧)+ 事件订阅做过程输出,
**不做 SSE 重连** ⇒ 它需要 stage/tool_call/reasoning/delta,
但**不需要** agent_output(正文来自同步回执)与 sync_required(那是 SSE
断线重连专用的补拉信号)。GUI/鸿蒙是真 SSE 客户端,两者都需要全量对话事件。
第一版判据按同一份清单要求三者,误报了 CLI —— 那是判据的错,不是代码的错。
- handler_chat.go:subTypes 删 `agent_error`、补 `system`
- dashboard.js / ChatSse.ets:补 `system` 消费(WebUI 用 toast,鸿蒙用 setStage)
- ChatSse.ets:幽灵分支 `agent_error` 改为真实类型 `error`
- cli/plugin.go:补 `stage`(过滤 `_consolidation_`)与 `system` 订阅
- cmd/waiter:补 Phase/Event/Plugin 字段与 stage/system 渲染
- DeviceBridge.ets:静默丢弃改为带 hilog 日志(区分「不支持」与「没收到」)
## 判据(internal/plugins/webui/handler_sse_protocol_test.go,7 条)
TestSSESubTypesAreRealEventTypes(幽灵类型)/
TestEveryPublishedEventHasAnOutwardPath(发了没人收,例外须显式声明理由)/
TestClientsRecognizeServerEventTypes(按客户端分档)/
TestNoPhantomFrameTypes / TestStageFrameFieldsMatchServer /
TestSseWhitelistParsesToExpectedSet(**解析器反向自检** —— 解析器失效会让整组
判据静默变成空集并全绿,是最危险的假绿)/
TestDeviceBridgeOpCoverage。已接入 `make check-client-protocol`(进 make test)。
变异 M1–M8 全部验证。其中 M5 首轮未报红:判据只查前缀 `json:"phase`,
把 tag 改成 `phaseX` 仍匹配 ⇒ 改为匹配**完整 tag** 后才咬住。
## 端到端实测(不只看测试绿)
- 真实 SSE 流收到 stage×15 / content_delta×111 / memory_access×3 /
agent_llm_chain / tool_call×2,**agent_error 0 次**(幽灵已清)
- kill 一个 plugin.bin → SSE 收到 `event: system`
`{"event":"plugin_crashed","plugin":"a2a"}`,日志确认「自动重启 a2a」
- WebUI 真弹出 toast「插件 acp 异常退出,内核正在重启它」(MutationObserver 捕获)
This commit is contained in:
@ -113,8 +113,25 @@ export function applyChatSse(ev: SseEvent, sink: ChatStreamSink): void {
|
||||
} else if (type === 'sync_required') {
|
||||
// 断线重连时服务端要求补拉历史(ring 里没有可重放的聚合事件)
|
||||
sink.reloadHistory();
|
||||
} else if (type === 'agent_error') {
|
||||
// 后端错误一律转人话,技术细节不上 UI
|
||||
} else if (type === 'system') {
|
||||
// 内核级状态事件(目前唯一发布者是 plugin_crashed)。
|
||||
//
|
||||
// ★ 为何必须补上(2026-10-13 协议审计):该事件一直在发
|
||||
// (internal/plugin/dynamic_proc.go:175,注释还写着「webui/诊断插件
|
||||
// 可订阅」),但鸿蒙端从未处理 —— 插件崩溃对手机用户不可见。
|
||||
if (payload['event'] === 'plugin_crashed') {
|
||||
const nm: string = payload['plugin'] as string ?? '?';
|
||||
// 用 stage 条告知(鸿蒙端 sink 没有 toast 能力;stage 是已有的轻量提示位)。
|
||||
// 不用 setLoading(false):崩溃不影响本轮对话本身。
|
||||
sink.setStage('插件 ' + nm + ' 异常退出,内核正在重启它');
|
||||
sink.refresh();
|
||||
}
|
||||
} else if (type === 'error') {
|
||||
// ★ 这里原本是 'agent_error' —— 那是一个**不存在的类型**(2026-10-13 审计)。
|
||||
// events 包与 SDK 都没有这个常量,全仓无任何发布者,生产日志 0 次。
|
||||
// 而真正的失败帧叫 'error'(见 webui/handler_chat.go 与 cli/plugin.go
|
||||
// 都发 {type:"error", ...}),于是「本轮处理失败」提示根本没机会显示。
|
||||
// 改用真实类型名。
|
||||
sink.setLoading(false);
|
||||
sink.setStage('本轮处理失败,请重试');
|
||||
sink.refresh();
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { webSocket } from '@kit.NetworkKit';
|
||||
import { hilog } from '@kit.PerformanceAnalysisKit';
|
||||
import { CapResult } from './BridgeCaps';
|
||||
import { BridgeCmdHandler, bridgeHelloFrame, bridgeBindFrame, bridgeResultFrame,
|
||||
bridgeDataStartFrame, bridgeDataEndFrame, bridgeEventFrame, bridgeStatusFrame,
|
||||
@ -225,7 +226,22 @@ export class DeviceBridgeClient {
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (op !== 'cmd' || !this.bound) {
|
||||
if (op !== 'cmd') {
|
||||
// ★★ 2026-10-13 协议审计:这里原本是静默 return,而服务端**会**发
|
||||
// op=push / cmd_speech_start / cmd_speech_end / hello_ack。
|
||||
// 本机(鸿蒙端)只做「被控设备」,不做输出通道,所以收到这些 op
|
||||
// 属预期——但**静默丢弃**会让「服务端以为发出去了、本端却毫无痕迹」,
|
||||
// 排查时只能对时序猜。至少留一行日志,让“收到了但不支持”与
|
||||
// “根本没收到”可区分。
|
||||
//
|
||||
// 为何不报错回去:这些 op 不要求回执(服务端 pushToDevice 不等
|
||||
// device→server 的应答),回一个错误帧反而会被当成未知帧处理。
|
||||
if (op.length > 0) {
|
||||
hilog.debug(0x0000, 'HomeAgent', 'device bridge: 忽略不支持的 op %{public}s', op);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!this.bound) {
|
||||
return;
|
||||
}
|
||||
const reqId: string = obj['req_id'] as string ?? '';
|
||||
|
||||
Reference in New Issue
Block a user