为 DeepSeek Harness 提供结构化、机器可读的 CI 输出:JSON/NDJSON 会话报告、JUnit XML、语义化退出码、工件收集与隐私脱敏——一个 dsh profile bundle 外加独立 CLI。
安装
# GitHub 源码(首次需按提示配置 allowBuilds 构建授权后重试)
dsh plugin --profile web add github:JohnXu22786/headless-json
装任何插件都等于在你的机器上跑第三方代码,权限和你本人一样大——能读你的文件、用你的凭据、访问网络,工具审批管不到它。GitHub 来源的插件还会在安装时执行构建脚本——pnpm 默认拦截,所以安装可能停在 ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED 或 ERR_PNPM_IGNORED_BUILDS;dsh 会打印出需要添加的确切键名,把它加进该 profile 的 pnpm-workspace.yaml 的 allowBuilds 下,重跑一次即可装上。放行构建本身就是一次信任判断:请只安装可信来源,并尽量锁定 commit(github:owner/repo#sha)。
README
面向 DeepSeek Harness(dsh)的结构化、机器可读 CI 输出。
English documentation: README.md
dsh 官方 headless 模式只打印最后一条助手文本并以 0/1 退出——适合快速冒烟,但对 CI 来说信息太薄。dsh-headless-json 是一个 profile bundle,把任意 dsh 会话变成真正的 CI 产物:
- 事务级 JSON 报告(会话元数据 + 每个结构化事件 + 结果 + 统计);
- 增量 NDJSON 事件流,运行期间可实时
tail; - JUnit XML 报告,供 GitLab CI / Jenkins / Azure DevOps 等直接展示;
- 语义化退出码,区分成功、失败、超时、阻断、中止与中断;
- 工件清单,收集会话输出中引用的文件路径;
- 隐私层(文本截断、参数隐藏、密钥掩码、路径相对化);
- 双入口:运行时内的 dsh 工具(
output_status、output_events、set_options)+ 独立 CLI,用于离线渲染与退出码接线。
一切皆确定:给定相同的会话日志与相同的选项,产出的字节完全相同。
目录
功能
| 领域 | 内容 |
|---|---|
| 会话事件流化 | 订阅 dsh 会话事件源(session/created、session/event、session/flush、session/disposed),为每个 turn/step/工具调用派生出结构化事件:类型、模型、耗时、token、工具名、参数摘要、结果、错误、状态。 |
| JSON 输出 | 完整事务级报告:会话元数据 + 事件列表 + 结果/退出码 + 统计。可配置增量 NDJSON 流。 |
| JUnit XML 输出 | 工具调用、步骤与轮次映射为测试用例,任何 CI 系统都能像展示测试一样展示 dsh 运行。 |
| 退出码语义 | 稳定分类(成功/失败/超时/阻断/空/中止/中断),默认码全部可覆盖。 |
| 工件收集 | 用户/助手/工具文本中引用的文件路径收集为清单;同机运行时记录存在性与大小。 |
| 隐私开关 | 工具输出截断、参数隐藏、密钥掩码、路径相对化——挂载时配置,运行时经 set_options 调整。 |
| 工具链 | 三个 dsh 工具(output_status、output_events、set_options)+ 独立 CLI(dsh-headless-json render|exit)。 |
工作原理
本包是一个 dsh profile bundle:npm 包,manifest 声明:
"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }
补丁向加载器树插入一个插件(headless-json)。插件订阅会话服务事件,为每个会话维护一个 Capture,事件到达时实时写出 NDJSON 行,并在会话销毁时(dsh 正常优雅关停的一部分)写出 JSON/JUnit 报告。
dsh 会话事件流
│ session/created · session/event · session/flush · session/disposed
▼
┌─────────────────── CaptureManager ───────────────────┐
│ 每会话 Capture:派生结构化事件 │
│ (turn/step/tool 耗时、token、模型、错误) │
│ + 工件扫描 + 原始类型分布 │
└────────────┬──────────────────────────┬─────────────┘
│ 实时 NDJSON 行 │ 会话结束时
▼ ▼
events.ndjson report.json · junit.xml
- 采集与输出分离:事件在内存中只派生一次;脱敏在输出时应用,因此用
set_options改选项会影响其后每一次写出。 - 不打扰 stdout:插件从不写 stdout,与官方 headless runner(独占 stdout)干净共存。
- 错误隔离:每个监听器都有防护;畸形事件永远不会拖垮插件树或污染报告。
包结构
headless-json/
├── package.json # dsh.bundle.patch 清单 + bin
├── cordis.patch.yml # 加载器补丁(插入插件行)
├── lib/ # 编译产物 ESM(tsc 输出)
├── bin/dsh-headless-json.js # CLI 入口
├── src/ # TypeScript 源码(仅仓库)
├── test/ # node:test 测试套件(仅仓库)
├── examples/ # 样例会话日志 + 生成产物
├── README.md / README.zh.md
└── LICENSE # MIT
上面是仓库目录树;发布的 npm 包只包含 lib、bin、cordis.patch.yml、examples、两篇 README 与 LICENSE。
安装与接入
要求:Node.js ≥ 18,以及一个组合了 sessions 服务的 dsh 安装(任何 base/headless profile 都满足)。
1. 构建(仅从源码开发时需要)
npm install
npm run build # 或:npm test(构建 + 跑完整测试套件)
2. 把 bundle 加进 dsh profile
用 CLI(转发给 pnpm):
dsh plugin --profile headless add /path/to/headless-json
或直接从本仓库安装:
dsh plugin --profile headless add github:JohnXu22786/headless-json
或手动:在 profile 的 package.json 依赖中加上 dsh-headless-json,并加入有序的 dsh.profile.bundles 列表,然后在 profile 目录 pnpm install。dsh 下次运行时会把已安装的 bundle 与 dsh.profile.bundles 对账。
3. 运行
dsh --profile headless "run the test suite"
运行结束后,在工作目录的 dsh-output/ 下得到:
dsh-output/report.json # 事务级 JSON 报告
dsh-output/junit.xml # JUnit XML 报告
dsh-output/events.ndjson # 仅当 output.ndjson = true 时
4. 接入 CI
语义退出码在报告中;CLI 把它变成进程退出码:
dsh --profile headless "run the test suite"
code=$(dsh-headless-json exit dsh-output/report.json)
exit $code
或直接把 JUnit 报告交给 CI 收集器:
dsh-headless-json render dsh-output/report.json --format junit --out junit.xml
一次运行产出什么
每个会话,插件写出:
| 文件 | 时机 | 内容 |
|---|---|---|
report.json |
会话结束 | 完整报告(见 JSON 报告) |
junit.xml |
会话结束 | JUnit XML(见 JUnit XML) |
events.ndjson |
实时 | 每事件一行 JSON,实时追加,末尾一行 session_end |
文件名支持 {session} 占位符(如 "json_file": "report-{session}.json");未用时,同一进程内的第二个会话会在扩展名前追加 -<id>。output.write_empty 为 false 时,零事件的会话不写任何文件。
dsh 工具
本 bundle 经 ctx.tools.register 注册三个工具,对所有 agent 可见,按调用方 agent 的会话解析。
| 工具 | 参数 | 返回 |
|---|---|---|
output_status |
无 | 实时采集状态:事件/turn/工具计数、挂起中的工具调用、脱敏设置、流状态。 |
output_events |
types?(kind 数组)、since?(seq)、limit?(默认 200) |
请求的结构化事件,与文件相同的脱敏处理。 |
set_options |
部分 output / redact / artifacts / events / exit |
校验后的新生效选项。 |
对话示例:
set_options({ "redact": { "args": "hide" } })
→ { "applied": true, "redact": { "args": "hide", ... } }
CLI 参考
dsh-headless-json render <input> [options] 渲染报告/事件文件
dsh-headless-json exit <input> [options] 打印语义退出码
dsh-headless-json --version | --help
<input> 可以是:
- 插件产出的
report.json; - 插件产出的
events.ndjson流; - 原始 dsh 会话
.jsonl日志(头部行 +session/*事件行); - 原始会话事件的 JSON 数组。
| 选项 | 含义 |
|---|---|
--format json|junit|ndjson |
输出格式(默认 json)。 |
--out <file> |
写入文件而非 stdout。 |
--pretty |
美化 JSON 报告。 |
--set <key=value> |
覆盖选项,如 --set redact.text_length=1200,可重复。 |
--text-length <n> |
--set redact.text_length=<n> 的简写。 |
--arg-length <n> |
--set redact.arg_length=<n> 的简写。 |
--args full|truncate|hide |
--set redact.args=<mode> 的简写。 |
--paths relative|absolute |
--set redact.paths=<mode> 的简写。 |
--no-secrets |
关闭密钥掩码。 |
--max-events <n> |
--set events.max_events=<n> 的简写。 |
--cwd <dir> |
路径相对化的基准目录。 |
--include-log-only |
把仅日志类事件类型以 other 呈现。 |
说明:
exit打印退出码并用该码退出进程——CI 接线的关键。- 脱敏类
--set覆盖只对原始会话日志重新渲染生效。events.ndjson流与report.json在采集写出时已脱敏,再输出时保持原样。
配置
所有键为 snake_case,可通过 bundle 补丁的 config 块、set_options 工具或 CLI --set 配置。配置文件容忍未知键(便于前向兼容的部署);set_options 与 CLI --set 拒绝未知键,包括未知的嵌套键。
# cordis.patch.yml(profile 覆盖示例)
- id: headless-json
config:
output:
dir: dsh-output
json: true
junit: true
ndjson: true
redact:
text_length: 4000
args: truncate
paths: relative
secrets: true
| 键 | 默认 | 说明 |
|---|---|---|
output.dir |
dsh-output |
报告目录(相对 cwd)。 |
output.json |
true |
写 report.json。 |
output.junit |
true |
写 junit.xml。 |
output.ndjson |
false |
实时流式写 events.ndjson。 |
output.json_file |
report.json |
JSON 文件名(支持 {session} 占位符)。 |
output.junit_file |
junit.xml |
JUnit 文件名。 |
output.ndjson_file |
events.ndjson |
NDJSON 文件名。 |
output.write_empty |
true |
为零事件的会话也写报告。 |
output.pretty |
false |
美化 JSON 报告。 |
redact.text_length |
4000 |
输出中任何文本值的最大字符数(0 = 不限)。 |
redact.arg_length |
500 |
工具参数摘要的最大字符数。 |
redact.args |
truncate |
full(解析对象)、truncate(摘要)、hide([hidden])。 |
redact.paths |
relative |
relative(相对会话 cwd)或 absolute。 |
redact.secrets |
true |
掩码形似密钥的字符串。 |
redact.secret_patterns |
[] |
额外的正则源字符串,在内置模式之外追加应用。 |
artifacts.collect |
true |
扫描文本中的文件路径引用。 |
artifacts.check_exists |
true |
stat 命中的路径(仅运行机上有意义)。 |
artifacts.max_entries |
500 |
工件清单最大条目数。 |
artifacts.pattern_extras |
[] |
额外的路径正则源字符串。 |
events.max_events |
1000 |
报告事件列表的最大条数(0 = 不限)。 |
events.trim |
balanced |
head 只保留前 N 条;balanced 保留头尾。 |
events.include_log_only |
false |
把仅日志类型(request/header、session/end-seed 等)以 other 呈现。 |
exit.* |
见表格 | 各类别退出码(见 退出码语义)。 |
capture.text_cap |
100000 |
单段存储文本的硬上限(字符)。 |
输出格式
JSON 报告
{
"schema_version": 1,
"plugin": { "name": "dsh-headless-json", "version": "0.1.0" },
"generated_at": 1753000000000, // 会话结束时间(派生,确定性)
"session": {
"id": "...", "cwd": ".", "cwd_name": "repo",
"created_at": ..., "started_at": ..., "ended_at": ...,
"event_count": 42,
"parent_session": null, "agent_preset": "minimal", "delegation_depth": null
},
"outcome": {
"status": "success", // 稳定分类
"exit_code": 0, // 语义退出码
"reason": "completed", // 原始 turn/end 原因
"complete": true,
"undelivered_tool_calls": [],
"error": null // 出错时为 {code, message, status?}
},
"stats": {
"duration_ms": 4123.5,
"turns": 1, "steps": 2,
"assistant_messages": 3, "user_messages": 1,
"tool_calls": 2, "tool_errors": 0, "undelivered_tool_calls": 0,
"chunk_count": 14,
"tokens": { "input": 900, "output": 300, "cache_read": 0, "cache_write": 0, "reasoning": 0 },
"events_by_type": { "assistant/message": 3, "tool/call": 2, ... }, // 原始 dsh 类型
"by_tool": { "bash": { "calls": 2, "errors": 0, "latency_ms_total": 800, "latency_ms_max": 500 } }
},
"events": [ /* 见下 */ ],
"events_truncated": null, // 被裁剪时为 {kept,total,dropped}
"artifacts": [ { "path": "src/options.ts", "kind": "file", "size": 4281, "references": 3 } ]
}
事件
每个事件都带 seq(会话日志序号,稳定排序键)、time(Unix 毫秒)与 kind:
| kind | 附加字段 |
|---|---|
turn_start |
turn |
turn_end |
turn、reason、error(code/message/status)、cause(中止原因)、latency_ms、complete |
step_start / step_end |
turn、step、(latency_ms) |
user_message |
turn、step、text、reasoning、blocks |
assistant_message |
turn、step、provider、model、latency_ms、usage、text、reasoning、blocks、stream(chunk 数量/时间) |
tool_call |
turn、step、call_id、tool、args_mode、args、args_summary、latency_ms、undelivered |
tool_result |
turn、step、call_id、tool、status(success/error)、latency_ms、text、blocks、error |
todo_write |
count、todos |
request_context |
provider、model、context_window |
other |
type(仅 events.include_log_only 时) |
工具调用与结果按 call_id 关联;耗时由会话日志时间戳计算。会话结束时仍未收到 tool/result 的 tool_call 会标记 undelivered: true,并列入 outcome.undelivered_tool_calls。
NDJSON 流
每行一个 JSON 对象,按到达顺序即时写出:同样的结构化事件,末尾追加一行汇总:
{"kind":"session_end","session_id":"...","generated_at":...,
"outcome":{"status":"success","exit_code":0,"reason":"completed"},
"stats":{"duration_ms":4123,"turns":1,"steps":2,"tool_calls":2,"tool_errors":0}}
每次 session/flush 检查点都会 fsync 文件,whenIdle() 后读存储的消费者能看到持久化的流。tool_call 行在结果到达之前即被流式写出,因此这些行上的 latency_ms / undelivered 是暂定值;关联的 tool_result 行(或最终报告)携带权威值。
JUnit XML
| dsh 概念 | JUnit 映射 |
|---|---|
| 整个会话 | <testsuites name="dsh-headless-json"> → 一个 <testsuite>,属性含会话 id、cwd、status、exit_code、reason、插件版本 |
| 总体结果 | <testcase name="run"> — 仅当 outcome.status === "success" 时通过,否则 <error>/<failure> |
| 每个 turn | <testcase name="turn-N"> — completed 通过;error/max-tokens 失败;blocked/aborted/interrupted 跳过 |
| 每个 step | <testcase name="step-N"> 带耗时;未关闭则跳过 |
| 每个工具调用 | <testcase name="tool:NAME"> 带耗时,结果文本在 <system-out>;失败产生 <failure type="dsh:tool"> |
时间以十进制秒计,timestamp 为 ISO-8601 UTC,所有文本经 XML 1.0 转义(控制字符替换)。
退出码语义
最终的 turn/end 原因映射为稳定分类与文档化退出码;每个值都可经 exit.* 覆盖:
| 分类 | 最终 turn/end 原因 | 默认码 |
|---|---|---|
success |
completed |
0 |
error |
error |
1 |
timeout |
max-tokens |
2 |
blocked |
blocked |
3 |
empty |
(完全没有事件) | 4 |
aborted |
aborted |
130 |
interrupted |
interrupted |
130 |
有事件但没有闭合 turn/end 的会话按 error/incomplete 报告。130 遵循 SIGINT 惯例,表示用户主动中止。
隐私与脱敏
默认设置保守但可用。一切都在输出时应用。
- 文本截断 — 每个文本/reasoning 值按
redact.text_length截断,带确定的…[+N more chars]标记。 - 参数处理 —
redact.args:full:解析后的参数对象,字符串值掩码/截断;truncate(默认):掩码摘要,上限redact.arg_length;hide:字面量[hidden]。
- 密钥掩码(
redact.secrets)— 内置覆盖sk-…密钥、Bearer …头、PEM 私钥、GitHub/Google token、JWT、长 hex(保留短前缀以便辨认如 commit SHA)与形似 token 的字符串。turn/end 的错误消息按自由文本处理,同样掩码。可用redact.secret_patterns(正则源字符串,配置期校验)追加。 - 路径相对化 —
redact.paths: relative(默认)从工件路径中剥离会话 cwd,并把session.cwd渲染为.;absolute保留完整路径。
实际效果:默认输出不包含绝对工作区路径、不包含原始密钥串、不包含无界工具输出。
工件
artifacts.collect 开启时,每个用户/助手/工具文本都会扫描形似路径的候选(带分隔符的 POSIX/Windows 绝对路径与相对路径;URL 与邮箱被排除)。每个唯一候选成为一条工件条目并记录引用次数;artifacts.check_exists(默认开)且在同机运行时,kind(file/dir/missing)与 size 由文件系统填充。清单以 artifacts.max_entries 为界。
确定性
给定相同的会话事件与相同的生效选项:
- JSON 报告逐字节一致——对象键顺序由构造固定,事件按
seq排序,映射型字段按排序键序列化,数字统一保留 3 位小数; - JUnit XML 与 NDJSON 行出自同一序列化核心,三种格式永远一致。
generated_at(以及 JUnit 的 timestamp)由会话自身时间线派生——取最后一条事件的时间,因此从不依赖墙钟。
开发
npm install
npm run build # tsc -> lib/
npm test # 构建 + 跑完整套件(node:test)
npm run test:only # 对当前构建跑测试
npm run typecheck # tsc --noEmit
测试套件覆盖:事件订阅接线(基于最小假 context)、采集派生、序列化确定性、脱敏、退出码映射、JUnit 结构与转义、NDJSON 往返、CLI 端到端。
局限与兼容性
- 预览期 API。 dsh 处于开发者预览阶段,会话事件形态与服务语义可能演进。采集管线对所有事件做防御性读取(畸形输入只计数、不致命),所用的事件词汇均来自已发布的会话类型定义。
- 工具 schema 形态。 三个 dsh 工具以符合
ctx.tools.register期望形状的普通对象注册;若未来 dsh 改变定义 DSL,只需更新src/tools.ts。组合中没有ctx.tools时,插件仍然采集与报告,只是跳过工具注册(带日志警告)。 - 退出码与官方 headless。 官方 headless runner 按自身契约以 0/1 退出;本插件不触碰
headlessIo。需要细粒度退出码请用exit命令(或读report.json)。 - 重渲染脱敏。 只有原始会话日志能用不同的
--set脱敏重渲染(脱敏是采集时属性,report.json与 NDJSON 行已脱敏,再输出时保持原样)。
License
MIT — 见 LICENSE。
链接
同类插件
yjh051108/dsh-routing-suite★ 7005
一个仓库三件套:DSH 插件包的运行时注入器(注入、热重载、卸载、开发侧挂区一键转正、路由自愈,外带设置页插件管理:列出、卸载、拖入文件夹内化)、任务感知的思维模式路由 agent 预设(router-standard / router-spec / router-react)、以及分级两级任务协议(commit_star / lock_stage / revise_do / edit_plan / mark_task / redteam_verdict 六个工具,任务状态落盘)。注入器实现直接在库内,安装的是它自己的行为而不是一份依赖清单。
strukto-ai/mirage#dsh★ 3675
把文件系统与 bash 提供者换成 mirage 虚拟工作区:文件工具与 shell 命令作用于挂载的资源(RAM、S3、Redis、Slack、Gmail、Notion、Postgres)而非宿主磁盘,支持按挂载点设置读/写/执行模式、按命令选择沙箱(进程内 monty、pyodide、quickjs;远程 docker、e2b、daytona),并可在虚拟终端中安装 CLI(git、gh、slack、linear、ntn、gws,或自行注册的程序树)作为命令头词。
hust-open-atom-club/oh-dsh★ 322
社区发行版:TUI、桌面端与 Web UI 统一体验,分层安装、一步到位。
weijiafu14/pi2dsh★ 212
Pi Host ABI 兼容引擎:装一次之后,npm 上的 Pi 扩展原包经 `dsh plugin add <pi-package>` 直接作为 DSH 原生插件挂载。已在官方 DSH 上端到端验证 pi-mcp-adapter(完整 MCP 管理面:OAuth、resources、prompts、MCP Apps、elicitation、sampling)、@tintinweb/pi-subagents、pi-code、pi-hermes-memory、pi-background-tasks;`pi2dsh inspect` 在安装前报告一个包的兼容情况。
Fishquito7/dsh-skill-mcp-panel★ 179
在 DSH Web 设置中管理技能与 MCP 服务器:技能卡片热启停、工作区作用域、分组、批量迁移与拖拽导入,以及 stdio/HTTP MCP 增删改查、连接测试、密钥脱敏,并附带统一 dsh-panel 命令行。
lire1131/dsh-undo-savepoint★ 176
DSH 撤销/回退系统:配置变更自动存档,一键撤销/恢复/回退到任意版本,支持 WebUI 与离线 CLI/GUI 工具(DSH 启动失败也能救)。
社区评论
评论公开保存在 GitHub Discussions。加载评论会连接 GitHub 和 Giscus;发表内容需要 GitHub 账号。