132 Commits

Author SHA1 Message Date
oudecheng
d3ee430aa9 test(web): MessageList 虚拟化的对抗性 smoke test
对抗性审查 commit 03d8840 的虚拟化实现,新增 5 个组件测试:

- 用例 1: 空消息渲染空状态
- 用例 2: 100 条消息不崩溃(虚拟化 smoke test)
- 用例 3: highlight 指向视口外消息不崩溃
- 用例 4: viewKey 切换不崩溃
- 用例 5: 流式 rerender 不崩溃

发现:jsdom 下 getBoundingClientRect 返回 0 + rAF 同步化,
virtualizer 测量后可见范围可能变空(textContent 不可靠)。
这是测试环境限制,不反映浏览器真实行为(浏览器 rAF 异步、
高度真实)。用例 5 聚焦不崩溃验证,内容正确性由真实布局保证。
2026-08-07 08:34:17 +08:00
oudecheng
2367d87db6 perf(web): 消息列表虚拟化,长对话不再卡顿
原实现 messages.map 渲染所有消息,500 条消息会产生 10000+ DOM
节点(每条 MessageBubble 含 ReactMarkdown 解析),导致首次渲染
卡顿、滚动掉帧、流式输出每 token 都全量 diff。

引入 @tanstack/react-virtual 实现虚拟化:
- 只渲染视口内 + overscan=6 条消息,DOM 节点数恒定
- measureElement 动态测量变高度消息(一行文本 vs 50 行代码块)
- estimateSize=120 提供初始估计,测量后自动校正
- Firefox 特殊处理 measureElement(getBoundingClientRect)

适配现有功能:
- 自动滚到底部:bottomRef.scrollIntoView 改为 virtualizer.scrollToIndex
- viewKey 滚动位置记忆:保留 scrollTop 保存/恢复逻辑
- highlightedMessageId:先 scrollToIndex 渲染目标项,rAF 后加 class
- newMessageCount:流式 delta 不计数(消息条数不变)
- space-y-6 改为每项 pb-6(虚拟化下相邻 margin 不生效)

性能对比(500 条消息):
- DOM 节点:10000+ → ~200(仅可见+overscan)
- 首次渲染:数百 ms → <16ms
- 流式 diff:全量列表 → 仅可见项
2026-08-07 08:34:07 +08:00
oudecheng
934c7aa804 test(web): 补充流式断连的对抗性测试(stream_end 幂等性 + 多周期循环)
对抗性审查 commit 30b7d90 的修复健壮性,新增两个边缘场景测试:

- 用例 18: 验证断连后 stream_end 到达是幂等的(不崩溃、不改变
  已 flush 内容),且后续 assistant_response 能正常按 id 替换
- 用例 19: 验证多次断连-重连循环下 finishStreaming 幂等
  (连续调用不崩溃,消息状态稳定)
2026-08-07 08:33:57 +08:00
oudecheng
c749df4e0a fix(web): WebSocket 断连时清理流式状态,防止重连后消息串流
流式优化引入的回归风险:当 WebSocket 在流式输出中途断连时,
streamingRef(contentChunks/index)不会被重置。重连后新消息的
delta 会通过 s.index >= 0 分支追加到旧消息的错误位置。

修复链路:useWebSocket 的 onDisconnect 回调 → useChat 的
finishStreaming → useMessages 的 finishStreaming(重置 ref +
flush pending chunks)。

- useMessages.ts: 在 UseMessagesReturn 接口和 return 对象暴露
  finishStreaming(原为内部函数)
- useChat.ts: 在返回对象透传 finishStreaming
- App.tsx: useWebSocket 调用新增 onDisconnect 回调触发 finishStreaming
- useChat.test.ts: 新增用例 17 验证断连后新 delta 不污染旧消息
2026-08-07 08:33:48 +08:00
oudecheng
00735395ca fix(web): 修复正在执行的会话消息泄露到定时任务视图
handleSchedulerMessage (Tier 1 路由) 原先无条件吞掉所有可转换的
chat 消息,导致用户打开定时任务历史视图时,当前主会话正在执行
的 stream_delta/assistant_response 等实时消息被错误追加到
schedulerView.messages。

修复:调度器视图是历史会话查看器,只接收不带 topic_id 的历史
加载消息;实时流式消息(stream_delta/stream_end)和带 topic_id
的实时消息 fall through 到 Tier 2/3 路由到主视图,不再泄露。

- useSchedulerView.ts: handleSchedulerMessage 增加流式和 topic_id
  过滤,实时消息 fall through
- useChat.test.ts: 新增用例 16 验证流式和带 topic_id 消息不进入
  schedulerView,无 topic_id 历史消息仍正常进入
2026-08-07 08:33:37 +08:00
oudecheng
f8c984aef4 perf(web): 优化流式 delta 处理,避免每 token 全数组拷贝
用 ref 累加 delta chunks + requestAnimationFrame 批量 flush,
将每秒数十次 O(n) 数组拷贝合并为一帧一次状态更新。

