dsh-profile-lab
给 DeepSeek Harness(DSH)用的临时 profile 实验台:一条命令建出隔离的 profile,装插件、跑 headless 探针会话、起驻留 web 实例验 HTTP/渲染面、读回结构化摘要、安全回收。
它存在的唯一理由:改插件时怕把本体改暴毙——先在隔离环境里验,确认无误再装进日常用的 profile。
- 宿主半侧插件(Cordis),注册 6 个
tmpprofile_*模型工具;没有浏览器半侧,不声明dsh.client。 inject只写['tools'](不写webServer)——所以在 headless / tui 这类没有 HTTP 服务的 profile 里不会整体 pending。- 零第三方运行时依赖:只用
node:fs/node:path/node:child_process/node:zlib/node:crypto。
它解决什么
| 老做法(每次现写脚本) | 本插件 |
|---|---|
想隔离测试,但官方 --from-default-profile 只能从 5 个内置模板初始化、不能从现有 profile 克隆,还要求目标目录不存在 → 手工 New-Item + 写 4 个文件 |
tmpprofile_create:模板建 或 clone: "desktop" 克隆(配置复制 + node_modules 逐项链接复用) |
以为 --from-default-profile 必然启动,没法"只建不跑" |
内部走 --dump-config 短路 boot(只建、不启动) |
跑探针要手拼 DSH_HOME + 环境变量 + dsh --profile X "任务",跑完还要自己去 sessions/ 里翻会话 |
tmpprofile_run:一次调用完成"装配 → 跑 → 返回 sessionId 与事件摘要" |
验不了渲染端 / HTTP 面(headless 根本没有 HTTP 服务),只能手搓 pwsh 起 dsh web --port 再 curl |
tmpprofile_web_start / web_probe / web_stop:起驻留 web 实例、探任意路由、按 pid 收(跨插件重载也能收) |
读回验证要自己解多帧 zstd(session.v4.jsonl.zstd),几乎每个会话都重写一遍 Python |
tmpprofile_session:内置逐帧解码,直接给 preset / 工具面 / 上下文 / token |
| 清理靠自觉(历史上真的漏过两次,残留至今) | tmpprofile_clean:清单白名单 + junction 感知删除 + 残留自检 |
junction 直接 Remove-Item -Recurse 有删进源仓库的风险 |
先 lstat 判链接、解链接,再递归删目录;绝不跟随链接 |
安装
# 从本地目录装(开发态)
dsh plugin --profile desktop add link:C:/repository/dsh-profile-lab
# 或从 GitHub 装
dsh plugin --profile desktop add github:<你的账号>/dsh-profile-lab
装完重启 DSH(插件在启动时装配)。桌面版的 profile 由 Electron 独占管理,命令行直连会被拒——用桌面版插件页装,或走应用内通道。
十个工具
| 工具 | 干什么 | 关键参数 |
|---|---|---|
tmpprofile_create |
建隔离 profile(模板 / 克隆)+可选装包 | name、template(默认 headless)、clone、packages、purpose、overwrite |
tmpprofile_install |
往已登记的临时 profile 追加安装 / 移除 | name、packages、remove |
tmpprofile_run |
跑一次 headless 探针会话(同步、带超时) | name、task、sessionId、env、timeoutMs、json |
tmpprofile_web_start |
起驻留 web 实例(真实 HTTP 服务,验 host 路由/渲染面) | name、port、probePath、waitMs、cwd、env |
tmpprofile_web_probe |
对驻留实例发一次 HTTP 探针 | name、path、method、timeoutMs |
tmpprofile_web_stop |
停掉驻留实例(taskkill /T /F + 确认端口释放) |
name、waitMs |
tmpprofile_session |
读会话日志给结构化摘要 | sessionId 或 name、sections、limit |
tmpprofile_list |
列出「本插件管的」与「未登记的」profile | — |
tmpprofile_clean |
清理(默认 dry-run,需 confirm: true;会先停实例) |
names、confirm、sessions、archiveTo |
tmpprofile_fingerprint |
采集 profiles/ 指纹,自查有没有碰到别的 profile |
exclude |
工具名以
tmpprofile_开头,便于在dsh-tool-manager里整组启用/禁用。
典型闭环(模型视角):
# A. host 面:装配 + 工具面 + 会话行为
tmpprofile_create { name: "probeA", template: "headless", purpose: "验证 dsh-xxx 的工具面", packages: ["link:C:/plugins/dsh-xxx"] }
tmpprofile_run { name: "probeA", task: "只回复两个字:OK" }
tmpprofile_session { name: "probeA", sections: ["preset","tools","contexts"] }
# B. 渲染 / HTTP 面:起真实例,探插件的 host 路由
tmpprofile_create { name: "webA", template: "web", packages: ["link:C:/plugins/dsh-xxx"] }
tmpprofile_web_start { name: "webA", probePath: "/api/dsh-xxx/status" }
tmpprofile_web_stop { name: "webA" }
tmpprofile_clean { names: ["probeA", "webA"], confirm: true }
web 实例与 headless 探针验的不是同一件事:headless 给 host 侧装配事实 (哪个 entry 没激活、卡在等哪个服务),web 实例给 HTTP/渲染面事实 (端口真的在听、插件的 host 路由真的注册了、页面能加载)。两者都要,才有完整画像。 注意
web_start要求 profile 的 bundles 里有@deepseek-ai/dsh-web-app—— 用template: "web"建,或从带 web-app 的 profile 克隆。
更多配方见 docs/RECIPES.md。
安全护栏(都是刻意的)
- 只动自己建的:只有登记在
$DSH_HOME/profile-lab.json里的临时 profile 才可被写/删;desktop/backup/web/headless/dsh-tui/rescue/reserve/acp/sdk/sdk-minimal一律硬拒。 - 删链接不删目标:
node_modules里的 junction/symlink 先lstat判定再解链接,然后才递归删目录。 - 克隆不共享写面:克隆时
node_modules里的每一项单独链接(不是整目录一个链接),所以随后pnpm add只改写新 profile,源 profile 一个字节都不动。 - clean 默认演练:不传
confirm: true只返回"将要删除什么"。 - 覆盖要显式:
overwrite: true只允许覆盖已登记的同名 profile,旧的挪到$DSH_HOME/profile-lab-trash/,不直接删。 - 失败留痕:profile 先登记再装包,装包失败也留在清单里,能被
tmpprofile_clean回收。 - 进程内 + 跨进程锁:同一 profile 不允许并发运行;跨进程写操作走
$DSH_HOME/profile-lab.lock(陈旧锁 120s 自动抢占)。 - 审计日志:每次 create / install / run / clean 追加一行到
$DSH_HOME/profile-lab-log.jsonl。 - 驻留实例是刻意的例外,且加倍设防:
tmpprofile_web_start起的 web 实例会驻留(这是它存在的意义),所以:同一 profile 只允许一个实例;端口从 19399 起自动挑空闲的;stdout/stderr 只留尾部环形缓冲;进程不 detach(留在宿主 Job Object 内,宿主退出连带杀掉);插件卸载时killAllWebInstances()再扫一遍;tmpprofile_clean删目录前先按清单 pid 停实例。 - 不做后台任务:DSH 的子进程在宿主的 Job Object 里(宿主退出会连带杀死),所以
tmpprofile_run一律同步 + 超时(默认 300s,上限 900s,超时taskkill /T /F杀整棵进程树);唯一的常驻形态就是上面那三条web_*工具,并且必须由调用方显式停止(或用clean兜底)。
配置(cordis 行 config,全部可选)
- insert:
- id: dsh-profile-lab
name: 'dsh-profile-lab'
config:
home: 'C:/Users/you/.dsh' # 缺省 $DSH_HOME 或 ~/.dsh
dshBin: '' # 显式指定 dsh 的 lib/bin.js(缺省自动探测)
nodePath: '' # 显式指定跑子进程的 node(缺省 process.execPath)
tools:
disabled: ['tmpprofile_fingerprint'] # 按名字关掉工具
dsh 可执行文件是怎么找的
按优先级探测,每条都用 -V 实测(app.asar 是文件,existsSync 看不到内部路径,只能试跑):
config.dshBin/ 环境变量DSH_LAB_DSH_BIN- 宿主安装锚点:宿主进程的
profileContext.installAnchor→<安装>/…/@deepseek-ai/dsh/lib/bin.js,用process.execPath+ELECTRON_RUN_AS_NODE=1(版本与宿主一致,兼容性最好) - npm 全局安装:
%APPDATA%\npm\node_modules\@deepseek-ai\dsh\lib\bin.js C:\dsh-runtime\<版本>\...
实测样例(Windows + 桌面版宿主):宿主锚点 → 0.2.0-rc.2(host 同版本);兜底 npm 全局 → 0.2.1-alpha.1。
会话日志是怎么读的
session.v4.jsonl.zstd 是多帧 zstd(DSH 每追加一批事件压一帧)。Node 的 zstdDecompressSync 对多帧输入只解第一帧且不报错,也不暴露"剩余未消费字节"——整段丢进去只拿到 254 字节的 header。
官方 @deepseek-ai/dsh-session-persistence-jsonl 的做法是:按 zstd 魔数(0xFD2FB528,小端 28 B5 2F FD)扫描帧起点,再逐帧解压。本插件用同一思路并利用"one-shot 只解第一帧"的特性把实现压到最短:对每个魔数偏移试解一次,成功即得该帧,伪魔数(解出来不是 { 开头)直接跳过。
实测:6.51 MiB 压缩 → 4322 帧 → 20 MiB 文本 → 6901 事件,摘要正确。
两个 DSH 侧的坑(本插件都已堵掉)
- 工具返回值必须是 lossless JSON。只要返回值里出现
undefined(哪怕只是某个可选字段),DSH 会把整条结果拒收,模型看到的是tool "x" returned invalid output: value is not lossless JSON而不是数据。本插件在所有工具出口统一做了一次净化(丢undefined、非有限数转null)。 - headless 的
--json是投影,不是日志。它用自己的一套事件名(tool_call、tool_result、status、thinking…),不含日志里的tool/call; 官方文档也明说它"caps every string except the terminalfinalat 8 KiB and omits events the projection does not model"。 所以「到底调了哪些工具」必须以磁盘上的完整会话日志为准——tmpprofile_run已经替你把这一步做了:返回值里的session.tools就是完整日志的工具面。
自检
装好之后(或改动代码之后)可以直接跑内置自检——它会真建一个临时 profile、装本插件、跑一次 headless 让模型调用 tmpprofile_list、再清理:
node scripts/selfcheck.mjs
退出码 0 表示插件在真实 DSH 里能加载、工具可被调用、清理无残留。
测试
# 单元测试(全部在临时 DSH_HOME 里跑,不碰真实环境)
npm test
# 真实闭环(会真建 profile、真跑一次 headless、真清理;消耗极少量 token)
$env:DSH_LAB_LIVE='1'; node --test tests/live.test.js
live 测试的硬断言:除本次建的临时 profile 外,profiles/ 里其他 profile 的指纹(hash + mtime)必须一个字节不变。
限制(明确不做)
- 不是危机救援工具:主力 profile 起不来时,本插件也起不来。要救援请用
rescue/reserve实例。 - web 实例只验到"服务端+静态资源"这一层:端口在听、路由在答、页面 HTML 能取到;页面里 JS 真的执行、UI 真的渲染出来,只有真浏览器加载才会发生(需要的话自己打开
http://127.0.0.1:<port>/看)。 - 不做脱离宿主的任务:见上文 Job Object。
- 不自动纳管已有的临时 profile:
profiles/里历史残留(例如早年手工建的目录)会出现在unmanaged列表里,需要你确认后用adoptProfile显式纳管,本插件不会自动动它们。 - 共享
DSH_HOME:探针会话仍写在主力的sessions//storages/下(同一 home 无跨进程锁)。要彻底隔离得用独立DSH_HOME,尚未实现。
文件位置
| 内容 | 路径 |
|---|---|
| 清单 | $DSH_HOME/profile-lab.json |
| 审计日志 | $DSH_HOME/profile-lab-log.jsonl |
| 跨进程锁 | $DSH_HOME/profile-lab.lock |
| 覆盖时丢弃的旧 profile | $DSH_HOME/profile-lab-trash/ |
| 临时 profile | $DSH_HOME/profiles/<name>/ |
许可
MIT
No comments yet. Be the first to write one.