466 Commits

Author SHA1 Message Date
oudecheng
f2fc5e97ac feat(security): 部署安全加固(排除 token 项)
- 安全响应头中间件:nosniff / X-Frame-Options DENY / Referrer-Policy / CSP / Permissions-Policy,最外层覆盖所有响应
- loopback CORS 由 mirror_request 改为 loopback origin 白名单(原实现会回显任意 Origin,允许恶意网页跨域读取本地网关)
- 新增 gateway.allowed_origins 配置:非 loopback 部署可收紧 CORS,未配置时保持 permissive 并打 warn
- loopback 免认证模式强制 Host 头为 loopback,阻断 DNS rebinding
- loopback 免认证模式 WS Origin 必须为 loopback 来源,防跨站 WebSocket 劫持(CSWSH)
- WS 消息/帧大小显式上限 80MiB/16MiB(顺带修复 50MB 附件 base64 超 tungstenite 默认 64MB 上限的问题)
- HTTP 请求体显式限制 2MB(DefaultBodyLimit)
- 前端 HTTPS 页面自动使用 wss://,避免混合内容拦截 WebSocket
- 新增 10 个安全相关单元测试(host/origin loopback 判定、WS Origin 校验)
2026-08-17 06:47:35 +08:00
oudecheng
1019dbe8cc refactor(code-quality): 清理 clippy 存量告警(unwrap/clone/redundant 等)
- 移除无用克隆与冗余引用,减少不必要内存分配
- 规范 unwrap/expect 使用,修复可提前失败路径
- 修复 anthropic provider llm_timeout_secs 死代码并补全超时日志
- cargo fmt 统一格式
2026-08-16 23:22:22 +08:00
oudecheng
692cc93075 fix(ws): 历史加载时对账 task running 占位,修复刷新/切话题后子代理卡片永远显示运行中
DB messages 表的 task tool_result 固化在 spawn 时刻的 running 状态,完成信号只更新
pending_subagents 表;send_topic_history 原样发送导致刷新后卡片退回黄色运行中。
新增 reconcile_running_in_messages:发送前按 pending_subagents 实际状态替换发送副本,
复用 session.rs 提取/格式化函数(改为 pub(crate)),不落库且幂等,无占位时零查询。
2026-08-16 17:27:55 +08:00
oudecheng
0f60b02ede chore(log): 缩减 INFO 日志量,高频诊断日志降级/删除
1. 删除缓存诊断日志 4 处(openai.rs):排查已完结,字段已补齐,每次 LLM 调用都刷屏。
2. 删除 AgentPromptProvider 模型配置日志(agent_prompt_provider.rs):排查遗留,每次请求刷。
3. AgentFactory 创建日志 info→debug(agent_factory.rs):每轮对话刷,降级保留供偶发排查。
4. Calling tool / Tool calls detected info→debug(agent_loop.rs):每次工具调用都刷,全量参数打印开销大,tracing 在未启用级别时不评估字段。
5. LoadTaskMessages 3 条日志 info→debug(load_task_messages.rs):每次前端 trigger 都刷,一次 3 条。
6. 旧结果诊断日志 info→debug(execution.rs):每条消息首次 agent 迭代都触发。
7. pending subagents 日志 info→debug(processor.rs):子代理运行期高频率重复触发。
8. Updating current_session_id 仅变化时打(ws.rs):old==new 时每条消息都刷,无信息量。
编译通过,测试全绿。
2026-08-15 23:10:39 +08:00
oudecheng
ca7dcabbde fix(protocol): TopicTokenStats 补齐 cached_tokens 字段,修复前端缓存占比恒为 0
根因:command 层与 protocol 层存在两个同名 TopicTokenStats 结构体,websocket 适配器做 JSON 往返时 protocol 版缺 cached_tokens 字段,serde 反序列化静默丢弃。后端解析/存库/聚合均正常(DB 实测 8832/13916 命中),仅传输层断链。补字段并加往返回归测试固化两结构体对齐不变量。
2026-08-15 21:02:29 +08:00
oudecheng
011b23b54e feat(provider): 缓存命中诊断日志与两种 usage 缓存格式解析测试
排查平台有缓存命中但前端占比为 0 的问题:set_usage/非流式/回退三处输出 cache diagnostics 日志;新增 DeepSeek 顶层与 OpenAI 嵌套缓存字段解析回归测试
2026-08-15 20:17:30 +08:00
oudecheng
b26a2c2512 feat(token): 新增缓存占比统计,聚合查询收敛到共享列清单
- 解析: OpenAIUsage 支持 DeepSeek prompt_cache_hit_tokens + OpenAI prompt_tokens_details.cached_tokens fallback;Anthropic 接入 cache_read_input_tokens

