首页
/ opencode 自定义命令实战:以 .opencode/command/commit.md 为例解析 Frontmatter、Shell 内联执行与 subtask 机制

opencode 自定义命令实战:以 .opencode/command/commit.md 为例解析 Frontmatter、Shell 内联执行与 subtask 机制

2026-09-06 11:03:29作者:廉皓灿Ida

在 opencode(开源编码代理)中,"自定义命令"(Custom Command)允许开发者把一段可复用的提示词模板写成 Markdown 文件,放在项目的 .opencode/command/ 目录下,之后即可在 TUI 中以 /commit 等斜杠命令触发。本文以 opencode 仓库自身使用的 .opencode/command/commit.md 命令文件为主体,完整拆解它的每一项 frontmatter 配置(descriptionmodelsubtask)、正文提示词的写法约束、!git 命令`` 内联 Shell 执行的底层实现,以及 subtask 模式下命令实际如何被派发给子代理执行,帮助读者掌握编写、调试和原理级理解 opencode 自定义命令的完整方法。

命令文件全貌:一个真实可用的 commit 工作流

opencode 仓库根目录下的 .opencode/command/commit.md 是团队日常提交代码时使用的自定义命令,全文如下(原样保留,未做删减):

---
description: git commit and push
model: opencode/kimi-k2.5
subtask: true
---

commit and push

make sure it includes a prefix like
docs:
tui:
core:
ci:
ignore:
wip:

For anything in the packages/web use the docs: prefix.

prefer to explain WHY something was done from an end user perspective instead of
WHAT was done.

do not generic messages like "improved agent experience" be very specific
about what user facing changes were made

if there are conflicts DO NOT FIX THEM. notify me and I will fix them

## GIT DIFF

!`git diff`

## GIT DIFF --cached

!`git diff --cached`

## GIT STATUS --short

!`git status --short`

这个文件由两部分组成:YAML frontmatter 声明命令元数据,正文是执行时发送给模型的实际提示词。逐段来看它的工程价值:

  • frontmatterdescription: git commit and push 让命令在 TUI 命令列表中展示可读描述;model: opencode/kimi-k2.5 强制该命令使用指定模型执行;subtask: true 声明命令以 subtask(子任务)方式运行,即由一个独立的子代理会话完成,而不污染主会话上下文。
  • 正文约束:要求代理执行 commit 并 push;提交信息必须带模块前缀(docs:tui:core:ci:ignore:wip:),且 packages/web 下的改动一律使用 docs: 前缀;强调从"终端用户视角解释为什么(WHY)"而非"做了什么(WHAT)",并明确禁止 improved agent experience 这类空泛措辞。
  • 冲突处理策略if there are conflicts DO NOT FIX THEM. notify me and I will fix them —— 这是一个很典型的代理行为边界设定:遇到冲突不自行解决,而是停下来通知人工处理,避免代理自动改写他人代码。
  • 动态上下文注入:正文末尾通过三段 !`git ...` 语法,在命令展开时实时执行 git diffgit diff --cachedgit status --short,并把真实输出内联进提示词。这样模型看到的不是抽象指令,而是当前工作区的实际改动内容。

frontmatter 字段解析:源码中的 Schema 定义

frontmatter 支持哪些字段、每个字段的类型是什么,直接由 opencode 的 Schema 定义约束。核心定义位于 ConfigCommand.Info

export class Info extends Schema.Class<Info>("ConfigV2.Command")({
  template: Schema.String,                    // 正文(frontmatter 之后的 Markdown 内容)
  description: Schema.optional(Schema.String), // 命令描述,用于列表展示
  agent: Schema.optional(Schema.String),       // 指定执行该命令的 agent
  model: Schema.optional(Schema.String),       // 指定模型,如 opencode/kimi-k2.5
  variant: Schema.optional(Schema.String),     // 模型变体(variant)
  subtask: Schema.optional(Schema.Boolean),   // 是否以 subtask 方式运行
}) {}

commit 命令用到的三个字段均可在此对号入座。加载过程在 ConfigCommandPlugin 中实现,关键事实有两条:

  1. 命令名来自文件相对路径loadDirectory 通过 glob 模式 {command,commands}/**/*.md 扫描目录(因此 .opencode/command/.opencode/commands/ 都有效),decode 函数把文件名中的 command/(或 commands/)前缀和 .md 后缀剥掉得到命令名。也就是说 commit.md 注册为 /commit,若放到 packages/web/.opencode/command/commit.md 则为该子目录项目内的 /commit
  2. model 字段会被解析为 provider + modelID 二元组。插件在 command.model 存在时调用 ModelV2.parse(command.model) 得到 { providerID, modelID }variant 则进一步设置 item.model.variant。这解释了为什么 commit.md 中写的是 opencode/kimi-k2.5 这种 providerID/modelID 格式。

命令注册后的完整结构在 V1 实现中可见 Command.Infonamedescriptionagentmodelsource"command" | "mcp" | "skill")、templatesubtaskhints。其中 hints 由模板中出现的 $1$ARGUMENTS 等占位符自动推导(见 hints 函数);commit.md 模板里没有占位符,所以不接受额外参数,直接 /commit 触发即可。

!cmd`` 内联 Shell 执行:动态上下文的底层实现

commit 命令最实用的设计是 !`git diff` 这类语法。它并非 Markdown 普通代码,而是 opencode 命令模板的专用语法,由 packages/opencode/src/config/markdown.ts 中的正则识别:

