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/scripts/smoke-server.mjs
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
|
-
// dsh-plugin-windows-guard 服务端冒烟:假 ctx
|
|
1
|
+
// dsh-plugin-windows-guard 服务端冒烟:假 ctx 走全链路
|
|
2
|
+
// (技能注册 → 工具注册 → pre/post hook 决策 → 工具执行 → 修复往返 → 清理)。零依赖。
|
|
2
3
|
import assert from 'node:assert'
|
|
4
|
+
import fs from 'node:fs'
|
|
5
|
+
import { tmpdir } from 'node:os'
|
|
6
|
+
import { join } from 'node:path'
|
|
3
7
|
import * as mod from '../lib/index.js'
|
|
4
8
|
|
|
5
|
-
//
|
|
9
|
+
// ---- 构造假 ctx ----
|
|
6
10
|
const registered = []
|
|
11
|
+
const registrations = {}
|
|
12
|
+
const listeners = {}
|
|
7
13
|
const logs = []
|
|
14
|
+
|
|
8
15
|
const ctx = {
|
|
9
16
|
skills: {
|
|
10
17
|
register: (def) => {
|
|
@@ -12,15 +19,17 @@ const ctx = {
|
|
|
12
19
|
return () => {} // disposer
|
|
13
20
|
}
|
|
14
21
|
},
|
|
22
|
+
tools: { register: (def) => { registrations[def.name] = def; return () => {} } },
|
|
23
|
+
on: (event, fn) => { listeners[event] = fn; return () => {} },
|
|
15
24
|
}
|
|
16
25
|
const origLog = console.log
|
|
17
26
|
console.log = (...args) => { logs.push(args.join(' ')) }
|
|
18
27
|
|
|
19
|
-
mod.apply(ctx)
|
|
28
|
+
const dispose = mod.apply(ctx)
|
|
20
29
|
|
|
21
30
|
console.log = origLog
|
|
22
31
|
|
|
23
|
-
// 断言:拆分后的两个技能
|
|
32
|
+
// ---- 断言:拆分后的两个技能 ----
|
|
24
33
|
const names = registered.map((s) => s.name).sort()
|
|
25
34
|
assert.deepEqual(names, ['windows-enc', 'windows-sys'], '应注册 windows-enc 与 windows-sys')
|
|
26
35
|
for (const skill of registered) {
|
|
@@ -34,4 +43,67 @@ const sys = registered.find((s) => s.name === 'windows-sys')
|
|
|
34
43
|
assert.ok(sys.content.includes('Stop-Process'), 'windows-sys 正文含进程守则')
|
|
35
44
|
assert.ok(logs.some((l) => l.includes('技能已注册:windows-enc')), '日志有注册记录')
|
|
36
45
|
|
|
37
|
-
|
|
46
|
+
// ---- 断言:合并的主动防护层(0.2.0)----
|
|
47
|
+
assert.ok(registrations.windows_encode_detect, '注册 windows_encode_detect')
|
|
48
|
+
assert.ok(registrations.windows_encode_fix, '注册 windows_encode_fix')
|
|
49
|
+
assert.ok(listeners['tools/pre-execute'] && listeners['tools/post-execute'], '注册 hooks')
|
|
50
|
+
|
|
51
|
+
// ---- pre-execute 决策 ----
|
|
52
|
+
const denyExec = { name: 'pwsh', arguments: { command: 'Set-Content -Path a.json -Value $x' } }
|
|
53
|
+
const preDeny = await listeners['tools/pre-execute'](denyExec, async () => ({ kind: 'allow' }))
|
|
54
|
+
assert.equal(preDeny.kind, 'deny', '危险写命令被拒')
|
|
55
|
+
assert.ok(preDeny.reason.includes('[windows-guard]'), '拒绝理由带 [windows-guard] 标签')
|
|
56
|
+
const preAllow = await listeners['tools/pre-execute']({ name: 'pwsh', arguments: { command: 'Get-Content x.md' } }, async () => ({ kind: 'allow' }))
|
|
57
|
+
assert.equal(preAllow.kind, 'allow', '读命令放行')
|
|
58
|
+
|
|
59
|
+
// ---- post-execute 决策(乱码 → 附加提示)----
|
|
60
|
+
const postHit = await listeners['tools/post-execute'](
|
|
61
|
+
{ name: 'pwsh' },
|
|
62
|
+
{ isError: false, content: [{ type: 'text', text: '# DSH 鍚姩鏁呴殰璁板綍锛堜簨鏁呭洓鎺樼栵?' }] },
|
|
63
|
+
async () => ({ kind: 'accept', content: [{ type: 'text', text: 'fallback' }] })
|
|
64
|
+
)
|
|
65
|
+
assert.equal(postHit.kind, 'accept')
|
|
66
|
+
assert.ok(postHit.content.some((c) => typeof c.text === 'string' && c.text.includes('[windows-guard]')), '附加 windows-guard 提示')
|
|
67
|
+
assert.ok(postHit.content.some((c) => typeof c.text === 'string' && c.text.includes('windows_encode_detect')), '提示引导到新工具名')
|
|
68
|
+
|
|
69
|
+
const postClean = await listeners['tools/post-execute'](
|
|
70
|
+
{ name: 'pwsh' },
|
|
71
|
+
{ isError: false, content: [{ type: 'text', text: 'all good here' }] },
|
|
72
|
+
async () => ({ kind: 'accept', content: [{ type: 'text', text: 'fallback' }] })
|
|
73
|
+
)
|
|
74
|
+
assert.equal(postClean.kind, 'accept')
|
|
75
|
+
assert.ok(!postClean.content.some((c) => typeof c.text === 'string' && c.text.includes('[windows-guard]')), '正常输出不附加')
|
|
76
|
+
|
|
77
|
+
// ---- 非 pwsh 工具不受影响 ----
|
|
78
|
+
const postOther = await listeners['tools/post-execute'](
|
|
79
|
+
{ name: 'read' },
|
|
80
|
+
{ isError: false, content: [{ type: 'text', text: '鎻掍欢鏋勮瑙勮寖' }] },
|
|
81
|
+
async () => ({ kind: 'accept', content: [{ type: 'text', text: 'fallback' }] })
|
|
82
|
+
)
|
|
83
|
+
assert.equal(postOther.kind, 'accept')
|
|
84
|
+
assert.ok(!postOther.content.some((c) => typeof c.text === 'string' && c.text.includes('[windows-guard]')), '非 pwsh 工具不附加')
|
|
85
|
+
|
|
86
|
+
// ---- 工具执行 ----
|
|
87
|
+
const tmp = join(tmpdir(), 'windowsguard-smoke')
|
|
88
|
+
fs.mkdirSync(tmp, { recursive: true })
|
|
89
|
+
const f = join(tmp, 'sample.gbk.txt')
|
|
90
|
+
fs.writeFileSync(f, Buffer.from([0xb2, 0xe5, 0xbc, 0xfe, 0xb9, 0xe6, 0xb7, 0xb6])) // GBK 插件规范(近似)
|
|
91
|
+
const fakeExec = { agent: { session: { header: { cwd: tmp } } } }
|
|
92
|
+
const detectResult = await registrations.windows_encode_detect.execute({ path: 'sample.gbk.txt' }, fakeExec)
|
|
93
|
+
assert.equal(detectResult.encoding, 'gbk', 'GBK 文件被正确检出')
|
|
94
|
+
const dryRun = await registrations.windows_encode_fix.execute({ path: 'sample.gbk.txt', dryRun: true }, fakeExec)
|
|
95
|
+
assert.ok(dryRun.notice.includes('dryRun'), 'dryRun 不写文件')
|
|
96
|
+
assert.ok(!fs.existsSync(`${f}.windowsguard.bak`), 'dryRun 无备份')
|
|
97
|
+
const fixResult = await registrations.windows_encode_fix.execute({ path: 'sample.gbk.txt' }, fakeExec)
|
|
98
|
+
assert.equal(fixResult.before, 'gbk')
|
|
99
|
+
assert.equal(fixResult.after, 'utf-8')
|
|
100
|
+
const reread = fs.readFileSync(f, 'utf8')
|
|
101
|
+
assert.ok(reread.includes('插件'), '修复后为 UTF-8 中文')
|
|
102
|
+
assert.ok(fs.existsSync(`${f}.windowsguard.bak`), '备份存在(.windowsguard.bak)')
|
|
103
|
+
|
|
104
|
+
// ---- 清理函数可调用(HMR 幂等)----
|
|
105
|
+
dispose()
|
|
106
|
+
|
|
107
|
+
fs.rmSync(tmp, { recursive: true, force: true })
|
|
108
|
+
|
|
109
|
+
console.log(`smoke: 全部通过(技能 ${registered.length} + 工具 ${Object.keys(registrations).length} + hooks ${Object.keys(listeners).length},修复往返 OK)`)
|
package/skills/windows-enc.md
CHANGED
|
@@ -30,7 +30,11 @@ whenToUse: 读/写文本与配置文件、处理中文输出、命令引号转
|
|
|
30
30
|
| pwsh 7 写文件 | `Out-File -Encoding utf8`(5.1 下 = 带 BOM;7 下无 BOM) | `Out-File -Encoding utf8NoBOM`(仅 pwsh 7) |
|
|
31
31
|
|
|
32
32
|
补充:
|
|
33
|
-
-
|
|
33
|
+
- **配套零 token 工具**(本插件注册,优先于手写嗅探/转换命令):
|
|
34
|
+
`windows_encode_detect`(诊断 BOM/UTF-8/GBK/UTF-16 + 置信度)与
|
|
35
|
+
`windows_encode_fix`(转成 UTF-8 无 BOM,自动备份 `<path>.windowsguard.bak`,
|
|
36
|
+
支持 `dryRun`)。遇到乱码或 JSON/YAML 解析失败时,先 detect 后 fix,再复检。
|
|
37
|
+
- **查文件编码**(无工具可用时的替代):`Format-Hex -Path a.json -Count 3`——
|
|
34
38
|
`EF BB BF` = UTF-8 BOM(解析器可能拒绝);`FF FE` = UTF-16LE(必然炸 JSON);
|
|
35
39
|
无 BOM 且含 `FF FD` 替换符 = 文件本来就被写坏了。
|
|
36
40
|
- **GBK→UTF-8 转换**(确证文件是 GBK 时,PowerShell 无直接 cmdlet,用 .NET):
|
|
@@ -64,3 +68,6 @@ whenToUse: 读/写文本与配置文件、处理中文输出、命令引号转
|
|
|
64
68
|
1. **不猜**——乱码文本语义不可信,别试图「意思理解一下」继续用;
|
|
65
69
|
2. **不引用**——不要把它当答案、文件名、配置值传给下游;
|
|
66
70
|
3. **先重读**——回到源头(原文件原命令)按守则重新读/重新执行。
|
|
71
|
+
|
|
72
|
+
附:pwsh 工具输出出现乱码时,本插件会自动在结果后追加 `[windows-guard]` 修复提示;
|
|
73
|
+
文件类乱码直接走 `windows_encode_detect` / `windows_encode_fix` 工具链。
|