职责概述

解决的问题:用户在 CC 中能做的不只是聊天——还要切换模型、压缩上下文、查看配置、审查代码等等。需要一个命令系统把这些操作统一为 /xxx 斜杠命令,让用户快速触发而不需要记住复杂的操作路径。

应用场景:/model sonnet 快速切换模型 ② /compact 手动触发上下文压缩 ③ /review 对当前变更做代码审查 ④ /permissions 管理工具权限规则 ⑤ /buddy 孵化一只宠物 ⑥ 团队自定义的 /deploy/test 等工作流命令。

一句话理解:就像微信的斜杠命令——输入 / 就弹出所有可用操作,选一个直接执行。

架构设计

命令发现层
commands.ts — COMMANDS() 注册表
skills/loadSkillsDir.ts — 技能目录扫描
plugins/loadPluginCommands.ts — 插件命令加载
getBundledSkills() — 打包技能
命令过滤层
meetsAvailabilityRequirement()
isCommandEnabled()
filterCommandsForRemoteMode()
isBridgeSafeCommand()
命令路由层
findCommand() / getCommand()
PromptInput — Typeahead 匹配
getSkillToolCommands() — 模型可调用技能
命令执行层
prompt — getPromptForCommand()
local — load().call()
local-jsx — load().call() → ReactNode

核心数据流

命令注册与发现流程

1. 静态注册
COMMANDS() 通过 lodash memoize 返回 70+ 内置命令数组。条件编译(feature flags)动态注入实验性命令。
2. 动态加载
loadAllCommands(cwd) 并行加载:技能目录命令、插件技能、打包技能、内置插件技能、工作流命令,合并到最终列表。
3. 过滤可见命令
getCommands(cwd) 对全量命令执行 meetsAvailabilityRequirement()(认证过滤)和 isCommandEnabled()(特性开关过滤),返回用户可用的命令子集。
4. 命令路由
用户输入 /xxx,PromptInput 触发 typeahead,findCommand() 匹配 name 或 aliases,定位到具体命令对象。
5. 惰性执行
调用 cmd.load() 动态 import 实现模块,按 type 分发:prompt 类型调用 getPromptForCommand(),local/local-jsx 调用 call()

命令类型分发

prompt 类型
调用 getPromptForCommand(args, context),返回 ContentBlockParam[]。内容直接注入当前对话作为用户消息,模型据此执行任务。
local 类型
调用 load().call(args, context),返回 LocalCommandResult(text / compact / skip)。纯函数式,不渲染 UI。支持非交互模式。
local-jsx 类型
调用 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 memoizecwd 缓存,避免每次按键触发 typeahead 时重新扫描文件系统。 clearCommandMemoizationCaches()clearCommandsCache() 提供了精确和全局两种缓存失效机制。

4. 双重安全过滤

命令执行受两层安全机制保护:

5. 技能分层架构

命令系统实现了清晰的来源分层:

// commands.ts:449-469 — loadAllCommands 合并顺序
return [
  ...bundledSkills,          // 打包技能(内置)
  ...builtinPluginSkills,    // 内置插件技能
  ...skillDirCommands,       // .claude/commands/ 用户自定义
  ...workflowCommands,       // 工作流命令
  ...pluginCommands,         // 插件命令
  ...pluginSkills,           // 插件技能
  ...COMMANDS(),             // 70+ 内置命令
]

开发者实践指南

新增命令只需 3 步:创建命令目录、定义 Command 对象、在 commands.ts 中注册。

添加新命令

步骤 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.tsCOMMANDS() 数组中添加:

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 />
}

命令调试

架构师决策指南

命令数量与可维护性的权衡

754 行管理 70+ 命令的设计刻意避免了过度抽象。每个命令只是一个满足 Command 类型的扁平对象, 用 satisfies Command 确保类型安全。这种 "注册表模式" 的优势是添加新命令零耦合—— 只需在数组中追加一项,不会影响其他命令。劣势是随着命令增长,commands.ts 的导入区会变长, 但惰性加载确保了运行时不加载未使用的实现。

三种命令类型的适用场景