- useMessages.ts: 新增 streamingRef 累加器 + rAF 调度,所有 delta
  只 push 到 ref,由 flushStreaming 批量落盘;用 rafScheduledRef
  boolean 追踪调度状态(避免 rAF handle 为 0 时与 null 检查冲突);
  flushStreaming 基于 prev 实际状态判断创建/更新消息(批处理安全)
- useChat.ts: 5 处 setMessages([]) 改为 clearMessages(),切话题/
  会话/通道时重置流式 ref,避免脏状态残留
- test/setup.ts: 同步化 rAF 并返回递增非零 handle,保持测试同步语义
- useChat.test.ts: 新增对抗性测试(批量 delta 累加、stream_end 后
  assistant_response 替换含 reasoning)

复杂度:O(n²·L) → O(n) 每帧,15 个测试全部通过
2026-08-07 08:33:27 +08:00
oudecheng
7b459b8ca1 feat(config): 压缩算法关键参数暴露到设置页面
将 4 个压缩参数(threshold_ratio、llm_compaction_threshold_ratio、
truncate_max_tokens、preserve_count)从硬编码提取到 config.json 顶层
compaction 节,前端新增 CompactionTab 供用户调整。

后端:
- config: 新增 CompactionConfig 结构体,Config 新增 compaction 字段
- context_compressor: 4 个参数内聚到 ContextCompressor 实例字段;
  新增 with_compaction_config() 构造函数和 truncate_tool_results() 方法;
  对用户配置做防御性 clamp(ratio ∈ [0.1,1.0],整数 ≥ 1)
- agent_loop: 调用 compressor.truncate_tool_results() 实现零参数耦合
- agent_factory: 新增 build_compressor() 方法,in-loop 与 sync 兜底
  两条压缩路径共用同一套用户配置
- session: with_factories 改用 agent_factory.build_compressor(),修复
  sync 压缩路径忽略用户配置的 P0 问题

前端:
- types: 新增 CompactionConfig 接口,TabId 新增 'compaction'
- 新建 CompactionTab.tsx,4 个 number input 分两组 SectionCard
- constants + ConfigPage: 注册"上下文压缩" Tab(Archive 图标)

清理:删除无调用方的 from_provider_config/from_runtime_config/with_config
方法及 DEFAULT_THRESHOLD_RATIO/LLM_COMPACTION_THRESHOLD_RATIO 常量。