- 存储: messages 表新增 cached_tokens 列(幂等迁移),贯穿 INSERT/SELECT/聚合

- 解耦加固: 抽取 MESSAGE_LOAD_COLUMNS 收敛 6 处消息加载 SELECT;抽取 USAGE_SUM_COLUMNS + read_usage_sum_row 收敛 topic/session 两份重复 SUM 聚合

- 协议: SessionTokenStats/TopicTokenStats 新增 cached_tokens 字段

- 前端: 面板新增缓存命中行 + 缓存命中率进度条(命中率越高越绿,与 ctx 占用方向相反)
2026-08-15 19:21:51 +08:00
oudecheng
82b6a882a2 test(scheduler): 调度器非阻塞派发与并发容量回归测试
- 慢执行器(400ms sleep)双任务:process_tick 必须立即返回且两任务并发完成(串行需 >=800ms,断言 <700ms)

- worker_queue_capacity=1:槽位耗尽时多余到期任务被推迟(保持 Scheduled),槽位释放后下个 tick 正常派发

- 执行完成后状态机验证:回到 Scheduled、status=ok、next_fire_at 推进
2026-08-15 19:04:54 +08:00
oudecheng
9b8c64bd21 refactor(scheduler): 任务执行移出 tick 循环,worker_queue_capacity 并发约束生效
原 process_tick 内联 await execute_job:长耗时 agent 任务(可达数分钟)会串行阻塞整个调度循环,拖后所有其他 job 的触发;worker_queue_capacity 配置项从未被使用。

- 到期 job 的执行与事后状态写入移入 tokio::spawn 后台任务,tick 循环只做派发

- 新增 worker_semaphore(容量=worker_queue_capacity):槽位耗尽时 job 保持 Scheduled 留待下个 tick,形成有界背压

- job 派发前置为 Running 状态,is_due 要求 Scheduled,天然防止重复派发

- after_execution 状态推进失败时回退为 Scheduled,避免 job 永久卡在 Running

- execute_job 拆分为 execute_job_inner(不依赖 &self,可移入后台任务)
2026-08-15 18:50:40 +08:00
oudecheng
4b93c84447 test(gateway): 定时任务 topic 补齐与消息送达回归测试
- 新增捕获请求的 mock OpenAI server,直接断言任务 prompt 进入 LLM 输入

- 静默任务按生产默认 fresh_session=true 连续执行两轮,验证清空-重建时序下 prompt 仍送达、topic 自动创建并持久化、系统提示词含送达提示

- ensure_topic_for_chat 幂等性与全新 chat 默认 topic 创建测试

- append_persisted_message 无 topic 回退 chat_id 键内存历史测试

- SessionManager/SessionLifecycleService 暴露 get_scheduler_session 供测试断言
2026-08-15 18:17:29 +08:00
oudecheng
cbe2ff1339 feat(gateway): 静默定时任务增加主动送达提示,避免结果静默丢失
silent_agent_task 的最终响应发往 scheduler/ 虚拟会话、被出站分发器跳过,成功时用户收不到任何消息,只能依赖 agent 主动调用 send_session_message。

- 定时任务系统提示词仅对 scheduler/ 虚拟会话追加送达提示(避免普通 agent_task 重复发送)

