配置系统

九层配置来源与合并顺序、JSONC 与变量替换、Markdown 定义的 agent 与命令、独立的 tui.json,以及通往 V2 的兼容层。

作者 David更新于 第 23 篇(共 38 篇)

现役(V1)配置的加载逻辑在 packages/opencode/src/config/,字段 schema 在 packages/core/src/v1/config/,新内核的版本是 packages/core/src/config.ts。先看「写在哪、谁覆盖谁」,再看实现里几处值得留意的设计。

九层来源

loadInstanceState()(packages/opencode/src/config/config.ts:328)按下表顺序深度合并,后者覆盖前者:

顺序来源位置
1远端 well-known每个 wellknown 类型认证凭据对应的 <url>/.well-known/opencode
2全局配置全局目录(默认 ~/.config/opencode/)下依次读 config.json、opencode.json、opencode.jsonc
3自定义文件环境变量 OPENCODE_CONFIG 指向的文件
4项目配置从 git 工作树根到当前目录逐层的 opencode.json(c),越近越优先
5.opencode 目录各级 .opencode/、~/.opencode/、OPENCODE_CONFIG_DIR 中的 opencode.json(c)
6内联配置环境变量 OPENCODE_CONFIG_CONTENT 里的整段 JSON
7控制台组织登录控制台并选定组织时拉取的 <url>/api/config
8托管目录macOS /Library/Application Support/opencode,Windows %ProgramData%\opencode,其他 /etc/opencode
9macOS MDMai.opencode.managed 偏好域的 plist,注释写着“override everything”

几个细节:

  • 同一目录下 .jsonc 晚于 .json 合并,并存时 .jsonc 胜出;
  • 合并用 remeda 的 mergeDeep,只有 instructions 拼接去重;plugin 按包名(本地文件按 URL)去重、后声明者胜,并记下来源文件与作用域(派生字段 plugin_origins);
  • OPENCODE_DISABLE_PROJECT_CONFIG 同时跳过第 4 层与项目内的 .opencode;
  • 第 5 层的顺序容易踩坑:ConfigPaths.directories()(paths.ts:23)向上收集 .opencode 时「近者在前」,却没有像 files() 那样 toReversed(),所以多级 .opencode 并存时外层反而覆盖内层。新内核扳正了这一点(见文末)。

合并之后的修正

config.ts:550-598 还做了几步语义修正:废弃的 mode 并入 agent 并强制为 primary;OPENCODE_PERMISSION 深合并进 permission;旧的 tools 开关表翻译成权限,write、edit、patch 折叠为 edit,显式的 permission 优先;autoshare: true 等价于 share: "auto"。V2 设计文档称这种翻译为“lossy compatibility input”,决定不再保留。

JSONC:宽进严出

ConfigParse.jsonc() 基于 jsonc-parser,允许注释与尾逗号,报错带行号、列号和指向出错处的 ^。ConfigParse.schema() 解码时用 errors: "all" 与 onExcessProperty: "ignore":不认识的字段静默忽略,已知字段写错类型直接报错。内置 skill 的注释说得直白——“opencode hard-fails on invalid config”。

变量替换:先换文本,再做解析

ConfigVariable.substitute() 在 JSONC 解析之前处理原始文本:

/** Apply {env:VAR} and {file:path} substitutions to config text. */
export async function substitute(input: SubstituteInput) {
  const missing = input.missing ?? "error"
  let text = input.text.replace(/\{env:([^}]+)\}/g, (_, varName) => {
    return (input.env?.[varName] ?? process.env[varName]) || ""
  })

(packages/opencode/src/config/variable.ts:33-38)

  • {env:VAR} 缺失时替换为空串,不报错;
  • {file:path} 相对声明它的配置文件所在目录解析,支持 ~/,内容 trim() 后按 JSON 字符串转义嵌入;文件不存在报 bad file reference(TUI 配置改为空串);整行 // 注释里的 {file:...} 会被跳过。
{
  "$schema": "https://opencode.ai/config.json",
  "instructions": ["CONTRIBUTING.md"],
  "permission": { "bash": { "*": "ask", "git status": "allow" } },
  "mcp": {
    "docs": {
      "type": "remote",
      "url": "https://docs.example.com/mcp",
      "headers": { "Authorization": "Bearer {env:DOCS_TOKEN}" }
    }
  }
}

$schema 与 JSON Schema

