mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-10-03 07:43:58 +00:00
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 授权限制装不上,与既有记录一致)。
This commit is contained in:
@ -364,6 +364,16 @@ type scheduler struct {
|
||||
// 被挂起 4 帧;L4 之上没有更高级别,链到此为止。超限只可能是内核 bug,
|
||||
// 因此这里只做防御性计数,**不降级、不丢弃帧**。
|
||||
maxInterruptFrames int
|
||||
|
||||
// cancelBudget 是「停止」剩下的短路配额(见 Agent.RequestStop)。
|
||||
//
|
||||
// 语义(用户明确的设计):停止 = ①立即结束当前 LLM 推理;②对**停止那一刻
|
||||
// 已排队**的 x 条消息,后续依次在 pre-action 阶段短路,而不是把它们当
|
||||
// 中断/新输入再跑一遍。配额是快照值:停止之后**新到**的输入不受影响。
|
||||
cancelBudget int
|
||||
// stopArmed 标记“下一条待收尾的任务是因为用户按了停止”。
|
||||
// 取消 LLM 后 stepLLM 默认重跑本步;用户停止时必须改为直接收尾。
|
||||
stopArmed bool
|
||||
}
|
||||
|
||||
// suspendedTask 是一个被抢占任务的现场。
|
||||
@ -573,6 +583,53 @@ func (s *scheduler) setImmediateLocked(t *Task) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// armStop 处理一次「停止」指令:登记短路配额并返回**停止那一刻的排队深度**。
|
||||
//
|
||||
// 语义(用户明确的设计):停止 = ①立即结束当前 LLM 推理;②对停止那一刻
|
||||
// 已排队的 x 条消息,后续依次在 pre-action 阶段短路。配额是**快照值**:
|
||||
// 停止之后新到的输入不受影响(否则停止会变成一个永远生效的“黑洞”)。
|
||||
//
|
||||
// 多次按停止取**较大值**而不是累加:两个客户端同时按下时配额不应翻倍。
|
||||
func (s *scheduler) armStop() int {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if n := s.pendingQueueLenLocked(); n > s.cancelBudget {
|
||||
s.cancelBudget = n
|
||||
}
|
||||
s.stopArmed = true
|
||||
return s.cancelBudget
|
||||
}
|
||||
|
||||
// pendingQueueLenLocked 统计**尚未执行**的排队输入数量(不含中断队列/挂起)。
|
||||
func (s *scheduler) pendingQueueLenLocked() int {
|
||||
return len(s.queue)
|
||||
}
|
||||
|
||||
// takeStop 消费「当前任务应被立即结束而不是重试」这一次标记。
|
||||
//
|
||||
// 取消 LLM 后 stepLLM 会看到 context.Canceled 并 outcomeContinue 重跑;
|
||||
// 若这是用户按下的停止,重跑就是错的——应该直接收尾。
|
||||
func (s *scheduler) takeStop() bool {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if !s.stopArmed {
|
||||
return false
|
||||
}
|
||||
s.stopArmed = false
|
||||
return true
|
||||
}
|
||||
|
||||
// consumeCancel 消费一格短路配额;true 表示本任务在 pre-action 阶段直接收尾。
|
||||
func (s *scheduler) consumeCancel() bool {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.cancelBudget <= 0 {
|
||||
return false
|
||||
}
|
||||
s.cancelBudget--
|
||||
return true
|
||||
}
|
||||
|
||||
func removeTask(list []*Task, target *Task) []*Task {
|
||||
for i, t := range list {
|
||||
if t == target {
|
||||
|
||||
Reference in New Issue
Block a user