Commit Graph

5 Commits

Author SHA1 Message Date
97111778a9 feat(webui): 数据查询 API + 前端 keyed 对账,去掉「局部重建」的闪烁
jianf:局部重建的闪烁几乎消不掉,应暴露数据查询 api,前端轮询后增量更新视图,
聊天记录也用这套。

后端(数据查询 api):
- ChatMsg 增加 seq(服务端单调递增、随记录落盘);老记录加载时补 1..n,重启不重编号。
- /api/v1/chat/history 增加 after=<seq> 增量通道:只回 seq 更大的消息,返回 last_seq
  作下次游标;一批超 limit 时回**最旧**的一批(回最新会把被挤掉的旧消息永久漏掉)。
  普通响应也带 last_seq,客户端首次全量后据此初始化游标。
- 测试 TestChatHistoryIncrementalAfterCursor 钉住「不重不漏 + 截断停在返回的最后一条」。

前端:
- 新增通用 morph():按「子节点位置 + nodeName」递归对账 DOM,同名节点复用、只同步
  变化的属性与文本。运行态面板的 put() 由 innerHTML 重建改为 morph —— SVG 圆环、
  队列条、数字块这些未变节点不再被替换,CSS 过渡与动画不再从头播。
- 聊天列表改用 keyed commitChatList():按 data-key(服务端 seq / 本地临时 key)对账,
  未变消息节点一个字节都不动,只替换真正变化的那条。
- syncChatFromHistory 改走游标:pollChatIncremental() 用 after 拿增量 + tail=1 探尾部
  原地更新(工具调用/最终文本是原地改的,不产生新 seq);聊天页可见时 3s 轮询。
2026-09-14 15:56:27 +08:00
e4d69fa140 feat(webui): 总览改版 —— 阶段管道滑块 / per-agent 负载环 / 通道→agent 拓扑与光点
总览页此前是一堆数字与文字块,看不出「这一轮走到哪、谁忙、消息从哪进哪出」。
本次把运行态面板改成以图形为主:

- 阶段管道:七阶段滑块,由 SSE stage 事件驱动,当前阶段高亮、滑块滑过去;
  一轮结束(after_output 或 2.5s 无事件)自动回到空闲,不做假动画。
- 队列与中断栈:沿用五条进度条(L1–L4 + 排队),中断栈补一条深度进度条。
- Agent 拓扑:改成「每 agent 一条横带」——左 inputch、中 agent 节点(圆环 = 负载)、
  右 outputch,连线即路由;删掉旧的「归属框 + 单个内核盒」画法(看得出哪个子接了哪条输入)。
- 光点动画:channel_input(新增轻量 SSE 事件)沿 inputch→agent 连线跑;
  agent_output 沿 agent→outputch 连线跑。用 SMIL animateMotion,不需要 rAF 循环。
- 负载:由该 agent **自己的**调度器积压(排队 / 四级中断 / 中断栈)按级别加权折算,
  环形图展示。为此把驻留子的调度器积压透出到状态面(SDK 纯追加字段)。

后端:sdk.ResidentStatus / core.ResidentInfo 增加子 agent 调度器积压四项;
WebUI SSE 增加 channel_input 轻量事件(只带通道名与 agent id,不带正文)。

顺带收口对话区视觉(页签改分段控件、消息间距/气泡区分、输入区分隔线)。
2026-09-14 15:33:23 +08:00
c0e9dc1818 fix(webui): 聊天记录不再"每次都发完整记录",并修掉视口跳顶
两个都是你指出的症状,都定位到了具体代码路径。

① 「每次都发完整聊天记录」
   a) API 缺省值错了:/api/v1/chat/history 的 limit 缺省是 0 = **不限制**,
      于是任何不带 limit 的调用每次都拿到整段记录。实测(126 条):
      不带 limit 635,297 字节;现在默认只回一页 180,668 字节。
      显式 limit=0 仍可整取(逃生口)。WebUI/GUI 本来都带 limit,不受影响。
   b) 前端 30s 轮询(以及每次 SSE 报错)都直接拉一页 40 条:
      浏览器实测单次 180,813 字节。现在先做"尾巴探测"(limit=1,362 字节),
      尾巴一致就直接跳过;不一致才拉整页。

