审核方:门下省
日期:2026-03-20
审核范围:/settings、/menu 命令,SettingsService,SettingsCapability/UIBuilder 接口,FeishuChannel 实现
构建验证:go build ./... ✅ | go vet ./... ✅ | go test ./agent/ ./channel/ ✅
| # | 严重程度 | 文件 | 问题描述 |
|---|---|---|---|
| 1 | 🔴 P0 | command_builtin.go | channelFinder 为 nil 时 /settings panic |
| 2 | 🔴 P0 | command_builtin.go | /settings set 无 schema 校验,任意 key/value 均可写入 |
| 3 | 🟡 P1 | command_builtin.go | settingsCmd.Concurrent() 对写操作返回 true,违反项目惯例 |
| 4 | 🟡 P1 | command_builtin.go | /settings set 命令 value 被全部 lowercased,破坏大小写敏感值 |
| 5 | 🟡 P1 | command_builtin.go | help 文本未包含 /settings、/menu、/browse 等新命令 |
| 6 | 🟡 P1 | settings.go | SubmitSettings() 方法已定义但无调用方,死代码 |
| 7 | 🟢 P2 | settings.go | agent 包直接 import xbot/channel,违背 channelFinder 依赖反转设计 |
| 8 | 🟢 P2 | feishu.go | BuildSettingsUI() 与 BuildTextSettingsUI() 大量逻辑重复 |
| 9 | 🟢 P2 | feishu.go | HandleSettingSubmit 不校验 value 是否合法(select 类型范围、toggle 布尔值) |
| 10 | 🟢 P2 | settings.go | GetSettingsUI 返回 “当前渠道没有可配置的设置项” 与 “当前渠道不支持设置” 语义模糊 |
文件:agent/command_builtin.go L436-439
现状:
func (c *settingsCmd) Execute(ctx context.Context, a *Agent, msg bus.InboundMessage) (*bus.OutboundMessage, error) {
// ...
ch, ok := a.channelFinder(msg.Channel) // ← 若 channelFinder == nil → panic
if !ok {
return &bus.OutboundMessage{...Content: "当前渠道不支持设置"}, nil
}
Agent.channelFinder 是 func(string) (channel.Channel, bool) 类型,默认值为 nil。虽然 main.go 中无条件调用了 SetChannelFinder(disp.GetChannel),但:
- 测试环境若不设
channelFinder,任何/settings触发即 panic - 未来若有代码路径创建 Agent 但未注入 finder,同样 panic
修复建议:
if a.channelFinder == nil {
return &bus.OutboundMessage{Channel: msg.Channel, ChatID: msg.ChatID,
Content: "SettingsService: channelFinder 未注入"}, nil
}
影响:运行时 panic,服务不可用。
文件:agent/command_builtin.go L427-435
现状:
if strings.HasPrefix(args, "set ") {
setParts := strings.Fields(strings.TrimPrefix(args, "set "))
if len(setParts) < 2 {
return ... "用法:...", nil
}
key := setParts[0]
value := strings.Join(setParts[1:], " ")
err := a.settingsSvc.SetSetting(msg.Channel, msg.SenderID, key, value)
SetSetting 直接写入数据库,不检查:
- key 是否在
SettingsSchema()中定义 - value 是否符合 schema 约束(select 类型只能从 Options 中选,toggle 只能 true/false)
- value 类型是否匹配 SettingType(number 类型应验证数字)
用户可执行 /settings set nonexistent_key anything,数据被写入但永远不生效。
修复建议:
/settings set 应先获取 channel 的 schema,校验 key 存在性和 value 合法性:
if strings.HasPrefix(args, "set ") {
setParts := strings.Fields(strings.TrimPrefix(args, "set "))
if len(setParts) < 2 {
return ... "用法:...", nil
}
key := setParts[0]
value := strings.Join(setParts[1:], " ")
// Schema 校验
ch, ok := a.channelFinder(msg.Channel)
if !ok {
return ... "当前渠道不支持设置", nil
}
if err := a.settingsSvc.ValidateSetting(ch, key, value); err != nil {
return ... fmt.Sprintf("设置无效:%v", err), nil
}
err := a.settingsSvc.SetSetting(msg.Channel, msg.SenderID, key, value)
在 SettingsService 中新增 ValidateSetting:
func (s *SettingsService) ValidateSetting(ch channel.Channel, key, value string) error {
sc, ok := ch.(channel.SettingsCapability)
if !ok {
return fmt.Errorf("当前渠道不支持设置")
}
for _, def := range sc.SettingsSchema() {
if def.Key != key { continue }
switch def.Type {
case channel.SettingTypeSelect:
for _, opt := range def.Options {
if opt.Value == value { return nil }
}
return fmt.Errorf("无效值 %q,可选: %v", value, def.Options)
case channel.SettingTypeToggle:
if value != "true" && value != "false" {
return fmt.Errorf("toggle 类型只接受 true 或 false")
}
case channel.SettingTypeNumber:
if _, err := strconv.ParseFloat(value, 64); err != nil {
return fmt.Errorf("number 类型需要数字值")
}
}
return nil
}
return fmt.Errorf("未知设置项: %s", key)
}
影响:数据污染,用户困惑。
文件:agent/command_builtin.go L415
现状:
func (c *settingsCmd) Concurrent() bool { return true }
项目中 /settings set 会执行数据库写入(SetSetting)。对比项目中其他写命令:
| 命令 | 操作 | Concurrent() |
|---|---|---|
/new | 清空 session | false |
/set-llm | 写 LLM 配置 | false |
/compress | 压缩 session | false |
/install | 安装 skill/agent | false |
/settings set | 写用户设置 | true ← 不一致 |
修复建议:拆分为两个命令,或将整个 settingsCmd 改为 Concurrent() = false。
推荐拆分:
settingsListCmd→Concurrent() = true(只读)settingsSetCmd→Concurrent() = false(写操作)
影响:并发下可能产生不可预期的写入顺序,虽然 SQLite 序列化写入不会数据损坏,但与项目惯例不一致。
文件:agent/command_builtin.go L424-425
现状:
args := strings.TrimPrefix(strings.ToLower(content), "/settings ")
content 被 ToLower() 后,/settings set language English 的 value 变成 "english"。
当前 schema 中的 option value 全部是小写("zh", "en", "ja", "concise", "detailed"),所以暂时无影响。但一旦有大小写敏感的设置项(如用户自定义的名称、API endpoint),此行为会导致数据错误。
修复建议:
content := strings.TrimSpace(msg.Content)
// 只对命令部分做大小写不敏感匹配,保留原始 value
lower := strings.ToLower(content)
args := strings.TrimPrefix(strings.TrimSpace(strings.TrimPrefix(lower, "/settings ")), "")
但更简洁的做法是在解析 key 时 lowercased,value 保留原文:
if strings.HasPrefix(args, "set ") {
rest := strings.TrimPrefix(content, "/settings ") // 原始 content
rest = strings.TrimSpace(rest)
rest = strings.TrimPrefix(rest, "set ") // 或 "SET "
setParts := strings.Fields(rest)
key := strings.ToLower(setParts[0]) // key 小写匹配
value := strings.Join(setParts[1:], " ") // value 保留原文
影响:当前 schema 无大小写敏感值,暂无实际影响,但属于定时炸弹。
文件:agent/command_builtin.go L55-68
现状:/help 输出中无以下命令:
/settings— 个人设置/menu— 主菜单/browse— 浏览市场/publish— 发布/install— 安装/uninstall— 卸载/my— 我的 skills/agents
修复建议:在 help 文本中追加这些命令说明。
文件:agent/settings.go L60-96
SubmitSettings 方法实现了完整的 schema-aware 提交流程(委托 SettingsCapability.HandleSettingSubmit 或解析 key=value 文本),但没有任何命令或代码路径调用它。
/settings set 命令绕过了 SubmitSettings,直接调用 SetSetting。
修复建议:
方案 A(推荐):/settings set 命令改为调用 SubmitSettings,并补充 schema 校验逻辑到 SubmitSettings 内部。
方案 B:删除 SubmitSettings,将其校验逻辑合并到 SetSetting 或新建的 ValidateSetting 中。
文件:agent/settings.go L5
现状:
import (
"xbot/channel"
"xbot/storage/sqlite"
)
GetSettingsUI 方法接收 channel.Channel 接口并对其做 type assertion。项目在 main.go 中刻意使用 channelFinder 回调避免 agent → channel 的直接依赖。settings.go 打破了这个隔离层。
风险:如果未来 channel 包需要引用 agent 包的任何类型,将形成循环依赖。
修复建议:
将 channel.Channel 参数改为使用 channelFinder 回调获取的 channel.Channel,但保持接口在 agent 包内定义(SettingsUIBuilder 接口),由 main.go 的适配器桥接。或者接受当前状态但加注释标注风险。
当前项目结构下循环依赖概率低,标记为 P2。
文件:channel/feishu.go L858-908 vs channel/capability.go L49-84
两个函数实现几乎相同的逻辑(按 category 分组、渲染 label/value/options),仅格式细节不同:
| 特性 | BuildTextSettingsUI | FeishuChannel.BuildSettingsUI |
|---|---|---|
| 标题 | # ⚙️ 设置 | ## ⚙️ 设置 |
| 选项展示 | `value` | ✓ Label / Label |
| 末尾提示 | ---\n使用 /settings set... | ---\n使用 /settings set... |
修复建议:
抽取公共渲染逻辑到 channel 包的辅助函数(如 renderSettingsMarkdown(schema, values, optionRenderer)),让 FeishuChannel.BuildSettingsUI 调用它,仅传入自定义的 option 渲染器。
文件:channel/feishu.go L834-856
现状:HandleSettingSubmit 只做 JSON 解析和 key=value 文本解析,不校验 value 是否在 schema Options 范围内。
由于此方法当前为死代码(P1-6),实际暂无影响。但一旦被启用,同样存在 P0-2 的 schema 绕过问题。
修复建议:HandleSettingSubmit 内部应引用 SettingsSchema() 进行校验。
文件:agent/command_builtin.go L437-439 vs agent/settings.go L47
两处返回不同的"不支持"提示:
channelFinder找不到 channel →"当前渠道不支持设置"- channel 存在但无
SettingsCapability→"当前渠道没有可配置的设置项"
从用户视角,两者含义相同(设置不可用),但表述不一致。
修复建议:统一措辞为 "当前渠道不支持设置"。
| 阶段 | 任务 | 工作量 |
|---|---|---|
| 立即修复 | P0-1 nil guard、P0-2 schema 校验 | ~1h |
| 本轮跟进 | P1-3 拆分命令、P1-4 value 大小写、P1-5 help 文本、P1-6 死代码清理 | ~1h |
| 后续优化 | P2-7 依赖隔离、P2-8 去重、P2-9 HandleSettingSubmit 校验、P2-10 措辞统一 | ~2h |
- 接口设计清晰:
SettingsCapability+UIBuilder分离关注点,channel 可按需实现 - 向后兼容好:不实现
SettingsCapability的 channel 不会出错,有优雅降级 - 测试覆盖:
GetSettings、GetSettingsUI、SubmitSettings均有单元测试 BuildTextSettingsUI作为 fallback 设计合理,避免强制所有 channel 实现自定义 UIchannelFinder回调模式(main.go 中注入)方向正确,仅settings.go未遵循
门下省 · 审核完毕