自定义提供方
仪表板中的 Providers 标签页展示的是 ADHDev 在某台机器上能检测和驱动什么, 而不是一个从零构建新提供方的地方。要让 ADHDev 支持它还不认识的 CLI、IDE 或代理,你需要写一个小的 JSON 文件("提供方清单"),放进守护进程已经在 监视的文件夹里。本页从头到尾走一遍这个流程。
什么是提供方
ADHDev 有四个提供方类别:
cli— 通过 PTY(伪终端)驱动的终端代理,例如 Claude Code 或 Codex CLI。守护进程往终端里输入内容,再把屏幕内容读回来。acp— 通过 Agent Client Protocol 在 stdio 上通信的代理(结构化消息,而不是屏幕抓取)。ide— 通过 Chrome DevTools Protocol 检查和控制的、CDP 驱动的代码 编辑器(Cursor、VS Code 分支等)。extension— CDP 驱动的 IDE 扩展 webview(Cline、Roo Code 等)。
现实中值得手写的是 cli 类别。 一个 CLI 提供方只需要一个 JSON 清单, 外加一个描述如何在屏幕上区分"生成中"、"空闲"、"等待批准"的 JSON 状态机 规范 —— 不需要写代码。
ide 和 extension 提供方则完全不同:它们需要真正的 CDP 自动化脚本 (JavaScript),用于打开面板、读取聊天、发送消息等操作。这比描述一个 终端的屏幕状态要繁重得多,超出了本页的范围 —— 如果你仍想尝试,可以参考 下面链接的 Provider SDK 指南。 acp 提供方同样可以手写(不需要 PTY 屏幕抓取),但比 CLI 提供方少见。
本页余下的内容都是关于 CLI 提供方的。
文件放在哪里
守护进程按以下优先级顺序从少数几个目录加载提供方(对同一个提供方类型, 后面的会覆盖前面的):
- 上游 —
<configDir>/providers/.upstream/—— 官方自动同步的 捆绑包。不要手动编辑,它会被覆盖。 - 外部源 —
<configDir>/external/<source-name>/—— 你在仪表板 Sources 面板中注册的第三方 git 源。 - 用户覆盖 —
<configDir>/providers/(除了.upstream/之外的 一切)—— 你自己创建或覆盖的清单。这个始终优先。 在普通安装中<configDir>是~/.adhdev,所以这里就是~/.adhdev/providers/。
每个提供方位于:
<root>/<category>/<type>/
provider.v1.json ← 清单(必需)
specs/
<version>.json ← FSM 状态机规范(cli 类别必需)把 cli/<type>/provider.v1.json 放进 ~/.adhdev/providers/ 就够了 —— 守护进程不需要在别处再注册它。
如果什么都没出现,先检查是否已经设置了 providerDir。 一个显式的 providerDir(通过 Sources 面板或配置设置 —— 见下文"让守护进程指向 另一个文件夹")会替换默认的 ~/.adhdev/providers/ 作为用户覆盖 文件夹,而不是在它之上追加。守护进程的日志会把这记录为 userDirSource: "explicit"。如果某台机器已经配置了这个,你的新清单 需要放进那个文件夹,而不是 ~/.adhdev/providers/;热重载也只会 监视这一个当前生效的文件夹(见下文"如何判断哪个文件夹在生效")。
"覆盖上游"究竟意味着什么
如果你创建的提供方 type 与守护进程已经内置的某个相同(比如一个改过的 claude-cli),那么 ~/.adhdev/providers/cli/claude-cli/ 里你的副本会 取代上游版本被加载。守护进程会明确把这一点写进日志 (⚠ OVERRIDES upstream),所以这种情况发生时是可见的。这也是在不等待 上游修复的情况下,给内置提供方的某个字段打补丁的方法 —— 把整个提供方 目录复制到你的用户文件夹,然后在那里编辑。
让守护进程指向另一个文件夹
默认情况下,守护进程只监视 ~/.adhdev/providers/ 作为用户覆盖目录。 如果你想把自定义提供方放在别处(比如一个正在编辑的 git 检出目录), 设置一个显式的提供方目录:
- 仪表板: Machine 页面 → Providers 标签页 → Advanced → 设置 Provider source mode(
normal或no-upstream)和 Provider directory,然后 Apply & reload。no-upstream会完全禁用自动 同步的上游捆绑包,只提供你自己的目录以及external/中的内容。 - 配置文件: 在守护进程配置中设置
providerDir(以及可选的providerSourceMode)。
无论哪种方式,应用更改都会调用守护进程的 set_provider_source_config 命令,立即重新加载提供方映射 —— 不需要重启守护进程。
如何判断哪个文件夹在生效
因为显式的 providerDir 是替换默认值而不是追加,在排查清单为何没有 加载之前,先确认一下守护进程实际在监视哪个文件夹是值得的:
- 仪表板: Machine 页面 → Providers 标签页 → Advanced 会显示 当前的 Provider directory 值(留空表示使用默认值
~/.adhdev/providers/)。 - 守护进程日志: 查找这一行
Hot-reload watcher active: <dir>——<dir>就是实际被监视的文件夹。 无论是哪种安装方式(云端守护进程、standalone、还是预览实例),守护 进程日志都位于~/.adhdev/logs/daemon-YYYY-MM-DD.log。
用户覆盖文件夹的热重载
守护进程运行期间,只会监视用户覆盖目录(~/.adhdev/providers/, 或你设置的 providerDir)这一个文件夹中的 .js 和 .json 文件变化—— 生效的用户覆盖目录始终只有一个,绝不会同时监视两个。在那里编辑 清单大约 300ms 内就会生效 —— 无需重启,也无需手动重新加载。这个监视 不覆盖 .upstream/ 或 external/ 源目录树 —— 那里的更改需要 adhdev provider reload(或仪表板刷新)才能生效。
快速开始:adhdev provider init
把下面两个文件最快落到磁盘上的方法,是让 CLI 帮你生成:
adhdev provider init my-cli --dir ./my-cli这会按当前的 v1 形态脚手架出 provider.v1.json + specs/1.0.json —— 和本页接下来手把手讲解的是同一种形态,二进制名从 <type> 推导(去掉 末尾的 -cli/-acp,或用 --binary 显式指定),规范里的占位正则表达式 仍需要你自己替换成真正匹配目标命令屏幕输出的模式。 adhdev provider init my-acp --category acp --dir ./my-acp 对声明式 ACP 提供方做同样的事情(只有一个 provider.v1.json,没有规范文件 —— 见上文"什么是提供方")。init 会拒绝 --category ide 和 --category extension:这两类需要针对某个具体 IDE/webview 手写的 CDP 自动化脚本,不存在能生成可运行结果的通用模板。
紧接着运行一次 adhdev provider validate ./my-cli —— 它会在占位正则 表达式上失败(它们永远不会匹配),这正是设计的用意:准确告诉你哪些 还是占位符,哪些已经符合 schema。
一个最小可用的 CLI 提供方
CLI 提供方通过一个声明式的有限状态机引擎("规范")来路由。规范是 必需的 —— 旧的基于脚本的 CLI 引擎已被移除,现在如果一个 CLI 提供方 找不到可解析的规范,启动会直接失败并给出明确错误,而不会退回到较弱的 引擎。所以一个最小的提供方由两个文件组成:清单,以及一个有效的最小规范。 (这正是上面 adhdev provider init 脚手架出来的形态 —— 无论你是要编辑 生成的文件,还是想手写,下面都会讲每一部分是怎么运作的。)
选择目标命令
要写一个能真正现场测试的清单,选一个真实存在、无害、且已经安装好的 交互式命令。python3 -q 是个不错的选择:几乎所有 macOS 和 Linux 机器 都自带它;它以"安静"模式启动(跳过版本横幅,需要匹配的内容更少);它的 >>> 提示符是一个简单、稳定的"空闲"标记,不像原始的 bash 提示符 (PS1)那样依赖 shell 配置。
provider.v1.json
{
"$schema": "https://registry.adhf.dev/schemas/v1/cli/provider.schema.json",
"type": "python-repl",
"name": "Python REPL",
"category": "cli",
"binary": "python3",
"spawn": {
"command": "python3",
"args": ["-q"],
"shell": false
},
"compatibility": [
{ "ideVersion": ">=0.0.0", "spec": "specs/1.0.json" }
]
}只包含 schema 实际要求的字段(type、name、category、binary、 spawn),加上 compatibility —— 这是加载器找到下面这个规范文件的方式。
specs/1.0.json
{
"$schema": "adhdev:cli/spec@4",
"id": "python-repl",
"name": "Python REPL",
"binary": "python3",
"send_message": {
"submit_key": "\r"
},
"states": [
{ "id": "starting", "label": "Starting", "initial": true, "status": "idle" },
{ "id": "idle", "label": "Ready", "status": "idle" },
{ "id": "busy", "label": "Evaluating", "status": "generating" }
],
"transitions": [
{
"label": "startup → idle",
"from": "starting",
"to": "idle",
"when": { "matches": ">>>\\s*(?=\\n|$)" }
},
{
"label": "idle → busy",
"from": "idle",
"to": "busy",
"min_hold_ms": 200,
"when": { "not": { "matches": ">>>\\s*(?=\\n|$)" } }
},
{
"label": "busy → idle",
"from": "busy",
"to": "idle",
"min_hold_ms": 300,
"when": {
"all": [
{ "matches": ">>>\\s*(?=\\n|$)" },
{ "stable_ms": 500 }
]
}
}
]
}这就是 FSM 校验器实际要求的最小内容:id、binary、 send_message.submit_key、一个非空的 states[](其中恰好一个 状态标记 initial: true),以及一个 transitions[] 数组。真实提供方 规范中的其他一切(批准弹窗检测、区段窗口、原生聊天历史、自动批准模式 等)都是在这个基础形态之上叠加的 —— 等这个例子跑通之后,可以参考 adhdev-providers/cli/*/specs/ 下的内置规范获取更完整的例子。
为什么用
(?=\n|$)而不是单纯的$?matches模式是针对整个 屏幕缓冲区运行的,而不是逐行运行;引擎默认不带m标志编译这个模式, 所以一个未转义的$会锚定在整个屏幕的末尾,而不是提示行的末尾 —— 只要提示符下面还有内容,它就会悄悄地永远不匹配。规范的 lint 工具正是 因为这个原因,把几个不需要m标志的行边界写法列入了白名单(行尾用(?=\n|$),行首用(?:^|\n));用这些写法而不是裸的锚点,正是这个 模式能够可靠匹配到提示行的原因。
把两个文件放到 ~/.adhdev/providers/cli/python-repl/provider.v1.json 和 ~/.adhdev/providers/cli/python-repl/specs/1.0.json,先启用该 提供方(adhdev provider enable python-repl 或仪表板的 Machine Providers 开关 —— 仪表板自身的检测检查会拒绝一个已禁用的提供方,见 下文),再针对它启动一个会话试试。因为 python3 已经装好了,试用前 不需要再安装任何东西。
上面示例中的文件也一并保存在本指南源文件旁边的 custom-provider-example/ 目录中,可以直接原样复制使用。
验证提供方
adhdev provider validate <path>会用守护进程加载时使用的同一份 v1 JSON Schema 检查provider.v1.json(或旧版provider.json),并 报告它属于哪个层级。对于cli清单,它还会按守护进程在启动时使用的 同一套顺序去解析规范(compatibility[].spec→specs/default.json→spec.json),并用守护进程使用的同一个 FSM 校验器来检查 —— 一个 通过了 schema 但找不到可解析规范的清单,或者状态机形状有问题的 规范,会在这里就被报告为无效,而不是等到之后启动时才失败 (formatNoResolvableSpecError)。acp清单没有规范这个概念,会 跳过这一步。对上面的示例,它会打印tier: extended-legacy和python-repl@unversioned—— 这两个都是 正常且无害的:extended-legacy只是表示"不属于另外两个层级中的任何一个":extended表示清单声明了一个overrides块(自定义 JS 钩子),declarative-only表示它声明了一个tui块。像这个例子一样,既 没有overrides也没有tui的纯规范路由清单,按排除法就落在extended-legacy。这不意味着缺了什么。unversioned只是表示清单里没有providerVersion字段。 schema 并不要求这个字段,加载器也不依赖它来加载或运行一个 CLI 提供方 —— 它存在纯粹是为了让provider list/validate有内容 可打印。如果你想显示一个真实的值,可以添加"providerVersion": "1.0.0"字段,但对这样的清单来说这只是外观 问题。
adhdev provider list(加--json获得机器可读输出)会显示守护 进程当前从所有源目录加载的每一个提供方,以及规范版本锁定情况。adhdev provider detect [type]会检查底层二进制文件是否真的已 安装并在PATH上。从 CLI 运行时,它会刻意绕过启用门槛,所以即使在 你启用该提供方之前也能工作。仪表板/守护进程侧的对应功能 (detect_provider)更严格:如果提供方尚未启用,会以Provider is disabled on this machine拒绝。所以如果你是从仪表板 而不是 CLI 检查检测结果,请先启用该提供方。- 在守护进程运行期间编辑用户覆盖文件夹中的清单,会触发上面描述的热 重载监视器 —— 查看守护进程日志中的
File changed: provider.v1.json, reloading...以及任何⚠校验警告。
对于这种手写的、树外(out-of-tree)的提供方,没有仪表板"fix"或自动 修复流程 —— 那套工具针对的是内置提供方捆绑包自身的脚本布局,而不是 你自己维护的自定义清单。
分享
如果你希望自己的提供方成为大家都能自动获得的内置集合的一部分,可以 向 github.com/vilmire/adhdev-providers 贡献上游 —— 守护进程的 .upstream/ 捆绑包正是从这个仓库构建的。按照 仓库自己的贡献指南 提交一个添加 cli/<type>/ 目录的 Pull Request。注意,进入该仓库只是 让提供方成为内置清单的一部分;这并不会自动使它成为一个已验证的 提供方 —— 两者的区别参见 提供方问题。
在此之前(或者作为替代),提供方目录就是一个普通文件夹 —— 你可以把它 纳入版本控制、打包成 zip 分享,或者让同事的 providerDir 指向同一个 git 检出目录。
