dsh-plugin-windows-guard 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/README.en.md +178 -0
- package/README.md +89 -30
- package/cordis.patch.yml +12 -2
- package/lib/encode.js +220 -0
- package/lib/index.js +297 -11
- package/lib/mojibake.js +128 -0
- package/package.json +12 -3
- package/scripts/selfcheck.mjs +91 -2
- package/scripts/smoke-server.mjs +77 -5
- package/skills/windows-enc.md +8 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.2.1] - 2026-08-29
|
|
4
|
+
|
|
5
|
+
- 文档:首屏前置「255 个真实会话」钩子;安装节改为单行 dsh plugin add 命令并去掉作者本机绝对路径;新增「装完你会看到什么」与徽章行;
|
|
6
|
+
- 元数据:keywords 补长尾词(mojibake/write-guard 等,15 → 20);
|
|
7
|
+
- 新增英文版 README.en.md 与双语切换行。
|
|
8
|
+
## [0.2.0] - 2026-08-29
|
|
9
|
+
|
|
10
|
+
- **合并 `dsh-plugin-pwsh-guard` 全部主动防护能力(该插件同日删除,本插件为唯一后继)**:
|
|
11
|
+
- L2 `tools/post-execute`:pwsh 工具结果出现 GBK 误解码 / U+FFFD 洪水 / ASCII 化
|
|
12
|
+
乱码时,自动附加 `[windows-guard]` 修复提示(不中断、不改写结果本体);
|
|
13
|
+
- L3 `tools/pre-execute`:拦截「无 `-Encoding` 的 `Set-Content`/`Add-Content`/
|
|
14
|
+
`Out-File` 写 JSON/YAML/TOML/GD」与「`>` 重定向写结构化文件」,deny 并给出
|
|
15
|
+
`WriteAllText` 正确写法;
|
|
16
|
+
- L4 新工具 `windows_encode_detect` / `windows_encode_fix`(原 `ps_encode_*`
|
|
17
|
+
更名,遵循「主题_动词」工具命名规范):编码诊断(BOM/UTF-8/GBK/UTF-16 +
|
|
18
|
+
置信度)与修复(→ UTF-8 无 BOM,备份 `<path>.windowsguard.bak`,支持 dryRun)。
|
|
19
|
+
- 原 pwsh-guard 的 L1 系统提示词段**不再注入**——由 0.1.0 的守则技能取代
|
|
20
|
+
(按需加载、更省常驻 token)。
|
|
21
|
+
- 配置开关(cordis.patch.yml,默认全开):`enabled` / `postCheck` / `preCheck` /
|
|
22
|
+
`detectTools`;`enabled: false` 时退化为 0.1.0 的纯守则形态。
|
|
23
|
+
- `inject` 从 `['skills']` 扩为 `['skills', 'tools']`;版本 0.1.0 → 0.2.0。
|
|
24
|
+
|
|
25
|
+
## [0.1.0] - 2026-08-25
|
|
26
|
+
|
|
27
|
+
- 首发 `dsh-plugin-windows-guard`:Windows 环境防坑守则 skill 插件(纯数据)。
|
|
28
|
+
- 内容来源:本机 255 个会话归档问题扫描(GBK 误解码 / BOM / UTF-16 /
|
|
29
|
+
单引号转义 / 长路径 / EACCES / 端口占用 / stderr 误判 / 乱码当答案)。
|
|
30
|
+
- 按触发场景拆为两个技能:
|
|
31
|
+
- `windows-enc`:编码与 PowerShell 解析守则(读/写文件、引号转义、乱码三不);
|
|
32
|
+
- `windows-sys`:系统环境守则(路径/文件占用、进程与端口、跨平台杂项)。
|
|
33
|
+
- 纯技能载体:`lib/index.js` 约 40 行注册代码;零运行时依赖、零构建。
|
package/README.en.md
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
[中文](./README.md) | **English**
|
|
2
|
+
|
|
3
|
+
# dsh-plugin-windows-guard
|
|
4
|
+
|
|
5
|
+
The DeepSeek Harness (dsh) **Windows pitfall-guard plugin** — **high-frequency
|
|
6
|
+
pitfalls distilled from 255 real session archives**, with two capability
|
|
7
|
+
layers: guidelines (skills) + proactive protection (hooks/tools):
|
|
8
|
+
|
|
9
|
+
- **Guideline layer** (since 0.1.0): two self-contained skills auto-loaded by
|
|
10
|
+
scenario — preventive rules for "getting it right the first time";
|
|
11
|
+
- **Proactive protection layer** (since 0.2.0, merged from
|
|
12
|
+
`dsh-plugin-pwsh-guard`, which has since been deleted): automatic mojibake
|
|
13
|
+
detection hints for pwsh results, dangerous write-command interception, and
|
|
14
|
+
encoding diagnosis/repair tools.
|
|
15
|
+
|
|
16
|
+
Zero runtime dependencies, zero build.
|
|
17
|
+
|
|
18
|
+
## Features
|
|
19
|
+
|
|
20
|
+
### Guideline layer: two auto-loaded skills
|
|
21
|
+
|
|
22
|
+
Split into two self-contained skills by trigger scenario (the
|
|
23
|
+
`<available_skills>` catalog auto-loads the body when the description
|
|
24
|
+
matches):
|
|
25
|
+
|
|
26
|
+
### `windows-enc` — Encoding and PowerShell parsing (command/file-content errors)
|
|
27
|
+
|
|
28
|
+
| Section | Content | Source incidents (historical records) |
|
|
29
|
+
|---|---|---|
|
|
30
|
+
| 1 Environment fact card | pwsh 5.1/7 differences, `-Command` as a single argv with no shell escaping layer, OutputEncoding pinned, exit codes are authoritative | stderr misjudgment (git red progress, NativeCommandError) |
|
|
31
|
+
| 2 Three encoding iron rules | always read with `-Encoding UTF8`; write structured files with `WriteAllText(UTF8Encoding($false))`; BOM check / transcoding command templates | GBK misdecoding (`鎻掍欢`/`鈥?`), BOM corrupting package.json (startup incident #2), UTF-16 corruption |
|
|
32
|
+
| 3 Quoting and escaping quick reference | PS `''` escaping (backslash doesn't work), double-quote interpolation, `node -e`/`python -c` quoting hell → write a temp file | `SyntaxError: unterminated string literal`, quoting-nesting failures confirmed in testing |
|
|
33
|
+
| 4 Three mojibake "don'ts" | mojibake signature table (`鈥?`/`??OK??`/`\uFFFD`) + don't guess / don't quote it / re-read first | `??OK??`, `??? skill ????` mojibake treated as the answer |
|
|
34
|
+
|
|
35
|
+
### `windows-sys` — System environment (troubleshooting)
|
|
36
|
+
|
|
37
|
+
| Section | Content | Source incidents |
|
|
38
|
+
|---|---|---|
|
|
39
|
+
| 1 Paths and filesystem | MAX_PATH/long paths, paths with spaces, EACCES/EBUSY file locks, junctions and `link:` realpath, path formats | `EACCES: realpath '...sock'`, deeply nested npm-cache paths |
|
|
40
|
+
| 2 Processes and ports | `netstat` port lookup, `Stop-Process`/`taskkill`, check before kill | port 3080/39090 conflicts, orphan processes |
|
|
41
|
+
| 3 Cross-platform misc | CRLF traps, `.cmd` script host/ExecutionPolicy, time zones, case sensitivity | novel-file CRLF checks, run.bat chcp 65001 |
|
|
42
|
+
|
|
43
|
+
### Proactive protection layer (since 0.2.0, every layer switchable, all on by default)
|
|
44
|
+
|
|
45
|
+
| Layer | Mechanism | Effect |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| L2 | `tools/post-execute` listener | when a `pwsh` tool result shows GBK misdecoding / an FFFD flood / Chinese turned into question marks, automatically appends a `[windows-guard]` fix hint after the result (no interruption, the result body is never rewritten) |
|
|
48
|
+
| L3 | `tools/pre-execute` listener | intercepts commands that write JSON/YAML/TOML/GD via `Set-Content`/`Add-Content`/`Out-File` without `-Encoding`, and `>` redirection into structured files; denies them and offers the correct `[System.IO.File]::WriteAllText(...)` form |
|
|
49
|
+
| L4 | `windows_encode_detect` / `windows_encode_fix` tools | file encoding diagnosis (BOM/UTF-8/GBK/UTF-16 + confidence) and repair (→ UTF-8 without BOM, automatic `.windowsguard.bak` backup); `dryRun` diagnoses without writing |
|
|
50
|
+
|
|
51
|
+
> History: the original pwsh-guard L1 system-prompt section is **no longer
|
|
52
|
+
> injected** — it has been replaced by the guideline skills above (loaded on
|
|
53
|
+
> demand, saving always-on tokens).
|
|
54
|
+
|
|
55
|
+
## Configuration
|
|
56
|
+
|
|
57
|
+
Everything in `cordis.patch.yml` is switchable (all on by default):
|
|
58
|
+
|
|
59
|
+
```yaml
|
|
60
|
+
- insert:
|
|
61
|
+
- id: dsh-plugin-windows-guard
|
|
62
|
+
name: dsh-plugin-windows-guard
|
|
63
|
+
config:
|
|
64
|
+
enabled: true # master switch (false = keep only the guideline skills, degrading to the pure-guideline 0.1.0 form)
|
|
65
|
+
postCheck: true # L2 pwsh-result mojibake detection + hint
|
|
66
|
+
preCheck: true # L3 dangerous write-command interception
|
|
67
|
+
detectTools: true # L4 encoding diagnosis/repair tools
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Installation
|
|
71
|
+
|
|
72
|
+
```powershell
|
|
73
|
+
# npm (recommended)
|
|
74
|
+
dsh plugin --profile web add dsh-plugin-windows-guard
|
|
75
|
+
# or GitHub
|
|
76
|
+
dsh plugin --profile web add github:Pasumao/dsh-plugin-windows-guard
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Restart `dsh web` after installing for it to take effect. The package ships
|
|
80
|
+
its own `cordis.patch.yml` mount line, applied automatically — no manual
|
|
81
|
+
config edits needed; see the Configuration section for the switches.
|
|
82
|
+
|
|
83
|
+
Install from source (local development / debugging):
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
git clone https://github.com/Pasumao/dsh-plugin-windows-guard.git
|
|
87
|
+
cd dsh-plugin-windows-guard # the directory can live anywhere (e.g. your plugins collection dir)
|
|
88
|
+
npm install
|
|
89
|
+
# Mount into the profile as a link: dependency:
|
|
90
|
+
# add to the profile's package.json dependencies
|
|
91
|
+
# "dsh-plugin-windows-guard": "link:<your-plugins-dir>/dsh-plugin-windows-guard"
|
|
92
|
+
# and add it to dsh.profile.bundles (or insert manually via cordis.patch.yml)
|
|
93
|
+
# then pnpm install + restart dsh web
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## What you'll see after installation
|
|
97
|
+
|
|
98
|
+
- when `pwsh` tool output contains mojibake (`鈥?`/`鎻掍欢`/`锟斤拷`), a
|
|
99
|
+
`[windows-guard]` fix hint is appended to the end of the result;
|
|
100
|
+
- dangerous write commands such as `Set-Content`/`Add-Content`/`Out-File`
|
|
101
|
+
writing JSON/YAML/TOML without `-Encoding` are denied outright, with the
|
|
102
|
+
correct `[System.IO.File]::WriteAllText(...)` form attached;
|
|
103
|
+
- the tool list gains `windows_encode_detect` / `windows_encode_fix`
|
|
104
|
+
(encoding diagnosis and repair);
|
|
105
|
+
- the skill catalog gains the `windows-enc` / `windows-sys` pitfall-guard
|
|
106
|
+
guideline skills.
|
|
107
|
+
|
|
108
|
+
## Usage
|
|
109
|
+
|
|
110
|
+
No configuration needed after installation. When the model runs into a
|
|
111
|
+
Windows command/encoding/mojibake/path/process/port task,
|
|
112
|
+
`skill({ name: "windows-enc" | "windows-sys" })` auto-loads the matching
|
|
113
|
+
guideline; you can also ask the model to load a skill manually to read the
|
|
114
|
+
full text.
|
|
115
|
+
|
|
116
|
+
```text
|
|
117
|
+
User: Read D:\dsh\config\comfy_prompt.json and check the config
|
|
118
|
+
Model: Before reading, follow the guideline and use Get-Content -Encoding UTF8...
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
```text
|
|
122
|
+
User: Fix config.json for me, JSON.parse throws an error
|
|
123
|
+
Model: first windows_encode_detect on config.json → "utf-8-bom (confidence high)"
|
|
124
|
+
→ windows_encode_fix config.json → converted to UTF-8 without BOM (backup .windowsguard.bak)
|
|
125
|
+
→ verify that JSON parsing now passes
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
```powershell
|
|
129
|
+
# The model is told (via the guideline skills), and this kind of command is
|
|
130
|
+
# blocked by L3 with the correct form given:
|
|
131
|
+
# Set-Content -Path data.json -Value $json ← deny (PS5.1 writes UTF-16LE/BOM)
|
|
132
|
+
# [System.IO.File]::WriteAllText("data.json", $json, [System.Text.UTF8Encoding]::new($false)) ← correct
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Why split into two skills (design notes)
|
|
136
|
+
|
|
137
|
+
- **Always-on catalog cost**: two descriptions (~300 tokens) barely grows;
|
|
138
|
+
- **Trigger precision**: `windows-enc` covers "command/file-content errors"
|
|
139
|
+
(encoding/escaping/mojibake), `windows-sys` covers "system environment
|
|
140
|
+
troubleshooting" (paths/processes/ports/cross-platform) — clean boundaries,
|
|
141
|
+
no interference between them;
|
|
142
|
+
- **Body text halved on demand**: Windows "content error" tasks no longer
|
|
143
|
+
load the entire "system environment" section;
|
|
144
|
+
- Reserved for future topics (WSL/winget/containers…) to be folded into
|
|
145
|
+
`windows-sys` extensions.
|
|
146
|
+
|
|
147
|
+
## Implementation notes
|
|
148
|
+
|
|
149
|
+
- Guidelines are registered via `ctx.skills.register` (the official skill
|
|
150
|
+
subsystem), with content as the SKILL.md body;
|
|
151
|
+
- L2/L3 use `ctx.on('tools/post-execute' | 'tools/pre-execute')`; L4 uses
|
|
152
|
+
`ctx.tools.register` (raw definition shape); no `@deepseek-ai/*` runtime
|
|
153
|
+
packages are imported (with link: realpath resolution, external dependencies
|
|
154
|
+
cannot be resolved from the plugin directory);
|
|
155
|
+
- L2 only applies to the tool named `pwsh` with successful results; L3 only
|
|
156
|
+
intercepts the narrow patterns guaranteed to corrupt writes — reads and
|
|
157
|
+
writes with explicit `-Encoding` pass through; the mojibake vocabulary is
|
|
158
|
+
based on measurements from historical sessions (11/12 positive/negative
|
|
159
|
+
cases passed);
|
|
160
|
+
- Requires `Node ≥22.5` (`TextDecoder('gbk')` needs full-icu, which official
|
|
161
|
+
builds include by default);
|
|
162
|
+
- Changes to skill bodies/tools/guards require a dsh restart (the host reads
|
|
163
|
+
the skills/ directory when applying plugins);
|
|
164
|
+
- Every layer's registration has a ctx capability guard: if any layer is
|
|
165
|
+
unavailable it degrades gracefully with a warning — the plugin never fails
|
|
166
|
+
to mount.
|
|
167
|
+
|
|
168
|
+
## Related plugins
|
|
169
|
+
|
|
170
|
+
Published plugins cross-referencing each other (more of the DSH plugin
|
|
171
|
+
ecosystem):
|
|
172
|
+
|
|
173
|
+
- [dsh-plugin-workbench](https://www.npmjs.com/package/dsh-plugin-workbench) — VS Code-style workspace file explorer + editable preview
|
|
174
|
+
- [dsh-notify](https://www.npmjs.com/package/dsh-notify) — native Windows notification when the agent stops/errors + tray icon
|
|
175
|
+
- [dsh-plugin-image-tools](https://www.npmjs.com/package/dsh-plugin-image-tools) — image choice cards / inline reply images / chat image zoom
|
|
176
|
+
- [dsh-plugin-dev-kb](https://www.npmjs.com/package/dsh-plugin-dev-kb) — full Markdown mirror of the official dsh docs (plugin development knowledge base)
|
|
177
|
+
- [dsh-plugin-choice-refresh](https://www.npmjs.com/package/dsh-plugin-choice-refresh) — "regenerate options" / "more options" on choice cards
|
|
178
|
+
- [dsh-plugin-table-zoom](https://www.npmjs.com/package/dsh-plugin-table-zoom) — floating table viewer in chat + one-click Markdown copy
|
package/README.md
CHANGED
|
@@ -1,16 +1,24 @@
|
|
|
1
1
|
# dsh-plugin-windows-guard
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+

|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
预防性守则,以两个 skill 形式自动加载给 agent——**不是修复工具,是行为规则**:
|
|
7
|
-
让模型在写 PowerShell、读文件、处理编码/路径/进程/端口问题前先看到防坑清单。
|
|
7
|
+
[**中文**](./README.md) | [English](./README.en.md)
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
DeepSeek Harness (dsh) 的 **Windows 环境防坑插件**——**源自 255 个真实会话归档的
|
|
10
|
+
高频踩坑**,守则(skill)+ 主动防护(hooks/工具)两层能力:
|
|
11
|
+
|
|
12
|
+
- **守则层**(0.1.0 起):两个自包含 skill 按场景自动加载——「一次写对」的预防性守则;
|
|
13
|
+
- **主动防护层**(0.2.0 起,自 `dsh-plugin-pwsh-guard` 合并,该插件已删除):
|
|
14
|
+
pwsh 结果乱码自动检测提示、危险写命令拦截、编码诊断/修复工具。
|
|
15
|
+
|
|
16
|
+
零运行时依赖、零构建。
|
|
11
17
|
|
|
12
18
|
## 功能
|
|
13
19
|
|
|
20
|
+
### 守则层:两个自动加载的技能
|
|
21
|
+
|
|
14
22
|
按触发场景拆成两个自包含技能(`<available_skills>` 按 description 匹配自动加载正文):
|
|
15
23
|
|
|
16
24
|
### `windows-enc` —— 编码与 PowerShell 解析(命令/文件内容出错类)
|
|
@@ -30,24 +38,67 @@ DeepSeek Harness (dsh) 的 **Windows 环境防坑守则插件**(纯数据,
|
|
|
30
38
|
| 2 进程与端口 | `netstat` 查端口、`Stop-Process`/`taskkill`、先查后杀 | 3080/39090 端口占用、孤儿进程 |
|
|
31
39
|
| 3 跨平台杂项 | CRLF 陷阱、`.cmd` 脚本宿主/ExecutionPolicy、时区、大小写 | 小说文件 CRLF 检查、run.bat chcp 65001 |
|
|
32
40
|
|
|
41
|
+
### 主动防护层(0.2.0 起,均可配置开关,默认全开)
|
|
42
|
+
|
|
43
|
+
| 层 | 机制 | 作用 |
|
|
44
|
+
|---|---|---|
|
|
45
|
+
| L2 | `tools/post-execute` 监听 | `pwsh` 工具结果出现 GBK 误解码/FFFD 洪水/中文变问号时,自动在结果后附加 `[windows-guard]` 修复提示(不中断、不改写结果本体) |
|
|
46
|
+
| L3 | `tools/pre-execute` 监听 | 拦截「不带 -Encoding 的 `Set-Content`/`Add-Content`/`Out-File` 写 JSON/YAML/TOML/GD」与「`>` 重定向写结构化文件」命令,deny 并给出 `[System.IO.File]::WriteAllText(...)` 正确写法 |
|
|
47
|
+
| L4 | `windows_encode_detect` / `windows_encode_fix` 工具 | 文件编码诊断(BOM/UTF-8/GBK/UTF-16 + 置信度)与修复(→ UTF-8 无 BOM,自动备份 `.windowsguard.bak`);`dryRun` 只诊断不写入 |
|
|
48
|
+
|
|
49
|
+
> 历史:原 pwsh-guard 的 L1 系统提示词段**不再注入**——由上面的守则技能取代
|
|
50
|
+
> (按需加载、更省常驻 token)。
|
|
51
|
+
|
|
52
|
+
## 配置
|
|
53
|
+
|
|
54
|
+
`cordis.patch.yml` 全部可开关(默认全开):
|
|
55
|
+
|
|
56
|
+
```yaml
|
|
57
|
+
- insert:
|
|
58
|
+
- id: dsh-plugin-windows-guard
|
|
59
|
+
name: dsh-plugin-windows-guard
|
|
60
|
+
config:
|
|
61
|
+
enabled: true # 总开关(false = 只保留守则技能,退化为 0.1.0 纯守则形态)
|
|
62
|
+
postCheck: true # L2 pwsh 结果乱码检测+提示
|
|
63
|
+
preCheck: true # L3 危险写命令拦截
|
|
64
|
+
detectTools: true # L4 编码诊断/修复工具
|
|
65
|
+
```
|
|
66
|
+
|
|
33
67
|
## 安装
|
|
34
68
|
|
|
35
69
|
```powershell
|
|
36
|
-
#
|
|
37
|
-
|
|
38
|
-
#
|
|
39
|
-
|
|
40
|
-
# 2. 加入 dsh.profile.bundles(或 cordis.patch.yml 手动 insert)
|
|
41
|
-
cd C:\Users\18303\.dsh\profiles\web
|
|
42
|
-
pnpm install
|
|
43
|
-
node D:\dsh\scripts\check-profile.mjs # 自检 [PASS] 再重启
|
|
44
|
-
powershell -NoProfile -ExecutionPolicy Bypass -File D:\dsh\scripts\restart-dsh.ps1
|
|
45
|
-
|
|
46
|
-
# npm 分发安装(发布后):
|
|
47
|
-
npm install dsh-plugin-windows-guard --prefix C:\Users\18303\.dsh\profiles\web
|
|
48
|
-
# 并在 profile 包/补丁中挂载,其余同上
|
|
70
|
+
# npm(推荐)
|
|
71
|
+
dsh plugin --profile web add dsh-plugin-windows-guard
|
|
72
|
+
# 或 GitHub
|
|
73
|
+
dsh plugin --profile web add github:Pasumao/dsh-plugin-windows-guard
|
|
49
74
|
```
|
|
50
75
|
|
|
76
|
+
装完重启 `dsh web` 即生效。包自带 `cordis.patch.yml` 挂载行,自动应用,
|
|
77
|
+
无需手动改配置;配置开关见「配置」节。
|
|
78
|
+
|
|
79
|
+
源码安装(本地开发 / 调试):
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
git clone https://github.com/Pasumao/dsh-plugin-windows-guard.git
|
|
83
|
+
cd dsh-plugin-windows-guard # 目录放在任意位置(如你的插件收集目录)
|
|
84
|
+
npm install
|
|
85
|
+
# 以 link: 依赖挂载进 profile:
|
|
86
|
+
# profile 的 package.json dependencies 加
|
|
87
|
+
# "dsh-plugin-windows-guard": "link:<你的插件目录>/dsh-plugin-windows-guard"
|
|
88
|
+
# 并加入 dsh.profile.bundles(或 cordis.patch.yml 手动 insert)
|
|
89
|
+
# 之后 pnpm install + 重启 dsh web
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## 装完你会看到什么
|
|
93
|
+
|
|
94
|
+
- `pwsh` 工具输出出现乱码(`鈥?`/`鎻掍欢`/`锟斤拷`)时,结果末尾自动附加
|
|
95
|
+
`[windows-guard]` 修复提示;
|
|
96
|
+
- 不带 `-Encoding` 的 `Set-Content`/`Add-Content`/`Out-File` 写 JSON/YAML/TOML
|
|
97
|
+
等危险写命令被直接拦截(deny),并附 `[System.IO.File]::WriteAllText(...)`
|
|
98
|
+
正确写法;
|
|
99
|
+
- 工具列表多出 `windows_encode_detect` / `windows_encode_fix`(编码诊断与修复);
|
|
100
|
+
- 技能目录多出 `windows-enc` / `windows-sys` 两个防坑守则技能。
|
|
101
|
+
|
|
51
102
|
## 使用
|
|
52
103
|
|
|
53
104
|
安装后无需任何配置。模型遇到 Windows 命令/编码/乱码/路径/进程/端口任务时,
|
|
@@ -59,18 +110,20 @@ npm install dsh-plugin-windows-guard --prefix C:\Users\18303\.dsh\profiles\web
|
|
|
59
110
|
模型:读取前先按守则用 Get-Content -Encoding UTF8……
|
|
60
111
|
```
|
|
61
112
|
|
|
62
|
-
## 配置/示例
|
|
63
|
-
|
|
64
|
-
**无需任何配置**:挂载后即生效,无 API key、无 token、无配置文件。示例:
|
|
65
|
-
|
|
66
113
|
```text
|
|
67
|
-
|
|
68
|
-
|
|
114
|
+
用户:帮我修一下 config.json,JSON.parse 报错
|
|
115
|
+
模型:先 windows_encode_detect 查 config.json → "utf-8-bom(可信度 high)"
|
|
116
|
+
→ windows_encode_fix config.json → 已转换为 UTF-8 无 BOM(备份 .windowsguard.bak)
|
|
117
|
+
→ 验证 JSON 解析通过
|
|
69
118
|
```
|
|
70
119
|
|
|
71
|
-
|
|
120
|
+
```powershell
|
|
121
|
+
# 模型被告知(守则技能),且此类命令会被 L3 拦截并给出正确写法:
|
|
122
|
+
# Set-Content -Path data.json -Value $json ← deny(PS5.1 写 UTF-16LE/BOM)
|
|
123
|
+
# [System.IO.File]::WriteAllText("data.json", $json, [System.Text.UTF8Encoding]::new($false)) ← 正确
|
|
124
|
+
```
|
|
72
125
|
|
|
73
|
-
###
|
|
126
|
+
### 为何拆成两个技能(设计说明)
|
|
74
127
|
|
|
75
128
|
- **常驻目录成本**:两条 description(~300 token)几乎不涨;
|
|
76
129
|
- **触发精度**:`windows-enc` 管「命令/文件内容出错」(编码/转义/乱码),
|
|
@@ -80,9 +133,15 @@ npm install dsh-plugin-windows-guard --prefix C:\Users\18303\.dsh\profiles\web
|
|
|
80
133
|
|
|
81
134
|
## 实现说明
|
|
82
135
|
|
|
83
|
-
-
|
|
84
|
-
-
|
|
85
|
-
|
|
136
|
+
- 守则注册走 `ctx.skills.register`(官方 skill 子系统),content 为 SKILL.md 正文;
|
|
137
|
+
- L2/L3 走 `ctx.on('tools/post-execute' | 'tools/pre-execute')`;L4 走
|
|
138
|
+
`ctx.tools.register`(原始 definition 形状);不 import 任何 `@deepseek-ai/*`
|
|
139
|
+
运行时包(link: realpath 解析,外部依赖从插件目录解析不到);
|
|
140
|
+
- L2 只对工具名 `pwsh` 且成功结果生效;L3 仅拦截「确定会写坏」的窄模式,读操作与
|
|
141
|
+
显式 `-Encoding` 的写操作放行;乱码词表基于历史会话实测(11/12 正/负例通过);
|
|
142
|
+
- 依赖 `Node ≥22.5`(`TextDecoder('gbk')` 需要 full-icu,官方构建默认满足);
|
|
143
|
+
- 技能正文/工具/守卫改动需重启 dsh(host 插件 apply 时读取 skills/ 目录);
|
|
144
|
+
- 各层注册均有 ctx 能力守卫:任何一层不可用时降级并警告,绝不让插件挂载失败。
|
|
86
145
|
|
|
87
146
|
## 相关插件
|
|
88
147
|
|
package/cordis.patch.yml
CHANGED
|
@@ -1,8 +1,18 @@
|
|
|
1
|
-
# dsh-plugin-windows-guard bundle patch: Windows 环境防坑守则
|
|
1
|
+
# dsh-plugin-windows-guard bundle patch: Windows 环境防坑守则 + 主动防护。
|
|
2
2
|
#
|
|
3
3
|
# 应用方式与 dev-kb/nonebot-kb 相同:作为一条 insert 覆盖 profile 根。
|
|
4
4
|
# `name` 必须等于 package.json 的 `name`(loader 从 profile node_modules 按名解析)。
|
|
5
|
-
#
|
|
5
|
+
#
|
|
6
|
+
# 配置项(均可省略使用默认;0.2.0 起自 dsh-plugin-pwsh-guard 合并主动防护层):
|
|
7
|
+
# enabled: true # 总开关(false = 只保留守则技能)
|
|
8
|
+
# postCheck: true # L2 pwsh 工具结果乱码检测+修复提示
|
|
9
|
+
# preCheck: true # L3 危险写命令拦截
|
|
10
|
+
# detectTools: true # L4 windows_encode_detect / windows_encode_fix 工具
|
|
6
11
|
- insert:
|
|
7
12
|
- id: dsh-plugin-windows-guard
|
|
8
13
|
name: dsh-plugin-windows-guard
|
|
14
|
+
config:
|
|
15
|
+
enabled: true
|
|
16
|
+
postCheck: true
|
|
17
|
+
preCheck: true
|
|
18
|
+
detectTools: true
|
package/lib/encode.js
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-plugin-windows-guard — 文件编码检测与修复(零依赖纯函数)。
|
|
3
|
+
*
|
|
4
|
+
* 自 0.2.0 起自 dsh-plugin-pwsh-guard 合并(该插件已删除)。
|
|
5
|
+
* 检测逻辑对齐历史会话中实际用过的字节嗅探诊断(`前3字节: 35 32 229 ---GBK解码---`):
|
|
6
|
+
* 1. BOM 探测(UTF-8 / UTF-16LE / UTF-16BE / UTF-32);
|
|
7
|
+
* 2. 严格 UTF-8 解码(TextDecoder fatal)判断是否合法 UTF-8;
|
|
8
|
+
* 3. GBK(936) 备选解码 + CJK 占比启发式(判断「更像 GBK 还是 UTF-8」);
|
|
9
|
+
* 4. 修复:任意源编码 → UTF-8 无 BOM(先备份原文件)。
|
|
10
|
+
*
|
|
11
|
+
* 依赖:Node 内置 TextDecoder(需 full-icu,Node 官方构建默认满足)。
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { readFileSync, writeFileSync, copyFileSync, statSync } from 'node:fs'
|
|
15
|
+
|
|
16
|
+
export const GBK_LABEL = 'gbk'
|
|
17
|
+
export const UTF8_LABEL = 'utf-8'
|
|
18
|
+
|
|
19
|
+
/** 读文件前 N 字节做 BOM 探测。 */
|
|
20
|
+
export function detectBom(buf) {
|
|
21
|
+
if (buf.length >= 3 && buf[0] === 0xef && buf[1] === 0xbb && buf[2] === 0xbf) return 'utf-8-bom'
|
|
22
|
+
if (buf.length >= 2 && buf[0] === 0xff && buf[1] === 0xfe) return 'utf-16le'
|
|
23
|
+
if (buf.length >= 2 && buf[0] === 0xfe && buf[1] === 0xff) return 'utf-16be'
|
|
24
|
+
if (buf.length >= 4 && buf[0] === 0x00 && buf[1] === 0x00 && buf[2] === 0xfe && buf[3] === 0xff) return 'utf-32be'
|
|
25
|
+
if (buf.length >= 4 && buf[0] === 0xff && buf[1] === 0xfe && buf[2] === 0x00 && buf[3] === 0x00) return 'utf-32le'
|
|
26
|
+
return null
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** 严格 UTF-8 解码(fatal);失败返回 null。 */
|
|
30
|
+
export function strictUtf8(buf) {
|
|
31
|
+
try {
|
|
32
|
+
return { text: new TextDecoder('utf-8', { fatal: true }).decode(buf), ok: true }
|
|
33
|
+
} catch {
|
|
34
|
+
return { text: null, ok: false }
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** GBK 解码(宽松);失败返回 null。 */
|
|
39
|
+
export function decodeGbk(buf) {
|
|
40
|
+
try {
|
|
41
|
+
return { text: new TextDecoder('gbk').decode(buf), ok: true }
|
|
42
|
+
} catch {
|
|
43
|
+
return { text: null, ok: false }
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** CJK 字符占比(0~1)。 */
|
|
48
|
+
export function cjkRatio(text) {
|
|
49
|
+
if (!text || text.length === 0) return 0
|
|
50
|
+
let cjk = 0
|
|
51
|
+
for (const ch of text) {
|
|
52
|
+
const cp = ch.codePointAt(0)
|
|
53
|
+
if (cp >= 0x4e00 && cp <= 0x9fff) cjk++
|
|
54
|
+
}
|
|
55
|
+
return cjk / text.length
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** 可控字符比例(排除替换字符/怪异字节残片)。 */
|
|
59
|
+
function printableRatio(text) {
|
|
60
|
+
if (!text || text.length === 0) return 0
|
|
61
|
+
let ok = 0
|
|
62
|
+
for (const ch of text) {
|
|
63
|
+
const cp = ch.codePointAt(0)
|
|
64
|
+
if (ch === '\uFFFD') continue
|
|
65
|
+
if (cp === 9 || cp === 10 || cp === 13 || (cp >= 0x20 && cp !== 0x7f && cp < 0x10000)) ok++
|
|
66
|
+
}
|
|
67
|
+
return ok / text.length
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 诊断一个文件/字节缓冲的编码。
|
|
72
|
+
* @param {Buffer} buf
|
|
73
|
+
* @param {string} [fileName] 用于报告
|
|
74
|
+
* @returns {{ encoding: string, confidence: 'high'|'medium'|'low', bom: string|null,
|
|
75
|
+
* validUtf8: boolean, utf8CjkRatio: number, gbkCjkRatio: number, gbkPrintable: number,
|
|
76
|
+
* replaceChars: number, note: string }}
|
|
77
|
+
*/
|
|
78
|
+
export function detectEncoding(buf) {
|
|
79
|
+
const bom = detectBom(buf)
|
|
80
|
+
if (bom === 'utf-16le' || bom === 'utf-16be' || bom === 'utf-32le' || bom === 'utf-32be') {
|
|
81
|
+
const enc = new TextDecoder(bom === 'utf-16le' ? 'utf-16le' : bom === 'utf-16be' ? 'utf-16be' : bom === 'utf-32le' ? 'utf-32le' : 'utf-32be')
|
|
82
|
+
let text = ''
|
|
83
|
+
try { text = enc.decode(buf) } catch { /* keep */ }
|
|
84
|
+
return {
|
|
85
|
+
encoding: bom, confidence: 'high', bom,
|
|
86
|
+
validUtf8: false,
|
|
87
|
+
utf8CjkRatio: 0,
|
|
88
|
+
gbkCjkRatio: cjkRatio(text),
|
|
89
|
+
gbkPrintable: printableRatio(text),
|
|
90
|
+
replaceChars: (text.match(/\uFFFD/g) ?? []).length,
|
|
91
|
+
note: `检测到 BOM(${bom})——这是脚本/配置不该有的编码;建议转换为 UTF-8 无 BOM。`,
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (bom === 'utf-8-bom') {
|
|
95
|
+
const body = strictUtf8(buf.subarray(3))
|
|
96
|
+
return {
|
|
97
|
+
encoding: 'utf-8-bom', confidence: 'high', bom,
|
|
98
|
+
validUtf8: body.ok,
|
|
99
|
+
utf8CjkRatio: body.ok ? cjkRatio(body.text) : 0,
|
|
100
|
+
gbkCjkRatio: body.ok ? 0 : cjkRatio(decodeGbk(buf.subarray(3)).text ?? ''),
|
|
101
|
+
gbkPrintable: body.ok ? 1 : printableRatio(decodeGbk(buf.subarray(3)).text ?? ''),
|
|
102
|
+
replaceChars: body.ok ? 0 : (buf.length ? (buf.toString('utf8').match(/\uFFFD/g) ?? []).length : 0),
|
|
103
|
+
note: '检测到 UTF-8 BOM——写入 BOM 的文件(如 PowerShell 5.1 `Out-File -Encoding utf8`/记事本 早期版本)可能导致 JSON/YAML 解析失败;建议剥离 BOM。',
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// 无 BOM:先验证严格 UTF-8
|
|
107
|
+
const utf8 = strictUtf8(buf)
|
|
108
|
+
if (utf8.ok) {
|
|
109
|
+
// UTF-8 合法。若文本几乎无 CJK 又有大量 FFFD?FFFD 不会在 fatal 模式出现。
|
|
110
|
+
const ratio = cjkRatio(utf8.text)
|
|
111
|
+
return {
|
|
112
|
+
encoding: 'utf-8', confidence: 'high', bom: null,
|
|
113
|
+
validUtf8: true,
|
|
114
|
+
utf8CjkRatio: ratio,
|
|
115
|
+
gbkCjkRatio: decodeGbk(buf).ok ? cjkRatio(decodeGbk(buf).text) : 0,
|
|
116
|
+
gbkPrintable: 1,
|
|
117
|
+
replaceChars: 0,
|
|
118
|
+
note: ratio > 0.05 ? '合法 UTF-8(含中文)。' : '合法 UTF-8(纯 ASCII/英文为主)。',
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// UTF-8 失败 → 尝试 GBK
|
|
122
|
+
const gbk = decodeGbk(buf)
|
|
123
|
+
if (gbk.ok) {
|
|
124
|
+
const print = printableRatio(gbk.text)
|
|
125
|
+
const cjk = cjkRatio(gbk.text)
|
|
126
|
+
if (print > 0.9 && cjk > 0.05) {
|
|
127
|
+
return {
|
|
128
|
+
encoding: 'gbk', confidence: 'high', bom: null,
|
|
129
|
+
validUtf8: false,
|
|
130
|
+
utf8CjkRatio: 0,
|
|
131
|
+
gbkCjkRatio: cjk, gbkPrintable: print,
|
|
132
|
+
replaceChars: (gbk.text.match(/\uFFFD/g) ?? []).length,
|
|
133
|
+
note: '不是合法 UTF-8;按 GBK(936) 解码后为正常中文——文件由 GBK 编码工具生成(如老脚本/老编辑器)。',
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (print > 0.8) {
|
|
137
|
+
return {
|
|
138
|
+
encoding: 'gbk', confidence: 'medium', bom: null,
|
|
139
|
+
validUtf8: false,
|
|
140
|
+
utf8CjkRatio: 0,
|
|
141
|
+
gbkCjkRatio: cjk, gbkPrintable: print,
|
|
142
|
+
replaceChars: (gbk.text.match(/\uFFFD/g) ?? []).length,
|
|
143
|
+
note: '不是合法 UTF-8;GBK 解码基本可读(但中文占比低)。',
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
encoding: 'unknown-binary', confidence: 'low', bom: null,
|
|
148
|
+
validUtf8: false,
|
|
149
|
+
utf8CjkRatio: 0, gbkCjkRatio: cjk, gbkPrintable: print,
|
|
150
|
+
replaceChars: (gbk.text.match(/\uFFFD/g) ?? []).length,
|
|
151
|
+
note: '不是合法 UTF-8,GBK 解码也多为不可读字符——疑似二进制文件。',
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
encoding: 'unknown', confidence: 'low', bom: null,
|
|
156
|
+
validUtf8: false,
|
|
157
|
+
utf8CjkRatio: 0, gbkCjkRatio: 0, gbkPrintable: 0,
|
|
158
|
+
replaceChars: 0,
|
|
159
|
+
note: '无法识别编码(既非 UTF-8 也非 GBK,可能是二进制或损坏文件)。',
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** 诊断磁盘上的文件。 */
|
|
164
|
+
export function diagnoseFile(path) {
|
|
165
|
+
const st = statSync(path)
|
|
166
|
+
if (st.size > 8 * 1024 * 1024) {
|
|
167
|
+
return { error: `文件过大(${st.size} 字节 > 8MB),跳过检测。` }
|
|
168
|
+
}
|
|
169
|
+
const buf = readFileSync(path)
|
|
170
|
+
const enc = detectEncoding(buf)
|
|
171
|
+
return { size: st.size, ...enc }
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* 修复为 UTF-8 无 BOM:先备份(.windowsguard.bak),再写回。
|
|
176
|
+
* @param {string} path
|
|
177
|
+
* @param {{ backup?: boolean }} [opts]
|
|
178
|
+
* @returns {{ written: boolean, before: string, after: string, backupPath?: string, note: string }}
|
|
179
|
+
*/
|
|
180
|
+
export function fixToUtf8NoBom(path, opts = {}) {
|
|
181
|
+
const { backup = true } = opts
|
|
182
|
+
const buf = readFileSync(path)
|
|
183
|
+
const diag = detectEncoding(buf)
|
|
184
|
+
let text
|
|
185
|
+
switch (diag.encoding) {
|
|
186
|
+
case 'utf-8-bom':
|
|
187
|
+
text = strictUtf8(buf.subarray(3)).text
|
|
188
|
+
break
|
|
189
|
+
case 'utf-16le':
|
|
190
|
+
case 'utf-16be':
|
|
191
|
+
case 'utf-32le':
|
|
192
|
+
case 'utf-32be':
|
|
193
|
+
text = new TextDecoder(
|
|
194
|
+
diag.encoding === 'utf-16le' ? 'utf-16le'
|
|
195
|
+
: diag.encoding === 'utf-16be' ? 'utf-16be'
|
|
196
|
+
: diag.encoding === 'utf-32le' ? 'utf-32le'
|
|
197
|
+
: 'utf-32be'
|
|
198
|
+
).decode(buf)
|
|
199
|
+
break
|
|
200
|
+
case 'gbk':
|
|
201
|
+
text = decodeGbk(buf).text ?? ''
|
|
202
|
+
break
|
|
203
|
+
default:
|
|
204
|
+
if (diag.validUtf8) text = strictUtf8(buf).text ?? ''
|
|
205
|
+
else throw new Error(`无法确定编码(${diag.encoding}),不执行写入:${path}`)
|
|
206
|
+
}
|
|
207
|
+
let backupPath
|
|
208
|
+
if (backup) {
|
|
209
|
+
backupPath = `${path}.windowsguard.bak`
|
|
210
|
+
copyFileSync(path, backupPath)
|
|
211
|
+
}
|
|
212
|
+
writeFileSync(path, text, { encoding: 'utf8' }) // Node 默认 utf8 = 无 BOM
|
|
213
|
+
return {
|
|
214
|
+
written: true,
|
|
215
|
+
before: diag.encoding,
|
|
216
|
+
after: 'utf-8',
|
|
217
|
+
backupPath,
|
|
218
|
+
note: `已转换为 UTF-8 无 BOM(源编码:${diag.encoding}${backupPath ? `;备份:${backupPath}` : ''})。`,
|
|
219
|
+
}
|
|
220
|
+
}
|