☰
oh-my-pi coding-agent 斜杠命令深度解析:从多源发现到模板展开
2026/10/19 0:46:43 网站建设 项目流程

oh-my-pi coding-agent 斜杠命令深度解析:从多源发现到模板展开

【免费下载链接】oh-my-pi⌥ Coding agent with the IDE wired in项目地址: https://gitcode.com/GitHub_Trending/oh/oh-my-pi

oh-my-pi 的coding-agent支持来自多个外部工具(Claude Code、OpenAI Codex、OpenCode 等)以及自身原生目录的 Markdown 斜杠命令。本文基于仓库内 slash-command-internals.md 的设计文档,结合 capability 注册表、命令展开工具 与 交互式模式实现 逐层剖析:命令如何被发现、按什么优先级去重、在交互界面里如何呈现,以及提交 prompt 时如何被展开并路由到 LLM。读完你能掌握「命令从磁盘文件到实际 prompt」的完整链路,并能准确解释同名命令为何某个赢、某个被标记为 shadowed。

一、发现模型:capability 注册表与 first-wins 去重

斜杠命令在coding-agent中被建模为一个 capability(能力),其定义位于 slash-command.ts:

export const slashCommandCapability = defineCapability<SlashCommand>({ id: "slash-commands", displayName: "Slash Commands", description: "Custom slash commands defined as markdown files", key: cmd => cmd.name, toExtensionId: cmd => `slash-command:${cmd.name}`, validate: cmd => { /* name/path/content/level 校验 */ }, });

核心契约:

  • id 为slash-commands:所有 provider 通过这个 id 注册自己,注册入口是 builtin.ts 等文件里的registerProvider<SlashCommand>(slashCommandCapability.id, {...})调用。
  • key 是命令名(cmd.name):去重完全以名字为准。
  • validate 校验四要素:name非空、path非空、content非 undefined、level必须是user/project/native三者之一。校验失败的条目会被注册表丢弃并记录警告。

优先级排序与 first-wins 语义

capability/index.ts 中的registerProvider会把 provider 按 priority降序插入数组(findIndex(p => p.priority < provider.priority)),加载时loadImpl并行调用所有 provider,然后按注册顺序遍历条目执行去重。源码中的去重逻辑:

