Commit Graph

2 Commits

Author SHA1 Message Date
ccc2ac2d4d fix(stop): 停止按钮真正生效——停止 ≠ 空中断;鸿蒙 screensue 支持 HTML
两处鸿蒙端缺陷 + 一个跨端(WebUI/GUI/鸿蒙)的停止语义缺陷。

## 症状(实测取证)

1. **鸿蒙终止按钮按下没反应**。POST /chat/interrupt 带空 body,接口回 200
   `{"status":"interrupted"}`,但 journalctl 零中断日志、生成继续跑到自然结束。
2. **鸿蒙 screensue 不解析 HTML**,把标签当普通字符串显示。

## 根因

停止按钮走的是「空内容中断」,而 interceptLoop 有一行
`if text == "" { continue }` —— 空内容被判为「无事发生」直接丢弃。
所以停止指令从未到达调度器;接口那个 200 是不诚实的。

另查明两条会放大症状的既有问题(停止后仍在跑):
- `chatStreamWithFallback`:流式连接失败时无条件回退非流式 `Chat`。
  上下文已取消时这等于**再发一次完整请求**(停止后模型继续生成)。
- `stepLLM`:`context.Canceled` 一律 `outcomeContinue` 重跑本步。
  这是给「被更高中断抢占」用的(现场要交出去、稍后继续),
  但用户按停止是「不要了」,重跑就是停止没生效。

## 修法(按用户明确的设计)

停止 = ①立即结束当前 LLM 推理(不重试、不恢复);
②对**停止那一刻已排队**的 x 条消息,后续在 pre-action 阶段依次短路。

- scheduler:新增 `armStop`(登记快照配额并返回当时排队深度)/`takeStop`/
  `consumeCancel`。配额取快照值(停止后新到的输入不受影响),
  重复按停止取 max 不累加(两个客户端同时按不该翻倍)。
- `interceptLoop`:读 `stop` 标记。停止时 armStop + cancelCurrentLLM;
  **纯停止不再进中断队列**(旧实现把它当空中断入队,所以停完还会活)。
  带注释的停止(`/stop 换个话题`)仍走中断路径。
- `stepLLM`:取消 + `takeStop()` → 直接 `outcomeDone`(不再重跑)。
- `stepPrepare`:`consumeCancel()` 命中即在 pre-action 短路收尾。
- `chatStreamWithFallback`:以 **ctx.Err()** 为判据拒绝回退(不是「错误是不是
  Canceled」——很多 provider 用 Canceled 表示「不支持流式」,那种必须继续回退,
  否则会把探测误判成取消;这条区分是跑全量测试时才暴露的)。
- WebUI handler / CLI `/stop`:空消息时带 `stop:true`。

## 鸿蒙端

- `BridgeCaps.ets`:新增 `looksLikeHtml`(首字符 '<' + 字母开头标签名,
  避免误判 "<3" 这类文本)、`screensueHtml`、`escapeHtmlText`。
- `ScreensuePage.ets`:HTML 走 **RichText**(只解析 HTML 子集、无脚本无网络),
  纯文本仍走 Text。不用 Web 组件:agent 下发的是第三方内容,
  Web 默认带 javaScriptAccess/fileAccess,等于让远端内容在客户端执行脚本。
  注入主题前景色,避免 RichText 用系统默认色导致深色主题下黑字不可见。
- `ChatSession.ets`:`interruptChat` 改发 `{stop:true}`(含类型声明,
  ArkTS 禁止无类型对象字面量),并在本地即时复位忙态 + 提示「已停止」。

## 验证

- 新增 `stop_semantics_test.go`:停止终结任务不重试(provider 调用次数恒为 1)、
  配额是快照(x 条短路、随后新到的不受影响)、重复 arm 取 max。
- `go test ./internal/... ./cmd/...` 全绿。
- 鸿蒙 HAP 构建通过;unsigned 包已装进模拟器(signed 包受
  READ_PASTEBOARD 授权限制装不上,与既有记录一致)。
2026-09-18 11:26:12 +08:00
642e1c39b1 refactor(webui): handler.go 2993 行按资源拆成 11 个同包文件
拆法:按「资源面」搬家,每个顶层声明(func/type/var/const)整体搬到目标文件,
声明体一字未改,各文件按实际用到的包重新生成 import。文件头加一行说明本文件负责哪一面。

  handler.go              骨架:嵌入前端资源、Handler/构造、路由表、鉴权会话日志中间件、静态页
  handler_chat.go         对话面:消息模型与内存历史、SSE 事件订阅、对话/历史接口
  handler_upload.go       上传面:handleChatFile / handleUploads / 中断对话
  handler_memory.go       记忆面:图/文档/文本记忆、知识库、LLM 源、变更追踪
  handler_agents.go       内核与代理面:状态、kernel、人格、代理/快照/回滚
  handler_settings.go     设置与插件面:配置读写、插件列表详情(含 pluginmgr 反代)
  handler_terminal.go     终端面:终端会话、终端接口、命令历史
  handler_sse.go          SSE 环形缓冲(断线重连补发)
  handler_openai.go       OpenAI 兼容面:/v1/chat/completions
  handler_device.go       设备网关反代(HTTP + WS 升级透传)
  handler_files.go        /files/ 与 /uploads/ 下载

零漂移校验:拿重构前的 handler.go 与新 11 个文件逐行比对(忽略空行、package/import 头),
**丢失行 0**;新增行恰好是 11 个文件头注释(14 行)。

顺带修掉 import 里两处假使用:handler_openai 的 sdk 只作为 Handler 字段名出现(h.sdk.),
handler_settings 的 fmt 只出现在注释里 —— 都从 import 里去掉。

验证:go build ./... / go vet / webui+config+sdk 测试全绿;
起真实实例(沿用已有 data 目录)后 /status /settings /chat/history /plugins /terminals
/kernel /memory /config /login 全部 200,设置在注入 5MB 历史的情况下仍是 33,921 字节。

最大文件从 2993 → 706 行(handler_chat.go)。
2026-09-14 07:03:47 +08:00