在上一篇文章中,我们剖析了 Claude Code 命令系统的底层架构——从 commands.ts 的注册表组装、命令类型分类(prompt / local-jsx / local),到命令解析与分发流程。本文将继续深入,聚焦七个具有代表性的进阶命令:/plan、/review、/resume、/diff、/export、/doctor 和 /upgrade。这些命令覆盖了工作流控制、代码审查、会话管理、变更追踪、数据导出、环境诊断和版本升级七大场景,是日常高频使用的核心功能。
1. plan 命令:Plan Mode 的入口与编排
1.1 命令注册与类型定义
/plan 命令在 src/commands/plan/index.ts 中注册(第 1–8 行):
// src/commands/plan/index.ts, 第 1–8 行
import type { Command } from '../../commands.js'
const plan = {
type: 'local-jsx',
name: 'plan',
description: 'Enable plan mode or view the current session plan',
argumentHint: '[open|<description>]',
load: () => import('./plan.js'),
} satisfies Command
export default plan这里的关键是 type: 'local-jsx'——/plan 不是简单的文本 prompt 命令,而是一个带有交互式 React 组件的本地命令。它的参数提示 [open|<description>] 表明两种用法:不带参数时进入 Plan Mode;带 open 参数时用外部编辑器打开当前计划文件。
1.2 Plan Mode 的状态切换核心
/plan 命令的核心实现在 src/commands/plan/plan.tsx 中(第 1–120 行,基于 source map 还原)。该组件的核心逻辑分为两条分支:
分支一:当前不在 plan 模式 → 启用 plan 模式
// src/commands/plan/plan.tsx, 第 35–55 行(基于 source map 还原)
const appState = getAppState();
const currentMode = appState.toolPermissionContext.mode;
if (currentMode !== 'plan') {
handlePlanModeTransition(currentMode, 'plan');
setAppState(prev => ({
...prev,
toolPermissionContext: applyPermissionUpdate(
prepareContextForPlanMode(prev.toolPermissionContext),
{ type: 'setMode', mode: 'plan', destination: 'session' }
)
}));
// ...
}这里涉及三个关键函数:
handlePlanModeTransition(oldMode, 'plan'):来自src/bootstrap/state.js,负责在状态层面记录模式切换的副作用(如清理 Auto Mode 的状态标记)。prepareContextForPlanMode(context):来自src/utils/permissions/permissionSetup.ts(第 1–50 行),执行 Plan Mode 的权限上下文预处理——当用户默认模式为auto时,它会触发分类器(classifier)的激活副作用。applyPermissionUpdate(..., { type: 'setMode', mode: 'plan', destination: 'session' }):将权限模式强制设为plan,并限定作用域为当前session。
分支二:已在 plan 模式 → 显示或编辑计划
// src/commands/plan/plan.tsx, 第 57–85 行
const planContent = getPlan();
const planPath = getPlanFilePath();
if (!planContent) {
onDone('Already in plan mode. No plan written yet.');
return null;
}
const argList = args.trim().split(/\s+/);
if (argList[0] === 'open') {
const result = await editFileInEditor(planPath);
// ...
}当用户输入 /plan open 时,命令会调用 editFileInEditor 打开计划文件。计划文件的路径由 getPlanFilePath() 决定,该函数在 src/utils/plans.ts 中实现(第 50–90 行),它基于会话 ID 生成一个唯一的 word slug(如 brave-fox-1234.md),并将计划文件存储在 ~/.claude/plans/ 目录下。
1.3 与 EnterPlanModeTool / ExitPlanModeTool 的关系
Plan Mode 有两条进入路径:用户主动输入 /plan,或 Claude 主动调用 EnterPlanModeTool。后者的实现在 src/tools/EnterPlanModeTool/EnterPlanModeTool.ts(第 1–90 行):
// src/tools/EnterPlanModeTool/EnterPlanModeTool.ts, 第 55–85 行
async call(_input, context) {
if (context.agentId) {
throw new Error('EnterPlanMode tool cannot be used in agent contexts')
}
const appState = context.getAppState()
handlePlanModeTransition(appState.toolPermissionContext.mode, 'plan')
context.setAppState(prev => ({
...prev,
toolPermissionContext: applyPermissionUpdate(
prepareContextForPlanMode(prev.toolPermissionContext),
{ type: 'setMode', mode: 'plan', destination: 'session' }
)
}))
// ...
}对比可见,EnterPlanModeTool.call() 与 /plan 命令的核心状态更新逻辑完全一致——都调用相同的 handlePlanModeTransition 和 prepareContextForPlanMode。差异仅在于触发源:一个是用户通过斜杠命令触发,另一个是模型在 tool use 中自行决定进入 Plan Mode。这种设计确保了无论哪条路径进入 Plan Mode,系统状态都是一致的。
退出 Plan Mode 则通过 ExitPlanModeTool 完成,其常量定义在 src/tools/ExitPlanModeTool/constants.ts(第 1–2 行):
export const EXIT_PLAN_MODE_TOOL_NAME = 'ExitPlanMode'
export const EXIT_PLAN_MODE_V2_TOOL_NAME = 'ExitPlanMode'值得注意的是,当 --channels 参数激活时(KAIROS 功能),EnterPlanModeTool 的 isEnabled() 会返回 false,防止模型进入一个无法通过终端对话框退出的陷阱状态。
flowchart TD
A[用户输入 /plan] --> B{当前模式}
B -->|非 plan| C[handlePlanModeTransition]
C --> D[prepareContextForPlanMode]
D --> E[applyPermissionUpdate setMode=plan]
E --> F[进入 Plan Mode]
B -->|已是 plan| G{参数}
G -->|无参| H[getPlan 显示内容]
G -->|open| I[editFileInEditor 打开文件]
J[模型调用 EnterPlanModeTool] --> C
K[模型调用 ExitPlanModeTool] --> L[退出 Plan Mode]2. review 命令:本地代码审查与 Ultrareview
2.1 命令注册与双轨架构
/review 命令定义在 src/commands/review.ts(第 1–57 行),它实际上是一个双轨架构:本地 /review 和远程 /ultrareview。
// src/commands/review.ts, 第 1–57 行
import type { ContentBlockParam } from '@anthropic-ai/sdk/resources/messages.js'
import type { Command } from '../commands.js'
import { isUltrareviewEnabled } from './review/ultrareviewEnabled.js'
const CCR_TERMS_URL = 'https://code.claude.com/docs/en/claude-code-on-the-web'
const LOCAL_REVIEW_PROMPT = (args: string) => `
You are an expert code reviewer. Follow these steps:
1. If no PR number is provided in the args, run \`gh pr list\` to show open PRs
2. If a PR number is provided, run \`gh pr view <number>\` to get PR details
3. Run \`gh pr diff <number>\` to get the diff
4. Analyze the changes and provide a thorough code review...
`
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) }]
},
}/review 的类型是 prompt,这意味着它不像 /plan 那样渲染自定义 React 组件,而是构造一段文本 prompt 发送给模型。这段 prompt 明确指示模型:
- 若无 PR 号,先执行
gh pr list列出开放的 PR - 获取 PR 详情:
gh pr view <number> - 获取 diff:
gh pr diff <number> - 从代码正确性、项目规范、性能影响、测试覆盖、安全考量五个维度输出审查报告
2.2 Ultrareview:远程 Bug Hunter 路径
// src/commands/review.ts, 第 46–57 行
const ultrareview: Command = {
type: 'local-jsx',
name: 'ultrareview',
description: `~10–20 min · Finds and verifies bugs in your branch. Runs in Claude Code on the web. See ${CCR_TERMS_URL}`,
isEnabled: () => isUltrareviewEnabled(),
load: () => import('./review/ultrareviewCommand.js'),
}/ultrareview 是唯一进入远程 bughunter 路径的入口。它的启用由 isUltrareviewEnabled() 控制,该函数在 src/commands/review/ultrareviewEnabled.ts(第 1–14 行)中实现:
// src/commands/review/ultrareviewEnabled.ts, 第 1–14 行
import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js'
export function isUltrareviewEnabled(): boolean {
const cfg = getFeatureValue_CACHED_MAY_BE_STALE<Record<
string,
unknown
> | null>('tengu_review_bughunter_config', null)
return cfg?.enabled === true
}这里使用了 GrowthBook 的功能开关(feature flag)进行运行时门控。当 tengu_review_bughunter_config.enabled 为 false 时,isEnabled() 返回 false,命令甚至不会出现在 getCommands() 的结果中,对用户完全不可见。这种设计在产品灰度发布(staged rollout)中非常常见。
/ultrareview 的 type: 'local-jsx' 意味着它有一个自定义的 React 组件界面。当用户的免费审查次数耗尽时,local-jsx 类型会渲染一个overage permission dialog(超额权限对话框),提示用户升级或购买额度。
2.3 审查流程的完整链路
一条典型的 /review 42 执行链路如下:
- 用户在输入框中输入
/review 42 CommandParser将args解析为"42"review.getPromptForCommand("42")返回包含LOCAL_REVIEW_PROMPT("42")的ContentBlockParam[]QueryEngine将该 prompt 附加到消息列表中,发送给 Claude- Claude 按 prompt 指示,依次调用
BashTool执行gh pr view 42和gh pr diff 42 - Claude 综合 PR 描述、diff 内容和项目上下文,生成结构化审查报告
整个流程中,Claude Code 本身不直接解析 PR 数据——它依赖模型通过 gh CLI 工具获取数据并自行分析。这种"prompt-driven"架构极大简化了命令实现,但也对模型的指令遵循能力提出了较高要求。
3. resume 命令:会话恢复与跨项目导航
3.1 命令注册与别名
// src/commands/resume/index.ts, 第 1–10 行
import type { Command } from '../../commands.js'
const resume: Command = {
type: 'local-jsx',
name: 'resume',
description: 'Resume a previous conversation',
aliases: ['continue'],
argumentHint: '[conversation id or search term]',
load: () => import('./resume.js'),
}
export default resume/resume 有一个别名 /continue,这在用户肌肉记忆兼容性上做了考虑。它的参数可以是会话 ID(UUID)或搜索关键词,也可以留空以打开选择器。
3.2 恢复流程的三层搜索策略
/resume 的核心逻辑在 src/commands/resume/resume.tsx 中(第 1–220 行,基于 source map 还原)。当用户提供参数时,命令执行三层递进式搜索:
第一层:UUID 精确匹配
// src/commands/resume/resume.tsx, 第 155–175 行
const maybeSessionId = validateUuid(arg);
if (maybeSessionId) {
const matchingLogs = logs.filter(l => getSessionIdFromLog(l) === maybeSessionId)
.sort((a, b) => b.modified.getTime() - a.modified.getTime());
if (matchingLogs.length > 0) {
const log = matchingLogs[0]!;
const fullLog = isLiteLog(log) ? await loadFullLog(log) : log;
void onResume(maybeSessionId, fullLog, 'slash_command_session_id');
return null;
}
// 若 enriched logs 未找到,尝试直接文件查找
const directLog = await getLastSessionLog(maybeSessionId);
if (directLog) {
void onResume(maybeSessionId, directLog, 'slash_command_session_id');
return null;
}
}这里有一个关键细节:isLiteLog(log) 的检查。Claude Code 的会话日志有两种存储格式:
- Lite Log:只包含元数据(标题、时间戳、消息摘要),用于快速列表渲染
- Full Log:包含完整的消息内容,用于恢复会话
当通过 UUID 找到的是 lite log 时,必须调用 loadFullLog() 加载完整内容后才能恢复。
第二层:自定义标题精确匹配(需功能开启)
// src/commands/resume/resume.tsx, 第 177–195 行
if (isCustomTitleEnabled()) {
const titleMatches = await searchSessionsByCustomTitle(arg, { exact: true });
if (titleMatches.length === 1) {
const log = titleMatches[0]!;
const sessionId = getSessionIdFromLog(log);
if (sessionId) {
const fullLog = isLiteLog(log) ? await loadFullLog(log) : log;
void onResume(sessionId, fullLog, 'slash_command_title');
return null;
}
}
if (titleMatches.length > 1) {
// 多个匹配 → 提示用户用 /resume 选择特定会话
const message = resumeHelpMessage({ resultType: 'multipleMatches', arg, count: titleMatches.length });
return <ResumeError message={message} args={arg} onDone={() => onDone(message)} />;
}
}第三层:无匹配 → 错误提示
// src/commands/resume/resume.tsx, 第 197–202 行
const message = resumeHelpMessage({ resultType: 'sessionNotFound', arg });
return <ResumeError message={message} args={arg} onDone={() => onDone(message)} />;3.3 跨项目恢复与工作树检测
当用户未提供参数时,ResumeCommand 组件会渲染一个交互式选择器(LogSelector),支持在同仓库的不同工作树(worktree)甚至跨项目之间恢复会话。
跨项目恢复的逻辑在 src/utils/crossProjectResume.ts(第 1–55 行):
// src/utils/crossProjectResume.ts, 第 18–55 行
export function checkCrossProjectResume(
log: LogOption,
showAllProjects: boolean,
worktreePaths: string[],
): CrossProjectResumeResult {
const currentCwd = getOriginalCwd()
if (!showAllProjects || !log.projectPath || log.projectPath === currentCwd) {
return { isCrossProject: false }
}
// 检查是否为同一 repo 的不同 worktree
const isSameRepo = worktreePaths.some(
wt => log.projectPath === wt || log.projectPath!.startsWith(wt + sep),
)
if (isSameRepo) {
return { isCrossProject: true, isSameRepoWorktree: true, projectPath: log.projectPath }
}
// 不同 repo → 生成 cd 命令并复制到剪贴板
const sessionId = getSessionIdFromLog(log)
const command = `cd ${quote([log.projectPath])} && claude --resume ${sessionId}`
return { isCrossProject: true, isSameRepoWorktree: false, command, projectPath: log.projectPath }
}这里的工程考量非常细致:
- 同一 repo 的 worktree:可以直接恢复,无需切换目录
- 不同项目:生成
cd <path> && claude --resume <id>命令,自动复制到剪贴板,用户粘贴即可执行
3.4 无参数模式:交互式选择器
// src/commands/resume/resume.tsx, 第 205–220 行
export const call: LocalJSXCommandCall = async (onDone, context, args) => {
const arg = args?.trim();
if (!arg) {
return <ResumeCommand key={Date.now()} onDone={onDone} onResume={onResume} />;
}
// ... 参数匹配逻辑
}ResumeCommand 组件内部使用 useState 管理 logs、loading、resuming 等状态,并通过 useTerminalSize 动态计算选择器高度。当 showAllProjects 切换时,它会重新调用 loadAllProjectsMessageLogs() 或 loadSameRepoMessageLogs() 加载对应的会话列表。
4. diff 命令:Git Diff 与回合级变更追踪
4.1 极简的命令入口
// src/commands/diff/index.ts, 第 1–7 行
import type { Command } from '../../commands.js'
export default {
type: 'local-jsx',
name: 'diff',
description: 'View uncommitted changes and per-turn diffs',
load: () => import('./diff.js'),
} satisfies Command/diff 的命令入口极其简洁,所有复杂度都下沉到了 DiffDialog 组件中。
4.2 DiffDialog:双层数据来源
src/commands/diff/diff.tsx(第 1–10 行)只做一件事:动态加载并渲染 DiffDialog:
// src/commands/diff/diff.tsx, 第 1–10 行
import * as React from 'react';
import type { LocalJSXCommandCall } from '../../types/command.js';
export const call: LocalJSXCommandCall = async (onDone, context) => {
const { DiffDialog } = await import('../../components/diff/DiffDialog.js');
return <DiffDialog messages={context.messages} onDone={onDone} />;
};DiffDialog 在 src/components/diff/DiffDialog.tsx(第 1–120 行)中实现,它管理两种 diff 数据来源:
- Current Diff(当前未提交的 Git 变更):通过
useDiffData()hook 获取 - Turn Diff(每个 AI 回合产生的文件变更):通过
useTurnDiffs(messages)hook,从消息历史中解析出每个回合的StructuredPatchHunk
// src/components/diff/DiffDialog.tsx, 第 35–55 行
function turnDiffToDiffData(turn: TurnDiff): DiffData {
const files = Array.from(turn.files.values()).map(f => ({
path: f.filePath,
linesAdded: f.linesAdded,
linesRemoved: f.linesRemoved,
isBinary: false,
isLargeFile: false,
isTruncated: false,
isNewFile: f.isNewFile
})).sort((a, b) => a.path.localeCompare(b.path));
const hunks = new Map<string, StructuredPatchHunk[]>();
for (const f of turn.files.values()) {
hunks.set(f.filePath, f.hunks);
}
return { stats: turn.stats, files, hunks, loading: false };
}4.3 视图模式与交互
DiffDialog 内部维护 ViewMode('list' | 'detail')和 sourceIndex(数据来源索引)。用户可以在:
- 列表视图:查看所有变更文件的概览(新增/删除行数)
- 详情视图:查看选中文件的完整 hunk 级 diff
sourceIndex 的索引 0 总是对应 current(Git 工作区未提交变更),索引 1+ 对应历史回合的变更。这种设计让用户能够追溯任意 AI 回合所产生的精确代码变更,对于审查 AI 的编辑行为至关重要。
5. export 命令:会话导出与文件生成
5.1 双模式导出:直接文件 vs 交互式对话框
/export 命令定义在 src/commands/export/index.ts(第 1–10 行):
// src/commands/export/index.ts, 第 1–10 行
import type { Command } from '../../commands.js'
const exportCommand = {
type: 'local-jsx',
name: 'export',
description: 'Export the current conversation to a file or clipboard',
argumentHint: '[filename]',
load: () => import('./export.js'),
} satisfies Command
export default exportCommand核心实现在 src/commands/export/export.tsx(第 1–110 行)。它支持两种调用方式:
方式一:直接指定文件名
// src/commands/export/export.tsx, 第 65–85 行
const filename = args.trim();
if (filename) {
const finalFilename = filename.endsWith('.txt') ? filename
: filename.replace(/\.[^.]+$/, '') + '.txt';
const filepath = join(getCwd(), finalFilename);
try {
writeFileSync_DEPRECATED(filepath, content, { encoding: 'utf-8', flush: true });
onDone(`Conversation exported to: ${filepath}`);
return null;
} catch (error) {
onDone(`Failed to export conversation: ${error instanceof Error ? error.message : 'Unknown error'}`);
return null;
}
}这里有个细节:writeFileSync_DEPRECATED 被标记为 deprecated,说明团队可能正在迁移到异步文件写入,但导出场景下同步写入的简单性仍有其价值。
方式二:无参数 → 渲染 ExportDialog
// src/commands/export/export.tsx, 第 95–105 行
const firstPrompt = extractFirstPrompt(context.messages);
const timestamp = formatTimestamp(new Date());
let defaultFilename: string;
if (firstPrompt) {
const sanitized = sanitizeFilename(firstPrompt);
defaultFilename = sanitized ? `${timestamp}-${sanitized}.txt` : `conversation-${timestamp}.txt`;
} else {
defaultFilename = `conversation-${timestamp}.txt`;
}
return <ExportDialog content={content} defaultFilename={defaultFilename} onDone={...} />;5.2 默认文件名生成策略
默认文件名采用 YYYY-MM-DD-HHMMSS-<sanitized-prompt>.txt 格式,其中:
extractFirstPrompt()从消息列表中提取第一条用户消息的内容,取第一行并截断至 50 字符sanitizeFilename()将特殊字符替换为连字符,确保文件名合法
// src/commands/export/export.tsx, 第 25–55 行
export function extractFirstPrompt(messages: Message[]): string {
const firstUserMessage = messages.find(msg => msg.type === 'user');
if (!firstUserMessage || firstUserMessage.type !== 'user') {
return '';
}
const content = firstUserMessage.message?.content;
let result = '';
if (typeof content === 'string') {
result = content.trim();
} else if (Array.isArray(content)) {
const textContent = content.find(item => item.type === 'text');
if (textContent && 'text' in textContent) {
result = textContent.text.trim();
}
}
result = result.split('\n')[0] || '';
if (result.length > 50) {
result = result.substring(0, 49) + '…';
}
return result;
}
export function sanitizeFilename(text: string): string {
return text.toLowerCase()
.replace(/[^a-z0-9\s-]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '');
}5.3 ExportDialog:剪贴板与文件二选一
ExportDialog(src/components/ExportDialog.tsx,第 1–100 行)提供了一个简单的选项列表:
- Copy to clipboard:调用
setClipboard(content)直接复制到系统剪贴板 - Save to file:进入文件名输入子界面,用户可编辑默认文件名后按 Enter 保存
当用户在文件名输入界面按 Escape 时,对话框会返回选项列表而非直接关闭——这种"子屏幕回退"行为通过 handleCancel 和 handleGoBack 的协调实现,避免了用户误操作导致导出取消。
6. doctor 命令:环境诊断与健康检查
6.1 命令注册与条件启用
// src/commands/doctor/index.ts, 第 1–11 行
import type { Command } from '../../commands.js'
import { isEnvTruthy } from '../../utils/envUtils.js'
const doctor: Command = {
name: 'doctor',
description: 'Diagnose and verify your Claude Code installation and settings',
isEnabled: () => !isEnvTruthy(process.env.DISABLE_DOCTOR_COMMAND),
type: 'local-jsx',
load: () => import('./doctor.js'),
}
export default doctor/doctor 可以通过环境变量 DISABLE_DOCTOR_COMMAND 完全禁用,这在 CI/CD 环境或企业部署中可能很有用。
6.2 极简入口,复杂屏幕
// src/commands/doctor/doctor.tsx, 第 1–8 行
import React from 'react';
import { Doctor } from '../../screens/Doctor.js';
import type { LocalJSXCommandCall } from '../../types/command.js';
export const call: LocalJSXCommandCall = (onDone, _context, _args) => {
return Promise.resolve(<Doctor onDone={onDone} />);
};命令层几乎没有任何逻辑,所有诊断工作都在 Doctor 屏幕组件中完成。Doctor 屏幕在 src/screens/Doctor.tsx(第 1–120 行)中实现,它集成了大量诊断模块:
6.3 诊断维度全景
Doctor 屏幕的诊断信息涵盖以下维度:
| 维度 | 检查内容 | 相关模块 |
|---|---|---|
| 安装信息 | 安装方式(npm-global / native / package-manager 等)、版本号、安装路径 | doctorDiagnostic.ts |
| 自动更新 | 更新通道、最新版本号、更新权限 | autoUpdater.ts |
| 配置验证 | 设置文件语法错误、无效值、冲突项 | useSettingsErrors.ts |
| 快捷键 | 重复绑定、保留键冲突、无效配置 | KeybindingWarnings.ts |
| MCP 服务 | MCP 配置文件解析错误、连接失败 | McpParsingWarnings.ts |
| 沙箱 | 沙箱状态、权限问题 | SandboxDoctorSection.ts |
| 上下文 | 上下文警告(文件过多、token 超限等) | doctorContextWarnings.ts |
| 锁文件 | PID 锁文件状态、陈旧锁清理 | pidLock.ts |
| ripgrep | rg 可用性、工作模式(system / builtin / embedded) | ripgrep.ts |
安装类型诊断在 src/utils/doctorDiagnostic.ts(第 1–90 行)中实现:
// src/utils/doctorDiagnostic.ts, 第 25–55 行
export type DiagnosticInfo = {
installationType: InstallationType
version: string
installationPath: string
invokedBinary: string
configInstallMethod: InstallMethod | 'not set'
autoUpdates: string
hasUpdatePermissions: boolean | null
multipleInstallations: Array<{ type: string; path: string }>
warnings: Array<{ issue: string; fix: string }>
recommendation?: string
packageManager?: string
ripgrepStatus: { working: boolean; mode: 'system' | 'builtin' | 'embedded'; systemPath: string | null }
}getCurrentInstallationType() 函数通过检测 process.argv[1]、process.execPath 以及多种包管理器(Homebrew、Winget、Mise、asdf、Pacman、RPM、APK、Deb)的存在性,推断出当前 Claude Code 的安装方式。这种多路径检测确保了无论用户通过哪种方式安装,都能获得准确的诊断信息。
版本信息的获取则通过 getNpmDistTags() 和 getGcsDistTags() 同时查询 npm registry 和 Google Cloud Storage 的 dist-tags,确保即使某一上游不可用,也能获取到版本信息。
7. upgrade 命令:版本检查与订阅升级
7.1 命令注册与可用性控制
// src/commands/upgrade/index.ts, 第 1–14 行
import type { Command } from '../../commands.js'
import { getSubscriptionType } from '../../utils/auth.js'
import { isEnvTruthy } from '../../utils/envUtils.js'
const upgrade = {
type: 'local-jsx',
name: 'upgrade',
description: 'Upgrade to Max for higher rate limits and more Opus',
availability: ['claude-ai'],
isEnabled: () =>
!isEnvTruthy(process.env.DISABLE_UPGRADE_COMMAND) &&
getSubscriptionType() !== 'enterprise',
load: () => import('./upgrade.js'),
} satisfies Command
export default upgrade/upgrade 有两个关键的可用性限制:
availability: ['claude-ai']:仅在使用 Claude AI 认证时可用(API Key 模式不适用)getSubscriptionType() !== 'enterprise':企业订阅用户不需要通过此命令升级
7.2 升级流程:浏览器跳转与重新登录
核心实现在 src/commands/upgrade/upgrade.tsx(第 1–70 行):
// src/commands/upgrade/upgrade.tsx, 第 15–55 行
export async function call(onDone, context) {
try {
// 检查是否已是最高 Max 计划(20x)
if (isClaudeAISubscriber()) {
const tokens = getClaudeAIOAuthTokens();
let isMax20x = false;
if (tokens?.subscriptionType && tokens?.rateLimitTier) {
isMax20x = tokens.subscriptionType === 'max' && tokens.rateLimitTier === 'default_claude_max_20x';
} else if (tokens?.accessToken) {
const profile = await getOauthProfileFromOauthToken(tokens.accessToken);
isMax20x = profile?.organization?.organization_type === 'claude_max'
&& profile?.organization?.rate_limit_tier === 'default_claude_max_20x';
}
if (isMax20x) {
setTimeout(onDone, 0, 'You are already on the highest Max subscription plan...');
return null;
}
}
const url = 'https://claude.ai/upgrade/max';
await openBrowser(url);
return <Login startingMessage={'Starting new login following /upgrade...'} onDone={success => {
context.onChangeAPIKey();
onDone(success ? 'Login successful' : 'Login interrupted');
}} />;
} catch (error) {
logError(error as Error);
setTimeout(onDone, 0, 'Failed to open browser...');
}
return null;
}升级流程设计得非常简洁:
- 前置检查:若用户已是 Max 20x 计划,直接提示无需升级
- 打开浏览器:调用
openBrowser('https://claude.ai/upgrade/max'),将用户引导至网页端完成支付 - 重新登录:支付完成后,渲染
Login组件让用户重新认证,以刷新 OAuth token 中的订阅信息 - 刷新 API Key:登录成功后调用
context.onChangeAPIKey(),通知系统重新加载带有新权限的凭证
值得注意的是,订阅 tier 的判定有两条路径:
- 本地 token 缓存:优先检查内存中的
tokens.subscriptionType和tokens.rateLimitTier - 远程 profile 查询:若本地信息不完整,则通过
getOauthProfileFromOauthToken()调用远程 API 获取最新组织信息
这种双重检查确保了即使在 token 刷新延迟的情况下,也能准确判断用户当前的订阅状态。
8. 总结:命令设计的工程哲学
通过对这七个进阶命令的源码分析,我们可以总结出 Claude Code 命令系统的几条核心设计原则:
1. 分层解耦:命令层薄,能力层厚
所有命令的 index.ts 和 call() 入口都极其简洁。/diff 只有 5 行代码,/doctor 只有 3 行。真正的复杂度被下沉到 components/、screens/、utils/ 等复用层中。这种"薄命令、厚能力"的分层使得新增一个斜杠命令的成本极低。
2. 统一的 Local JSX 模式
/plan、/resume、/diff、/export、/doctor、/upgrade 都采用了 type: 'local-jsx',共享同一套 LocalJSXCommandCall 接口契约(onDone、context、args)。这种一致性让开发者可以像写普通 React 组件一样写命令界面,同时通过 onDone 回调与主循环无缝衔接。
3. Prompt 驱动的简单命令
/review 采用了 type: 'prompt',不渲染任何自定义 UI,仅靠一段精心设计的 prompt 让模型自行完成全部工作。对于流程明确、依赖外部 CLI 工具(如 gh)的场景,prompt-driven 架构比硬编码逻辑更具灵活性和可维护性。
4. 渐进式搜索与降级策略
/resume 的三层搜索(UUID → 标题 → 错误提示)、/upgrade 的双重 tier 判定(本地 token → 远程 profile)都体现了渐进式探测的工程思想:先尝试最快路径,失败后再降级到更重的查询,最终给出明确的用户反馈。
5. 功能开关与环境隔离
/ultrareview 使用 GrowthBook feature flag,/doctor 和 /upgrade 都支持通过环境变量完全禁用,/upgrade 还区分了 claude-ai 和 API Key 两种 availability。这些机制让 Claude Code 能够在同一套代码库上支持免费/付费、个人/企业、稳定/实验等多种产品形态。
在下一篇文章中,我们将继续深入命令系统,探讨 /config、/mcp、/login 等涉及外部集成和配置管理的命令实现。