无界Harness文档
无界Harness文档
欢迎快速开始

基础

工作区与成员Issue频道项目标签工作流收件箱与订阅

智能体

认识智能体创建和配置智能体分配 issue 给智能体对话小队Skills

自动化

自动化运行方式

知识与能力

文档知识能力中心连接器

治理

治理

LocalRun 插件开发

从零开发、安装和调试 external localrun provider 插件。

LocalRun 插件开发

这份文档面向 third-party 或内部工具开发者,说明如何实现一个可被 wujie run <issue> -- <provider> [args...] 调用的 external localrun provider 插件。协议 v1 始终是 Issue-first;需要支持 wujie discuss -- <provider> [args...] 时必须实现协议 v2。

如果你要修改 Wujie 内置 provider 的 Go 实现,先读 Local Run Provider。本页只覆盖外部插件的包结构、manifest、运行方式输入、stdout NDJSON 协议和本地验收方法。

插件包结构

一个插件目录至少包含:

fake-provider/
  provider.json
  wujie-localrun-fake
  • provider.json 是 manifest,声明 provider 名称、协议和 adapter 命令。
  • wujie-localrun-fake 是本地可执行 adapter,可以是 shell、Node.js、Python、Go、Rust 或任何可执行文件。
  • command 如果是相对路径,会按 provider.json 所在目录解析。

安装后,CLI 会把插件复制到当前 profile/config 的 state dir 下,并记录到 localrun_providers 配置。

仓库样板:Grok Build

仓库提供可安装样板 localrun-providers/grok/,对齐 Hermes 的 stdout=NDJSON / TUI→stderr 模式:

localrun-providers/grok/
  provider.json
  wujie-localrun-grok
  test_wujie_localrun_grok.py

安装与验收:

wujie local-run provider install ./localrun-providers/grok
wujie local-run provider list   # kind=plugin, name=grok
wujie run <id> -- grok
wujie discuss -- grok
python3 localrun-providers/grok/test_wujie_localrun_grok.py

Adapter 行为摘要:

  • 自建 PTY 启动用户原始 grok ...(拒绝 -p / streaming-json / 手动 --rules)
  • 绑定 issue 时从 WUJIE_ISSUE_ID 注入 --rules 只读上下文
  • 新会话注入唯一 --session-id 并锁定对应 transcript;命令行 resume/continue 首次定位后不再跟随其他终端的 session
  • 当前 TUI 提交 /resume 后短暂允许一次重绑定,只切换到随后确实追加事件的同 cwd transcript;跳过历史行并继续同步新 prompt、回复和 usage
  • 发现并 tail ~/.grok/sessions/<url.PathEscape(cwd)>/<sessionId>/updates.jsonl
  • 映射 user_message_chunk / agent_thought_chunk / agent_message_chunk / tool_call* / turn_completed 为 NDJSON 事件
  • 启动时 snapshot baseline,resume 会话不重复上报历史行
  • 使用 v2 协议,在仅有 WUJIE_LOCALRUN_SESSION_ID 时也能启动讨论,并输出原生 session 事件

provider.json

最小 manifest:

{
  "schema_version": 1,
  "name": "fake-provider",
  "display_name": "Fake Provider",
  "command": "./wujie-localrun-fake",
  "protocol": "wujie-localrun-provider.v2",
  "capabilities": {
    "interactive_pty": false,
    "structured_events": true,
    "issue_context": "env",
    "usage": true,
    "native_plan_handoff": false
  }
}

字段说明:

字段必填说明
schema_version是当前只支持 1。
name是Provider 调用名,例如 fake-provider。会被规范化为小写并去掉首尾空白。不能覆盖内置 provider 名称。
display_name否wujie local-run provider list 展示用名称。
command是Adapter 可执行文件。包含路径分隔符或以 . 开头时按 manifest 目录解析;否则按 PATH 查找。
protocol是v1 只支持 Issue-first;v2 必须同时支持前置讨论,并可选支持原生 handoff。
capabilities否运行能力声明。前置讨论由协议版本决定,不再使用独立 opt-in 字段。

capabilities 当前约定字段:

字段说明
interactive_ptyAdapter 是否需要交互式终端能力。
structured_eventsAdapter 是否能输出结构化事件。外部插件应设为 true。
issue_contextIssue context 来源。当前插件通过环境变量获得运行上下文,可写 "env"。
usageAdapter 是否会上报 token usage。
native_plan_handoff是否会发出 provider 原生的当前上下文/清空上下文方案执行动作;仅 v2 可设为 true。

执行模型

用户执行:

wujie run WUJ-123 -- fake-provider --model demo

Wujie 会:

  1. 创建或绑定一个 local run。
  2. 解析 fake-provider:先查内置 provider,再查 localrun_providers 配置,最后查已安装目录。
  3. 启动 manifest 中的 command。
  4. 把 provider 后面的参数传给 adapter。以上例为例,adapter 收到的 argv 是 --model demo。
  5. 从 adapter stdout 按行读取 NDJSON 事件,并同步为 local run message 或 usage。