- scheduler_manage 工具描述新增 Silent Mode Delivery 指引:用户期望收到结果时,prompt 必须要求用 send_session_message 交付
2026-08-15 18:17:07 +08:00
oudecheng
508806a408 fix(gateway): 定时任务执行前自动补齐 topic,修复 agent 空历史执行
scheduler/ 虚拟会话从不经用户消息分配 topic,导致定时任务的系统提示词与任务 prompt 只落库、不进内存历史(append_persisted_message 在 topic=None 时跳过内存写入),agent 以空历史执行,任务实际未运行、消息无法送达会话。

- Session 新增 ensure_topic_for_chat:内存无 topic 时从 DB 恢复最近活跃 topic,无则自动创建(先确保持久会话以规避外键约束)

- prepare_and_execute_scheduled_task 捕获 topic 前调用 ensure,保证 original_topic_id 恒为 Some

- append_persisted_message 无 topic 时回退到 chat_id 键内存历史,兜底所有无 topic 路径
2026-08-15 18:16:50 +08:00
oudecheng
dc693fa80b feat(model): 主代理/子代理模型解析链 topic>session>expert>config + TaskToolResult 附带实际模型
- 主代理解析链新增 topic 级选择(最高优先级),session 级首命中时物化固化为话题选择
- 子代理解析链共享 helper:def frontmatter > 话题级 > session 级 > 全局基础配置
- TaskToolResult 携带子代理实际使用的 provider/model,供前端差异展示
2026-08-15 15:34:18 +08:00
oudecheng
414105d419 feat(model): 话题级模型选择持久化与 API(topics 表加 provider/model 列 + 双写内存缓存)
- topics 表迁移新增 provider/model 列,记录话题级显式模型选择
- storage 层新增 update_topic_model / list_topic_model_selections
- 新增 POST /api/topic/select-model 与 GET /api/topic/selected-model
- 双写 key 取 topic 行自带 session_id(不信任请求体,防污染其他会话)
- 清除时同步清 session 级选择,避免'重置为默认'被物化逻辑重新覆盖
2026-08-15 15:33:58 +08:00
oudecheng
fc3a95b152 feat: 异步子代理 v8 实现与 /stop 取消流程修复
后端核心实现:
- 新增 wait_coordinator:释放/重获取 serial_lock 的 in-tool waiting 模式,替代旧的 break-exit 方案
- 新增 wait_for_subagents 工具:select! 等待子代理完成/用户消息/超时,支持 try_drain 批量消费
- task 工具异步 spawn 路径:CancellationToken 注册表 + RAII guard + Semaphore 并发限流
- process exit 安全检查:pending 子代理存在时抑制 ExecutionCompleted
- 崩溃恢复:启动时标记 running->interrupted,history 加载时对账占位

/stop 取消流程修复:
- wait_coordinator select! 添加 cancel 分支,完整清理状态
- ToolContext 注入 cancel_rx(watch::Receiver clone)
- agent_loop 工具执行 select! 对 wait 工具跳过竞速,防止 drop coordinator 清理逻辑
- 子代理完成状态通过 execution_completed metadata 传播

存储层:
- pending_subagents 表 + 条件 UPDATE
- mark_all_running_as_interrupted 崩溃恢复
2026-08-13 22:12:20 +08:00
oudecheng
e0313ab8f3 fix: 增强 panic 安全与计算器健壮性
- agent_loop/processor: catch_unwind 隔离 panic,防止消息静默丢失
- calculator: 拒绝 NaN/Infinity 输入,修复阶乘溢出(上限 34),拒绝非有限表达式结果
- message: 修复 sanitize 两阶段删除索引未排序导致的越界 panic
- utils: 新增 panic_payload_message 提取可读 panic 消息
- .gitignore: 忽略 artifacts/ 测试产物目录
2026-08-13 08:38:36 +08:00
oudecheng
29060315a3 feat(observability): 端到端可观测性整改,修复 trace_id 断链与指标配对
- 传播 trace_id:BusToolCallEmitter/SubAgentEmitter/processor 全链路设置

