fengbai2233/dsh-pwsh-quoting-guard0

dsh-pwsh-quoting-guard

DSH 插件:pwsh_script 与 run_argv 两个工具,从结构上消除 Windows PowerShell 的命令字符串引号转义错误。 · Two DSH tools that remove the PowerShell command-string quoting layer: a multi-line body passed as one argv element with structured $DSH_ARGS data, plus a shell-free argv runner.

包名
dsh-pwsh-quoting-guard
版本
1.0.1
许可证
MIT
最近更新
2026年9月11日

安装

$npx -p @deepseek-ai/dsh dsh plugin --profile web add github:fengbai2233/dsh-pwsh-quoting-guard

dsh-pwsh-quoting-guard

DSH(DeepSeek Harness)插件:让模型写 PowerShell 时不再需要跟引号搏斗

它提供两个模型可见工具 —— pwsh_scriptrun_argv —— 从结构上移除"命令字符串"这一层转义负担,数据通过结构化数组传递。适用于 Windows 上以 Windows PowerShell 5.1 为执行器的部署。

  • 包名:dsh-pwsh-quoting-guard
  • 平面:宿主组合(host composition)—— 向 tools / systemPrompt 注册表贡献内容,消费宿主提供的 subprocess
  • 依赖:零运行时依赖(本地 link 安装的包按真实路径解析导入,因此刻意不 import 任何 @deepseek-ai/*
  • 版本:1.0.0(对应开发过程中的 v5)

1. 它解决什么问题

现象:模型执行 PowerShell 命令时,经常因为嵌套引号、$%、反斜杠、含空格或非 ASCII 的路径而出错;而且失败往往以"一大坨报错 + 重试"的形式消耗 token。

根因(在本部署实测确认,逐条有证据):

#事实证据
1官方 pwsh 工具的 command单个字符串,经 pwsh -Command 执行 —— 引号/转义负担 100% 在模型侧@deepseek-ai/dsh-tool-pwsh README
2本机 pwsh 根本不在 PATH,实际执行器是 Windows PowerShell 5.1Get-Command pwsh 空;工具内 $PSVersionTable.PSVersion = 5.1.26100.4652
3PS 5.1 向原生程序传参时会吃掉内嵌双引号,且不报错node -e ... 'a"b''c' 返回 ab'c(期望 a"b'c),零报错;$PSNativeCommandArgumentPassing 是 PS 7.3+ 才有的开关
4PS 5.1 的 ConvertFrom-Json 不展开顶层 JSON 数组@(ConvertFrom-Json '["a","b"]') 只有 1 个元素(且该元素是数组本身),$DSH_ARGS[0] 不是字符串
5-EncodedCommand 传脚本会让 PS 5.1 把错误流序列化成 CLIXML一次单行报错从 322 字符涨到 504 字符(+53% token),-OutputFormat Text 实测无效

第 3 条尤其危险:结果错了但没有任何报错,token 指标完全看不见,只能靠人发现。


2. 两个工具

pwsh_script

参数类型必填说明
scriptstringPowerShell 正文,逐字传递,可多行。字面量数据请放进 args,不要写进正文。
argsstring[]逐个绑定为 $DSH_ARGS[0]$DSH_ARGS[1] ……
// 读取一个含空格/中文/$/%/单引号的路径
{
  "script": "(Get-Content -LiteralPath $DSH_ARGS[0] -Raw).Trim()",
  "args": ["E:\\deepseekwork\\插件\\.dsh-tmp\\$100 %TEMP% it's dir\\sample file.txt"]
}

run_argv

参数类型必填说明
programstring可执行文件名(PATH 上)或绝对路径
argsstring[]参数向量,一个元素一个参数,逐字传递
cwdstring工作目录;默认会话工作区
// 在指定目录里跑 node,同时保证参数含引号也逐字保真
{
  "program": "node",
  "args": ["-e", "console.log(process.cwd(), process.argv.slice(1))", "a\"b'c"],
  "cwd": "E:\\deepseekwork\\插件\\.dsh-tmp\\$100 %TEMP% it's dir"
}

3. 工作原理(为什么不会再出错)

  1. 正文作为单个 argv 元素交给 -Command。它不经过任何 shell、不做二次拼接、不被再次当作字符串嵌入 —— 与官方执行器同款配方。
  2. 数据走 args:插件用 PowerShell 数组字面量 把参数绑成 $DSH_ARGS,只对单引号做加倍 —— 转义由插件代码完成,模型永远不转义。刻意不用 ConvertFrom-Json(见根因 4)。
  3. 第 1 行 prelude 同时做三件事:钉住 [Console]::OutputEncoding / $OutputEncoding 为 UTF-8、静默进度流、绑定 $DSH_ARGS;全部放在同一行,因此模型自己写的行号仍然准确(报错会指到 At line:1 char:188 这样的真实位置)。
  4. run_argv 完全不经过 shellctx.subprocess.spawn(argv) 是"argv 即最终 argv"的接缝,参数不会被解析、拆分或展开;cwd 直接作为子进程工作目录。
  5. 结果词表与官方 pwsh 工具一致:stdout、可选 [stderr] 段、[output truncated; full output: ][exit code: N](0 不输出标记)、超时/中断标记。成功路径上两者的结果侧 token 完全相同。
  6. 环境对齐:注入 NO_COLOR=1PAGER=catGIT_PAGER=cat,并通过 shellEnv 注册表带上受管的 DSH_*DSH_HOMEDSH_SHELLDSH_SESSION_ID …)。
  7. 沙箱read-only / workspace-write 下经 ctx.sandbox.confine() 包装,包装失败即 fail closed(绝不静默放开);danger-full-access 下跳过包装(该模式下 ACL runner 本身拒绝 danger-full-access,包装会导致 100% 失败)。

4. 安装与卸载

本地 link 安装(开发态):

dsh plugin --profile web add link:E:\deepseekwork\插件\dsh-pwsh-quoting-guard

该命令会:pnpm 安装依赖 → 识别包内 dsh.bundle.patch → 自动登记进 profile 的 dsh.profile.bundles重启 dsh web 后生效(组合在启动时装载)。

卸载:

dsh plugin --profile web remove dsh-pwsh-quoting-guard

发布到 npm 后也可直接 dsh plugin --profile web add dsh-pwsh-quoting-guard

重启后自检(三条,各一次即可)

// 1. 工具在不在:应看到 pwsh_script / run_argv 两个工具
{ "program": "git", "args": ["--version"] }                       // run_argv → git version 2.x

// 2. 中文/空格/含引号的路径,数据走 args,正文里不出现任何字面量
{ "script": "(Get-Content -LiteralPath $DSH_ARGS[0] -Raw).Trim()",
  "args": ["E:\\some dir\\中文 目录\\sample file.txt"] }

// 3. 参数保真:期望输出 a"b'c(含内嵌双引号)
{ "program": "node", "args": ["-e", "console.log(process.argv[1])", "a\"b'c"] }

三条都通过即安装成功。若工具列表里没有它们,说明组合尚未重新装载 —— 重启 dsh web

插件契约:inject 是硬性的(1.0.0 就是在这里翻车的)

Cordis 不允许在没有声明的情况下访问 ctx.而且代价不是局部报错 —— 一行抛错会让整个插件树装载失败,dsh web 直接起不来

dsh: plugin tree failed to load: failed to apply loader entry dsh-pwsh-quoting-guard:
cannot get property "tools" without inject
export const inject = ['subprocess', 'tools']   // 因为用到了 ctx.subprocess 与 ctx.tools
写法是否需要 inject
ctx.tools.register(...)ctx.subprocess.spawn(...)✅ 必须声明
ctx.get('systemPrompt')ctx.get('sandbox') 等可选读取❌ 不需要 —— 这正是"服务可能不在"的表达方式
ctx.on(...)ctx.effect(...)ctx.provide(...)❌ 不是服务,是 Context API
npm run check      # 等价于 node scripts/check-inject.mjs

该检查列出每个 ctx. 访问及其真实行号、对照 inject 声明,缺一个就 exit 1;同时报告"声明了却没用上"的服务(那会让插件无谓地等待)。它已针对真实的故障版本做过阴性验证:对 lib/index.js.bak-before-inject-fix 运行会精确报出 line 247 ctx.tools -> add 'tools' to inject

⚠️ 另外两点:

  • 提示段名 pwsh-quoting-guard(order 106)在同一层内必须唯一:把本插件同时装进 profile 某个 preset 会因重名而装载失败。
  • 改动 lib/index.js 后先跑一次 npm run check,再重启。

结构与平面lib/index.js 导出 Cordis 插件契约(name / inject / apply),cordis.patch.yml 声明插入的行。工具注册进宿主 tools 注册表、提示段注册进 systemPrompt,因此属于宿主平面,与 tool-pwshtool-bash 同一层。


5. 实测数据(token)

定价使用宿主自己的估算器@deepseek-ai/dsh-token-meter/estimateceil(chars/4)+4,即 context 表所用口径),全部为真实进程实测。

单任务结果侧(tok)

任务官方 pwshpwsh_script / run_argv
引号 + $ 正则77
中文/空格/$/%/' 路径1212
多行 + 中文输出1010
错误路径8585(且无 CLIXML)
外部程序 hostile argv5 —— 但结果是错的6 —— 结果正确

成功路径两者相同(都是 PowerShell 自己的输出)。差异出现在失败与重试:失败的旧路线上,一次报错要多花 53%(85 → 130 tok)、一次参数失败要 236 tok。

固定开销(每个请求)

项目tok
两个工具的 schema263
提示段(1 行)50
合计313 tok/请求

盈亏平衡

按实测"一次失败 85–236 tok + 重试参数 17–44 tok"计,每个请求只要避免约 1–3 次含引号的失败调用即可回本。纯短命令(ls 级别)用官方 pwsh 更省。


6. 与官方 pwsh 工具的关系

并存,不替换。 官方 pwsh 仍负责:单行短命令、run_in_background 后台任务、sandbox_permissions 升级通道。

选择表("在目录 X 里跑程序 P,且参数含引号"):

路线目录可控参数保真
官方 pwsh + workdir❌ 引号被吞
pwsh_script + Set-Location❌ 引号被吞
run_argv(不带 cwd❌ 只能会话工作区
run_argv + cwd

只有最后一行两者兼得 —— 这也是 cwd 只挂在 run_argv 上的原因。


7. 已知限制

  • run_in_background:长任务请用官方 pwsh
  • 单次调用预算 300s(由部署的 timeout policy 执行);输出每流 64 KB,超出写 spill 文件(上限 64 MB)并给出路径。
  • pwsh_script 有意不提供 cwd:需要换目录时用 Set-Location(文件与 cmdlet 操作完全正确),但经 PowerShell 向原生程序传参仍会失真 —— 那种情况请改用 run_argv
  • 参数值中含换行时,prelude 之后的行号会偏移对应行数(罕见;正确性不受影响)。
  • read-only 模式下 PowerShell 处于 ConstrainedLanguage,非核心 .NET 静态调用会被拒(部署既有约束,与本插件无关)。
  • 依赖会话上下文解析默认工作目录:显式 cwd → 会话工作区 → sandboxPolicy.workspaceRoot;三者都拿不到时给出教学式报错。

8. 故障排查

现象原因 / 处理
dsh web 起不来,日志里 cannot get property "tools" without inject用了 ctx. 却没声明。1.0.0 的缺陷;1.0.1 已修。修法:export const inject = ['subprocess', 'tools'],并跑 npm run check
dsh web 起不来,日志里提示段重名同一层装了本插件两份(如 profile + preset 各一份)。只保留一处
工具列表里看不到两个工具未重启:组合在 dsh web 启动时装载。重启后确认
每次调用都返回 exit code: 127 + windows-acl-run: unknown mode沙箱包装被套在 danger-full-access 模式上(≤v2 的缺陷)。确认运行的是 ≥v3 的代码
cannot resolve a PowerShell executable (tried pwsh, pwsh.exe, powershell.exe, powershell)PATH 上没有任何 PowerShell
sandbox confinement failed under "read-only" ...受限模式下包装失败,插件拒绝不包装执行;改用官方 pwsh + sandbox_permissions
$DSH_ARGS 只有一个元素、内容是所有参数拼起来用了 ConvertFrom-Json 版本(≤v2)。≥v3 用数组字面量
报错里出现 `` 的大段 XML用了 -EncodedCommand 版本(≤v2)导致 CLIXML。≥v3 用 -Command

9. 版本历史

版本变化结果
v1-EncodedCommand + ConvertFrom-Json 前置本环境 100% 失败(沙箱包装 exit 127)
v2danger-full-access 跳过 confine可用性恢复,但参数绑定错误 + CLIXML 噪音
v3-Command 单 argv + 数组字面量 + UTF-8 钉住行为正确;但描述冗长(634 tok/请求)
v4描述精简、去 cwd、提示段压一行289 tok/请求(-56%)
v5 = 1.0.0run_argv 加回 cwd;注入 DSH_*;修正无 cwd 时的死胡同报错313 tok/请求,逻辑正确 —— 但移植时漏声明 inject: ['tools'],导致 dsh web 无法启动
1.0.1补上 inject 中的 tools;新增 scripts/check-inject.mjs 守卫(已对该故障版本做阴性验证);README 补"插件契约"一节启动正常;三条自检实测通过

教训:动态 Cordis 插件用 harness.registerTool(ctx, …)从不直接访问 ctx.tools;移植成永久插件改成 ctx.tools.register(…) 时,这个声明就漏了 —— 而静态桩测试因为桩上下文无条件暴露 tools,照不出这个问题。npm run check 正是为补上这个盲区而写。

10. 许可

MIT。