Adapter 的工作目录是用户执行 wujie run 时的当前目录。

执行环境变量

Wujie 会注入以下上下文:

环境变量说明
WUJIE_RUN_ID当前 local run ID;前置讨论完成绑定前不存在。
WUJIE_ISSUE_ID当前 issue ID;前置讨论完成绑定前不存在。
WUJIE_LOCALRUN_SESSION_IDv2 前置讨论的稳定 session ID;同一个 discussion 恢复后保持不变。
WUJIE_WORKSPACE_ID当前 workspace ID。
WUJIE_SERVER_URL当前 Wujie server URL。
WUJIE_TOKEN当前运行可用的 Wujie token;未提供时会注入禁用占位 token。

插件元数据使用 WUJIE_LOCALRUN_PROVIDER_*:

环境变量说明
WUJIE_LOCALRUN_PROVIDER_NAMEmanifest 中规范化后的 provider 名称。
WUJIE_LOCALRUN_PROVIDER_PROTOCOLmanifest 声明的 wujie-localrun-provider.v1 或 .v2。
WUJIE_LOCALRUN_PROVIDER_ORIGINAL_COMMAND用户在 wujie run 后选择的 provider 命令名。
WUJIE_LOCALRUN_PROVIDER_ORIGINAL_ARGS原始 provider argv 的 JSON 数组,包括 provider 命令名本身。

不要把 WUJIE_TOKEN 打到 stdout 或日志里。

前置讨论环境