- AgentEnd 配对:补发 5 个 cancel/defensive 路径,闭合 AgentStart 指标

- LLM 计时修正:attempt_start 移入 retry 循环,排除退避等待时间

- /metrics auth:非 loopback 部署时纳入 Bearer token 校验

- recorder 复用:OnceLock 缓存 PrometheusHandle,热重启后不再返回 503

- 结构化日志:新增 tracing_ctx + JSON 日志格式支持
2026-08-12 08:26:42 +08:00
oudecheng
7cb170e0c2 fix(gateway): 压缩期间释放 session 锁,修复新建话题列表刷新延迟
压缩任务在 LLM 调用(2-5s)期间持有 session 锁,阻塞 create_session
命令获取锁执行 switch_topic,导致新建话题后列表刷新延迟。

将压缩重构为 4 阶段:
1. 短暂持锁:读取 history/compressor/store/session_id/provider_config
2. 释放锁:LLM 压缩调用(2-5s)
3. 释放锁:DB 写入(store 为 Arc,DB 层自带事务保护)
4. 短暂持锁:reload 内存历史

并发安全:同 topic 由调用方的 per-topic serial lock 保证串行;
不同 topic 完全并行不受影响。
2026-08-12 08:24:46 +08:00
oudecheng
77a0eac2c8 fix(mcp): 修复前端空输入重置为不超时,增加超时路径单元测试
P3: 清空输入框时回退到默认值 300 而非 0;新增两个 tokio 测试覆盖有超时和无超时(error pass-through)路径
2026-08-11 23:07:00 +08:00
oudecheng
3e97ed903c feat(mcp): 为 MCP 工具调用增加超时保护,默认 5 分钟
在 McpToolWrapper 适配层用 tokio::time::timeout 包裹 call_tool,防止外部 MCP server 挂起导致 agent loop 无限阻塞。超时时间通过 config.mcp_tool_timeout_secs 配置(默认 300 秒,0=不超时),前端 McpTab 设置页提供输入框。
2026-08-11 21:53:07 +08:00
oudecheng
f26ec49182 chore: 修复 release 构建的编译器警告
- 为 regex::Captures 补全显式生命周期标注
- 将仅用于 debug 日志的局部变量 (media_count/len/total) 用 cfg(debug_assertions) 条件化
- 为仅 debug 读取的 AnthropicProvider::llm_timeout_secs 字段添加 cfg_attr allow(dead_code)
2026-08-07 14:36:27 +08:00
oudecheng
79d099a3bc chore(compaction): 调整压缩阈值默认值
工程化压缩阈值 threshold_ratio: 0.7 → 0.5(50%)
LLM 压缩阈值 llm_compaction_threshold_ratio: 0.5 → 0.3(30%)

降低触发门槛使压缩更积极介入,减少上下文溢出风险。
同步更新相关注释、日志信息和前端 hint 文案。
2026-08-07 11:53:54 +08:00
oudecheng
a9429a5657 fix(web): 输入框/发送按钮状态按话题隔离,避免处理中仍可发送
将单一全局 isLoading 布尔值重构为按 topic_id 跟踪的 processingTopicIds
集合,isLoading 派生自当前选中话题是否在集合中。导航响应不再清空处理
状态,切换话题后切回原话题仍能正确禁用输入。重连时通过新增的
/api/executions 端点对账后端权威执行状态,修正断连期间丢失的
execution_completed 信号导致的状态漂移。
2026-08-07 11:45:47 +08:00
oudecheng
feeb9d9c18 chore: 降低 list_todos 日志级别为 debug
前端每次 todo_write 后自动刷新会触发 list_todos 命令,子代理多次更新待办时产生大量 INFO 日志噪音。将 handler 与 gateway 转发两处日志降级为 debug,默认不输出,需要排查时通过 RUST_LOG=debug 开启。
2026-08-07 09:01:33 +08:00
oudecheng
e3e32c725d style: 对存储模块应用 rustfmt 格式化 2026-08-07 08:34:30 +08:00
oudecheng
510520e08e refactor: 提取公共工具函数,修复飞书正则重复编译和网关路由DRY问题,添加CI安全审计 2026-08-07 08:33:15 +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
fe7037e4ad fix(feishu,channel): 修正 reaction 语义并增容 bus 队列,避免丢最后一条消息
- feishu.rs: 仅终态消息(AssistantResponse | ErrorNotification)成功发送
  后才移除 reaction,ToolCall 等中间过程不再触碰;发送失败/空内容
  保留 reaction 作为异常信号,解决『reaction 被提前移除掩盖最终响应
  丢失』的语义错位问题