测试:context_compressor 18 + session 31 + agent_loop 42 全通过。
2026-08-06 11:03:56 +08:00
oudecheng
fa420b713f feat(gateway): 添加 HTTP API 认证与 CORS 防护,修复密钥泄露风险
- 新增 auth_token 配置项:非 loopback 绑定强制认证,loopback 可选加固
- 新增 auth.rs:loopback 判定(IpAddr 解析)、常量时间 token 比较、RFC 6750 Bearer 解析
- 新增 Bearer token 中间件(/api/*)与 WebSocket ?token= 校验
- 新增 CORS layer:loopback 镜像 Origin 防 DNS rebinding,非 loopback permissive
- 修复 .gitignore 遗漏 tests/test.env,补充通用 *.env 规则
- 修复前端 experts/skills/subagents.ts 直接 fetch 不带 token(13 处),统一走 authedFetch
- config.json host 从 0.0.0.0 改回 127.0.0.1
- mask_config/save_config 处理 auth_token 掩码与保留
- ConnectionTab/GatewayTab 增加认证 token 配置入口
2026-08-05 22:41:00 +08:00
oudecheng
bb2774c6b5 feat(web): token 统计从侧边栏迁移到右侧面板,支持子代理每轮刷新
- 新增 TopicTokenStatsPanel 面板,显示五字段 + 上下文占用进度条

- 移除 TopicList 侧边栏的 token 统计显示

- 后端新增 get_session_token_stats,按 session_id 精确查询子代理 token

- TaskMessagesLoaded 协议扩展 token_stats 字段,ws.rs 转发至前端

- useMessages/useSubAgentView 每轮 assistant_response/execution_completed 触发刷新

- App.tsx 根据当前视图分派 list_topics(主代理)或 load_task_messages(子代理)

- 抽取 utils/tokenStats.ts 共享格式化与占用率计算函数
2026-08-05 12:09:40 +08:00
oudecheng
beb7b581a6 fix(web): 主代理执行结束时刷新 topic 列表以更新 token 统计
LLM usage 在消息持久化后已写入 DB,主代理本次执行结束时触发 bumpTopicRefreshTrigger,list_topics 重新读取最新 token 统计并更新侧边栏。
2026-08-05 11:15:12 +08:00
oudecheng
235f57c9ae feat(settings): 新增设置页重启按钮,区分保存后触发与手动触发
状态从 showRestartDialog: bool 改为 restartDialogMode: 'saved' | 'manual' | null,三态清晰区分触发来源。

改动:- 保存后自动弹出 'saved' 模式对话框(标题 '配置已保存',按钮 '稍后') - 新增独立 '重启' 按钮,点击弹出 'manual' 模式对话框(标题 '重启服务',按钮 '取消'),dirty 时提示未保存更改 - 重启中禁用保存与重启按钮,handleClose 阻断关闭并提示 - ESC 在对话框打开时仅关闭对话框,不关闭整个页面 - 重启按钮 loading 状态与 restarting 同步
2026-08-05 09:43:05 +08:00
oudecheng
4e74d66a7c fix(web): 修复流式期间消息重渲染、WebSocket 无谓重连与新消息计数错误
- MessageBubble 用 React.memo 包裹,流式 delta 只追加到最后一条消息,
  其余消息引用不变可被 memo 跳过,避免 N-1 条历史消息无谓重渲染
- useWebSocket 用 useRef 存储回调最新引用,connect 不再依赖回调引用,
  避免调用方回调变化触发 disconnect+connect 重连
- MessageList 用 prevMessageCountRef 追踪消息条数变化,仅当真正新增
  消息条数时才累加 newMessageCount;流式 delta 不增加条数不再误计数,
  滑到底部按钮触发的清零路径与时序经审查覆盖各种边界场景
2026-08-05 08:10:44 +08:00
oudecheng
adada2652c fix(web): 修复话题重命名预填字段与写入字段不一致
Bug: 点击铅笔编辑时,输入框显示的是 topic.title(系统生成的 id+时间),
而不是用户看到的 topic.description(AI 生成的摘要)。且后端 rename 写入 title,
但列表显示优先 description,导致改名后视觉上无变化。

根因:显示用 'description || title',编辑预填用 'title',写入也改 'title',
三者不一致。

修复:
- 前端 startEdit: 预填 'description || title'(与显示逻辑一致)
- 后端 rename_topic: 改用 update_topic_description 写入显示字段,
  保留 title 作为内部标识;'未变化'比较也基于当前显示值
- 新增测试: 验证有 description 时比较逻辑基于 description 而非 title
2026-08-04 23:57:04 +08:00
oudecheng
c2cc072b2e feat(settings): 拆分设置页为懒加载标签页并支持子代理创建/删除
- ConfigPage 拆分为 16 个懒加载 tab + 2 个 modal,首屏体积大幅下降
- 抽取 CapabilityTabs 共享组件,统一专家/子代理能力配置 UI
- 后端新增 POST /api/subagents/create、DELETE /api/subagents/delete
- SubagentRuntime 新增 create_subagent/delete_subagent,含路径校验与 builtin 保护
- SubagentModal 支持 create/edit 双模式、body 编辑、自身排除防自递归
- SubagentsTab 增加搜索、provider/model 标签、删除二次确认
- 修复 reload() 使用进程 cwd 而非 self.cwd 的隔离缺陷
2026-08-04 21:06:17 +08:00
oudecheng
2e4b1931a6 feat(tokens): 添加 topic 维度 token 消耗与上下文窗口占用统计
后端:
- openai provider 流式响应启用 stream_options.include_usage,从 SSE 末帧捕获 usage
- ChatMessage 新增 MessageUsage(prompt/completion/total/context_window_tokens)
- agent_loop 三处 LLM 调用点持久化 usage,含 context_window_tokens
- storage 新增 context_window_tokens 列及 migration,batch_session_token_stats 聚合查询
- last_prompt_tokens 查询过滤 prompt_tokens IS NOT NULL,跳过 error/cancel 消息
- build_topic_summaries 简化签名,context_window_tokens 从消息记录读取
- protocol 扩展 TopicTokenStats,按 session_id 聚合天然隔离子代理

前端:
- protocol.ts 新增 TopicTokenStats 类型
- useTopics 映射 token_stats 到 Topic
- TopicList 展示总 token 消耗与上下文占用百分比(绿/黄/红三色)

测试:outbound_dispatcher 4 个测试修复(drop(bus) 不关闭 bus,改用 abort)
2026-08-04 17:01:54 +08:00
oudecheng
457f6b2408 feat(retry): 为 LLM 主调用添加可配置重试机制,默认 3 次
- config: ProviderConfig 增加 max_retries 字段(serde default=3,向后兼容)
- config: LLMProviderConfig 透传 max_retries,不进 ProviderRuntimeConfig(保持 provider 构造包纯净)
- agent: AgentRuntimeConfig 增加 max_retries,归属 agent 行为层
- agent_loop: 流式 + summary 两个调用点实现重试循环
  - 指数退避 1s/2s/4s,仅对 429/502/503/504/timeout/connection reset 重试
  - 流式仅在未 emit delta 时重试(AtomicBool 跟踪),避免重复输出
  - 退避 sleep 期间响应 cancel_signal,取消优先
- 前端: ProviderConfig 类型和表单增加 max_retries 字段
- 测试: 7 个单元测试(3 判定 + 4 行为),540 个 lib 测试全绿
2026-08-04 12:53:33 +08:00
oudecheng
6901659849 fix(mcp): 限制 server 名称字符并兜底 tool_name 清洗
OpenAI 要求 function name 匹配 ^[a-zA-Z0-9_-]+$,否则整个请求 400。MCP 工具名由 mcp_{server_key}_{tool_name} 拼成,两个输入源:server_key 用户可控(前端 addMcp/renameMcp 正则校验 + toast),tool_name 由 MCP server 上报(后端 sanitize_tool_name 替换非法字符为 _)。

前端:MCP 卡片头部改用 MapEntryHeader 支持点击重命名,状态指示灯移到上方独立行。顺手修了 MapEntryHeader 进入编辑时 val 未同步当前 name 的 bug。

后端:保留 server_key 和 tool_name 原值用于路由,仅清洗 LLM 可见的 full_name,并在发生清洗时打 warn 日志便于定位。
2026-08-04 11:17:41 +08:00
oudecheng
14d903e067 fix: Agent 执行与显示层解耦,修复锁屏冻结根因
浏览器锁屏导致 WebSocket 半死,ws_sender.send().await 永久阻塞,
级联阻塞 dispatcher → MessageBus → Agent Loop,后端停止执行直到解锁。

基于第一性原理建立"执行-显示解耦"原则:agent 执行只依赖 SQLite
持久化,实时广播是可丢弃的最佳努力通道。

核心改动:
- MessageBus::publish_outbound 由 send().await 改为 try_send(),
  bus 满时丢弃消息并告警,agent 不再被显示层阻塞
- WebSocket writer task 包裹 30s 超时,使用每连接独立的
  CancellationToken(非共享 CliChannel 级 token),避免一个连接
  超时关闭所有连接;writer 退出时 cancel 通知主 loop 退出
- CliChannel::send 由 send().await 改为 try_send(),避免 dispatcher
  单线程被卡住的 writer 阻塞 37s
- 全仓 13 处 publish_outbound 调用统一区分 Dropped(warn)/Closed(error)
- scheduler 3 处 ? 改为 warn,避免 Dropped 触发 misfire 重试风暴
- 前端 WebSocket 添加 25s ping + 指数退避重连(3s→60s封顶)
- 前端 session_list 区分重连恢复/首次连接,重连时保留 messages
  并刷新 topic 列表

经五轮对抗性审查验证,修复了共享 cancel token、dispatcher 阻塞、
load_chat_messages 跨 topic 污染、原 session 删除后状态不一致等
回归问题。

同时升级版本号至 0.3.0 并更新 CHANGELOG。

验证:cargo clippy --all-targets --all-features ✓
      npm run build ✓ | useChat.test.ts 13 passed ✓
2026-08-04 10:55:52 +08:00
oudecheng
c724bbf864 chore(web): prettier 一次性格式化并在 CI 启用 format:check
对 47 个前端文件统一执行 npm run format,消除存量格式差异。
随后在 CI 与 Makefile check 中启用 format:check,确保后续提交强制遵守 prettier 风格。
2026-08-03 23:25:22 +08:00
oudecheng
cda14360af chore: 建立工程化基线(rustfmt + clippy + CI + eslint + prettier)
配置:
- rustfmt.toml: 固化 max_width=100 / 4 空格缩进,cargo fmt 全量格式化
- Cargo.toml: 配置 [lints.rust] 与 [lints.clippy] 渐进式规则
- .github/workflows/ci.yml: Rust(fmt+clippy+test) + 前端(eslint+tsc+test) 双平台 CI
- Makefile: 新增 check/fmt/fix 目标,clippy 对齐 --all-targets --all-features
- web: eslint flat config + prettier 配置 + package.json 脚本与依赖
- src/main.rs: loop→while 修复 clippy::never_loop

对抗性审查发现并修复:
- eslint 缺 caughtErrorsIgnorePattern 导致 catch(_) 误报为 error
- 前端 lint 未接入 CI,现已补上 Lint 步骤
- Makefile 与 CI 的 clippy flags 不一致,已对齐
2026-08-03 23:24:02 +08:00
oudecheng
2a5a0277c0 feat(web): 添加话题重命名功能
新增 RenameTopic 命令与 TopicRenamed 协议消息,复用存储层已有的
update_topic_title 方法。后端响应携带刷新后的完整 topic 列表,
前端零额外往返即可同步侧边栏。

前端 TopicList 侧边栏增加内联编辑入口:悬停显示铅笔图标,点击进入
编辑模式,Enter 提交 / Esc 取消 / blur 取消;通过 onMouseDown
preventDefault 防止按钮点击时 input 提前失焦。

包含 5 个单元测试覆盖成功、空标题、不存在、标题未变跳过写入、
can_handle 等场景。
2026-08-03 22:10:25 +08:00
oudecheng
649ea2998d feat(web): 集成 ModelSelector 实现主 agent 模型选择 UI
新建 ModelSelector 组件(参考 ExpertSelector 交互模式): 显示当前生效模型,下拉支持 provider/model 选择,草稿暂存+应用提交,重置为默认。

ChatContainer 用共享 flex 容器并排渲染 ExpertSelector 和 ModelSelector,ExpertSelector 简化外层 wrapper(去除冗余 padding/max-width)。

API 层: 新增 sessionSelectedModel 端点常量和 getSelectedModel 函数; types.ts 为 ModelOptionsResponse 添加 current 字段。

按钮语义: 已覆盖时显示 provider/model,未覆盖时显示 '默认 provider/model'。
2026-07-30 23:25:14 +08:00
oudecheng
5ff1e8455c feat(web,docs): 前端支持模型下拉选择并更新 README 专家系统章节
- 前端类型 ExpertItem/SubagentItem 新增 provider/model 字段,新增 ModelOptionsResponse 类型

- API 层 createExpert/updateExpert/updateSubagent payload 支持 provider/model,新增 listModelOptions

- ConfigPage 专家/子代理模态框新增模型配置 SectionCard(Provider/Model 下拉框,留空继承默认)

- 空字符串转 undefined 传后端,避免 P0 反序列化为 Some(vec![]) 导致全禁

- README 新增第 8 节专家系统,更新子代理 SUBAGENT.md 字段说明(含 provider/model 和子代理黑白名单)

- README 新增 max_nesting_depth 配置项说明,重排后续章节编号
2026-07-30 22:44:28 +08:00
oudecheng
003eab4f21 fix(web): 修复点击设置后白屏(React hooks 顺序违规)
子模态框 ESC 处理的 useEffect 被放在 if(loading)/if(!config) 条件 return 之后,导致首次渲染(loading=true)时不执行该 hook,config 加载后重新渲染才执行 → hooks 数量不一致 → React error #310 白屏崩溃。

修复:将 useEffect 移到所有条件 return 之前,与其他 hooks 放在一起。
2026-07-30 18:56:43 +08:00
oudecheng
951e31aca4 feat(web): 子代理模态框改造并暴露 max_nesting_depth 配置项
对抗审查修复 4 个 P0 问题:
- 子代理 modal handleSave 缺失 allowed_subagents/denied_subagents 字段,导致保存时既有策略被后端 #[serde(default)] 静默清空(数据丢失)
- 子代理 modal 缺失子代理黑白名单 CheckboxList UI
- 子代理 modal 未改造为 ModalHeader/ModalFooter/SectionCard,无 X 关闭按钮与遮罩 onClick
- experts tab 未加载 subagentList,编辑专家时子代理勾选列表显示空

新增改动:
- ui.tsx 新增 ModalHeader/ModalFooter 通用组件
- ConfigPage renderTools 新增最大嵌套深度输入框(后端 TaskConfig.max_nesting_depth 早已存在,默认 2)
- 子代理 modal 子代理选项排除自身避免意外自递归
2026-07-30 17:38:08 +08:00
oudecheng
9381ed5dd4 feat(web): 专家/子代理 capability 字段改为勾选式编辑
- 专家编辑模态框:4 个 textarea 替换为 CheckboxList,技能/工具按来源分组展示

- 新增子代理编辑模态框:非 builtin 子代理显示编辑按钮,支持描述 + 4 个 capability 勾选字段

- 进入 experts/subagents 标签页时自动拉取技能与工具列表

- allowed_* 为空时传 undefined(后端 None=不限),避免空数组触发白名单空集语义
2026-07-30 16:55:38 +08:00
oudecheng
3ea0c19262 feat(web): 新增工具列表 API 客户端与 CheckboxList 勾选组件
- api/client.ts: 注册 tools 与 subagentsUpdate 端点常量

- api/tools.ts: 新增 listTools() 调用 /api/tools

- api/subagents.ts: 新增 updateSubagent() 调用 PUT /api/subagents/update

- types.ts: 新增 ToolItem / ToolsListResponse 类型

- ui.tsx: 新增 CheckboxList 组件,支持分组展示、legacy 已选值以可移除标签呈现
2026-07-30 16:55:12 +08:00
oudecheng
cea4bd3cfb feat: 子代理支持数据驱动的工具过滤(白名单+黑名单),删除内置 explore
SUBAGENT.md frontmatter 新增 denied_tools 黑名单字段,启用已有的 allowed_tools 白名单;新增 ToolRegistry::only 白名单方法;抽取 build_subagent_tools_registry(白名单→黑名单→depth 兜底);安全修复:resume 在 def 失踪时拒绝恢复而非降级为完整工具集(避免权限提升);depth 阈值改为引用 max_nesting_depth 配置;SubagentWithStatus 暴露工具字段供前端只读展示;删除内置 explore 子代理及专属 explore_max_execution_secs 配置(全栈清理);新增 23 个测试覆盖过滤矩阵、frontmatter 解析、状态投影
2026-07-09 14:42:38 +08:00
oudecheng
0a8d21fe40 fix: 修复专家发现与前端选择多项问题
后端:
- split_frontmatter 兼容 CRLF/LF 行尾符,修复 Windows 下 EXPERT.md 解析失败
- Config::load_from 移除 fallback 到 cwd/config.json 的逻辑,避免静默加载项目目录配置导致 experts 字段缺失
- ExpertRuntime 新增 update_config 方法,save_config 时同步更新 ExpertRuntime 内部 config,sources 变更即时生效
- source_order 空数组不再兜底返回 [User, Project],尊重用户关闭所有源的意图
- from_config_with_cwd 增加诊断日志,输出 enabled/sources/cwd/discovered

前端:
- ExpertSelector 弹窗改为按钮上方居中展开
- 专家卡片 name 完整显示(break-all),路径显示在卡片底部
- "管理专家"入口仅在无专家时显示
- 每次打开下拉都重新拉取列表和选中状态
- 监听设置弹窗关闭事件刷新已选专家(处理已选专家被禁用的情况)
2026-07-08 14:53:30 +08:00
oudecheng
46a1ca6853 refactor: 修复 3 项 P1 技术债(硬编码 URL / any 类型 / dead_code)
P1-1: 硬编码 wechat URL 抽常量统一两处
  - config/mod.rs: 定义 pub const WECHAT_DEFAULT_BASE_URL
  - cli/init.rs: import 并引用常量,消除字面量重复
  - 消除两处不一致风险(default_wechat_base_url 与 init 向导)

P1-2: ConfigPage any 类型定义具体类型(8 处)
  - types.ts: 新增 FeishuChannelConfig/WechatChannelConfig/ChannelConfig/
    SchedulerJobConfig 类型;jobs?: any[] → SchedulerJobConfig[];
    channels: Record<string, any> → Record<string, ChannelConfig>
  - api/config.ts: 新增 RestartResponse 类型,data: any → RestartResponse
  - ConfigPage.tsx: catch (e: any) ×2 → catch (e: unknown) + 类型守卫;
    as any → as SchedulerConfig['misfire_policy'];
    Record<string, any> → Partial<ChannelConfig>;(ch: any) → (ch: ChannelConfig)

P1-3: dead_code 18 处逐一审查清理
  - 删除 10 处未使用函数/struct(YAGNI 原则):
    context_compressor.rs (into_messages, is_tool_round)
    agent_loop.rs (EmptySkillProvider struct + impl)
    memory_maintenance.rs (run_for_scope)
    session.rs (try_start/finish_background_compaction)
    session_history.rs (try_start/finish_background_compaction)
    tool_registry_factory.rs (shell_session_manager)
    task/runtime.rs (effective_allowed_tools)
  - 保留 8 处 serde 反序列化字段(删除会破坏 JSON 反序列化):
    feishu.rs/openai.rs/anthropic.rs/skills/mod.rs/task/runtime.rs
2026-07-08 14:51:42 +08:00
oudecheng
fef5ae7626 fix: 修复 P0 级技术债(useEffect 依赖项缺失 + prompt 注入吞错)
P0-1: web/src/App.tsx 话题刷新 useEffect 依赖项不完整
  - 原依赖数组仅 [topicRefreshTrigger],但 effect 体内使用了
    status/handleCommand/sendMessage/requestTopicList
  - 导致断线重连后 status 从 disconnected→connected 时
    effect 不会重新运行,话题刷新被静默丢失(stale closure bug)
  - 补全依赖数组为 [topicRefreshTrigger, status, handleCommand,
    sendMessage, requestTopicList],与同文件 200-208 行 effect 风格一致

P0-2: src/gateway/agent_prompt_provider.rs 静默吞错
  - record_injection 中 let _ = repository.mark_agent_prompt_reinjected(...)
    丢弃了 Result 错误
  - 失败时 Agent prompt 注入计数错乱且无任何日志
  - 改为 if let Err(e) = ... { tracing::warn!(...) } 记录 session_id 和 error
2026-07-08 14:33:22 +08:00
oudecheng
7c578af0bc refactor: 重写 useChat 为组合根,消除上帝 Hook
将 1290 行的 useChat 重写为 360 行组合根:handleServerMessage 从 462 行降为 80 行纯路由分发(Tier1 调度器→Tier2 子智能体→Tier3 主视图),消除组合根层 5 个跨域 ref 同步 effect。UseChatReturn 接口零变更,App.tsx 无需改动。对抗式审查修复 1 个 Critical 项(todo_write 自动刷新的 topic_id 检查)。
2026-07-08 11:28:51 +08:00
oudecheng
f3e3ebf55f test: 添加 useChat handleServerMessage 特征测试
新增 13 条特征测试,覆盖 handleServerMessage 全部 20 个 case 与 3 层路由(调度器→子智能体→主视图),锚定行为基线以保护后续组合根重写的回归风险。
2026-07-08 11:28:28 +08:00
oudecheng
7038d58207 refactor: 抽取 useChat 的领域子 hook 与 messageMappers
将原上帝 Hook 的状态域拆分为 7 个职责单一的子 hook:useConnection/useSessions/useTopics/useMessages/useSubAgentView/useSchedulerView/useSideData,并将消息映射纯函数提取到 messageMappers.ts。各子 hook 自管 ref 同步 effect,跨域依赖通过 options 注入,为组合根重写奠定基础。
2026-07-08 11:24:42 +08:00
oudecheng
f5ccf490ba test: 添加 vitest 测试基础设施
新增 vitest 配置、jsdom 环境与 jest-dom 匹配器,并在 package.json 中加入 test/test:watch 脚本与相关 devDependencies,为后续特征测试与子 hook 独立测试提供运行环境。
2026-07-08 11:24:10 +08:00
oudecheng
ffc1f79de4 refactor: 建立前端 API 客户端层,消除硬编码路径与重复 fetch
- 新增 web/src/api/ 目录(6 个文件):client.ts 基础设施 + 5 个领域模块
- 封装 18 处 fetch 调用为强类型函数,消除 17 处硬编码 /api/ 路径
- 统一 13 处重复的 fetch+json+错误处理模式
- 删除旧 web/src/components/Settings/api/expert.ts,合并到新 api/experts.ts
- ConfigPage.tsx 替换 15 处 fetch 调用,ExpertSelector.tsx 改用新 API
- 保持现有组件行为不变(静默失败仍静默,错误处理等价)
2026-07-08 10:03:34 +08:00
oudecheng
c8660df14b refactor: 拆分 storage/mod.rs 与 ConfigPage.tsx (P2)
storage/mod.rs (2957 -> 1834 行):
- 抽出 tests.rs: 17 个测试函数
- 抽出 row_mapping.rs: 8 个 row<->record 映射与单记录查询函数
- 抽出 migrations.rs: 7 个 schema 迁移函数 (ensure_*_schema, has_column, add_column_if_missing)

ConfigPage.tsx (1568 -> 1235 行):
- 抽出 types.ts: 所有接口/类型定义
- 抽出 constants.ts: TABS, inputCls, selectCls, TIMEZONE_OPTIONS
- 抽出 ui.tsx: Field, Toggle, TagEditor, SectionCard, SourceEditor, MapEntryHeader
- 抽出 api/expert.ts: getSelectedExpert, selectExpert (经 ConfigPage 再导出保持兼容)

验证: cargo build, cargo test --lib storage:: 17/17, npm run build
对抗性检查: 8/8 通过 (无丢失/重复函数, 无循环依赖, 无未使用导入)
2026-07-07 18:36:23 +08:00
oudecheng
f8d5f0253a refactor: 清理 P1 类型债与 React key 债
- 消除 useChat.ts 中 13 处 (message as any).timestamp,改用已收窄变量的 .timestamp
- 给 ExecutionCancelled 接口补 timestamp?: number 字段,与后端对齐
- 修复 MessageInput 附件列表 key={index}: 给 FileAttachment 加 id 字段(crypto.randomUUID())
- 修复 ConfigPage TagEditor key={i} → key={t}(tag 值唯一)
- 修复 ConfigPage SourceEditor customPaths key={i} → key={p}(路径值唯一)
2026-07-07 17:53:53 +08:00
oudecheng
7eb2933ca5 chore: 清理 P0 技术债
- 删除 web/src/components/Sidebar/.deprecated 目录(死代码)
- 清理 useChat.ts/useWebSocket.ts 中 13 条调试 console.log
- 修复 load_task_messages.rs 硬编码 task 状态: 新增 TaskSessionState::Unknown,
  DB 重建时返回 Unknown 而非误报 Completed, 前端显示"未知"
2026-07-07 17:45:07 +08:00
oudecheng
7eecd0b6bb feat: 专家配置保存后即时生效,无需重启网关
experts/mod.rs: config 改为 RwLock 支持 update_config 运行时更新; ExpertWithStatus 增加 body 字段供前端编辑模态框直接显示; 空 sources 时不再兜底返回 [User,Project],尊重用户关闭所有源的意图。

gateway/http.rs: save_config 同步调用 experts.update_config,返回消息从'需要重启'改为'已保存'。

ConfigPage.tsx: 编辑模态框宽度扩大到 max-w-3xl,textarea 高度增加到 360px,提示文案明确正文与名称/描述的注入关系。
2026-07-07 14:13:09 +08:00
oudecheng
fdd22556a1 fix: 高亮消息时不再退出子智能体视图
子智能体视图的消息源是同一 chat_id,高亮消息不需要退出子智能体栈。仅定时任务视图(另一个 chat_id)需要退出。
2026-07-07 14:12:42 +08:00
oudecheng
b9c880d823 fix: 同步前置 subAgentViewRef/subAgentStackRef,消除子智能体视图间歇性丢消息
根因: subAgentViewRef.current 在 setSubAgentStack updater 内部更新,updater 在 React render 阶段才执行,不是同步的。从 enterSubAgentView() 返回到 React render 之间存在竞态窗口,WebSocket 消息读到过时的 ref 被错误路由到主视图并丢弃。

修复: ref 赋值从 updater 内部移到 setSubAgentStack 之前同步执行。新增 subAgentStackRef 供 exit/navigate 在空依赖闭包中读取最新栈计算新栈顶。selectTopic/selectChannel/selectSession 清空栈时也同步设置 ref。updater 变纯函数,StrictMode 双调用无副作用。

Plan: fix-subagent-view-ref-race
2026-07-07 14:12:21 +08:00
oudecheng
6df87fe399 fix: 子智能体工具消息去重改为按 id+type,修复工具状态卡在执行中
后端为同一次工具调用的 tool_call/tool_result/tool_pending 生成相同的 id(= tool_call_id),上一提交的去重逻辑仅按 id 判断,导致 tool_result 被 tool_call 误判为重复而跳过,App.tsx 合并逻辑收不到 tool_result,工具永远停留在 calling 状态。

改为按 id + type 去重:同类型消息不会重复(解决 load_task_messages 并发重复),不同类型消息(tool_call vs tool_result)不会被误伤(修复工具状态卡死)。

同时修复 appendToSubAgentViewMessage 和 appendToSubAgentLayerMessage 两处。
2026-07-07 11:22:48 +08:00
oudecheng
fc050c5074 fix: 前端子智能体视图消息显示与刷新修复
protocol.ts: ExecutionCompleted/WsError 类型增加 subagent_task_id 字段。

useChat.ts 核心修复:

- getSubagentTaskId 识别 execution_completed/error 消息类型

- task_messages_loaded 处理增加 task_id 校验,避免快速切换视图时状态污染

- 主视图 execution_completed/error case 防护,子智能体事件不操作主视图

- 新增 appendToSubAgentLayerMessage,非栈顶子智能体消息遍历栈匹配层而非丢弃

- appendToSubAgentViewMessage 对 tool_call/tool_result/tool_pending 按 id 去重

- execution_completed/error 路由到匹配层更新 status

- exitSubAgentView/navigateToSubAgentLevel 返回 load_task_messages 命令重新拉取

Spec: fix-subagent-message-refresh-on-exit (Task 3-8)
2026-07-06 18:11:37 +08:00
oudecheng
61c2fca2a7 feat: 对话框上方集成专家选择器,支持实时选择与系统提示词注入
- 新建 ExpertSelector 组件,加载时查询 session 选中状态,失败降级显示无专家

- 下拉面板含无专家/专家列表/管理专家入口,乐观更新+失败回滚保证一致性

- ChatContainer 在 MessageList 与 MessageInput 之间渲染 ExpertSelector

- MessageInput 接收 selectedExpert,选中时 placeholder 显示以专家身份对话

- App.tsx 传递 sessionId/onOpenSettings 给 ChatContainer,支持从对话框直达专家设置

- App.tsx 同时包含 subagent 导航 command 发送的连带修改
2026-07-06 18:08:19 +08:00
oudecheng
5651c4ae7f feat: 设置页新增专家 Tab,支持专家增删改查与启用切换
- 新增 ExpertsConfig/ExpertItem/ExpertListResponse 类型与 AppConfig.experts 字段

- TABS 数组加入专家 Tab (UserCheck 图标),TabId 含 'experts'

- renderExperts 渲染启用开关、来源目录编辑器、已发现专家列表与添加按钮

- 专家编辑 modal 含 name(编辑时只读)/description(必填)/body(textarea) 三字段

- 导出 selectExpert 与 getSelectedExpert 模块级函数供 ExpertSelector 复用

- Tab 切换自动拉取列表,专家 toggle 即时生效不触发未保存警告

- initialTab prop 支持从对话框入口直达专家 Tab
2026-07-06 18:07:54 +08:00
oudecheng
8d30ccd020 feat: 新增子代理管理API,支持获取子代理列表和切换子代理状态 2026-07-06 14:41:30 +08:00
oudecheng
38b9f661ee feat: 新增技能管理API,支持获取技能列表和切换技能状态 2026-07-06 11:59:11 +08:00
oudecheng
cb0d3d932d feat: TodoPanel 改为常驻右边栏并支持点击跳转
- TodoPanel 移除浮动定位与拖动逻辑,改为常驻右边栏
- 右边栏与左边栏对称的折叠/展开逻辑,localStorage 持久化
- 待办/记忆/技能三 Tab 切换
- 点击已完成 todo 滚动并高亮对应 tool 消息
- 修复 MessageBubble isMergedTool 分支缺失 data-message-id 导致跳转失效
- ChatContainer 移除 todoPanel prop
2026-07-03 19:32:46 +08:00
oudecheng
994db87f11 feat: 新增 ExecutionCompleted 信号修复发送按钮状态
- 后端 OutboundEventKind/WsOutbound 新增 ExecutionCompleted 变体
- processor 在 handle_message 完成后发送该信号
- 飞书/微信通道过滤该信号(不发送)
- 前端 useChat 移除 stream_delta/assistant_response 的 isLoading=false
- 改由 execution_completed 事件统一设置 isLoading=false,确保智能体迭代期间按钮保持停止态
2026-07-03 19:32:14 +08:00