DSH HUB
HomePlugin StorePlugin PacksCommunityRankingsResourcesPublish Guide
Plugin source
Back to catalog

fengbai2233 /

fengbai2233/dsh-pwsh-quoting-guard

Verified

DSH plugin: pwsh_script + run_argv — structural immunity to PowerShell command-string quoting errors on Windows.

★ 0 Stars0 Forks0 IssuesN/A Community rating0 Confirmed installs
View on GitHub
READMESource: main@96ec422b

dsh-pwsh-quoting-guard

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

它提供两个模型可见工具 —— pwsh_script 与 run_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 <string> 执行 —— 引号/转义负担 100% 在模型侧 @deepseek-ai/dsh-tool-pwsh README
2 本机 pwsh 根本不在 PATH,实际执行器是 Windows PowerShell 5.1 Get-Command pwsh 空;工具内 $PSVersionTable.PSVersion = 5.1.26100.4652
3 PS 5.1 向原生程序传参时会吃掉内嵌双引号,且不报错 node -e ... 'a"b''c' 返回 ab'c(期望 a"b'c),零报错;$PSNativeCommandArgumentPassing 是 PS 7.3+ 才有的开关
4 PS 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

参数 类型 必填 说明
script string ✅ PowerShell 正文,逐字传递,可多行。字面量数据请放进 args,不要写进正文。
args string[] 逐个绑定为 $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

参数 类型 必填 说明
program string ✅ 可执行文件名(PATH 上)或绝对路径
args string[] 参数向量,一个元素一个参数,逐字传递
cwd string 工作目录;默认会话工作区
// 在指定目录里跑 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 完全不经过 shell:ctx.subprocess.spawn(argv) 是"argv 即最终 argv"的接缝,参数不会被解析、拆分或展开;cwd 直接作为子进程工作目录。
  5. 结果词表与官方 pwsh 工具一致:stdout、可选 [stderr] 段、[output truncated; full output: <path>]、[exit code: N](0 不输出标记)、超时/中断标记。成功路径上两者的结果侧 token 完全相同。
  6. 环境对齐:注入 NO_COLOR=1、PAGER=cat、GIT_PAGER=cat,并通过 shellEnv 注册表带上受管的 DSH_*(DSH_HOME、DSH_SHELL、DSH_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-pwsh、tool-bash 同一层。


5. 实测数据(token)

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

单任务结果侧(tok)

任务 官方 pwsh pwsh_script / run_argv
引号 + $ 正则 7 7
中文/空格/$/%/' 路径 12 12
多行 + 中文输出 10 10
错误路径 85 85(且无 CLIXML)
外部程序 hostile argv 5 —— 但结果是错的 6 —— 结果正确

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

固定开销(每个请求)

项目 tok
两个工具的 schema 263
提示段(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 用数组字面量
报错里出现 <Objs Version="1.1.0.1" ...> 的大段 XML 用了 -EncodedCommand 版本(≤v2)导致 CLIXML。≥v3 用 -Command

9. 版本历史

版本 变化 结果
v1 -EncodedCommand + ConvertFrom-Json 前置 本环境 100% 失败(沙箱包装 exit 127)
v2 danger-full-access 跳过 confine 可用性恢复,但参数绑定错误 + CLIXML 噪音
v3 改 -Command 单 argv + 数组字面量 + UTF-8 钉住 行为正确;但描述冗长(634 tok/请求)
v4 描述精简、去 cwd、提示段压一行 289 tok/请求(-56%)
v5 = 1.0.0 run_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。

—/ 5

No ratings yet

Verified DSH bundle

Commit 96ec422b5905

Community comments

No comments yet. Be the first to write one.

DSH HUB

A community index for DSH plugins. Not an official GitHub or DeepSeek AI product.

CommunityResourcesAPIAbout