- manager.rs: MessageBus 容量从 100 扩至 256,降低多 topic 并发场景下
  bus 级 try_send 的瞬时积压丢弃概率

与前一提交的 outbound_dispatcher 双队列优先级重构配合:AssistantResponse
/ ErrorNotification 走独立 high 队列必达,终态消息送达后才移除 reaction,
彻底解决『飞书经常丢最后一条消息且 reaction 状态误导』的问题。
2026-08-06 10:08:36 +08:00
oudecheng
e037de88a2 fix(agent,gateway): 替换 unreachable!() 为防御性错误返回
将 run_final_summary 和 send_with_retry 中的 unreachable!() 替换为
错误返回,避免未来重构导致循环体未返回时 panic 终止所有会话。

- agent_loop.rs: run_final_summary 兜底构造错误 AgentProcessResult,
  含 emit_live_tool_call_message 推送,与 Err 不可重试路径一致
- outbound_dispatcher.rs: send_with_retry 兜底返回 ChannelError::SendError,
  覆盖 RETRY_DELAYS_SECS 为空数组的边界情况

同时 outbound_dispatcher.rs 包含优先级队列重构:高优消息使用独立
high 队列和扩展重试策略,保证最终响应不被中间过程挤占丢弃。
2026-08-06 10:01:02 +08:00
oudecheng
bf8c227634 refactor: 迁移 parking_lot 锁并优化阻塞 IO 与内存管理
## 锁迁移:std::sync → parking_lot
消除锁中毒(poison)导致的级联崩溃风险。parking_lot 锁不会中毒,
且性能更优。迁移覆盖全部生产代码:
- experts/mod.rs: 4 RwLock + 13 expect
- skills/mod.rs: 1 RwLock + 11 expect
- tools/registry.rs: 1 RwLock + 2 expect
- gateway/model_selection.rs: 1 RwLock + 2 expect
- tools/task/repository.rs: 1 RwLock + 4 unwrap
- tools/task/runtime.rs: 2 RwLock + 17 expect
- gateway/session.rs + task/runtime.rs: stream_message_id Mutex
- gateway/processor.rs: description_generation_in_flight Mutex
- command/handler.rs + help.rs: metadata Mutex(公开 API)
- mcp/client.rs: stderr_lines Mutex

测试代码中的 std::sync::Mutex(串行化锁 + TestObserver)有意保留,
已通过 unwrap_or_else(|err| err.into_inner()) 做中毒恢复。

## P1: 阻塞 IO 迁移到 spawn_blocking
将 3 处阻塞 async worker 的操作迁移到 blocking 线程池:
- file_read.rs: read_to_string + 行处理 + base64 编码整体包入 spawn_blocking
- agent_loop.rs: 新增 preencode_images_for_request 两阶段预编码
  (顺序分配预算 → 并行 spawn_blocking 编码),build_llm_request 改为 async
- wechat.rs: media_to_send_content 改为 async,std::fs::read 用 spawn_blocking 包裹

## P2: session_history topic_histories 内存上限
新增 MAX_CACHED_TOPICS=32 上限和 evict_inactive_if_needed 方法。
超限时驱逐非活跃 topic(不在 chat_topic_ids、不在 compression_in_flight、
serial_lock 未被持有)。活跃 topic 永不误驱逐。
remove_history 同步清理 topic_serial_locks,防止无限增长。