prompt:适合将用户输入转换为模型提示的场景(如 /review/compact)。 不需要 UI,直接注入对话流。优点是可被模型自动调用(SkillTool),缺点是无法处理交互式流程。
local-jsx:适合需要全屏交互式 UI 的场景(如 /config 选择器、/doctor 诊断面板)。 通过 Ink 渲染 React 组件,功能最强大但不可在非交互模式使用。

性能考量

命令系统的性能瓶颈在文件系统扫描(技能目录、插件命令)。通过以下策略优化:

安全边界设计

命令系统的安全模型基于"默认拒绝"原则:

命令分类表

分类命令类型说明
会话管理/compactlocal压缩上下文保留摘要
/clearlocal清屏
/resumelocal-jsx恢复历史会话
/sessionlocal-jsx会话管理
模型与推理/modellocal-jsx切换 AI 模型
/fastlocal-jsx快速模式切换
/effortlocal-jsx推理力度调节
/costlocal查看会话成本
配置/configlocal-jsx配置面板
/permissionslocal-jsx权限管理
/keybindingslocal-jsx快捷键配置
/themelocal-jsx主题切换
开发工具/reviewpromptPR 代码审查
/difflocal-jsx查看代码变更
/doctorlocal-jsx安装诊断
扩展/mcplocal-jsxMCP 服务器管理
/skillslocal-jsx技能列表
/pluginlocal-jsx插件管理
安全与隐私/hookslocal-jsxHook 配置查看
/memorylocal-jsx记忆文件编辑
/planlocal-jsx计划模式

可视化处理拓扑图

用户输入 /commandREPL stdin → 解析前缀 /
1. 静态命令注册 — COMMANDS() 闭包commands.ts:258 — memoize(() => Command[])约 80 个内置命令顶层 import,按 feature flag 条件展开
2. 多源并行加载 — loadAllCommands()commands.ts:449 — memoize(async) 并行调用
getSkills()技能目录 + 插件技能
getPluginCommands()插件注册命令
getWorkflowCommands()工作流命令
Promise.all 并行加载,合并为统一数组 ↓
3. 动态技能注入 — getCommands()commands.ts:476 — getDynamicSkills() 运行时发现Set 去重 → 插入到内置命令之前(L504-516)
已加载命令列表~100+ 条命令(内置 + 插件 + 动态)
4. 可用性门控 — meetsAvailabilityRequirement()commands.ts:417 — 每次调用重新评估,不缓存auth state 可在会话中变化(如 /login 后)
遍历 cmd.availabilityclaude-ai / console / vertex / bedrock
通过
命令可用
认证不符 ↓
命令隐藏不显示在补全列表中
5. 命令路由 — findCommand() / getCommand()commands.ts:688-719 — 按 name + aliases 查找
远程模式过滤commands.ts:684 — REMOTE_SAFE_COMMANDS
远程会话
不安全命令过滤
通过 ↓
6. isCommandEnabled() 检查启用/禁用状态 → 最终可用命令
已路由命令Command 对象,type 字段判别
7. 类型分发 — Command.type 判断types/command.ts:205 — CommandBase & (Prompt | Local | LocalJSX)
type: promptPromptCommand
getPromptForCommand()生成文本注入对话流commands.ts — prompt 类型
文本注入到消息流
type: localLocalCommand
call(args, context)同步执行返回结果Promise<LocalCommandResult>
结果直接显示
type: local-jsxLocalJSXCommand
call(args, ctx, onDone)渲染 Ink 交互组件React 组件 → Reconciler
交互式 UI 呈现
缓存失效机制 — clearCommandMemoizationCaches()
commands.ts:523 — 清除三层 memoize 缓存:
① loadAllCommands 缓存(命令列表)
② getSkillToolCommands 缓存(技能工具)
③ getSlashCommandToolSkills 缓存(斜杠命令技能)
④ getSkillIndex 外层缓存需 clearSkillIndexCache() 显式清除
触发时机:插件变更、技能文件修改、/login 等认证状态变更
三层架构:静态注册(零 I/O,模块加载时完成)→ 动态发现(memoize 延迟执行,首次调用时磁盘扫描)→ 可用性检查(不缓存,每次重新评估认证状态)。类型系统通过判别联合(discriminated union)在编译期约束三种执行路径的行为。

核心处理流程详解