export const FILE_REGEX  = /(?<![\w`])@(\.?[^\s`,.]*(?:\.[^\s`,.]+)*)/g  // @file 引用
export const SHELL_REGEX = /!`([^`]+)`/g                                    // !`shell` 内联执行

执行逻辑位于 session/prompt.ts:模板展开阶段调用 ConfigMarkdown.shell(template) 取出所有 !cmd`` 匹配项,使用 Shell.preferred(cfg.shell) 解析出的用户首选 Shell 并行执行,再把执行结果文本逐位替换回模板原文:

const shellMatches = ConfigMarkdown.shell(template)
if (shellMatches.length > 0) {
  const cfg = yield* config.get()
  const sh = Shell.preferred(cfg.shell)
  const results = yield* Effect.promise(() =>
    Promise.all(
      shellMatches.map(async ([, cmd]) => (await Process.text([cmd], { shell: sh, nothrow: true })).text),
    ),
  )
  let index = 0
  template = template.replace(bashRegex, () => results[index++])
}

注意 nothrow: true:git 命令执行失败不会中断命令展开,空输出(如没有未提交改动时 git diff 为空)也是合法输入。因此当 commit.md 被触发时,最终发给模型的提示词是"静态约束 + 三份实时 git 输出"的组合,模型据此生成基于真实 diff 的提交信息,而不是凭空猜测改动内容。

同一模板语法中,@path/to/fileFILE_REGEX)可用于把文件内容注入提示词,这是编写仓库级自定义命令时可用的另一类动态上下文手段。

subtask: true 的执行语义

commit.md 声明了 subtask: true,这决定了命令的派发形态。判断与派发逻辑在 prompt.ts

const isSubtask = (agent.mode === "subagent" && cmd.subtask !== false) || cmd.subtask === true
const parts = isSubtask
  ? [
      {
        type: "subtask" as const,
        agent: agent.name,
        description: cmd.description ?? "",
        command: input.command,
        model: { providerID: taskModel.providerID, modelID: taskModel.modelID },
        prompt: templateParts.find((y) => y.type === "text")?.text ?? "",
      },
    ]
  : [...uniqueTemplateParts, ...(input.parts ?? [])]

含义是:当 cmd.subtask === true 时,展开后的提示词不会被追加到当前主会话的消息流中,而是包装成一个 subtask part,交给指定 agent 的独立子会话执行。对 commit 这类"机械但需要读大量 diff"的任务,subtask 模式带来两个直接好处:主会话上下文不被大段 git 输出污染;子会话可以用命令指定的模型(本例的 opencode/kimi-k2.5)执行,与主对话所用模型解耦。

subtask 的默认行为值得注意:若命令没有绑定 agent,或绑定了普通 agent,cmd.subtask === true 是唯一强制走子任务的开关;若绑定了 mode: "subagent" 的 agent,则默认即为 subtask,除非显式写 subtask: false。仓库自带的默认命令 /review 就是同样写法——Command 默认命令定义review 显式设置了 subtask: true,V2 核心中对应的插件见 CommandPlugin

模型解析的优先级链在同一文件中也有明确体现(prompt.ts):

  1. cmd.model —— commit 命令的 opencode/kimi-k2.5 在此命中,优先生效;
  2. cmd.agent 绑定 agent 的模型;
  3. 用户触发命令时临时指定的 input.model
  4. 当前会话的当前模型。

因此 commit.md 中 model: 字段的作用是保证无论主会话用什么模型,提交任务始终由该模型完成,行为可复现。

同目录参考与命令生态

opencode 仓库自身的 .opencode/command/ 目录是一个现成的命令范例库,除 commit 外还可对照阅读:

  • changelog.md:展示了 $ARGUMENTS 参数占位符与 !`bun script/raw-changelog.ts $ARGUMENTS` 组合,把脚本输出作为 <changelog_input> 块注入提示词,并配有一套严格的成文规则;
  • ai-deps.mdlearn.mdspellcheck.mdtranslate.mdissues.mdrmslop.mdspellcheck.md 等,分别覆盖依赖升级、文档校对、翻译、issue 处理等场景,可作为编写团队级命令的提示词风格参考。

项目级配置 .opencode/opencode.jsonc 与命令目录相互独立:opencode.jsonc 承载 provider、permission、references、tools 等配置,而命令统一以 Markdown 文件形式放在 command/commands/ 子目录中,两者都会被 ConfigCommandPlugin 的加载流程纳入同一个命令注册表。

要点总结

要素 commit.md 中的写法 源码依据
命令名 文件名 commit.md/commit decode 函数
description git commit and push ConfigCommand.Info
model opencode/kimi-k2.5,优先级最高的模型来源 模型解析链
subtask: true 派发到子代理会话执行,隔离主上下文 subtask 判定
!`git diff` 展开时实时执行并内联输出 SHELL_REGEX 与执行
提示词约束 前缀规范、WHY 优于 WHAT、冲突不自行修复 commit.md 正文

适用前提与限制:以上路径与行为基于当前仓库的实际源码(V1 的 packages/opencode 实现与 V2 核心的 packages/core 命令插件并存,frontmatter Schema 两者一致);model: opencode/kimi-k2.5 依赖对应的 provider 已在用户环境中可用,否则命令在模型解析阶段会报错。编写自己的命令时,建议照 commit.md 的骨架起步:frontmatter 声明 description 与执行模型,正文写清目标、约束与失败边界,再用 !git ...`` 注入实时上下文,最后按需要决定 subtask 是否开启。

登录后查看全文
热门项目推荐
相关项目推荐