Skip to main content
xbot
Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Back to homepage
Edit page

/settings 命令全链路审核报告

审核方:门下省
日期:2026-03-20
审核范围/settings/menu 命令,SettingsServiceSettingsCapability/UIBuilder 接口,FeishuChannel 实现
构建验证go build ./... ✅ | go vet ./... ✅ | go test ./agent/ ./channel/


一、问题汇总

#严重程度文件问题描述
1🔴 P0command_builtin.gochannelFinder 为 nil 时 /settings panic
2🔴 P0command_builtin.go/settings set 无 schema 校验,任意 key/value 均可写入
3🟡 P1command_builtin.gosettingsCmd.Concurrent() 对写操作返回 true,违反项目惯例
4🟡 P1command_builtin.go/settings set 命令 value 被全部 lowercased,破坏大小写敏感值
5🟡 P1command_builtin.gohelp 文本未包含 /settings/menu/browse 等新命令
6🟡 P1settings.goSubmitSettings() 方法已定义但无调用方,死代码
7🟢 P2settings.goagent 包直接 import xbot/channel,违背 channelFinder 依赖反转设计
8🟢 P2feishu.goBuildSettingsUI()BuildTextSettingsUI() 大量逻辑重复
9🟢 P2feishu.goHandleSettingSubmit 不校验 value 是否合法(select 类型范围、toggle 布尔值)
10🟢 P2settings.goGetSettingsUI 返回 “当前渠道没有可配置的设置项” 与 “当前渠道不支持设置” 语义模糊

二、逐项详细审核

P0-1: channelFinder 为 nil 时 /settings panic

文件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.channelFinderfunc(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,服务不可用。


P0-2: /settings set 无 schema 校验

文件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 直接写入数据库,不检查:

  1. key 是否在 SettingsSchema() 中定义
  2. value 是否符合 schema 约束(select 类型只能从 Options 中选,toggle 只能 true/false)
  3. 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)
}

影响:数据污染,用户困惑。


P1-3: Concurrent() 标记不当

文件agent/command_builtin.go L415

现状

func (c *settingsCmd) Concurrent() bool { return true }

项目中 /settings set 会执行数据库写入(SetSetting)。对比项目中其他写命令:

命令操作Concurrent()
/new清空 sessionfalse
/set-llm写 LLM 配置false
/compress压缩 sessionfalse
/install安装 skill/agentfalse
/settings set写用户设置true ← 不一致

修复建议:拆分为两个命令,或将整个 settingsCmd 改为 Concurrent() = false

推荐拆分:

  • settingsListCmdConcurrent() = true(只读)
  • settingsSetCmdConcurrent() = false(写操作)

影响:并发下可能产生不可预期的写入顺序,虽然 SQLite 序列化写入不会数据损坏,但与项目惯例不一致。


P1-4: value 被 lowercased

文件agent/command_builtin.go L424-425

现状

args := strings.TrimPrefix(strings.ToLower(content), "/settings ")

contentToLower() 后,/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 无大小写敏感值,暂无实际影响,但属于定时炸弹。


P1-5: /help 文本缺少新命令

文件agent/command_builtin.go L55-68

现状/help 输出中无以下命令:

  • /settings — 个人设置
  • /menu — 主菜单
  • /browse — 浏览市场
  • /publish — 发布
  • /install — 安装
  • /uninstall — 卸载
  • /my — 我的 skills/agents

修复建议:在 help 文本中追加这些命令说明。


P1-6: SubmitSettings() 死代码

文件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 中。


P2-7: agent 包直接 import xbot/channel

文件agent/settings.go L5

现状

import (
    "xbot/channel"
    "xbot/storage/sqlite"
)

GetSettingsUI 方法接收 channel.Channel 接口并对其做 type assertion。项目在 main.go 中刻意使用 channelFinder 回调避免 agentchannel 的直接依赖。settings.go 打破了这个隔离层。

风险:如果未来 channel 包需要引用 agent 包的任何类型,将形成循环依赖。

修复建议

channel.Channel 参数改为使用 channelFinder 回调获取的 channel.Channel,但保持接口在 agent 包内定义(SettingsUIBuilder 接口),由 main.go 的适配器桥接。或者接受当前状态但加注释标注风险。

当前项目结构下循环依赖概率低,标记为 P2。


P2-8: BuildSettingsUI 逻辑重复

文件channel/feishu.go L858-908 vs channel/capability.go L49-84

两个函数实现几乎相同的逻辑(按 category 分组、渲染 label/value/options),仅格式细节不同:

特性BuildTextSettingsUIFeishuChannel.BuildSettingsUI
标题# ⚙️ 设置## ⚙️ 设置
选项展示`value`✓ Label / Label
末尾提示---\n使用 /settings set...---\n使用 /settings set...

修复建议

抽取公共渲染逻辑到 channel 包的辅助函数(如 renderSettingsMarkdown(schema, values, optionRenderer)),让 FeishuChannel.BuildSettingsUI 调用它,仅传入自定义的 option 渲染器。


P2-9: HandleSettingSubmit 不校验 value

文件channel/feishu.go L834-856

现状HandleSettingSubmit 只做 JSON 解析和 key=value 文本解析,不校验 value 是否在 schema Options 范围内。

由于此方法当前为死代码(P1-6),实际暂无影响。但一旦被启用,同样存在 P0-2 的 schema 绕过问题。

修复建议HandleSettingSubmit 内部应引用 SettingsSchema() 进行校验。


P2-10: 错误信息语义模糊

文件agent/command_builtin.go L437-439 vs agent/settings.go L47

两处返回不同的"不支持"提示:

  1. channelFinder 找不到 channel → "当前渠道不支持设置"
  2. 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

四、正面评价

  1. 接口设计清晰SettingsCapability + UIBuilder 分离关注点,channel 可按需实现
  2. 向后兼容好:不实现 SettingsCapability 的 channel 不会出错,有优雅降级
  3. 测试覆盖GetSettingsGetSettingsUISubmitSettings 均有单元测试
  4. BuildTextSettingsUI 作为 fallback 设计合理,避免强制所有 channel 实现自定义 UI
  5. channelFinder 回调模式(main.go 中注入)方向正确,仅 settings.go 未遵循

门下省 · 审核完毕