命令系统是 Claude Code 用户交互的核心入口。当用户在 REPL 中输入 /command 时,系统需要从多个来源加载命令定义、执行可用性过滤、路由到正确的处理器,最终将结果返回给用户。整个流程涉及静态注册、动态发现、权限门控和类型分发四个关键阶段。

1. 静态命令注册 — COMMANDS() 闭包
commands.ts:258 通过 memoize((): Command[] => [...]) 延迟构建命令数组。约 80 个内置命令通过顶层 import 静态引入,按条件展开(feature flag、环境变量),形成完整的命令注册表。内部命令通过 INTERNAL_ONLY_COMMANDS(L225)在非 Ant 环境下过滤。
2. 多源并行加载 — loadAllCommands()
commands.ts:449 通过 memoize(async) 并行加载四种来源:getSkills()(技能目录 + 插件技能 + 内置技能 + 内置插件技能)、getPluginCommands()(插件命令)、getWorkflowCommands()(工作流命令)。所有来源合并为统一数组。
3. 可用性门控 — meetsAvailabilityRequirement()
commands.ts:417 对每个命令执行认证/提供商过滤。availability 字段声明命令所需的认证环境(claude-ai / console / vertex / bedrock)。每次调用 getCommands() 重新评估,确保 /login 等操作后状态立即生效。此检查在 isCommandEnabled() 之前执行。
4. 动态技能注入 — getCommands()
commands.ts:476 获取所有已加载命令后,调用 getDynamicSkills() 获取运行时发现的技能。通过 Set 去重确保动态技能不与已有命令冲突,并按优先级插入到内置命令之前(L504-516)。
5. 命令路由 — findCommand() / getCommand()
commands.ts:688-719 按名称查找命令。findCommand 返回 undefinedgetCommand 在未找到时抛出错误。查找同时匹配 namealiases(通过 getCommandName,types/command.ts:209)。
6. 类型分发 — Command.type 判断
根据 Command.type(types/command.ts:205)分为三种执行路径:prompt(生成提示词文本注入对话流)、local(同步执行返回结果)、local-jsx(渲染 Ink 交互组件)。PromptCommand 通过 getPromptForCommand() 获取文本内容。
7. 远程模式过滤 — filterCommandsForRemoteMode()
commands.ts:684REMOTE_SAFE_COMMANDS(L619)确保远程会话(--remote / Direct Connect)只暴露安全的命令子集。isBridgeSafeCommand(L672)针对 Bridge 模式做额外限制。
8. 缓存失效 — clearCommandMemoizationCaches()
commands.ts:523 清除 loadAllCommandsgetSkillToolCommandsgetSlashCommandToolSkills 三层 memoize 缓存。注意 getSkillIndex 是独立的外层缓存,需通过 clearSkillIndexCache() 显式清除(L531 注释解释了 lodash memoize 的穿透问题)。
命令系统采用"静态注册 + 动态发现"双层架构。静态注册在模块加载时完成 import(零 I/O),动态发现通过 memoize 延迟到首次调用时执行磁盘扫描。这种分层设计确保启动速度快(import 无 I/O),同时支持运行时动态扩展。可用性检查不缓存,因为认证状态可能在会话中变化(如 /login)。

设计精华

1. 三级命令类型系统的精妙分离

Command 联合类型(types/command.ts:205)将命令分为 promptlocallocal-jsx 三种执行模式。这种分类不是随意的——它精确对应了三种不同的用户交互需求:文本注入(不需要 UI)、同步执行(需要结果但不需要交互)、交互组件(需要完整的 React 渲染能力)。每种类型有独立的 call 签名和 load() 懒加载机制。

联合类型分发
Command 通过交叉类型 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>
这种设计的核心洞察:终端应用中"命令"的概念比 Web 应用复杂得多。一条命令可能是纯文本转换、可能是阻塞操作、也可能是完整的交互式 UI。通过类型系统而非 if-else 来约束行为,让编译器在编译期捕获错误(例如给 prompt 命令传递 onDone 回调)。

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 后命令列表立即更新。