当 manifest 使用 wujie-localrun-provider.v2 且用户执行 wujie discuss 时:

  • WUJIE_LOCALRUN_SESSION_ID 必定存在。
  • Issue 创建前,WUJIE_RUN_ID 和 WUJIE_ISSUE_ID 不存在。
  • 父 Wujie CLI 负责保存 discussion、创建 Issue、创建 local run 和切换 reporter;adapter 不应自行创建或查询 Issue。
  • 选择继续当前上下文后,原 provider 进程已经拥有完整对话。父进程只传递绑定结果并继续执行,不会要求 adapter 从 Issue 重建上下文。
  • stdout 仍使用同一套 NDJSON 事件。服务端只把 user_input、完整 final 和明确的用户问题/方案保存为讨论;thinking 和工具事件会被过滤。
  • 交互终端输入完整达到 /wujie 时,父 CLI 会先把触发字符写入 provider PTY,并在最多 100ms 内等待可见输出;检测到新输出后以 10ms 静默期确认本轮回显完成,再暂停 provider 并绘制候选菜单。Bracketed paste 会等到完整 ESC[201~ 结束帧已经写入 PTY 后才检查触发。父 CLI 会跨任意输入分块识别以 BEL/ST 终止的 OSC、DCS、APC、PM、SOS,以及 CPR、DA、DSR、模式报告、窗口尺寸等 CSI 终端回复;回复原样转发给 adapter,但不进入用户输入状态,也不会关闭已打开的菜单。菜单打开时,焦点切换和 SGR/X10 鼠标事件仍由父 CLI 丢弃,不会延迟转发给 adapter。Adapter 不应依赖父 CLI 伪造或重绘 provider 的输入行。
  • 候选菜单和 Issue 确认卡都使用输入框下方的专用插入区。父 CLI 会保存光标,先向下越过两行 adapter 自有区域,再插入所需行;这两行只移动锚点,不属于插入区。Issue 卡片会从终端可用高度中同步扣除这两行。完成、取消或失败时,父 CLI 回到同一锚点删除插入行并恢复原光标;不会删除两行 margin、覆盖 adapter 已渲染的终端区域或进入 alternate screen。
  • Adapter 新建或切换原生会话后,应输出 {"event":"session","session_id":"<provider-session-id>"}。父 CLI 会按顺序保存该 ID,使后续 provider 原生 resume 自动命中同一条未完成 discussion。

仓库中的 Hermes/Grok 样板 adapter 都使用 v2:它们能接受 WUJIE_LOCALRUN_SESSION_ID,在发现原生会话后输出 session 事件,因此可以直接用于前置讨论。两者当前没有声明 native_plan_handoff;用户通过 /wujiecreateissue 完成提升,或由将来支持握手的 adapter 发出原生 handoff。

v2 原生 handoff

需要把 Provider 自己的“保留上下文执行 / 清空上下文执行”动作映射到 Issue 创建时,在 v2 manifest 中声明 native_plan_handoff: true。这项能力是可选的;不声明不会影响普通 v2 前置讨论与 /wujiecreateissue。

Adapter 在已准备好原生执行动作、但尚未真正执行前,通过 stdout 输出:

{"event":"handoff_ready","request_id":"handoff-1","mode":"native_clear"}

mode 只能是 current_context 或 native_clear。父 CLI 会暂停该动作、显示确认卡并先完成 Issue/Run 绑定。原生确认卡不提供“继续讨论”:用户只能完成创建,或按 Ctrl+C 取消并结束 adapter;未完成 discussion 会保留,可从 provider 原生 session 恢复。普通 /wujiecreateissue 仍可返回讨论。随后通过 Adapter stdin 写入一条以 ASCII Record Separator 开头的控制帧:

\x1eWUJIE {"event":"handoff_decision","request_id":"handoff-1","accepted":true,"mode":"native_clear","issue_id":"...","issue_key":"OPE-123","run_id":"..."}

Adapter 必须从普通终端输入中剥离并解析该帧。只有 accepted=true 时才能继续原生动作;取消或创建失败时会返回 accepted=false 和脱敏后的 error。用户取消时,父 CLI 会先写出 accepted=false,再结束 adapter,并把 provider 的信号退出视为正常取消。控制帧不携带 Issue 描述或方案正文,Provider 继续使用自己已经持有的上下文。

stdout NDJSON 协议

Adapter 必须把标准事件以单行 JSON 写到 stdout:每行一个事件,不能跨行。空行会被忽略。

stdout 只写协议事件;普通日志、调试信息、第三方 CLI 原始输出都写 stderr。stdout 出现非 JSON 行会让本次 local run 失败。

消息事件可以省略 event,也可以写 "event": "message":

{"type":"text","content":"开始处理","source":"fake","source_key":"turn-1:text-1"}

支持的消息类型:

type字段说明
textcontentAgent 面向用户的普通输出。
thinkingcontentAgent 思考内容。只有你的 provider 语义确实支持时才写。
tool_usetool, input工具开始执行。input 是 JSON object。
tool_resulttool, output工具执行结果。output 是字符串。
user_inputcontent用户在本地 CLI 中输入的普通消息,会同步成 Wujie 用户评论。
questioncontentAgent 明确向用户提出的问题。讨论阶段会保存,普通 local run 仍按可见文本处理。
plancontent已完成的 Markdown 方案。讨论阶段优先把它作为创建 Issue 的最终方案。
finalcontentAgent 对当前用户输入的最终回复,会同步成 agent 回复评论。
errorcontent可进入执行日志的错误消息。进程失败本身仍应通过退出码表达。

question / plan 推荐使用标准消息包络:

{"event":"message","type":"question","content":"数据库选 PostgreSQL 还是 SQLite?","source":"fake","source_key":"turn-1:question"}
{"event":"message","type":"plan","content":"# 方案\n\n- 使用 PostgreSQL","source":"fake","source_key":"turn-1:plan"}

v2 讨论只持久化 user_input、question、plan 和完整 final。thinking、tool_use、tool_result、usage、session 和控制事件不会进入讨论详情。

所有消息事件都应带:

字段说明
sourceProvider 自己的稳定来源名,例如 fake-provider 或上游 CLI 名称。
source_key当前事件的稳定幂等键。必须在同一 run 内唯一且可重放。

usage 事件通过 usage 字段上报;当前实现只记录以下字段:

{
  "usage": {
    "provider": "fake-provider",
    "model": "demo",
    "input_tokens": 12,
    "output_tokens": 34,
    "cache_read_tokens": 0,
    "cache_write_tokens": 0
  }
}

字段说明:

字段说明
provider实际计费或模型 provider 名称。
model模型名称。
input_tokens输入 token 数。
output_tokens输出 token 数。
cache_read_tokens可选,cache read token 数。
cache_write_tokens可选,cache write token 数。

一个事件可以同时包含可识别的 type 和 usage,但建议分开发送,便于调试。

幂等规则

Wujie reporter 会使用 source / source_key 去重和重试。插件必须提供稳定值:

  • source 用固定 provider 名称或上游系统名称。
  • source_key 用上游事件 ID、session ID + turn ID、tool call ID、transcript 行 ID 等确定性信息。
  • 不要用当前时间、随机数、进程内递增计数作为唯一依据。
  • 重跑或重试同一个上游事件时,应输出相同 source_key。

推荐格式:

session:<session-id>:turn:<turn-id>:user
session:<session-id>:turn:<turn-id>:tool:<tool-call-id>:use
session:<session-id>:turn:<turn-id>:tool:<tool-call-id>:result
session:<session-id>:turn:<turn-id>:final

最小 fake plugin

创建目录:

mkdir -p fake-provider

写入 fake-provider/provider.json:

{
  "schema_version": 1,
  "name": "fake-provider",
  "display_name": "Fake Provider",
  "command": "./wujie-localrun-fake",
  "protocol": "wujie-localrun-provider.v1",
  "capabilities": {
    "structured_events": true,
    "issue_context": "env",
    "usage": true
  }
}

写入 fake-provider/wujie-localrun-fake:

#!/bin/sh
set -eu

if [ -z "${WUJIE_RUN_ID:-}" ]; then
  echo "WUJIE_RUN_ID is required" >&2
  exit 2
fi

printf '%s\n' '{"type":"user_input","content":"请验证 fake provider","source":"fake-provider","source_key":"fake:turn-1:user"}'
printf '%s\n' '{"type":"text","content":"fake provider 已启动","source":"fake-provider","source_key":"fake:turn-1:text-1"}'
printf '%s\n' '{"type":"tool_use","tool":"exec_command","input":{"command":"echo hello"},"source":"fake-provider","source_key":"fake:turn-1:tool-1:use"}'
printf '%s\n' '{"type":"tool_result","tool":"exec_command","output":"hello","source":"fake-provider","source_key":"fake:turn-1:tool-1:result"}'
printf '%s\n' '{"type":"final","content":"fake provider 验证完成。","source":"fake-provider","source_key":"fake:turn-1:final"}'
printf '%s\n' '{"usage":{"provider":"fake-provider","model":"fake-model","input_tokens":1,"output_tokens":1,"cache_read_tokens":0,"cache_write_tokens":0}}'

赋予执行权限:

chmod +x fake-provider/wujie-localrun-fake

安装并查看:

wujie local-run provider install ./fake-provider
wujie local-run provider list
wujie local-run provider list --output json

执行验证:

wujie run WUJ-123 -- fake-provider

验收点:

  • wujie local-run provider install ./fake-provider 成功。
  • wujie local-run provider list 能看到 fake-provider。
  • wujie run WUJ-123 -- fake-provider 能产生 user_input、tool_use、tool_result、final。
  • 插件日志只出现在 stderr,stdout 没有非 JSON 行。

手动配置开发中插件

开发时可以不复制安装目录,直接在当前 CLI config 里配置 manifest 路径:

{
  "localrun_providers": {
    "fake-provider": {
      "manifest_path": "/Users/me/dev/fake-provider/provider.json",
      "enabled": true
    }
  }
}

这种方式适合边改 adapter 边调试。manifest_path 支持 ~ 和环境变量展开。

禁用或移除:

wujie local-run provider disable fake-provider
wujie local-run provider remove fake-provider

内置 provider 不能被外部插件覆盖,也不能通过 provider 子命令禁用或移除。

开发 checklist

  • provider.json 存在,JSON 合法。
  • schema_version 是 1。
  • protocol 是受支持的 v1 或 v2,并与目标执行模型一致。
  • name 与 localrun_providers 配置 key 或安装目录名一致。
  • command 能从 manifest 目录解析到可执行文件。
  • Adapter stdout 只输出单行 JSON 事件。
  • 普通日志、第三方 CLI 原始日志、调试信息全部写 stderr。
  • 每个消息事件都有稳定 source 和 source_key。
  • user_input 只代表用户真实输入,不代表 bootstrap prompt 或 issue context。
  • final 只代表对当前用户输入的最终回复,不写 token delta 或状态文案。
  • 若使用 v2,在仅有 WUJIE_LOCALRUN_SESSION_ID 时也能启动、恢复并输出稳定幂等键;发现原生 session 后输出 session 事件。
  • 若声明 native_plan_handoff,必须在真正执行前等待匹配的 handoff_decision;accepted=false 时不得执行方案,并应结束 adapter。
  • tool_use.input 是 JSON object,tool_result.output 是字符串。
  • usage 只使用当前支持的字段。
  • 非 0 退出码用于表达 adapter 失败。

常见错误

错误现象修复
stdout 写了普通日志parse localrun plugin event 失败日志改写 stderr。
protocol 写错安装或执行时报 unsupported protocolIssue-first 使用 v1;需要讨论使用 v2。
v1 插件执行 wujie discussCLI 提示保持 Issue-first 合约升级 adapter 与 manifest 到 v2。
schema_version 不是 1安装或执行时报 unsupported schema固定写 1。
command 不可执行或路径错误启动 provider 失败使用相对 manifest 目录的路径,并确认权限。
插件名覆盖内置 provider发现 provider 时报 conflict换一个 name。
缺少稳定 source_key重试或断线后消息可能重复使用上游事件 ID 或 session/turn/tool ID 组合。
把 token delta 当 final评论里出现碎片化回复只在完整 assistant message 结束后写 final。
把 issue context 写成 user_input自动同步出多余用户评论只有用户真实输入才写 user_input。

本页目录

LocalRun 插件开发插件包结构仓库样板:Grok Buildprovider.json执行模型执行环境变量前置讨论环境v2 原生 handoffstdout NDJSON 协议幂等规则最小 fake plugin手动配置开发中插件开发 checklist常见错误