## P3: 减少 panic 面
agent_loop.rs retry 循环的 response.expect(...) 改为 ok_or_else(...)?
返回 AgentError::Other,逻辑 bug 不再导致整个 agent 崩溃。

## 对抗性审查修复
- preencode_images_for_request: 用 seen HashSet 去重,防止同 path 重复
  编码导致 HashMap entry 覆盖(NoBudget 覆盖 Encoded 等)
- evict_inactive_if_needed: 检查 topic_serial_lock.try_lock(),防止驱逐
  正在 agent 处理中的 topic(original_topic_id 不在 chat_topic_ids 但
  agent 仍持锁)
- remove_history: 清理 topic_serial_locks

## 验证
- cargo check: 通过(仅既有 lifetime 警告)
- cargo test: 559 passed / 3 failed(均为环境/sandbox 权限问题,与本次改动无关)
2026-08-06 08:18:04 +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
6252b600cd refactor(compression): 重构上下文压缩为两阶段+双阈值+三段保留
压缩时机从 process 整体返回后提前到每轮工具调用完成后,更及时控制上下文大小。

两阶段压缩:
- 工程化压缩(70% 阈值触发):截断非子代理 tool 结果到 100 token,仅改内存不影响 DB
- LLM 压缩(50% 阈值触发):工程化压缩后仍超 50% 才调 LLM,避免不必要的 LLM 调用

LLM 压缩三段保留策略:
- 保留最旧 5 个 unit 和最新 5 个 unit 原样
- 中间段用 LLM 生成摘要(heavy prompt)
- SystemGuard 永远保留不计入配额
- ToolRound 原子性由 parse_to_units 保证,切分在 unit 边界

工程化压缩:
- truncate_tool_results_in_place 截断 role="tool" 且 tool_name!="task" 的消息
- 子代理返回(tool_name="task")保持原样
- char_indices 确保 UTF-8 字符边界安全,截断后追加 "...[已截断]" 标记

AgentLoop 集成:
- 新增 CompactionSink trait,LLM 压缩结果通过 sink 回写 DB
- AgentProcessResult 新增 compaction_performed 和 engineering_compaction_applied 标记
- finalize_result 据此决定是否 reload DB 历史(避免重复 append)和跳过兜底压缩

错误恢复:
- LLM 压缩失败降级为仅工程化压缩,不中断 agent loop
- sink.compact 失败记 error 日志继续使用内存压缩结果,DB 下次 process 会重新触发压缩

清理旧的 two-segment 差异化压缩代码(OLDER_BUDGET_RATIO、find_safe_split_point、
build_light_summary_prompt 等),新增 unit_to_messages 辅助函数。
2026-08-05 21:56:41 +08:00
oudecheng
c790ee1609 fix(gateway): 修复已完成子智能体任务卡在"子智能体正在执行..."
TaskStarted 补发逻辑改为基于 tool_result 存在性判断(替代 state==Running),
并在补发事件中携带 tool_call_id 以实现前端精确匹配。

- TaskSession 新增 tool_call_id 字段,runtime.spawn 时从 parent_context 注入
- send_topic_history/send_task_messages 收集历史中已有 tool_result 的 tool_call_id
  集合,仅对没有 tool_result 的任务补发 TaskStarted(不论 Running/Completed)
- 补发时传入 task.tool_call_id,前端可精确匹配 task tool_call 卡片,
  避免多并行子智能体场景下 fallback 串扰
- 移除未使用的 TaskSessionState 导入

修复场景:父智能体启动多个并行子智能体,其中一个完成但另一个仍在运行时
重载 topic,已完成的子智能体因 state!=Running 被跳过,导致其 tool_call
卡片永远收不到 TaskStarted,卡在"子智能体正在执行..."无法变为"查看实时进度"。
2026-08-05 17:34:03 +08:00
oudecheng
81fbee4878 feat(storage): 历史压缩保留原消息,前端展示完整对话、LLM 用压缩摘要
将压缩流程从物理删除原消息改为标记位 is_compacted 分离两个视图:
- LLM 视角(load_messages_for_topic):只看 is_compacted=0 的压缩摘要+新消息
- UI 视角(load_messages_for_topic_full):看原消息(含 is_compacted=1)+新消息,排除摘要

