Slash 命令系统
昨天(Day 10)讲了上下文/token 怎么管理;今天讲你敲的 "/help /model /compact /init" 这些斜杠命令怎么定义、注册、解析分派,为明天 Day 12 的 MCP、Day 13 的 Skills 埋下"它们其实都是同一种 Command"的伏笔。贴 command.ts 真实类型逐段讲。
什么是 Slash 命令
/ 开头 = 我要按一个明确的功能键,走本地快捷通道,不必每次都劳驾"大脑"(模型)去理解。在输入框里以 / 开头的就是斜杠命令,比如 /help(看帮助)、/model(换模型)、/compact(手动压缩上下文,Day 10)、/clear(清空对话)。它们大多是不发给模型的本地功能,或者展开成一段给模型的指令(如 /init)。
核心文件:类型定义 src/types/command.ts、注册表 src/commands.ts、解析 src/utils/slashCommandParsing.ts、派发 src/utils/processUserInput/processSlashCommand.tsx。
三种命令形态(真实类型)
一个 Command = 公共基类 + 三选一形态。这是理解整个系统的钥匙。真实类型(command.ts:16、:25):
// local 命令的返回:文本 / 压缩结果 / 跳过
export type LocalCommandResult =
| { type: 'text'; value: string }
| { type: 'compact'; compactionResult: CompactionResult; ... }
| { type: 'skip' }
// prompt 命令:会展开成内容喂给模型
export type PromptCommand = {
type: 'prompt'
source: SettingSource | 'builtin' | 'mcp' | 'plugin' | 'bundled' // ← 记住这个 source
context?: 'inline' | 'fork' // inline=展进当前对话;fork=当子代理跑
hooks?: HooksSettings // 技能可自带 hooks
// ...
}
type: 'prompt'
展开成文本/内容块喂给模型。如 /init、技能。
type: 'local'
纯本地逻辑,不查模型,返回 text/compact/skip。如 /clear、/compact。
type: 'local-jsx'
渲染一个 Ink UI 面板。如 /help、/model、/mcp。
type 字段区分,派发时按类型走不同逻辑。注意 PromptCommand 里那个 source 字段(builtin/mcp/plugin/bundled)——L07 的大一统洞察就靠它。context:'fork' 表示这个命令作为独立子代理跑(Day 15)。命令注册表:memoize 的函数
注册中心 src/commands.ts。核心是 COMMANDS(:297)——一个 memoize 的函数,返回命令数组,而非模块级常量:
const COMMANDS = memoize((): Command[] => [
clearCmd, helpCmd, modelCmd, compactCmd,
feature('DAEMON') && daemonCmd, // ← feature 门控,关了就不加载
// ...
].filter(Boolean))
isEnabled 要读运行时配置——不能在模块加载那一刻就求值。用函数 + memoize(Day 02 讲过的"记忆化"):第一次调用才计算(此时配置已就绪),之后缓存复用。feature('X') && cmd + .filter(Boolean) = "X 功能开着就加这个命令,关着就过滤掉"(配合死代码消除)。命令来源不止内置。loadAllCommands(:536)并行加载多个来源:内置命令 + bundled skills + 插件命令 + 插件 skills + .claude/skills 目录 skills + workflow 命令。
懒加载:启动只读元数据
local 和 local-jsx 命令都用懒加载——注册时只声明元数据,真被调用时才 import 实现。/clear 就是范例:
// src/commands/clear/index.ts —— 只有元数据 + 一个 load()
export default {
type: 'local',
name: 'clear',
aliases: ['reset', 'new'],
description: '清空会话历史',
load: () => import('./clear.js'), // ← 用到才加载真正的实现
}
/clear 才加载。这跟 Day 02 cli.tsx 的 fast-path 动态 import 是同一种"按需加载优化冷启动"思路,贯穿整个项目。解析 "/x args"
你敲 /model opus,怎么拆成"命令 model + 参数 opus"?parseSlashCommand(slashCommandParsing.ts:25)——很朴素:
function parseSlashCommand(text) {
if (!text.trim().startsWith('/')) return null // 不是斜杠命令
const words = text.slice(1).split(/\s+/) // 去掉 /,按空格切
let name = words[0] // 第一个词 = 命令名
// 特判:第二个词是 (MCP) 就拼进名字、标记 isMcp
const args = words.slice(1).join(' ') // 其余 = 参数
return { name, args, isMcp }
}
/,按空格切开;第一个词是命令名,剩下拼成参数字符串。.slice(1) 去掉第一个字符;.split(/\s+/) 按空白切成数组;.slice(1).join(' ') 取除第一个外的其余、再拼回字符串。派发:按 type 走不同路
解析出命令后,processSlashCommand(processSlashCommand.tsx:427)先判断是不是真命令(不是就当普通消息发给模型,或提示"未知命令"),是就按 type switch 派发:
/clear → local → 直接清空历史、不惊动模型;输入
/model → local-jsx → 弹出一个模型选择面板(Ink UI);输入
/init → prompt → 展开成"请扫描本仓库并生成 CLAUDE.md……"这一大段,作为 user 消息发给模型。local-jsx:command.load() → mod.call(onDone, ctx, args),返回的 JSX 通过 setToolJSX 渲染成终端面板;选完调 onDone 把结果并回对话
local:mod.call(args, ctx),结果分 text/compact/skip 三种(就是 L02 的 LocalCommandResult)
prompt:command.getPromptForCommand(args, ctx) 拿到内容块,做参数替换 + @-mention 附件解析,作为 user 消息交给模型(context:'fork' 的走子代理)
/model 弹出的模型选择面板,你选完之后要"往对话里插一条'已切换到 opus'的消息"——这就是 onDone 干的。面板是临时 UI(覆盖层,Day 04),选完调 onDone 收尾。prompt 命令的 @-mention:/review @src/app.ts 里的 @文件 会被解析成附件塞进给模型的消息——命令能带上文件内容。大一统洞察:它们都是 Command(真实字段佐证)
💡 关键洞察:skills、插件命令、bundled 命令、MCP prompt —— 本质上都是 type:'prompt' 的 Command,和你手敲的斜杠命令共用同一套解析/派发管线。
证据就在 L02 那个真实类型里——PromptCommand.source 的类型是:
source: SettingSource | 'builtin' | 'mcp' | 'plugin' | 'bundled'
PromptCommand 类型,靠 source 字段区分它从哪来:内置命令(builtin)、MCP 提供的(mcp)、插件的(plugin)、打包的技能(bundled)……它们是同一种东西,只是来源标签不同。所以不用为它们各写一套机制。source。这是优秀抽象的威力:用一个统一模型覆盖多个表面不同的需求。Day 13 讲 skills 时你会看到,一个 SKILL.md 最后就是被包成一个 type:'prompt' 的 Command;Day 15 你会看到多代理只是递归 query——都是同一种"统一抽象"思想。今日小结 + 动手
🧠 今天你应该能回答
- 三种命令形态(prompt/local/local-jsx)各干什么?
- 注册表 COMMANDS 为什么是 memoize 函数而非常量?
- 命令为什么懒加载?
- "/x args" 怎么解析、怎么按 type 派发?
- 为什么说 skills/插件命令/MCP prompt 都是 Command?靠哪个字段区分来源?(source)
✋ 动手:对着真实代码读一遍
# 1. 命令三形态类型 + source 字段(L02/L07)
sed -n '16,55p' src/types/command.ts
# 2. 注册表 memoize 函数(L03)
sed -n '297,340p' src/commands.ts
# 3. 一个懒加载命令(L04)
cat src/commands/clear/index.ts
# 4. 解析(L05)
cat src/utils/slashCommandParsing.ts
# 5. 派发 switch(L06)
sed -n '691,760p' src/utils/processUserInput/processSlashCommand.tsx