命令系统
70+ 斜杠命令的注册、发现、过滤与执行机制
职责概述
解决的问题:用户在 CC 中能做的不只是聊天——还要切换模型、压缩上下文、查看配置、审查代码等等。需要一个命令系统把这些操作统一为 /xxx 斜杠命令,让用户快速触发而不需要记住复杂的操作路径。
应用场景:① /model sonnet 快速切换模型 ② /compact 手动触发上下文压缩 ③ /review 对当前变更做代码审查 ④ /permissions 管理工具权限规则 ⑤ /buddy 孵化一只宠物 ⑥ 团队自定义的 /deploy、/test 等工作流命令。
一句话理解:就像微信的斜杠命令——输入 / 就弹出所有可用操作,选一个直接执行。
架构设计
核心数据流
命令注册与发现流程
COMMANDS() 通过 lodash memoize 返回 70+ 内置命令数组。条件编译(feature flags)动态注入实验性命令。loadAllCommands(cwd) 并行加载:技能目录命令、插件技能、打包技能、内置插件技能、工作流命令,合并到最终列表。getCommands(cwd) 对全量命令执行 meetsAvailabilityRequirement()(认证过滤)和 isCommandEnabled()(特性开关过滤),返回用户可用的命令子集。/xxx,PromptInput 触发 typeahead,findCommand() 匹配 name 或 aliases,定位到具体命令对象。cmd.load() 动态 import 实现模块,按 type 分发:prompt 类型调用 getPromptForCommand(),local/local-jsx 调用 call()。命令类型分发
getPromptForCommand(args, context),返回 ContentBlockParam[]。内容直接注入当前对话作为用户消息,模型据此执行任务。load().call(args, context),返回 LocalCommandResult(text / compact / skip)。纯函数式,不渲染 UI。支持非交互模式。load().call(onDone, context, args),返回 React.ReactNode。在 Ink 终端中渲染全屏 UI(如 /config、/doctor、/model 选择器)。通过 onDone 回调结束。关键类型与接口
Command 联合类型
types/command.ts:175-206 — Command 是一个交叉类型,由 CommandBase 和三种命令变体之一组成:
// types/command.ts:175-206
export type CommandBase = {
availability?: CommandAvailability[] // 认证要求: 'claude-ai' | 'console'
description: string
hasUserSpecifiedDescription?: boolean
isEnabled?: () => boolean // 动态启用/禁用
isHidden?: boolean // 从 typeahead 隐藏
name: string
aliases?: string[]
argumentHint?: string // 参数提示(灰色显示)
whenToUse?: string // 详细使用场景
loadedFrom?: 'commands_DEPRECATED' | 'skills' | 'plugin'
| 'managed' | 'bundled' | 'mcp'
kind?: 'workflow'
immediate?: boolean // 绕过队列立即执行
isSensitive?: boolean // 参数从历史中脱敏
}
export type Command = CommandBase &
(PromptCommand | LocalCommand | LocalJSXCommand)
PromptCommand — 提示类命令
// types/command.ts:25-57
export type PromptCommand = {
type: 'prompt'
progressMessage: string
contentLength: number
argNames?: string[]
allowedTools?: string[]
model?: string
source: SettingSource | 'builtin' | 'mcp' | 'plugin' | 'bundled'
context?: 'inline' | 'fork' // inline=当前对话, fork=子代理
agent?: string // fork 时的代理类型
effort?: EffortValue
paths?: string[] // 文件路径匹配 glob
getPromptForCommand(args, context): Promise<ContentBlockParam[]>
}
典型命令定义示例
// commands/compact/index.ts — local 类型
const compact = {
type: 'local',
name: 'compact',
description: 'Clear conversation history but keep a summary...',
isEnabled: () => !isEnvTruthy(process.env.DISABLE_COMPACT),
supportsNonInteractive: true,
argumentHint: '<optional custom summarization instructions>',
load: () => import('./compact.js'), // 惰性加载
} satisfies Command
// commands/help/index.ts — local-jsx 类型
const help = {
type: 'local-jsx',
name: 'help',
description: 'Show help and available commands',
load: () => import('./help.js'),
} satisfies Command
// commands/review.ts — prompt 类型
const review: Command = {
type: 'prompt',
name: 'review',
description: 'Review a pull request',
progressMessage: 'reviewing pull request',
contentLength: 0,
source: 'builtin',
async getPromptForCommand(args): Promise<ContentBlockParam[]> {
return [{ type: 'text', text: LOCAL_REVIEW_PROMPT(args) }]
},
}
命令过滤函数
// commands.ts:417-443 — 认证过滤
export function meetsAvailabilityRequirement(cmd: Command): boolean {
if (!cmd.availability) return true
for (const a of cmd.availability) {
switch (a) {
case 'claude-ai':
if (isClaudeAISubscriber()) return true; break
case 'console':
if (!isClaudeAISubscriber() && !isUsing3PServices()
&& isFirstPartyAnthropicBaseUrl()) return true; break
}
}
return false
}
// commands.ts:672-676 — 桥接安全判定
export function isBridgeSafeCommand(cmd: Command): boolean {
if (cmd.type === 'local-jsx') return false // JSX 命令有 UI 副作用
if (cmd.type === 'prompt') return true // prompt 类型天然安全
return BRIDGE_SAFE_COMMANDS.has(cmd) // local 需白名单
}
设计模式与亮点
1. 惰性加载(Lazy Loading)
每个命令的 load() 返回一个动态 import()。用户输入 /doctor 时才加载 doctor.tsx(含完整的诊断逻辑和 UI)。
这使得主入口 commands.ts 保持轻量——754 行管理 70+ 命令,每个命令的实现延迟到首次调用。
2. 条件编译与特性开关
命令系统大量使用 feature('XXX') 做 Dead Code Elimination。在构建时,未启用的特性分支被完全移除:
// commands.ts:62-122 — 条件导入示例
const bridge = feature('BRIDGE_MODE')
? require('./commands/bridge/index.js').default : null
const voiceCommand = feature('VOICE_MODE')
? require('./commands/voice/index.js').default : null
const workflowsCmd = feature('WORKFLOW_SCRIPTS')
? require('./commands/workflows/index.js').default : null
3. Memoize 缓存策略
命令列表通过 lodash memoize 按 cwd 缓存,避免每次按键触发 typeahead 时重新扫描文件系统。
clearCommandMemoizationCaches() 和 clearCommandsCache() 提供了精确和全局两种缓存失效机制。
4. 双重安全过滤
命令执行受两层安全机制保护:
- Remote Mode 过滤:
REMOTE_SAFE_COMMANDS白名单限制--remote模式下可用的命令(仅 session、exit、clear、help 等) - Bridge 安全:
isBridgeSafeCommand()防止移动端/Web 客户端触发本地 JSX 渲染命令
5. 技能分层架构
命令系统实现了清晰的来源分层:
// commands.ts:449-469 — loadAllCommands 合并顺序
return [
...bundledSkills, // 打包技能(内置)
...builtinPluginSkills, // 内置插件技能
...skillDirCommands, // .claude/commands/ 用户自定义
...workflowCommands, // 工作流命令
...pluginCommands, // 插件命令
...pluginSkills, // 插件技能
...COMMANDS(), // 70+ 内置命令
]
开发者实践指南
添加新命令
步骤 1:创建 commands/mycommand/index.ts:
import type { Command } from '../../commands.js'
const myCommand: Command = {
type: 'local-jsx', // 或 'local' / 'prompt'
name: 'mycommand',
description: 'Do something useful',
load: () => import('./mycommand.js'), // 惰性加载
}
export default myCommand
步骤 2:在 commands.ts 的 COMMANDS() 数组中添加:
import myCommand from './commands/mycommand/index.js'
// ...
const COMMANDS = memoize((): Command[] => [
// ...existing commands
myCommand,
])
步骤 3:实现 commands/mycommand/mycommand.js(以 local-jsx 为例):
import type { LocalJSXCommandCall } from '../../types/command.js'
export const call: LocalJSXCommandCall = async (onDone, context, args) => {
// 处理逻辑...
onDone('操作完成', { shouldQuery: true })
return <MyComponent />
}
命令调试
- 使用
--debug标志查看命令加载日志 clearCommandsCache()强制重新加载所有命令(插件变更后使用)isHidden: true隐藏开发中的命令isEnabled: () => isEnvTruthy('MY_FEATURE')通过环境变量控制
架构师决策指南
命令数量与可维护性的权衡
754 行管理 70+ 命令的设计刻意避免了过度抽象。每个命令只是一个满足 Command 类型的扁平对象,
用 satisfies Command 确保类型安全。这种 "注册表模式" 的优势是添加新命令零耦合——
只需在数组中追加一项,不会影响其他命令。劣势是随着命令增长,commands.ts 的导入区会变长,
但惰性加载确保了运行时不加载未使用的实现。
三种命令类型的适用场景
/review、/compact)。
不需要 UI,直接注入对话流。优点是可被模型自动调用(SkillTool),缺点是无法处理交互式流程。
/config 选择器、/doctor 诊断面板)。
通过 Ink 渲染 React 组件,功能最强大但不可在非交互模式使用。
性能考量
命令系统的性能瓶颈在文件系统扫描(技能目录、插件命令)。通过以下策略优化:
- Memoize 缓存:
loadAllCommands、getSkillToolCommands、getSlashCommandToolSkills均按 cwd 缓存 - 并行加载:
Promise.all并行扫描技能目录、加载插件、加载工作流 - 惰性 import:命令实现延迟到首次调用,避免启动时加载全部依赖
- 增量失效:
clearCommandMemoizationCaches()只清内部缓存不清技能缓存,clearCommandsCache()全量清理
安全边界设计
命令系统的安全模型基于"默认拒绝"原则:
- remote 模式:只有
REMOTE_SAFE_COMMANDS白名单中的命令可用 - bridge 模式:JSX 命令一律禁止,prompt 命令一律放行,local 命令需显式白名单
- 模型调用:只有
type === 'prompt'且disableModelInvocation !== true的命令可被 SkillTool 调用
命令分类表
| 分类 | 命令 | 类型 | 说明 |
|---|---|---|---|
| 会话管理 | /compact | local | 压缩上下文保留摘要 |
/clear | local | 清屏 | |
/resume | local-jsx | 恢复历史会话 | |
/session | local-jsx | 会话管理 | |
| 模型与推理 | /model | local-jsx | 切换 AI 模型 |
/fast | local-jsx | 快速模式切换 | |
/effort | local-jsx | 推理力度调节 | |
/cost | local | 查看会话成本 | |
| 配置 | /config | local-jsx | 配置面板 |
/permissions | local-jsx | 权限管理 | |
/keybindings | local-jsx | 快捷键配置 | |
/theme | local-jsx | 主题切换 | |
| 开发工具 | /review | prompt | PR 代码审查 |
/diff | local-jsx | 查看代码变更 | |
/doctor | local-jsx | 安装诊断 | |
| 扩展 | /mcp | local-jsx | MCP 服务器管理 |
/skills | local-jsx | 技能列表 | |
/plugin | local-jsx | 插件管理 | |
| 安全与隐私 | /hooks | local-jsx | Hook 配置查看 |
/memory | local-jsx | 记忆文件编辑 | |
/plan | local-jsx | 计划模式 |
◈ 可视化处理拓扑图
① loadAllCommands 缓存(命令列表)
② getSkillToolCommands 缓存(技能工具)
③ getSlashCommandToolSkills 缓存(斜杠命令技能)
④ getSkillIndex 外层缓存需
clearSkillIndexCache() 显式清除触发时机:插件变更、技能文件修改、/login 等认证状态变更
⇉ 核心处理流程详解
命令系统是 Claude Code 用户交互的核心入口。当用户在 REPL 中输入 /command 时,系统需要从多个来源加载命令定义、执行可用性过滤、路由到正确的处理器,最终将结果返回给用户。整个流程涉及静态注册、动态发现、权限门控和类型分发四个关键阶段。
commands.ts:258 通过 memoize((): Command[] => [...]) 延迟构建命令数组。约 80 个内置命令通过顶层 import 静态引入,按条件展开(feature flag、环境变量),形成完整的命令注册表。内部命令通过 INTERNAL_ONLY_COMMANDS(L225)在非 Ant 环境下过滤。commands.ts:449 通过 memoize(async) 并行加载四种来源:getSkills()(技能目录 + 插件技能 + 内置技能 + 内置插件技能)、getPluginCommands()(插件命令)、getWorkflowCommands()(工作流命令)。所有来源合并为统一数组。commands.ts:417 对每个命令执行认证/提供商过滤。availability 字段声明命令所需的认证环境(claude-ai / console / vertex / bedrock)。每次调用 getCommands() 重新评估,确保 /login 等操作后状态立即生效。此检查在 isCommandEnabled() 之前执行。commands.ts:476 获取所有已加载命令后,调用 getDynamicSkills() 获取运行时发现的技能。通过 Set 去重确保动态技能不与已有命令冲突,并按优先级插入到内置命令之前(L504-516)。commands.ts:688-719 按名称查找命令。findCommand 返回 undefined,getCommand 在未找到时抛出错误。查找同时匹配 name 和 aliases(通过 getCommandName,types/command.ts:209)。Command.type(types/command.ts:205)分为三种执行路径:prompt(生成提示词文本注入对话流)、local(同步执行返回结果)、local-jsx(渲染 Ink 交互组件)。PromptCommand 通过 getPromptForCommand() 获取文本内容。commands.ts:684 和 REMOTE_SAFE_COMMANDS(L619)确保远程会话(--remote / Direct Connect)只暴露安全的命令子集。isBridgeSafeCommand(L672)针对 Bridge 模式做额外限制。commands.ts:523 清除 loadAllCommands、getSkillToolCommands、getSlashCommandToolSkills 三层 memoize 缓存。注意 getSkillIndex 是独立的外层缓存,需通过 clearSkillIndexCache() 显式清除(L531 注释解释了 lodash memoize 的穿透问题)。/login)。★ 设计精华
1. 三级命令类型系统的精妙分离
Command 联合类型(types/command.ts:205)将命令分为 prompt、local、local-jsx 三种执行模式。这种分类不是随意的——它精确对应了三种不同的用户交互需求:文本注入(不需要 UI)、同步执行(需要结果但不需要交互)、交互组件(需要完整的 React 渲染能力)。每种类型有独立的 call 签名和 load() 懒加载机制。
CommandBase & (PromptCommand | LocalCommand | LocalJSXCommand) 实现,type 字段作为判别式(discriminated union)// types/command.ts:205-206
export type Command = CommandBase &
(PromptCommand | LocalCommand | LocalJSXCommand)
// 每种类型有独立的 call 签名
type LocalCommandCall = (args, context) => Promise<LocalCommandResult>
type LocalJSXCommandCall = (args, context, onDone) => Promise<void>
2. Availability 门控的实时响应设计
meetsAvailabilityRequirement()(commands.ts:417)故意不做缓存。注释明确说明:"auth state can change mid-session (e.g. after /login), so this must be re-evaluated on every getCommands() call"。这意味着每次 REPL 渲染命令列表时,都会重新检查认证状态。代价是每次调用遍历命令数组的 O(n) 开销,收益是 /login 后命令列表立即更新。
// commands.ts:417-443
export function meetsAvailabilityRequirement(cmd: Command): boolean {
if (!cmd.availability) return true // 无声明 = 全局可用
for (const a of cmd.availability) {
switch (a) {
case 'claude-ai':
if (isClaudeAISubscriber()) return true
break
case 'console':
// 直接 API 用户(排除 3P 和 claude.ai)
if (!isClaudeAISubscriber() &&
!isUsing3PServices() &&
isFirstPartyAnthropicBaseUrl()) return true
break
// ... exhaustive check
}
}
return false
}
/login 或 /logout),但命令列表的渲染频率极高(每次按键都可能触发)。设计者选择了正确性优先、性能由 memoize 上层保证的策略。3. 动态技能的优先级插入算法
在 getCommands()(commands.ts:504-516)中,动态发现的技能需要插入到已有命令列表中。插入位置不是随意选择的——动态技能被插入到"插件技能之后、内置命令之前"。这确保了:内置命令优先级最高(同名冲突时内置胜出),插件技能次之,动态技能最低。
// commands.ts:504-516
const builtInNames = new Set(COMMANDS().map(c => c.name))
const insertIndex = baseCommands.findIndex(c => builtInNames.has(c.name))
return [
...baseCommands.slice(0, insertIndex), // 技能 + 插件命令
...uniqueDynamicSkills, // 动态技能
...baseCommands.slice(insertIndex), // 内置命令(最高优先级)
]
4. 三层缓存与精确失效
clearCommandMemoizationCaches()(commands.ts:523)需要清除三个独立的 memoize 缓存:loadAllCommands、getSkillToolCommands、getSlashCommandToolSkills。但更深层的问题是 getSkillIndex(skillSearch/localSearch.ts)是一个独立的外层缓存,它缓存了内层函数的结果。L527-531 的注释详细解释了 lodash memoize 的穿透陷阱:清除内层缓存后,外层仍返回旧结果。
clearSkillIndexCache(),而非依赖缓存穿透。这种显式失效策略比自动失效更容易理解和调试。5. 内部命令的编译时过滤
INTERNAL_ONLY_COMMANDS(commands.ts:225)数组收集所有仅限内部使用的命令。在 COMMANDS() 的末尾(L343-345),通过 process.env.USER_TYPE === 'ant' && !process.env.IS_DEMO 条件展开。这意味着外部构建完全不含这些命令——它们在编译时就被 tree-shaking 移除了,而非运行时隐藏。
// commands.ts:225-254
export const INTERNAL_ONLY_COMMANDS = [
backfillSessions, breakCache, bughunter, commit,
commitPushPr, ctx_viz, goodClaude, // ...
].filter(Boolean)
// commands.ts:343-345 — 条件展开
...(process.env.USER_TYPE === 'ant' && !process.env.IS_DEMO
? INTERNAL_ONLY_COMMANDS
: [])
◈ Agent 实践借鉴 — 客服 Agent 命令交互设计
1. 场景映射:客服坐席的 "/" 命令
想象一个客服坐席的日常工作面板。坐席不想点菜单、不想记快捷键,最自然的操作是输入 "/" 触发命令:
// 客服命令的真实分级
/问候 → 自动插入"您好,请问有什么可以帮您?"
纯文本注入,无 UI,无副作用 → 模板级
/查订单 ORDER-12345 → 调用订单系统,直接返回结果
同步查询,有网络 IO,不需要表单 → 查询级
/退单 → 弹出退单表单:选择原因 + 填写金额 + 上传凭证
需要交互式 UI,多步操作 → 表单级
/查物流 WAYBILL-67890 → 调用物流 API,返回时间线
同步查询 → 查询级
/记录 → 将本次服务摘要写入客户档案
同步操作 → 查询级
// Claude Code → 客服 Agent
// ──────────────────────────────────────────
type: "prompt" 文本注入 → 模板级:/问候、/结束语
type: "local" 同步执行 → 查询级:/查订单、/查物流
type: "local-jsx" 交互 UI → 表单级:/退单、/发券
// CC 的懒加载 → 客服也按需加载
// CC 的统一 Command 联合类型 → 客服统一注册调度
// CC 的命令自动补全 → 坐席输入 "/" 弹候选
2. 借鉴 CC + 客服改造:伪代码实现
以下是客服命令系统的核心实现,借鉴 CC 的三级命令分离、懒加载和统一调度,但去掉终端相关的 React Reconciler:
// 客服命令的三级类型 — 借鉴 CC 的 PromptCommand/LocalCommand/LocalJSXCommand
type CSCommand = TemplateCommand | QueryCommand | FormCommand
// 模板级:纯文本注入,无 UI,无副作用
interface TemplateCommand {
type: "template"
name: string // "/问候"
description: string // 自动补全的说明
transformInput: (args: string) => string // 模板渲染
}
// 查询级:同步调用后端,直接返回结果
interface QueryCommand {
type: "query"
name: string // "/查订单"
description: string
handler: (args: string) => Promise<QueryResult>
loadingText: string // "正在查询订单..."
}
// 表单级:需要弹表单收集参数
interface FormCommand {
type: "form"
name: string // "/退单"
description: string
schema: FormSchema // 表单字段定义
onSubmit: (formData: Record<string,any>) => Promise<FormResult>
}
// 表单 Schema 示例 — /退单
const refundFormSchema: FormSchema = {
fields: [
{ key: "reason", label: "退款原因", type: "select",
options: ["质量问题","七天无理由","发错货","其他"] },
{ key: "amount", label: "退款金额", type: "number", required: true },
{ key: "evidence", label: "凭证图片", type: "fileUpload", required: false },
]
}
// 统一调度器 — 借鉴 CC 的 switch(cmd.type) 模式
async function dispatchCSCommand(
cmd: CSCommand, args: string, ctx: CSCommandContext
) {
switch (cmd.type) {
case "template":
// 模板注入:直接插入到坐席的输入框或消息区
const text = cmd.transformInput(args)
ctx.insertToMessageArea(text) // 不需要弹 UI
break
case "query":
// 同步查询:显示 loading,调用后端,直接展示结果
ctx.showLoading(cmd.loadingText)
try {
const result = await cmd.handler(args)
ctx.displayResult(result) // 查订单→卡片,查物流→时间线
} finally {
ctx.hideLoading()
}
break
case "form":
// 弹表单:Web 框架渲染表单,收集参数后提交
ctx.openFormDialog({
title: cmd.name,
schema: cmd.schema,
onSubmit: async (formData) => {
const result = await cmd.onSubmit(formData)
ctx.displayResult(result)
}
})
break
}
}
// 懒加载注册表 — 借鉴 CC 的 CommandDescriptor
const csCommandRegistry = new Map<string, CSCommandDescriptor>()
function registerCSCommand(name: string, loader: () => Promise<CSCommand>) {
csCommandRegistry.set(name, {
loaded: false,
loader, // 工厂函数,首次调用时执行
instance: null,
// 自动补全信息在注册时就提供,不需要加载实现
autoComplete: { name, description: loader.description }
})
}
// 首次调用时才加载实现
async function getCSCommand(name: string): Promise<CSCommand> {
const desc = csCommandRegistry.get(name)
if (!desc.loaded) {
desc.instance = await desc.loader()
desc.loaded = true
}
return desc.instance
}
// 自动补全 — 坐席输入 "/" 时触发
function getAutoCompleteList(): AutoCompleteItem[] {
return Array.from(csCommandRegistry.values())
.map(desc => desc.autoComplete) // 只取 name + description
.filter(cmd => isAvailable(cmd.name)) // 权限过滤
}
3. 落地清单:必须抄 vs 不需要抄
- 三级命令分离 — 模板/查询/表单,编译期约束行为,防止模板命令产生副作用
- 懒加载注册 — 注册时只存工厂函数和描述,首次调用才加载实现,冷启动快
- 统一注册 + 调度 — 不同类型的命令统一注册到一个注册表,调度器按 type 字段分发
- 自动补全 — 注册时提供 name + description,输入 "/" 时弹出候选,不需要加载实现
- local-jsx 的 React Reconciler — 客服面板是 Web 应用,直接用 FormDialog 组件,不需要终端 Ink reconciler
- 缓存失效三层联动 — 客服命令数量通常不到 30,简单的 Map 清除就够
- 内部/外部命令隔离 — 客服场景没有"内部 build"概念,所有坐席用同一套命令
4. 常见坑
loadingText 字段,调度器在调用 handler 前先显示 loading,finally 中隐藏。
代码索引
| 文件 | 行数 | 说明 |
|---|---|---|
commands.ts | ~754 | 命令注册中心:导入、注册、过滤、路由 |
types/command.ts | ~217 | Command 联合类型定义、PromptCommand/LocalCommand/LocalJSXCommand |
commands/help/ | ~10 | 帮助命令(local-jsx) |
commands/config/ | ~10 | 配置面板命令(local-jsx),别名 settings |
commands/compact/ | ~15 | 上下文压缩命令(local),支持非交互 |
commands/model/ | ~16 | 模型选择器(local-jsx),dynamic description |
commands/fast/ | ~24 | 快速模式(local-jsx),带 availability 门控 |
commands/doctor/ | ~12 | 安装诊断面板(local-jsx) |
commands/review.ts | ~58 | PR 审查命令(prompt),含 ultrareview 变体 |
commands/mcp/ | ~12 | MCP 服务器管理(local-jsx),immediate 模式 |
commands/permissions/ | ~10 | 权限管理(local-jsx),别名 allowed-tools |
skills/loadSkillsDir.ts | ~200 | 技能目录扫描与命令加载 |
utils/plugins/loadPluginCommands.ts | ~150 | 插件命令加载 |