审核机构:门下省
审核日期:2026-03-19
审核范围:6 大代码变更方向,24 个新增/修改文件
审核基准:/workspace/xbot/plan/xbot-major-refactor-design.md (v3)
| 检查项 | 结果 |
|---|
go build ./... | ✅ 全量通过 |
go vet ./... | ✅ 全量通过 |
go test ./... | ✅ 全量通过 |
所有包编译、静态分析、测试均无错误。
方向 1:Channel 特化 Prompt 隔离
涉及文件:agent/channel_prompt.go、agent/channel_prompt_test.go、agent/middleware.go、agent/agent.go、channel/feishu.go、main.go
| 检查点 | 结论 |
|---|
ChannelPromptProvider 接口定义在 agent 包内 | ✅ 正确,不依赖 channel 包 |
ChannelPromptMiddleware 实现 MessageMiddleware 接口 | ✅ 优先级 50,插入在基础 prompt (0) 和 skills/agents (100+) 之间 |
feishuPromptAdapter 桥接模式(main.go 底部) | ✅ 正确避免 agent ↔ channel 循环依赖 |
main.go 已注入 SetChannelPromptProviders | ✅ line 154,仅在 feishuCh != nil 时注入 |
FeishuChannel.ChannelSystemParts() 返回 map[string]string | ✅ 与 SystemParts key-value 模式一致 |
| 检查点 | 结论 |
|---|
| 测试覆盖:mock provider + middleware 行为验证 | ✅ 3 个测试用例覆盖匹配/不匹配/空 parts |
ChannelSystemParts key 命名规范 | ✅ 使用 "00_feishu_rules" 前缀,符合 BuildSystemPrompt 字典序约定 |
| 依赖路径 | 结论 |
|---|
agent → channel | ❌ 无直接依赖(通过 feishuPromptAdapter 在 main.go 桥接) |
channel → agent | ❌ 无依赖 |
channel → bus | ✅ channel 只依赖 bus(OK) |
涉及文件:llm/types.go、llm/anthropic.go、llm/anthropic_test.go、agent/middleware.go
| 检查点 | 结论 |
|---|
ChatMessage.CacheHint 字段 (llm/types.go:31) | ✅ "static" / "" 两值语义清晰 |
Assemble() 设置 sysMsg.CacheHint = "static" (middleware.go:150) | ✅ system prompt 始终标记为可缓存 |
buildAnthropicSystem() 处理 CacheHint (anthropic.go) | ✅ static → cache_control: {"type": "ephemeral"},非 static 不标注 |
cache_control 格式符合 Anthropic API 规范 | ✅ {"type": "ephemeral"} 为官方标准格式 |
| 单条无缓存 system 返回 string(非 array) | ✅ 向后兼容,不破坏旧 API |
| 多条或含缓存标注时返回 array | ✅ 符合 Anthropic multi-block system 格式 |
| 检查点 | 结论 |
|---|
测试覆盖 (anthropic_test.go) | ✅ 5 个测试:无系统消息 / 单条无缓存 / 静态缓存 / 混合 / 多条动态 |
| OpenAI provider 不处理 CacheHint | ✅ openai.go 中无 CacheHint 引用,OpenAI 不支持 prompt caching(正确忽略) |
| 风险点 | 评估 |
|---|
| 非缓存 system 仍可被 Anthropic KV-cache | ✅ 无风险,KV-cache 是隐式的 |
| 缓存断开后首次请求额外延迟 | ✅ Anthropic 文档已说明,设计预期内 |
| CacheHint 字段序列化到 session | ✅ json:"cache_hint,omitempty" — 为空时不序列化,不污染持久化数据 |
涉及文件:agent/progress.go、agent/progress_test.go、agent/engine.go、agent/reply.go、agent/reply_test.go
| 检查点 | 结论 |
|---|
ProgressEvent / StructuredProgress / ToolProgress / ToolStatus 类型定义 | ✅ 结构合理,常量定义清晰(PhaseThinking/PhaseToolExec) |
RunConfig.ProgressEventHandler 字段 (engine.go:77) | ✅ 回调签名 func(event *ProgressEvent) |
Run() 中 structuredProgress 初始化与生命周期管理 | ✅ 仅在 ProgressEventHandler != nil 时创建 |
工具执行时状态流转 ToolPending → ToolRunning → ToolDone/ToolError | ✅ 每个工具独立跟踪,含耗时记录 |
ProgressNotifier 与 ProgressEventHandler 并存 | ✅ 旧回调保持不变,新回调是增强(非替代) |
| 检查点 | 结论 |
|---|
测试覆盖 (progress_test.go) | ✅ 验证事件创建、字段赋值、零值安全 |
| 迭代计数更新 | ✅ 每轮循环更新 structuredProgress.Iteration |
| 思考内容记录 | ✅ structuredProgress.ThinkingContent = cleanContent |
| 完成工具归档 | ✅ CompletedTools = append(CompletedTools, ActiveTools...) 后清空 |
| 项 | 说明 |
|---|
ProgressEventHandler 尚未被上层消费 | 当前 main.go 未注入 ProgressEventHandler,飞书卡片进度渲染需后续接入 |
| 检查点 | 结论 |
|---|
| 短内容(< 500 字符)原样返回 | ✅ |
| 长内容取最后一段 | ✅ |
| 最后一段过短(< 50 字符)时合并倒数两段 | ✅ |
| 检查点 | 结论 |
|---|
测试覆盖 (reply_test.go) | ✅ 3 个用例:短内容 / 正常长内容 / 末段过短 |
| 项 | 严重度 | 说明 |
|---|
| 死代码 | ⚠️ 中 | ExtractFinalReply 仅在测试中被调用,无任何生产代码引用。函数定义合理但尚未集成 |
方向 4:全局 Skill/Agent Registry
涉及文件:agent/registry.go、agent/command_builtin.go、storage/sqlite/registry.go、storage/sqlite/registry_test.go、storage/sqlite/db.go
| 检查点 | 结论 |
|---|
RegistryManager 结构体与 CRUD 方法 | ✅ Publish / Unpublish / Browse / Install / Uninstall / MyEntries 完整 |
SharedSkillRegistry 存储层 (storage/sqlite/registry.go) | ✅ 标准 SQLite CRUD,SQL 注入安全(参数化查询) |
type 约束 CHECK(type IN ('skill', 'agent')) | ✅ 数据库层防护 |
sharing 约束 CHECK(sharing IN ('private', 'public')) | ✅ |
v12 → v13 migration (db.go) | ✅ CREATE TABLE IF NOT EXISTS 幂等,含索引 |
| 新建数据库 schema 直接包含 v13 表 | ✅ createSchema() 中已包含 shared_registry 和 user_settings |
| 检查点 | 结论 |
|---|
测试覆盖 (registry_test.go) | ✅ CRUD + 边界(重复发布、未找到、类型过滤) |
内置命令注册 (command_builtin.go) | ✅ /publish、/unpublish、/browse、/install、/uninstall、/my 均已注册 |
nil 保护 (command_builtin.go:265) | ✅ a.registryManager == nil 检查后返回提示 |
| 项 | 严重度 | 说明 |
|---|
| RegistryManager 未注入 | 🔴 高 | Agent.registryManager 字段已声明(agent.go:254)但无公开 setter、main.go 未注入。/publish 等命令运行时会因 nil 而静默失败 |
| 缺少 setter 方法 | 🔴 高 | Agent 没有 SetRegistryManager(*RegistryManager) 方法,外部无法注入 |
| Unpublish 权限校验 | ⚠️ 低 | 仅检查 entry.Author == senderID,未校验 entry.Type 与请求类型匹配(但 data layer 已有 type 过滤) |
修改意见:
- 在
agent/agent.go 添加 SetRegistryManager(rm *RegistryManager) 方法 - 在
main.go 初始化 RegistryManager 并注入到 Agent - 注入时机:在
agentLoop := agent.New(...) 之后、Run() 之前
方向 5:Channel Settings Capability
涉及文件:channel/capability.go、channel/capability_test.go、agent/settings.go、agent/settings_test.go、storage/sqlite/user_settings.go、agent/command_builtin.go、channel/feishu.go
| 检查点 | 结论 |
|---|
SettingsCapability 接口 (channel/capability.go) | ✅ SettingsSchema() + HandleSettingSubmit() + BuildSettingsUI() + BuildProgressUI() |
UIBuilder 接口 | ✅ 与 SettingsCapability 组合使用 |
SettingDefinition 结构体 | ✅ key / label / type / options / required / default 字段完整 |
SettingsService (agent/settings.go) | ✅ 封装 UserSettingsService,增加 channel 层 UI 能力查询 |
UserSettingsService (storage/sqlite/user_settings.go) | ✅ 标准 CRUD,UNIQUE(channel, sender_id, key) |
FeishuChannel 实现 SettingsCapability | ✅ 4 个方法均已实现 |
内置命令 /set、/menu (command_builtin.go) | ✅ 已注册,nil 保护完备 |
user_settings 表 v13 migration | ✅ 幂等,含索引 |
| 项 | 严重度 | 说明 |
|---|
| SettingsService 未注入 | 🔴 高 | Agent.settingsSvc 字段已声明(agent.go:257)但无公开 setter、main.go 未注入。/set、/menu 命令运行时因 nil 静默失败 |
agent/settings.go import “xbot/channel” | ⚠️ 中 | 虽然 channel 不反向依赖 agent(无循环),但 agent 包直接依赖 channel 包打破了既有的分层架构。目前通过 import 只用了 SettingsCapability 接口和 UIBuilder 接口。建议:将这两个接口提取到独立包(如 channel/types 或 channel/capability 已在此包中,但 agent 仍直接引用 channel 包) |
| 依赖路径 | 状态 |
|---|
agent → channel | ⚠️ 新增(settings.go) |
channel → agent | ❌ 无 |
| 结论 | 当前无循环依赖,但 agent → channel 是架构上的方向反转 |
修改意见:
- 在
agent/agent.go 添加 SetSettingsService(svc *SettingsService) 方法 - 在
main.go 初始化 SettingsService 并注入到 Agent - (架构建议)考虑将
SettingsCapability / UIBuilder 接口从 channel 包提取为独立接口包,避免 agent → channel 的直接依赖
涉及文件:llm/types.go、llm/anthropic.go、llm/anthropic_test.go、llm/openai.go
已在方向 2 中一并审核。补充:
| 检查点 | 结论 |
|---|
ChatMessage 新增 CacheHint 字段 | ✅ json:"cache_hint,omitempty" — 空值不序列化,向后兼容 |
TokenUsage.Add() 方法 | ✅ 值类型语义正确 |
ToolCallDelta 新增 ID 和 Name 字段 | ✅ 支持流式工具调用增量 |
StreamEventType 枚举扩展 | ✅ 含 EventReasoningContent 支持 DeepSeek 推理模型 |
| # | 问题 | 影响范围 | 修复方案 |
|---|
| 1 | RegistryManager 无 setter、main.go 未注入 | /publish、/unpublish、/browse、/install、/uninstall、/my 6 个命令全部静默失败 | 添加 setter + main.go 注入 |
| 2 | SettingsService 无 setter、main.go 未注入 | /set、/menu 2 个命令全部静默失败 | 添加 setter + main.go 注入 |
| # | 问题 | 建议 |
|---|
| 3 | ExtractFinalReply 为死代码(无生产调用点) | 尽快集成到 reply 发送路径,或添加 // Planned: ... 注释说明意图 |
| 4 | ProgressEventHandler 未被 main.go 消费 | 飞书卡片进度渲染的接入需排入后续迭代 |
| 5 | agent/settings.go 直接 import channel 包 | 考虑提取接口到独立包,维持分层清晰 |
| 6 | Unpublish 缺少 type 匹配校验 | 建议在 Service 层增加校验(低优先级) |
| 项 | 结论 |
|---|
| 循环依赖 | ✅ 无循环(agent ↔ channel 通过 main.go 桥接,settings.go 的新依赖是单向的) |
| Go 接口惯用法 | ✅ 小接口(1-3 方法)、显式接口满足、nil 安全 |
| 向后兼容 | ✅ CacheHint omitempty、schema migration 幂等、旧 ProgressNotifier 保持不变 |
| 测试覆盖 | ✅ 每个新增模块均有对应测试文件,覆盖正常路径和边界条件 |
| 并发安全 | ✅ MessagePipeline 读写锁、Agent 字段初始化在 Run() 前 |
方向 1-3、6 实现质量优秀,代码风格一致,架构设计合理。但方向 4 和 5 存在同一类系统性遗漏:新增的服务字段(registryManager、settingsSvc)在 Agent struct 中声明、在 command_builtin.go 中消费,但缺少公开 setter 方法且 main.go 未注入初始化代码。这导致 8 个新增命令(/publish、/unpublish、/browse、/install、/uninstall、/my、/set、/menu)在运行时全部因 nil check 而静默返回提示信息,功能完全不生效。
agent/agent.go — 添加两个 setter:
func (a *Agent) SetRegistryManager(rm *RegistryManager) { a.registryManager = rm }
func (a *Agent) SetSettingsService(svc *SettingsService) { a.settingsSvc = svc }
main.go — 在 agentLoop := agent.New(...) 之后添加初始化:
// 注入 RegistryManager
sharedDB, _ := sqlite.Open(dbPath) // 复用已有连接
agentLoop.SetRegistryManager(agent.NewRegistryManager(sharedDB, xbotDir))
// 注入 SettingsService
agentLoop.SetSettingsService(agent.NewSettingsService(
sqlite.NewUserSettingsService(sharedDB),
disp, // channel.Dispatcher 用于查询 SettingsCapability
))
修复完成后可免审提交(门下省确认修改范围可控)。
门下省 · 审核完毕