新增 compact_topic_history:不删除原消息,仅打标记+插入摘要,
同事务内删除旧摘要避免累积。修复 token 统计因压缩丢失 usage 的问题。
2026-08-05 12:46:43 +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
6fc6d5628f perf(gateway): send_with_retry 对 ChannelFull 不重试,加速队列消费
ws 连接队列满时,sender task 原本重试 3 次浪费 1+2+4=7s,期间 channel 队列堆积。新增 ChannelError::ChannelFull 变体区分队列满错误,CliChannel::send 在 try_send Full 时返回该变体;send_with_retry 遇到 ChannelFull 立即返回不重试,sender task 记 warn(预期背压丢弃)而非 error。

- base.rs: 新增 ChannelFull 变体及 Display 实现
- cli.rs: try_send 错误区分 Full(ChannelFull,不可重试)与 Closed(SendError,可重试)
- outbound_dispatcher.rs: send_with_retry 短路返回 ChannelFull;run_sender_task 区分日志级别;新增 TestChannel.with_channel_full 及单测验证不重试(500ms 阈值内返回 + send 仅调用一次)
2026-08-05 11:14:53 +08:00
oudecheng
deab3699fe fix(web): token 统计按 topic_id 聚合,避免同 session 多 topic 共享总和
batch_session_token_stats 重命名为 batch_topic_token_stats,SQL 从 GROUP BY session_id 改为 GROUP BY topic_id,修复同一 session 下多个 topic 显示相同 token 总和的问题。

子代理天然隔离:子代理消息的 topic_id 属于子代理自身的 topic,不在主 topic 列表中。topic_id IN (...) 自动排除 NULL topic_id 的旧消息。

对抗性审查补充:latest assistant 消息查询的 JOIN 条件 m.topic_id = latest.topic_id AND m.seq = latest.max_seq 可能匹配到子代理消息(seq 是 session 级递增,主/子 session 可能有相同 seq)。在外层 JOIN 添加 WHERE m.session_id NOT LIKE 'sub:%',避免重复行导致 stats.entry(tid) 被覆盖、结果不确定。
2026-08-05 09:43:59 +08:00
oudecheng
2255d2e774 fix(tools): 修复 shell 工具超时未生效
根因 A:子进程派生守护进程(如 adb daemon)继承 stdout 管道写端,父进程退出后 EOF 永不到达,child.wait() 分支进入无界 drain 循环,select! 已退出导致 deadline 失效,永久阻塞。

根因 B:deadline 分支调用 is_process_waiting_on_stdin 误判 socket 等待为 stdin 等待,且 should_return_pending 关键词过宽,绕过硬 kill 转 pending 会话,表面未超时实则进程未终止。

修复:- 新增 STREAM_DRAIN_MS=1000,用 tokio::time::timeout 包裹 drain 循环 - 新增 kill_and_reap:start_kill → 5s wait 超时 → kill().await + 3s 兜底 - 收集 read_tasks JoinHandles,返回前 abort 避免任务泄漏 - deadline 分支移除绕过逻辑,一律硬 kill 并返回超时错误 - timeout 参数改用 .clamp(1, MAX_TIMEOUT_SECS) 防御 timeout:0
2026-08-05 09:42:53 +08:00
oudecheng
1e9075e1ed fix(subagent): 失败/超时子智能体返回结构化结果,支持前端点击查看详情
spawn()/resume() 在会话创建后的失败(含超时、执行错误、AgentCreationFailed)
现返回 Ok(TaskToolResult { status: "failed"/"timeout", task_id }) 而非 Err(e),
使 tool_result 内容始终为结构化 JSON。前端已有的 taskStatusConfig、StatusIcon、
"查看完整会话"按钮自动生效,同时修复服务重启后点击入口丢失的问题。