认证感知的命令过滤
availability 字段声明命令的认证依赖,运行时检查当前用户状态
// 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
}
这个设计选择反映了 CLI 工具的特殊约束:认证状态在会话中可变(用户可以 /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 缓存:loadAllCommandsgetSkillToolCommandsgetSlashCommandToolSkills。但更深层的问题是 getSkillIndex(skillSearch/localSearch.ts)是一个独立的外层缓存,它缓存了内层函数的结果。L527-531 的注释详细解释了 lodash memoize 的穿透陷阱:清除内层缓存后,外层仍返回旧结果。

这是缓存分层中的经典难题:当外层缓存依赖内层缓存的结果时,仅清除内层是无效的。Claude Code 的解决方案是显式调用 clearSkillIndexCache(),而非依赖缓存穿透。这种显式失效策略比自动失效更容易理解和调试。

5. 内部命令的编译时过滤

INTERNAL_ONLY_COMMANDS(commands.ts:225)数组收集所有仅限内部使用的命令。在 COMMANDS() 的末尾(L343-345),通过 process.env.USER_TYPE === 'ant' && !process.env.IS_DEMO 条件展开。这意味着外部构建完全不含这些命令——它们在编译时就被 tree-shaking 移除了,而非运行时隐藏。

编译时排除
内部命令通过条件展开和 feature flag 在编译时完全排除
// 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
  : [])
安全敏感的内部命令不应该是"隐藏但存在"的——它们应该从产物中完全移除。通过展开运算符 + 环境变量检查 + tree-shaking 的组合,确保外部用户无法通过任何方式访问这些命令。这比运行时权限检查更安全,因为即使绕过了前端过滤,命令处理函数也不在代码中。

Agent 实践借鉴 — 客服 Agent 命令交互设计

本节回答:如果你在设计一个客服 Agent 助手,坐席在面板中输入 "/" 快捷命令来操作,Claude Code 的命令系统有哪些值得抄、哪些不需要抄?

1. 场景映射:客服坐席的 "/" 命令

想象一个客服坐席的日常工作面板。坐席不想点菜单、不想记快捷键,最自然的操作是输入 "/" 触发命令:

坐席日常命令
按交互复杂度分三级
// 客服命令的真实分级
/问候   → 自动插入"您好,请问有什么可以帮您?"
         纯文本注入,无 UI,无副作用 → 模板级

/查订单 ORDER-12345 → 调用订单系统,直接返回结果
         同步查询,有网络 IO,不需要表单 → 查询级

/退单   → 弹出退单表单:选择原因 + 填写金额 + 上传凭证
         需要交互式 UI,多步操作 → 表单级

/查物流 WAYBILL-67890 → 调用物流 API,返回时间线
         同步查询 → 查询级

/记录   → 将本次服务摘要写入客户档案
         同步操作 → 查询级
CC 借鉴点映射
CC 的三级命令 → 客服的三级命令
// 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 的 dispatchCommand,但用 Web 表单替代终端 React
// 统一调度器 — 借鉴 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. 常见坑

坑 1:查询类命令超时没有 loading 反馈。坐席点了 /查订单 后页面没有任何反应,以为是卡死了。解决方案:QueryCommand 必须有 loadingText 字段,调度器在调用 handler 前先显示 loading,finally 中隐藏。
坑 2:模板命令偷偷加了副作用。有人给 /问候 命令加了"同时记录客户到访"的逻辑,结果坐席只是想插入一句问候语却触发了一次数据库写入。解决方案:严格遵循三级分离——模板级命令只能做文本变换,禁止网络请求和状态修改。
坑 3:所有命令在面板初始化时全部加载。30+ 命令全部 import,首屏加载从 500ms 涨到 3s。解决方案:注册时只存工厂函数和自动补全元数据,首次调用时才动态 import 实现。
坑 4:自动补全列表包含无权限的命令。坐席看到 /退单 命令,点击后提示"无权限",体验很差。解决方案:自动补全生成时就根据坐席角色过滤,不是在执行时才检查。

代码索引

文件行数说明
commands.ts~754命令注册中心:导入、注册、过滤、路由
types/command.ts~217Command 联合类型定义、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~58PR 审查命令(prompt),含 ultrareview 变体
commands/mcp/~12MCP 服务器管理(local-jsx),immediate 模式
commands/permissions/~10权限管理(local-jsx),别名 allowed-tools
skills/loadSkillsDir.ts~200技能目录扫描与命令加载
utils/plugins/loadPluginCommands.ts~150插件命令加载