refactor(cli): replace slash command whitelist with capability-based filtering (Phase 1) (#3283)

* refactor(cli): replace slash command whitelist with capability-based filtering (Phase 1)

## Summary

Replace the hardcoded ALLOWED_BUILTIN_COMMANDS_NON_INTERACTIVE whitelist with a
unified, capability-based command metadata model. This is Phase 1 of the slash
command architecture refactor described in docs/design/slash-command/.

## Key changes

### New types (types.ts)
- Add ExecutionMode ('interactive' | 'non_interactive' | 'acp')
- Add CommandSource ('builtin-command' | 'bundled-skill' | 'skill-dir-command' |
  'plugin-command' | 'mcp-prompt')
- Add CommandType ('prompt' | 'local' | 'local-jsx')
- Extend SlashCommand interface with: source, sourceLabel, commandType,
  supportedModes, userInvocable, modelInvocable, argumentHint, whenToUse,
  examples (all optional, backward-compatible)

### New module (commandUtils.ts + commandUtils.test.ts)
- getEffectiveSupportedModes(): 3-priority inference
  (explicit supportedModes > commandType > CommandKind fallback)
- filterCommandsForMode(): replaces filterCommandsForNonInteractive()
- 18 unit tests

### Whitelist removal (nonInteractiveCliCommands.ts)
- Remove ALLOWED_BUILTIN_COMMANDS_NON_INTERACTIVE constant
- Remove filterCommandsForNonInteractive() function
- Replace with CommandService.getCommandsForMode(mode)

### CommandService enhancements (CommandService.ts)
- Add getCommandsForMode(mode: ExecutionMode): filters by mode, excludes hidden
- Add getModelInvocableCommands(): reserved for Phase 3 model tool-call use

### Built-in command annotations (41 files)
Annotate every built-in command with commandType:
- commandType='local' + supportedModes all-modes: btw, bug, compress, context,
  init, summary (replaces the 6-command whitelist)
- commandType='local' interactive-only: export, memory, plan, insight
- commandType='local-jsx' interactive-only: all remaining ~31 commands

### Loader metadata injection (4 files)
Each loader stamps source/sourceLabel/commandType/modelInvocable on every
command it emits:
- BuiltinCommandLoader: source='builtin-command', modelInvocable=false
- BundledSkillLoader: source='bundled-skill', commandType='prompt',
  modelInvocable=true
- command-factory (FileCommandLoader): source per extension/user origin,
  commandType='prompt', modelInvocable=!extensionName
- McpPromptLoader: source='mcp-prompt', commandType='prompt', modelInvocable=true

### Bug fix
MCP_PROMPT commands were incorrectly excluded from non-interactive/ACP modes by
the old whitelist logic. commandType='prompt' now correctly allows them in all
modes.

### Session.ts / nonInteractiveHelpers.ts
- ACP session calls getAvailableCommands with explicit 'acp' mode
- Remove allowedBuiltinCommandNames parameter from buildSystemMessage() —
  capability filtering is now self-contained in CommandService

* fix test ci

* fix memory command

* fix: pass 'non_interactive' mode explicitly to getAvailableCommands

- Fix critical bug in nonInteractiveHelpers.ts: loadSlashCommandNames was
  calling getAvailableCommands without specifying mode, causing it to default
  to 'acp' instead of 'non_interactive'. Commands with supportedModes that
  include 'non_interactive' but not 'acp' would be silently excluded.
- Apply the same fix in systemController.ts for the same reason.
- Update test mock to delegate filtering to production filterCommandsForMode()
  instead of duplicating the logic inline, preventing divergence.

Fixes review comments by wenshao and tanzhenxin on PR #3283.

* fix: resolve TypeScript type error in nonInteractiveHelpers.test.ts

* fix test ci
This commit is contained in:
顾盼 2026-04-20 14:34:43 +08:00 committed by GitHub
parent 6c999fe29f
commit a82d766727
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
62 changed files with 2350 additions and 307 deletions

View file

@ -0,0 +1,263 @@
# Slash Command 重构路线图
## 总体目标
用 Qwen 内部架构风格,交付一个在外部体验上 95% 对齐 Claude Code 的 command 平台同时修复三模式分裂、命令来源单一、prompt command 无法被模型调用三个核心问题。
---
## 核心设计原则
1. **每个 Phase 可独立 ship**:完成后行为是自洽的,不依赖未来 Phase 才能运行
2. **Phase 1 是纯基础设施**:除修复 MCP_PROMPT 被错误拦截外,不改变任何现有可用命令集
3. **行为变化与架构变化分开**Phase 1 做架构Phase 2 做能力扩展
4. **不照搬 Claude Code 内部架构**:但对齐用户可感知的能力面
---
## Phase 1基础设施重建纯架构零行为变化
### 目标
建立统一的命令元数据模型和跨模式管理机制,为后续所有 Phase 提供底层支撑。
### 功能点
#### 1.1 扩展 `SlashCommand` 元数据模型
在现有 `SlashCommand` 接口上新增以下字段:
**来源字段**
- `source: CommandSource`:命令来源枚举(`builtin-command` / `bundled-skill` / `skill-dir-command` / `plugin-command` / `mcp-prompt` 等)
- `sourceLabel?: string`:展示用的来源标签(如 `"Built-in"` / `"MCP: github-server"`
**模式能力字段**
- `supportedModes: ExecutionMode[]`:声明在哪些运行模式下可用(`interactive` / `non_interactive` / `acp`
**执行类型字段**
- `commandType: CommandType`:声明执行类型(`prompt` / `local` / `local-jsx`
**可见性字段**
- `userInvocable: boolean`:用户是否可通过 slash command 调用(默认 `true`
- `modelInvocable: boolean`:模型是否可通过 tool call 调用(默认 `false`
**辅助元数据字段**(为 Phase 3 预留Phase 1 仅定义,不使用)
- `argumentHint?: string`:参数提示,如 `"<model-id>"` / `"show|list|set"`
- `whenToUse?: string`:何时调用该命令的说明(供模型使用)
- `examples?: string[]`:使用示例
#### 1.2 Loader 填充 source/commandType 字段
每个 Loader 在构建 `SlashCommand` 时必须填充 `source``commandType`
| Loader | source | commandType |
| -------------------------------- | ------------------- | ------------------------------------- |
| `BuiltinCommandLoader` | `builtin-command` | 由各命令声明(`local` / `local-jsx` |
| `BundledSkillLoader` | `bundled-skill` | `prompt` |
| `FileCommandLoader`(用户/项目) | `skill-dir-command` | `prompt` |
| `FileCommandLoader`(插件) | `plugin-command` | `prompt` |
| `McpPromptLoader` | `mcp-prompt` | `prompt` |
#### 1.3 内置命令声明 `supportedModes``commandType`
为所有 built-in 命令显式声明:
- `commandType``local`(无 UI 依赖)或 `local-jsx`(依赖 dialog/React
- `supportedModes``local` 类命令声明 `['interactive', 'non_interactive', 'acp']``local-jsx` 类命令声明 `['interactive']`
#### 1.4 用 capability-based 过滤替换硬编码白名单
- 删除 `ALLOWED_BUILTIN_COMMANDS_NON_INTERACTIVE` 常量
- 删除 `filterCommandsForNonInteractive` 函数
- 新增 `filterCommandsForMode(commands, mode)` 函数,基于 `supportedModes` 字段过滤
- 新增 `getEffectiveSupportedModes(cmd)` 工具函数(考虑 CommandKind 默认策略)
- 修改 `handleSlashCommand` / `getAvailableCommands` 函数签名,移除 `allowedBuiltinCommandNames` 参数
#### 1.5 CommandService 升级为统一 Registry
- 新增 `getCommandsForMode(mode: ExecutionMode)` 方法
- 新增 `getModelInvocableCommands()` 方法Phase 2/3 使用Phase 1 提供接口)
- 现有 `getCommands()` 保持不变interactive 使用)
### 验收标准
- [ ] `SlashCommand` 接口包含所有新字段TypeScript 编译通过
- [ ] 所有 Loader 填充 `source``commandType` 字段
- [ ] 所有 built-in 命令声明 `commandType``supportedModes`
- [ ] `ALLOWED_BUILTIN_COMMANDS_NON_INTERACTIVE` 已删除,被 capability filter 取代
- [ ] **non-interactive 下可用命令集与重构前完全一致**(现有测试不 break
- [ ] MCP prompt commands 在 non-interactive/acp 下可正常执行(修复原有错误限制)
- [ ] `CommandService.getCommandsForMode('non_interactive')` 返回正确的命令集
- [ ] 所有现有测试通过
---
## Phase 2能力扩展命令整理与 prompt command 模型调用)
### 目标
基于 Phase 1 的元数据基础,扩展三种模式下的命令可用范围,并打通 prompt command 的模型调用通路。
### 功能点
#### 2.1 扩展 non-interactive / acp 可用命令集
将以下命令的 `supportedModes` 扩展到包含 `non_interactive``acp`,并确保其 action 实现可在无 UI 环境运行:
**直接可扩展**action 已无 UI 依赖):
- `/export`:文件 I/O返回 `message`
- `/memory`:文件 I/O返回 `message`
- `/plan`:返回 `submit_prompt`
- `/tools`:改为返回 `message`(文本列表,替换 UI 渲染)
- `/stats`:改为返回 `message`(文本格式,替换 UI 渲染)
**需要 local 子命令拆分**(当前只有 `local-jsx` 壳):
| 命令 | 新增的 local 子命令 |
| -------------- | ----------------------------------------------------------------------------- |
| `/model` | `show`(当前模型)、`list`(可选列表)、`set <id>`(切换) |
| `/permissions` | `show`(当前权限模式)、`set <mode>`(设置) |
| `/mcp` | `list`MCP 服务列表)、`show <server>`(服务详情)、`status`(所有服务状态) |
| `/memory` | 已有 `show`/`add`/`refresh`(确认 non-interactive 下可用) |
> **注意**:上述 UI 壳命令不会被删除,`/model` 不带子命令时仍然打开 dialoginteractive 模式)。新增子命令是 **在现有命令上追加**,不是替换。
#### 2.2 prompt command 模型调用打通
- 在 `CommandService`(或 `CommandRegistry`)中实现 `getModelInvocableCommands()`,返回所有 `modelInvocable: true` 的命令
- 将 `BundledSkillLoader``FileCommandLoader`(用户/项目命令)、`McpPromptLoader` 加载的命令标记为 `modelInvocable: true`
- 改造 `SkillTool`:从只消费 `SkillManager.listSkills()` 改为同时消费 `CommandService.getModelInvocableCommands()`
- 构建统一的模型可调用命令描述,注入 `SkillTool` 的 description
#### 2.3 mid-input slash command 检测(基础版)
- 在 `InputPrompt` 中检测光标附近的 slash token不限于行首
- 检测到 slash token 后触发补全菜单(展示命令名 + description
- 补全菜单弹出位置跟随光标
- **不**包含 argument hints、source badge 等Phase 3 做)
### 验收标准
- [ ] `/export``/memory``/plan``/tools``/stats` 在 non-interactive 模式下可正常执行并返回结构化输出
- [ ] `/model show``/model set <id>` 在 non-interactive / acp 下可执行
- [ ] `/permissions show``/permissions set <mode>` 在 non-interactive / acp 下可执行
- [ ] `/mcp list``/mcp show <server>` 在 non-interactive / acp 下可执行
- [ ] 模型在对话中可以通过 `SkillTool` 调用 bundled skill、file command用户/项目、MCP prompt
- [ ] 模型不可以调用 built-in commands`userInvocable: true``modelInvocable: false`
- [ ] mid-input slash在正文中输入 `/` 后触发命令补全菜单
- [ ] `SkillTool` 的 description 包含所有 `modelInvocable` 命令的描述
---
## Phase 3体验对齐补全增强 + Claude Code 命令补齐)
### 目标
在 Phase 1/2 的元数据和命令能力基础上,补齐补全体验,并补充 Claude Code 中存在而 Qwen Code 缺失的命令。
### 功能点
#### 3.1 补全体验增强
**source badge**
- 在补全菜单中展示命令来源标签(`[MCP]` 已有,扩展为 `[Skill]``[Custom]` 等)
- 使用 `source` / `sourceLabel` 字段渲染
**argument hint**
- 补全菜单中命令名后展示 `argumentHint`(如 `set <model-id>`
- `argumentHint` 由 Phase 1 元数据字段提供
**recently used 排序**
- 记录用户最近使用的命令session 级别,无需持久化)
- 在补全排序中给近期使用的命令加权
**alias 命中高亮**
- 当补全命中 `altNames` 而非主名时,在展示时注明(如 `help (alias: ?)`
**冲突策略对齐**
- 明确优先级built-in > bundled/skill-dir > plugin > mcp
- 冲突时将低优先级命令重命名(如 `pluginName.commandName`
#### 3.2 mid-input slash command 完整版
- 在 Phase 2 基础版上增加 argument hints 和 source badge 展示
- ghost text 提示(输入 `/he` 时显示 `/help` 的淡色提示)
- 有效命令 token 高亮(已完成匹配的 slash command 显示不同颜色)
#### 3.3 Help 目录重构
`/help` 从平铺列表改为分组目录:
- **Built-in Commands**local + local-jsx注明 mode
- **Bundled Skills**
- **Custom Commands**(用户/项目 file commands
- **Plugin Commands**
- **MCP Commands**
每条命令展示名称、argumentHint、description、source、supportedModes 标记
#### 3.4 ACP available commands 元数据增强
`sendAvailableCommandsUpdate()` 中将更多元数据暴露给 ACP 客户端:
- `argumentHint`
- `source`
- `supportedModes`
- `subcommands`(名称列表)
- `modelInvocable`
#### 3.5 Claude Code 缺失命令补齐
补充 Qwen Code 当前没有、Claude Code 有且常用的命令:
| 命令 | 类型 | 说明 |
| ---------------- | ------- | ---------------------------------------- |
| `/doctor` | `local` | 环境自检,输出配置/连接/工具状态诊断 |
| `/release-notes` | `local` | 展示当前版本的更新日志 |
| `/cost` | `local` | 展示当前 session 的 token 消耗和费用估算 |
> 注:`/review``/commit` 等任务类命令以 bundled skill 形式提供,不在此列。
### 验收标准
- [ ] 补全菜单展示 source badge`[MCP]``[Skill]``[Custom]`
- [ ] 补全菜单展示 argumentHint`set <model-id>`
- [ ] 近期使用的命令在补全列表中优先出现
- [ ] alias 命中时在补全项中注明原名
- [ ] mid-input slashghost text 提示正确渲染
- [ ] `/help` 输出按来源分组,每条命令展示支持模式标记
- [ ] ACP available commands 包含 `argumentHint``source``subcommands` 字段
- [ ] `/doctor``/release-notes``/cost` 三个命令可用
- [ ] `/doctor` 在 non-interactive 模式下可执行(返回 `message`
---
## 各 Phase 依赖关系
```
Phase 1元数据 + 统一过滤)
├──► Phase 2能力扩展
│ │
│ ├──► slash command 子命令拆分
│ └──► prompt command 模型调用(需要 getModelInvocableCommands()
└──► Phase 3体验对齐
├──► source badge需要 Phase 1 source 字段)
├──► argument hint需要 Phase 1 argumentHint 字段)
└──► Help 分组(需要 Phase 1 source 字段)
```
Phase 2 和 Phase 3 不互相依赖,可以并行推进(或根据优先级调换部分子项)。