$schema 指向的 https://opencode.ai/config.json 由 packages/opencode/script/schema.ts 从 ConfigV1.Info 生成,附加 allowComments 与 allowTrailingCommas;tui.json 的 schema 由 TuiConfig.Info 生成。加载器还会主动维护它:全局配置不存在时先写一个只含 $schema 的文件,读到缺 $schema 的配置就改写原文件补上(config.ts:245-249)。V2 设计文档的态度相反:“loading config must not insert it or create files for it”。

Markdown 定义的 agent、命令与更多

每个配置目录(全局目录、各级 .opencode/、~/.opencode/、OPENCODE_CONFIG_DIR)还会按约定子目录扫描:

模式产物
{agent,agents}/**/*.mdagent,frontmatter 是字段,正文是 prompt
{mode,modes}/*.md旧式 mode,强制为 primary agent
{command,commands}/**/*.md命令,正文是 template
{skill,skills}/**/SKILL.mdskill
{plugin,plugins}/*.{ts,js} 与 {tool,tools}/*.{js,ts}本地插件与自定义工具

名字取自相对路径,agents/review/security.md 得到 review/security。frontmatter 解析失败时还有一次宽松重试(packages/core/src/config/markdown.ts:20-21):

Other coding agents accept unquoted colons in frontmatter values. Retry those values as YAML block scalars so existing config files keep working.

opencode 包里的同名文件点名了 Claude Code。加载器还会在每个配置目录后台安装匹配版本的 @opencode-ai/plugin 并写好 .gitignore,让本地插件能直接 import 到 API。

TUI 配置单独成文件

终端界面相关设置拆进了 tui.json(packages/opencode/src/config/tui.ts),顺序与服务端平行:全局 tui.json(c) → OPENCODE_TUI_CONFIG → 项目各级 tui.json(c) → .opencode 目录;字段有 theme、keybinds、plugin、plugin_enabled、attention、diff_style、mouse 等。

容错策略与服务端相反:“every broken-config path degrades gracefully rather than crashing TUI startup”,坏文件记警告后跳过。老版本写在 opencode.json 里的 theme、keybinds、tui 由 tui-migrate.ts 按目录迁移到 tui.json,原文件先备份为 <原文件名>.tui-migration.bak。

生效时机:失效与重建

配置按实例(目录)缓存在 InstanceState 中,全局配置另有一份永不过期的缓存。现役代码里找不到监视配置文件的逻辑,改动靠「清缓存 + 丢弃实例」生效:通过 HTTP 接口改全局配置(updateGlobal,.jsonc 以补丁方式写回)后丢弃所有实例;改项目配置(update)则等响应发出后丢弃当前实例;TUI 进程收到 SIGUSR2 时让 worker 执行 reload。下一次请求到来时按新配置重建。opencode debug config 可以打印合并后的最终结果。

通往 V2:兼容层与新内核

V2 设计(specs/v2/config.md)逐组评审了 V1 字段:集合改复数(agents、permissions、plugins、providers),command 移除、改由 skills 承担,MCP 服务器收进 mcp.servers 并以 disabled 取代 enabled,权限改为有序的 { action, resource, effect } 规则数组。

兼容层 packages/opencode/src/config/v2-compat.ts 让 V1 读懂一部分 V2 写法:ConfigV2Compat.lower() 在校验前把 agents、commands、mcp.servers 等降级为 V1 字段,产出 invalid、unsupported、conflict(新旧并存时保留旧值)三类诊断;唯独 V2 的 permissions 直接报错,提示 run opencode2。

新内核 packages/core/src/config.ts 只认 opencode.json(c),只在打开位置时读一次,结果是按优先级排好的 Document / Directory 列表,而非合并好的大表。.opencode 的顺序在这里被显式扳正:

    const directories = [
      globalDirectory,
      ...discovered
        .filter((item) => path.basename(item) === ".opencode")
        .toReversed()
        .map((directory) => AbsolutePath.make(directory)),
    ]
    // A config closer to the opened directory should win over one higher up.
    // Search starts nearby, so reverse the results before applying them.
    const directPaths = discovered.filter((item) => path.basename(item) !== ".opencode").toReversed()

(packages/core/src/config.ts:186-195)

下一篇:Agent、命令与 Skills——七个内置 agent 的权限设计、命令模板的展开顺序与 SKILL.md 的发现规则。

本页目录