for (const item of allItems) { const key = capability.key(item); if (key === undefined) { deduped.push(item); continue; } const keySeen = seen.has(key); seen.add(key); // ... if (keySeen || aliasSeen) { item._shadowed = true; } else { deduped.push(item); } }

即先出现(优先级最高)的条目赢得 key 所有权,后来同名者被标记_shadowed = true。这个行为跨 provider 生效,同一 provider 内部返回重名也会触发。

当前 provider 优先级

优先级provider说明
100nativeOMP 原生(.omp目录),见 builtin.ts 中PRIORITY = 100
90omp-plugins扩展包 / npm / link 插件,见 omp-plugins.ts
80claudeClaude Code(.claude),见 claude.ts
70claude-pluginsClaude 插件市场/本地插件,见 claude-plugins.ts
70agents.agent/.agents标准目录,见 agents.ts
70codexOpenAI Codex,见 codex.ts
55opencodeOpenCode,见 opencode.ts

平级(tie)行为:优先级相同的 provider 保持注册顺序。当前导入顺序是claude-plugins→agents→codex,因此三者同名碰撞时插件命令胜出。这一点可以从 capability/index.ts 的插入策略推断:只有priority <才插到前面,相等时追加到队尾。

内建命令不走这个 capability

注意一个关键边界:内建命令(built-ins)不是slash-commandscapability 的条目。它们住在统一的内建注册表 builtin-registry.ts,在 TUI 和 ACP/RPC 模式下先于会话级扩展/自定义/文件命令被分派;自动补全与 ACP 可用性也优先预留内建名字和别名。这一设计让/help、/model等系统命令永远不会被用户文件命令覆盖。

文件扫描行为

各 provider 大多复用 helpers.ts 中的loadFilesFromDir(...)。该函数:

  • 默认非递归匹配*.md(recursive参数默认false,pattern 为*.md而非**/*.md);
  • 使用原生 glob,gitignore: true、hidden: false、fileType: FileType.File;
  • 并行读取匹配文件并转换(transform)成SlashCommand条目。

源码中有一段注释解释了为什么必须显式传递recursive:

原生 glob 默认recursive为 true 并会把*.{ts,js}重写为**/*.{ts,js},否则会遍历整棵子树(例如把~/.codex/tools下 venv 的 site-packages 当成工具导入,#8552)。

因此隐藏文件/目录不会被加载、被 git 忽略的路径会被跳过,文件顺序遵循原生 glob 返回顺序,除非 provider 自己追加排序。

二、各 provider 的源路径与本地优先级

nativeprovider(builtin.ts)

搜索根来自.omp目录,getConfigDirs()返回项目优先:

  • 项目级:<cwd>/.omp/commands/*.md
  • 用户级:当前激活 profile 的 agent 目录commands/*.md(默认 profile 是~/.omp/agent/commands/*.md;命名 profile 是~/.omp/profiles/<name>/agent/commands/*.md)

源码确认:

const projectDir = await ifNonEmptyDir(ctx.cwd, PATHS.projectDir); if (projectDir) result.push({ dir: projectDir, level: "project" }); const userDir = await ifNonEmptyDir(getAgentDir()); if (userDir) result.push({ dir: userDir, level: "user" });

所以项目级 native 命令在同名时压过用户级 native 命令。native 的 slash 命令 loader 只匹配md扩展名,命令名是文件名去掉.md后缀。

omp-pluginsprovider(omp-plugins.ts)

扫描已配置扩展包根目录以及已启用 npm/link 插件中的commands/*.md。根优先级依次为:调用/CLI → 项目 settings → 用户 settings → 已安装插件。市场(marketplace)根在此处被刻意排除,避免重复发现,改由claude-plugins处理。

claudeprovider(claude.ts)

受commands.enableClaudeUser与commands.enableClaudeProject两个 settings 门控:

  • 用户级:~/.claude/commands/**/*.md(递归)
  • 项目级:<cwd>/.claude/commands/**/*.md(递归)

递归扫描后有一个 Claude 特有的处理:子目录中的命令额外获得一个命名空间别名。addClaudeCommandNamespaceAliases把foo/bar.md同时注册为bar(原始名)和foo:bar(别名):

nestedCommands.push(command); aliases.push({ ...command, name: relativeName.replace(/[\\/]+/g, ":") });

注意该 provider 先 push 用户条目再 push 项目条目,所以在此 provider 内部同名碰撞时用户 Claude 命令压过项目 Claude 命令(跨 provider 仍由全局优先级裁决)。

codexprovider(codex.ts)

加载:

  • 用户级:~/.codex/commands/*.md
  • 项目级:<cwd>/.codex/commands/*.md

两侧加载后以用户优先顺序展平,故同 provider 内用户命令压过项目命令。Codex 命令内容经过parseFrontmatter剥离,frontmatter 中的name可以覆盖命令名,否则使用文件名。

opencodeprovider(opencode.ts)

受commands.enableOpencodeUser与commands.enableOpencodeProject门控:

  • 用户级:~/.config/opencode/commands/*.md
  • 项目级:<cwd>/.opencode/commands/*.md

同样用户优先展平,frontmattername可覆盖文件名。

claude-pluginsprovider(claude-plugins.ts)

通过listClaudePluginRoots(...)读取三份注册表:~/.claude/plugins/installed_plugins.json、~/.omp/plugins/installed_plugins.json,以及从 cwd 解析出的最近项目级注册表。对每个插件根扫描<pluginRoot>/commands/*.md(目录可被插件配置的commands/slash-commands键重映射),命令名加插件名前缀:<plugin>:<command>。

跨三份注册表时按优先级合并而非排序:--plugin-dir注入的根最先,项目级条目次之(对同 plugin id 遮蔽用户级条目),用户级条目最后;对同一 plugin id,OMP 注册表优先于 Claude 注册表。每份注册表内部保留 JSON 数据的逐插件顺序,没有额外排序步骤。

agentsprovider(agents.ts)

从 cwd 向上走到仓库根,扫描.agent/与.agents/下的非递归commands/*.md,然后扫描~/.agent/commands与~/.agents/commands。provider 内部顺序:最近的项目根第一;.agent先于.agents;项目条目先于用户条目。

三、物化为运行时FileSlashCommand

extensibility/slash-commands.ts 中的loadSlashCommands()把 capability 条目转换为 prompt 时刻使用的FileSlashCommand对象:

export interface FileSlashCommand { name: string; description: string; content: string; source: string; // 例如 "via Claude Code (User)" _source?: { providerName: string; level: "user" | "project" | "native" }; }

转换步骤(对每个命令):

  1. 解析 frontmatter/body(parseFrontmatter);
  2. description 来源:
    • 有frontmatter.description则取它;
    • 否则取正文第一行非空内容(超过 60 字符截断并追加...);
  3. 保留解析后的 body 作为可执行模板内容;
  4. 计算展示用 source 字符串,例如via Claude Code Project(via ${providerName} ${首字母大写的 level})。

frontmatter 解析的严重级别与条目 level 相关:

  • 发现来的 user/project 命令使用warn级别解析,解析失败时回退到 key/value 宽松解析;
  • 显式标记native的 capability 条目使用fatal解析;
  • 打包的 fallback 模板同样fatal。

源码对应行:

const { description, body } = parseCommandTemplate(cmd.content, { source: cmd.path ?? `slash-command:${cmd.name}`, level: cmd.level === "native" ? "fatal" : "warn", });

打包的 fallback 命令

文件命令加载完成后,task/commands.ts 中嵌入的EMBEDDED_COMMAND_TEMPLATES会以source: "bundled"追加——仅当同名命令尚未出现时。当前嵌入集合来自构建期以{ type: "text" }内联的 Markdown(如init命令):

import initMd from "../prompts/agents/init.md" with { type: "text" }; const EMBEDDED_COMMANDS = [{ name: "init.md", content: prompt.render(initMd) }]; export const EMBEDDED_COMMAND_TEMPLATES: ReadonlyArray<{ name: string; content: string }> = EMBEDDED_COMMANDS;

这意味着打包命令永远垫底,任何用户/项目/插件命令都可以按名字覆盖它们。

四、交互模式:命令列表从哪来

交互模式(interactive-mode.ts)把多个来源合并用于自动补全与命令路由。构造时先构建 pending 命令列表:

  • 内建命令(BUILTIN_SLASH_COMMANDS,含参数补全与选中命令的内联提示)
  • 扩展注册的命令(extensionRunner.getRegisteredCommands(...))
  • TypeScript 自定义命令(session.customCommands,映射为斜杠命令标签)
  • 可选的skill 命令(/skill:<name>,受skills.enableSkillCommands门控)

随后init()调用refreshSlashCommandState(...),加载文件命令并安装一个自动补全 provider(createPromptActionAutocompleteProvider,一个PromptActionAutocompleteProvider,内部包着CombinedAutocompleteProvider),其候选集包含:

  1. 上述 pending 命令;
  2. 发现的基于文件的命令;
  3. 名字未被内建/hook/自定义/skill/文件命令占用的 prompt-template 命令。

refreshSlashCommandState还会同步session.setSlashCommands(...),保证prompt 展开与自动补全看到同一份文件命令集合(agent-session.ts 的setSlashCommands)。交互模式中对 prompt-template 的排除逻辑值得注意(interactive-mode.ts):

const reservedNames = new Set<string>(); for (const command of this.#pendingSlashCommands) { reservedNames.add(command.name); for (const alias of command.aliases ?? []) reservedNames.add(alias); } for (const command of fileSlashCommands) { reservedNames.add(command.name); for (const alias of command.aliases ?? []) reservedNames.add(alias); } const promptTemplateCommands = this.session.promptTemplates .filter(template => !reservedNames.has(template.name)) .map(...);

注释解释了原因:AgentSession.prompt()先expandSlashCommand再expandPromptTemplate,内建命令执行会先解析别名,所以补全候选要镜像这个解析顺序,避免同一个 token 既指向文件命令又指向 prompt 模板。

刷新生命周期

斜杠命令状态在这些时机被刷新:

  • 交互模式初始化时;
  • /move切换工作目录后(applyCwdChange重置 capability 并对新 cwd 重新发现,interactive-mode.ts 中可见对refreshSlashCommandState(newCwd)的调用,且支持切回原目录时回滚);
  • 编辑器组件被替换时(selector-controller.ts);
  • 显式插件重载流程,如/reload-plugins。

没有持续的文件 watcher——命令目录变化不会实时反映,必须依赖上述刷新点。

其他展示面

Extensions 仪表盘也加载slash-commandscapability,展示 active/shadowed 命令条目,包括带_shadowed标记的重复项——这是诊断「为什么我的命令没生效」的直接入口。

五、路由与 prompt 管线中的位置

统一内建注册表在 TUI 与 ACP/RPC 模式下先于AgentSession.prompt(...)被检查。一个内建命令可以消费输入,也可以返回残余的 prompt 文本。TUI-only 内建命令不出现在 ACP 可用列表与分派中;ACP 可见的内建命令是有 text-modehandle的条目(见 acp-builtins.ts)。

越过内建边界后,AgentSession.prompt(...)在expandPromptTemplates !== false时按以下顺序处理斜杠输入(agent-session.ts):

  1. 扩展命令(#tryExecuteExtensionCommand):/name命中扩展注册命令时立即执行 handler,prompt 返回false(已本地消费,不发给 LLM);
  2. TypeScript 自定义命令与 MCP prompt 命令(#tryExecuteCustomCommand):命中时可能返回
    • string→ 用该字符串替换 prompt 文本(若为空串则表示已处理,不再发 LLM);
    • void/undefined→ 视为已处理,不产生 LLM prompt;
  3. 基于文件的斜杠命令(expandSlashCommand):文本仍以/开头时尝试 Markdown 命令展开;
  4. Prompt 模板(expandPromptTemplate):在斜杠/自定义处理之后应用;
  5. 投递:
    • idle:prompt 直接发给 agent;
    • streaming:按streamingBehavior排队为 steer/follow-up。

源码中这段管线一目了然:

if (expandPromptTemplates && text.startsWith("/")) { const handled = await this.#tryExecuteExtensionCommand(text); if (handled) return false; const customResult = await this.#tryExecuteCustomCommand(text); if (customResult !== null) { if (customResult === "") return false; text = customResult; } // Only if text still starts with "/" (wasn't transformed by custom command) if (text.startsWith("/")) { text = expandSlashCommand(text, this.#slashCommands); } } const expandedText = expandPromptTemplates ? expandPromptTemplate(text, [...this.#promptTemplates]) : text;

这也解释了两个设计决策:内建名字在文件命令之前被预留;文件命令展开先于 prompt-template 展开;自定义命令可以「抹掉」开头的/,从而阻止后续文件命令匹配。

六、基于文件的斜杠命令展开语义

expandSlashCommand(text, fileCommands)(extensibility/slash-commands.ts)的行为:

  • 仅当文本以/开头时运行;
  • 从/后的第一个 token 解析命令名(空格前);
  • 剩余文本经parseCommandArgs解析为参数;
  • 在已加载的fileCommands中做精确名字匹配(不模糊、不补全);
  • 命中后依次应用:
    • 位置替换:$1、$2…(1-based,越界得到空串);
    • 切片替换:$@[start]/$@[start:length](1-based,start 越界或 length 非正得到空串);
    • 聚合替换:$ARGUMENTS与$@;
    • prompt.render渲染,上下文为{ args, ARGUMENTS, arguments };
    • 当模板没有使用任何内联参数占位符时,把参数追加在末尾(inline-argument fallback,appendInlineArgsFallback)。

$替换的正则(command-args.ts):

content.replace( /\$@\[(\d+)(?::(\d*)?)?\]|\$ARGUMENTS|\$@|\$(\d+)/g, (match, startRaw?, lengthRaw?, positionalNum?) => { ... } );

文档特别强调:替换只发生在模板字符串上。参数值里包含$1、$@等模式不会被递归替换——这避免了二次注入式展开。

parseCommandArgs的注意点

该解析器是简单的引号感知分词(command-args.ts):

  • 支持'单引号'与"双引号"保留空格;
  • 剥离引号定界符;
  • 不实现反斜杠转义规则;
  • 未闭合引号不是错误:解析器会消费到字符串末尾。

例如/commit "fix: handle edge" 'raw $1'会得到两个参数:fix: handle edge与raw $1(后者原样保留)。

七、未知/...输入的行为

核心斜杠逻辑不会拒绝未知斜杠输入。当内建、扩展、自定义、文件命令都没有命中时,expandSlashCommand返回原文本,字面上的/...prompt 会继续走 prompt-template 展开并投递给 LLM。TUI 与 ACP/RPC 都在session.prompt(...)之前分派共享内建注册表;TUI-only 内建命令在 ACP 侧既不广播也不处理,因此某个拼写在 ACP 里可能作为普通 prompt 文本落下去。

八、ACP/RPC 可用性

available-commands.ts 中的buildAvailableSlashCommands(...)以 first-wins 顺序发布命令:

  1. 具备文本能力的内建命令;
  2. 可选 skill 命令;
  3. 扩展命令;
  4. TypeScript/MCP 自定义命令;
  5. 发现的基于文件的命令。

内建主名与别名被预留;名字前缀会被解析成内建命令的扩展名(例如model:foo,其前缀model是内建命令)会从 ACP 可用性中过滤掉,防止歧义。同一份文件命令加载会同步更新会话的展开集合,保证 ACP 广播与展开一致性。

九、Streaming 与 idle 的差异

idle 路径:session.prompt("/x ...")跑完整命令管线,要么立即执行命令,要么把展开后的文本直接发送。

streaming 路径(session.isStreaming === true):prompt(...)仍然先跑扩展/自定义/文件/模板变换;随后要求显式streamingBehavior:

  • "steer"→ 排队中断消息(agent.steer);
  • "followUp"→ 排队回合后消息(agent.followUp);
  • 缺省streamingBehavior时 prompt 抛出AgentBusyError(agent-session.ts):
if (this.isStreaming) { const streamingBehavior = options?.streamingBehavior; if (!streamingBehavior) throw new AgentBusyError(); ... await this.#queueUserMessage(expandedText, options?.images, streamingBehavior, submittedAt); return true; }

命令在 streaming 下的特殊行为

  • 扩展命令即使 streaming 中也立即执行(不排队成文本);
  • steer(...)/followUp(...)辅助方法通过#throwIfExtensionCommand拒绝扩展命令文本,避免把「必须同步执行的 handler」排成普通文本消息;
  • compaction 队列重放使用isKnownSlashCommand(...)判断:已知斜杠命令走session.prompt(...)(保证命令管线重跑),否则走裸 steer/follow-up 方法(ui-helpers.ts 中可见该判断被用于消息队列重放)。

十、错误处理与失败面

  • provider 加载失败相互隔离:loadImpl用Promise.all包裹每个 provider 的load,单个 provider 抛错只在该 provider 上记[{displayName}] Failed to load: ...警告,其余 provider 继续(capability/index.ts);
  • 无效斜杠命令条目被 capability 校验丢弃:缺 name/path/content 或 level 非法的条目被validate剔除并警告;
  • frontmatter 解析失败分级:
    • native 命令:fatal 解析错误向外冒泡;
    • 非 native 命令:warning + 回退 key/value 宽松解析;
  • 扩展/自定义命令 handler 异常被捕获并通过扩展错误通道报告(无扩展 runner 的自定义命令回退到 logger),且视为已处理——不会意外落回文件命令或模板展开继续执行。

十一、内建命令注记:/pause

/pause仅在交互式 TUI 可用。它为主 agent、进程内子 agent 与 advisor 开启一个进程级闸门;每个 agent 在下一个安全边界停泊:在途调用会跑完,不中止任何工作,闸门释放前不启动新工作。从暂停屏幕按 Esc、Enter、Space 或 Ctrl+C 恢复——注意Ctrl+C 是恢复而非中止任何 agent。

十二、实操要点速查

把上文收敛成可操作的清单:

  • 想让命令对所有项目生效:放到~/.omp/agent/commands/*.md(native,优先级 100,项目级.omp/commands可覆盖它);
  • 想复用 Claude Code 生态命令:保持.claude/commands/(递归)即可被发现;子目录命令额外获得dir:cmd别名;用户级需commands.enableClaudeUser: true(~/.claude属于外部工具用户源,默认关闭、按能力 opt-in,见 capability/index.ts 的FOREIGN_USER_PROVIDERS);
  • 想避免与内建命令撞名:内建名字全局预留,同名文件命令永远不会执行——可用 Extensions 仪表盘查看_shadowed条目定位冲突;
  • 模板参数约定:$1…位置参数、$@[2:3]从第 2 个取 3 个、$ARGUMENTS/$@全部参数;模板未引用任何占位符时参数自动追加在末尾;
  • 改完命令文件后:无需 watcher,/move、/reload-plugins或重开会话都会触发refreshSlashCommandState;
  • ACP/RPC 场景:只广播有文本能力的内建 + 非内建名冲突的扩展/自定义/文件命令;model:foo这类「前缀即内建名」的扩展命令会被过滤。

参考文件

主题文件
设计文档docs/slash-command-internals.md
capability 定义与校验packages/coding-agent/src/capability/slash-command.ts
注册表、优先级与去重packages/coding-agent/src/capability/index.ts
文件扫描公共工具packages/coding-agent/src/discovery/helpers.ts
native 发现packages/coding-agent/src/discovery/builtin.ts
运行时物化与展开packages/coding-agent/src/extensibility/slash-commands.ts
参数解析与替换packages/coding-agent/src/utils/command-args.ts
打包 fallback 命令packages/coding-agent/src/task/commands.ts
内建注册表 / ACP 可用性packages/coding-agent/src/slash-commands/builtin-registry.ts、packages/coding-agent/src/slash-commands/available-commands.ts
会话 prompt 管线packages/coding-agent/src/session/agent-session.ts
交互模式刷新与补全packages/coding-agent/src/modes/interactive-mode.ts、packages/coding-agent/src/modes/controllers/input-controller.ts

【免费下载链接】oh-my-pi⌥ Coding agent with the IDE wired in项目地址: https://gitcode.com/GitHub_Trending/oh/oh-my-pi

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询