② 「聊天记录会跳到顶部」——两条会导致视口丢失的路径都堵上
   a) syncChatFromHistory 在"找不到重合点"时直接 `state.messages = serverMsgs`:
      服务端只回一页,而本地可能已经向上翻了好几页;一覆盖,容器立刻变矮,
      视口被夹回顶部,用户翻过的旧消息也凭空消失。现在只在服务端页**不短于**本地时
      才整体替换。
   b) renderChat 全量重建 innerHTML 后,仅在粘底时滚到底;非粘底(用户正在向上读)
      时位置没人管。改为重建前记住 scrollTop、非粘底时原样还回去。

浏览器实测(CDP 驱动真实页面,126 条历史):
  * 15s/30s 定时器跑满 40s:聊天区滚动位置 **0 px 变化**,未跳顶;
  * 期间 chat/history 请求:limit=1 × 2(各 362 字节)+ 首屏 limit=40 一次;
  * 控制台无报错;新增消息后轮询仍能正确并进来(尾巴探测→拉整页→合并)。

测试:新增 TestChatHistoryDefaultIsPaged(默认一页 / has_more / limit=0 整取 / 显式分页)。
顺带修测试串味:迁移用例往 os.TempDir() 写共享历史文件,会让其它用例的
NewHandler 加载到脏历史(表现为条数多 1);现在各用例用自己的临时文件。
2026-09-14 08:06:16 +08:00
4cbfdc970c perf(webui)+feat(config): 聊天记录写盘节流 + 配置库空闲页回收
两条都是我上一封里点出、你说继续的问题。

① 聊天记录:每条消息都整段重写 → 节流合并写
   原来 persistChatLocked 每次变更就整段重写记录文件,而一轮对话会触发多次
   (用户消息、每个工具事件、收尾消息)。200 条上限下文件可达数 MB,单轮就能
   放大出几十 MB 写。文件里还留着一个 chatSaveThrottle=3s 常量——声明了但从未
   被使用(疑似上次 revert 的遗留),等于节流从来没生效。
   现在:persistChatLocked 只置脏 + 唤醒写盘协程;chatPersistLoop 去抖
   chatSaveThrottle(3s)、并以 chatSaveMaxDelay(10s) 兜底(持续输出也不会无限拖延);
   写盘前把快照拷出来,**不持 chatMu 做文件 IO**;写失败重新标脏下轮重试。
   插件 Stop 里调 Handler.Close():停协程 + 强制落最后一次(幂等),否则丢最后一轮。

   实测(临时实例,连发 3 条消息):3s 窗口内记录文件**尚未创建**(节流生效);
   SIGTERM 后文件出现且 6 条(3 用户 + 3 助手,无 LLM key 故为错误回复)全在
   ——关停落盘没丢。

② config.db:SQLite 的 DELETE 不缩文件 → 空闲页够多时 VACUUM
   新增 ConfigRegistry.MaybeCompact(minFreeBytes, minRatio):空闲页 >= 1MB 且
   占页数 >= 25% 才做一次 VACUUM,避免每次启动都重写整库。库里是 WAL 模式,
   VACUUM 之后必须再 wal_checkpoint(TRUNCATE),否则主库文件看着没变小。
   调用点放在插件加载**之后**(大值的搬走/删除发生在插件 Start 里,之前调没意义)。

   实测(一个刚被搬走 5MB 聊天记录的实例):
     freelist 1288 页 × 4096B;启动日志「配置库已压缩: 5394432 -> 118784 字节」
     config.db 5,394,432 → 118,784 字节;记录文件 5,279,491 字节完好未动。

测试:TestChatPersistenceIsThrottled(节流窗口内不写盘 + Close 必落盘 + Close 幂等)、
TestMaybeCompactReclaimsFreePages(删大值后文件确实变小 + 数据完好 + 阈值不达标时不白做功)。
2026-09-14 07:14:55 +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