- runtime.rs: 提取 handle_task_failure 辅助方法统一处理失败;将 model_resolver/
  create_subagent 的 ? 纳入 result 块,确保所有会话后失败都返回结构化结果;
  修复 resume() 未区分 timeout 的 bug
- tool.rs: 根据 task_result.status 判断 success,失败时 output 仍为 JSON
- agent_loop.rs: 失败时 result.output 非空则透传(仅影响 task 工具),其他工具回退
  到 "Error: {error}" 纯文本
2026-08-05 09:05:35 +08:00
oudecheng
5eec77ae10 perf: 减少热路径无谓拷贝与重复正则编译,并跳过已完成迁移
- agent_loop::filter_images_by_age_and_count 改返回 Cow<[ChatMessage]>,
  无图片或无需过滤时零拷贝返回原切片引用,避免每次 LLM 调用都完整克隆消息列表
- anthropic::convert_image_url_to_anthropic 用 OnceLock 缓存 data URL 正则,
  避免每次图片转换都重新编译正则
- memory_scope_key 迁移用 PRAGMA user_version 记录已迁移版本,
  避免每次启动都执行全表 DELETE + UPDATE
2026-08-05 08:10:36 +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
07247ed140 perf(storage): get_topic_message_count 改用 COUNT(*) 查询
原实现调用 load_messages_for_topic 加载该 topic 的所有消息
(含 content、tool_calls_json 等大字段反序列化)到内存,
仅为调用 .len() 计数。长会话可能加载数千条消息,且在
list_sessions 列出所有 topic 时会按 topic 数量倍增。

改为 SELECT COUNT(*) FROM messages WHERE topic_id = ?1,
在数据库侧计数:无需反序列化任何字段,命中已有的
idx_messages_topic_seq(topic_id, seq) 索引。

对 N 条消息的 topic,从 O(N) 内存 + 反序列化降为 O(1)
内存 + 索引扫描。新增测试覆盖计数正确性与隔离性。
2026-08-04 15:18:21 +08:00
oudecheng
924017fe7b fix(web_fetch): 加固 SSRF 防护,对齐 http_request 实现
修复 extract_host 的 4 处 host 解析缺口,使 web_fetch 与
http_request 的内网拦截策略完全一致:

- 拒绝 userinfo(authority 中的 "@"):防止 http://evil.com@127.0.0.1/
  以 evil.com 作为表面 host 绕过检查,实际请求发往 127.0.0.1
- 拒绝 IPv6 字面量(authority 以 "[" 开头):防止 http://[::1]/
  被拆分为 "[" 导致 IpAddr::parse 失败而放行
- trim_end_matches('.') 规范化末尾点:防止 http://127.0.0.1./
  因标准库解析失败绕过检查(reqwest 视为 FQDN 解析为 loopback)
- is_private_host 补充 is_broadcast/is_multicast:拦截
  255.255.255.255 与 224.0.0.1 等广播/组播地址

新增 5 个针对性测试覆盖以上场景。
2026-08-04 15:17:52 +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
1e2d64e28b fix(web_fetch): 禁用 HTTP 重定向,防止 SSRF 重定向绕过
validate_url 只校验初始 URL 的 host,若跟随 302 跳转,攻击者可用公网 URL
重定向到 169.254.169.254(云元数据)或 127.0.0.1 等内网地址,绕过 is_private_host
的 SSRF 防护。与 http_request 工具保持一致,使用 redirect::Policy::none()。
2026-08-04 08:25:57 +08:00
oudecheng
cd314742ad fix(test): 修复 anthropic 错误链嵌套测试为真正的 source 链
对抗性审查发现 test_format_error_chain_nested 是无效测试:
声称测嵌套错误链但实际没构造 source 链,inner 变量被 let _ 抑制。
改用 thiserror 构造真正的 #[source] 嵌套错误,验证 'caused by' 拼接。
2026-08-03 23:47:43 +08:00