dsh-plugin-windows-guard 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Pasumao
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,96 @@
1
+ # dsh-plugin-windows-guard
2
+
3
+ DeepSeek Harness (dsh) 的 **Windows 环境防坑守则插件**(纯数据,零代码逻辑)。
4
+
5
+ 把本机 255 个会话归档里反复出现的 Windows 高频坑,沉淀成「一次写对」的
6
+ 预防性守则,以两个 skill 形式自动加载给 agent——**不是修复工具,是行为规则**:
7
+ 让模型在写 PowerShell、读文件、处理编码/路径/进程/端口问题前先看到防坑清单。
8
+
9
+ 纯技能载体(对标 `dsh-plugin-dev-kb`):`lib/index.js` 只有约 40 行注册代码,
10
+ 全部内容在 `skills/` 目录。零运行时依赖、零构建。
11
+
12
+ ## 功能
13
+
14
+ 按触发场景拆成两个自包含技能(`<available_skills>` 按 description 匹配自动加载正文):
15
+
16
+ ### `windows-enc` —— 编码与 PowerShell 解析(命令/文件内容出错类)
17
+
18
+ | 章节 | 内容 | 来源问题(历史实录) |
19
+ |---|---|---|
20
+ | 1 环境事实卡 | pwsh 5.1/7 差异、`-Command` 单 argv 无 shell 转义层、OutputEncoding 已钉、退出码权威 | stderr 误判(git 红色进度、NativeCommandError) |
21
+ | 2 编码三铁律 | 读必带 `-Encoding UTF8`;写结构化文件用 `WriteAllText(UTF8Encoding($false))`;查 BOM/转码命令模板 | GBK 误解码(`鎻掍欢`/`鈥?`)、BOM 写坏 package.json(启动事故二)、UTF-16 写坏 |
22
+ | 3 引号转义速查 | PS `''` 转义(反斜杠无效)、双引号插值、`node -e`/`python -c` 引号地狱 → 写临时文件 | `SyntaxError: unterminated string literal`、实测引号嵌套失败 |
23
+ | 4 乱码三不原则 | 乱码特征表(`鈥?`/`??OK??`/`\uFFFD`)+ 不猜/不引用/先重读 | `??OK??`、`??? skill ????` 乱码当答案 |
24
+
25
+ ### `windows-sys` —— 系统环境(排查类)
26
+
27
+ | 章节 | 内容 | 来源问题 |
28
+ |---|---|---|
29
+ | 1 路径与文件系统 | MAX_PATH/长路径、空格路径、EACCES/EBUSY 文件占用、junction 与 `link:` realpath、路径格式 | `EACCES: realpath '...sock'`、npm-cache 深层路径 |
30
+ | 2 进程与端口 | `netstat` 查端口、`Stop-Process`/`taskkill`、先查后杀 | 3080/39090 端口占用、孤儿进程 |
31
+ | 3 跨平台杂项 | CRLF 陷阱、`.cmd` 脚本宿主/ExecutionPolicy、时区、大小写 | 小说文件 CRLF 检查、run.bat chcp 65001 |
32
+
33
+ ## 安装
34
+
35
+ ```powershell
36
+ # 本地 link 开发(本仓库):
37
+ git clone https://github.com/Pasumao/dsh-plugin-windows-guard.git D:\dsh\plugins\dsh-plugin-windows-guard
38
+ # 1. profile package.json dependencies 加
39
+ # "dsh-plugin-windows-guard": "link:D:/dsh/plugins/dsh-plugin-windows-guard"
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 包/补丁中挂载,其余同上
49
+ ```
50
+
51
+ ## 使用
52
+
53
+ 安装后无需任何配置。模型遇到 Windows 命令/编码/乱码/路径/进程/端口任务时,
54
+ `skill({ name: "windows-enc" | "windows-sys" })` 自动加载对应守则;也可手动让模型
55
+ 加载查看全文。
56
+
57
+ ```text
58
+ 用户:读一下 D:\dsh\config\comfy_prompt.json 看看配置
59
+ 模型:读取前先按守则用 Get-Content -Encoding UTF8……
60
+ ```
61
+
62
+ ## 配置/示例
63
+
64
+ **无需任何配置**:挂载后即生效,无 API key、无 token、无配置文件。示例:
65
+
66
+ ```text
67
+ 用户:读一下 D:\dsh\config\comfy_prompt.json 看看配置
68
+ 模型:读取前先按守则用 Get-Content -Encoding UTF8……
69
+ ```
70
+
71
+ 更完整的使用说明见上方「使用」节。
72
+
73
+ ### 为何拆成两个(设计说明)
74
+
75
+ - **常驻目录成本**:两条 description(~300 token)几乎不涨;
76
+ - **触发精度**:`windows-enc` 管「命令/文件内容出错」(编码/转义/乱码),
77
+ `windows-sys` 管「系统环境排查」(路径/进程/端口/跨平台),边界清晰、互不牵连;
78
+ - **正文按需减半**:Windows「内容出错」类任务不再加载「系统环境」整章;
79
+ - 预留给未来主题(WSL/winget/容器…)归入 `windows-sys` 扩展。
80
+
81
+ ## 实现说明
82
+
83
+ - 注册走 `ctx.skills.register`(官方 skill 子系统),content 为 SKILL.md 正文;
84
+ - 无工具、无命令、无服务端逻辑、无文件写入——纯预防性指令;
85
+ - skill 正文改动需重启 dsh(host 插件 apply 时读取 skills/ 目录)。
86
+
87
+ ## 相关插件
88
+
89
+ 已发布插件互相引用(更多 DSH 插件生态):
90
+
91
+ - [dsh-plugin-workbench](https://www.npmjs.com/package/dsh-plugin-workbench) — VS Code 风格工作区文件浏览器 + 可编辑预览
92
+ - [dsh-notify](https://www.npmjs.com/package/dsh-notify) — agent 停止/出错的 Windows 原生通知 + 托盘图标
93
+ - [dsh-plugin-image-tools](https://www.npmjs.com/package/dsh-plugin-image-tools) — 图片选择卡 / 回复内嵌图片 / 聊天图片放大
94
+ - [dsh-plugin-dev-kb](https://www.npmjs.com/package/dsh-plugin-dev-kb) — dsh 官方文档完整 Markdown 镜像(插件开发知识库)
95
+ - [dsh-plugin-choice-refresh](https://www.npmjs.com/package/dsh-plugin-choice-refresh) — 选择卡「重新生成选项」/「更多选项」
96
+ - [dsh-plugin-table-zoom](https://www.npmjs.com/package/dsh-plugin-table-zoom) — 聊天表格浮窗查看 + 一键复制 Markdown
@@ -0,0 +1,8 @@
1
+ # dsh-plugin-windows-guard bundle patch: Windows 环境防坑守则 skill。
2
+ #
3
+ # 应用方式与 dev-kb/nonebot-kb 相同:作为一条 insert 覆盖 profile 根。
4
+ # `name` 必须等于 package.json 的 `name`(loader 从 profile node_modules 按名解析)。
5
+ # 无配置项(纯数据插件);注册技能名 windows-guard(内容见 skills/windows-guard/SKILL.md)。
6
+ - insert:
7
+ - id: dsh-plugin-windows-guard
8
+ name: dsh-plugin-windows-guard
package/lib/index.js ADDED
@@ -0,0 +1,98 @@
1
+ /**
2
+ * dsh-plugin-windows-guard — 主机插件(纯技能载体,无工具/命令/修复逻辑)。
3
+ *
4
+ * 挂载于 profile 根层(全局层),唯一职责:把 skills/ 目录下的 windows-guard
5
+ * 技能注册为 runtime skill,让每个会话的 agent 在 Windows 上执行命令、读写
6
+ * 文件、处理编码/路径/进程/端口/乱码类任务时,按技能 description 自动加载
7
+ * 防坑守则——预防性规则,不提供修复工具(AI 按守则自行修复)。
8
+ *
9
+ * 守则内容来自本机 255 个会话归档的问题扫描(GBK 误解码 / BOM / UTF-16 /
10
+ * 单引号转义 / 长路径 / EACCES / 端口占用 / stderr 误判 / 乱码当答案等),
11
+ * 详见 skills/windows-guard/SKILL.md。
12
+ *
13
+ * 依赖纪律:本模块不 import 任何 @deepseek-ai/* 运行时包(插件以 link: 方式
14
+ * 装入 profile,Node ESM 按 realpath 解析链接包,外部依赖从插件目录解析不到)。
15
+ */
16
+
17
+ import { existsSync, readdirSync, readFileSync } from 'node:fs'
18
+ import { dirname, join } from 'node:path'
19
+ import { fileURLToPath } from 'node:url'
20
+
21
+ export const name = 'dsh-plugin-windows-guard'
22
+ export const inject = ['skills']
23
+
24
+ const PKG_DIR = dirname(fileURLToPath(import.meta.url))
25
+ const SKILLS_DIR = join(PKG_DIR, '..', 'skills')
26
+
27
+ /** 极简 frontmatter 解析(name/description/whenToUse)。 */
28
+ function parseFrontmatter(md) {
29
+ const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(md)
30
+ if (!match) return { name: undefined, description: undefined, whenToUse: undefined, body: md }
31
+ const meta = {}
32
+ for (const line of match[1].split(/\r?\n/)) {
33
+ const i = line.indexOf(':')
34
+ if (i <= 0) continue
35
+ const key = line.slice(0, i).trim()
36
+ if (key) meta[key] = line.slice(i + 1).trim().replace(/^["']|["']$/g, '')
37
+ }
38
+ return { ...meta, body: md.slice(match[0].length) }
39
+ }
40
+
41
+ /** 从 skills/ 目录加载技能定义(文件缺失/无 frontmatter 时跳过并警告,绝不让插件挂载失败)。 */
42
+ function loadSkills() {
43
+ const out = []
44
+ if (!existsSync(SKILLS_DIR)) {
45
+ console.warn('[dsh-plugin-windows-guard] skills 目录缺失,技能未加载:' + SKILLS_DIR)
46
+ return out
47
+ }
48
+ for (const file of readdirSync(SKILLS_DIR)) {
49
+ if (!file.endsWith('.md')) continue
50
+ try {
51
+ const fm = parseFrontmatter(readFileSync(join(SKILLS_DIR, file), 'utf8'))
52
+ if (!fm.name || !fm.description) {
53
+ console.warn(`[dsh-plugin-windows-guard] 技能文件缺少 name/description frontmatter,已跳过:${file}`)
54
+ continue
55
+ }
56
+ out.push({
57
+ name: fm.name,
58
+ description: fm.description,
59
+ whenToUse: fm.whenToUse,
60
+ body: fm.body
61
+ })
62
+ } catch (err) {
63
+ console.warn(`[dsh-plugin-windows-guard] 技能文件读取失败,已跳过:${file}(${err?.message ?? err})`)
64
+ }
65
+ }
66
+ return out
67
+ }
68
+
69
+ export function apply(ctx, config) {
70
+ const disposers = []
71
+ const skills = loadSkills()
72
+
73
+ for (const skill of skills) {
74
+ try {
75
+ // 契约(dsh-skill validateDefinition):name/description/source/content 四字符串;
76
+ // provider 缺省为 'runtime';纯正文技能无资源目录,不声明 resourceBase。
77
+ disposers.push(ctx.skills.register({
78
+ name: skill.name,
79
+ description: skill.description,
80
+ ...(skill.whenToUse ? { whenToUse: skill.whenToUse } : {}),
81
+ source: 'runtime',
82
+ content: skill.body,
83
+ }))
84
+ console.log(`[dsh-plugin-windows-guard] 技能已注册:${skill.name}`)
85
+ } catch (err) {
86
+ console.warn(`[dsh-plugin-windows-guard] 技能注册失败:${skill.name}(${err?.message ?? err})`)
87
+ }
88
+ }
89
+
90
+ console.log(`[dsh-plugin-windows-guard] 已挂载:${skills.length} 技能(守则在 skills/ 目录)`)
91
+
92
+ // 卸载清理(HMR 重载时避免重复注册)。
93
+ return () => {
94
+ for (const dispose of disposers) {
95
+ try { dispose() } catch { /* ignore */ }
96
+ }
97
+ }
98
+ }
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "dsh-plugin-windows-guard",
3
+ "description": "DeepSeek Harness (dsh) Windows 环境防坑守则 skill 插件(纯数据):GBK/BOM/UTF-16 编码坑、PowerShell 引号转义、长路径/文件占用/EACCES、进程与端口、CRLF、stderr 误判、乱码识别——预防性规则,无修复工具。纯技能载体,零运行时依赖,零构建。",
4
+ "version": "0.1.0",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "exports": {
8
+ ".": "./lib/index.js",
9
+ "./package.json": "./package.json"
10
+ },
11
+ "files": [
12
+ "lib",
13
+ "skills",
14
+ "cordis.patch.yml",
15
+ "README.md",
16
+ "scripts"
17
+ ],
18
+ "scripts": {
19
+ "pack": "npm pack",
20
+ "selfcheck": "node scripts/selfcheck.mjs",
21
+ "smoke": "node scripts/selfcheck.mjs && node scripts/smoke-server.mjs"
22
+ },
23
+ "peerDependencies": {
24
+ "@deepseek-ai/cordis": "^4.0.1"
25
+ },
26
+ "engines": {
27
+ "node": ">=22.5"
28
+ },
29
+ "keywords": [
30
+ "dsh",
31
+ "dsh-plugin",
32
+ "deepseek",
33
+ "harness",
34
+ "cordis",
35
+ "plugin",
36
+ "skill",
37
+ "windows",
38
+ "powershell",
39
+ "encoding",
40
+ "utf-8",
41
+ "gbk",
42
+ "bom"
43
+ ],
44
+ "license": "MIT",
45
+ "homepage": "https://github.com/Pasumao/dsh-plugin-windows-guard#readme",
46
+ "repository": {
47
+ "type": "git",
48
+ "url": "git+https://github.com/Pasumao/dsh-plugin-windows-guard.git"
49
+ },
50
+ "bugs": {
51
+ "url": "https://github.com/Pasumao/dsh-plugin-windows-guard/issues"
52
+ },
53
+ "dsh": {
54
+ "bundle": {
55
+ "patch": "./cordis.patch.yml"
56
+ }
57
+ }
58
+ }
@@ -0,0 +1,100 @@
1
+ // dsh-plugin-windows-guard 纯函数自检(离线、零依赖):node scripts/selfcheck.mjs
2
+ import assert from 'node:assert'
3
+ import fs from 'node:fs'
4
+ import path from 'node:path'
5
+ import { createRequire } from 'node:module'
6
+ import { fileURLToPath, pathToFileURL } from 'node:url'
7
+
8
+ const require = createRequire(import.meta.url)
9
+ const pkg = require('../package.json')
10
+ const PKG_DIR = path.dirname(fileURLToPath(import.meta.url))
11
+ const SKILLS_DIR = path.join(PKG_DIR, '..', 'skills')
12
+
13
+ let passed = 0
14
+ function t(name, fn) {
15
+ try {
16
+ fn()
17
+ passed++
18
+ console.log(`PASS ${name}`)
19
+ } catch (e) {
20
+ console.error(`FAIL ${name}: ${e.message}`)
21
+ process.exitCode = 1
22
+ }
23
+ }
24
+
25
+ // ---- package 完整性 ----
26
+ t('package.json name/main/patch 一致', () => {
27
+ assert.equal(pkg.name, 'dsh-plugin-windows-guard')
28
+ assert.equal(pkg.main, 'lib/index.js')
29
+ assert.ok(pkg.dsh?.bundle?.patch, '应有 bundle patch')
30
+ })
31
+
32
+ // ---- 技能文件:拆分后的两个技能 ----
33
+ t('存在 windows-enc.md 与 windows-sys.md,且旧合并文件已移除', () => {
34
+ assert.ok(fs.existsSync(path.join(SKILLS_DIR, 'windows-enc.md')), '缺 windows-enc.md')
35
+ assert.ok(fs.existsSync(path.join(SKILLS_DIR, 'windows-sys.md')), '缺 windows-sys.md')
36
+ assert.ok(!fs.existsSync(path.join(SKILLS_DIR, 'windows-guard.md')), '旧合并文件应已移除')
37
+ })
38
+
39
+ t('frontmatter 完整(name/description/whenToUse)+ 正文足够长', () => {
40
+ for (const file of ['windows-enc.md', 'windows-sys.md']) {
41
+ const md = fs.readFileSync(path.join(SKILLS_DIR, file), 'utf8')
42
+ const fm = /^---\r?\n([\s\S]*?)\r?\n---/.exec(md)
43
+ assert.ok(fm, `${file} 应有 frontmatter`)
44
+ const meta = fm[1]
45
+ for (const key of ['name:', 'description:', 'whenToUse:']) {
46
+ assert.ok(meta.includes(key), `${file} frontmatter 缺 ${key}`)
47
+ }
48
+ assert.ok(md.length > 2000, `${file} 正文过短: ${md.length} 字符`)
49
+ }
50
+ })
51
+
52
+ // ---- 正文关键内容抽查(防坑要点真实存在)----
53
+ t('windows-enc 覆盖编码/转义/乱码', () => {
54
+ const md = fs.readFileSync(path.join(SKILLS_DIR, 'windows-enc.md'), 'utf8')
55
+ for (const needle of ['Get-Content -Encoding UTF8', 'WriteAllText', '三不原则']) {
56
+ assert.ok(md.includes(needle), `windows-enc 缺少: ${needle}`)
57
+ }
58
+ })
59
+ t('windows-sys 覆盖路径/进程/跨平台', () => {
60
+ const md = fs.readFileSync(path.join(SKILLS_DIR, 'windows-sys.md'), 'utf8')
61
+ for (const needle of ['MAX_PATH', 'Stop-Process', 'netstat', 'autocrlf']) {
62
+ assert.ok(md.includes(needle), `windows-sys 缺少: ${needle}`)
63
+ }
64
+ })
65
+
66
+ // ---- lib 可加载且导出一致 ----
67
+ t('lib/index.js 导出 name/inject/apply', async () => {
68
+ const mod = await import(pathToFileURL(path.join(PKG_DIR, '..', 'lib', 'index.js')).href)
69
+ assert.equal(mod.name, 'dsh-plugin-windows-guard')
70
+ assert.ok(Array.isArray(mod.inject) && mod.inject.includes('skills'))
71
+ assert.equal(typeof mod.apply, 'function')
72
+ })
73
+
74
+ // ---- frontmatter 解析一致性(用 index.js 相同的解析逻辑)----
75
+ t('frontmatter name 正确且 description 足够触发词化', () => {
76
+ for (const [file, expectName] of [['windows-enc.md', 'windows-enc'], ['windows-sys.md', 'windows-sys']]) {
77
+ const md = fs.readFileSync(path.join(SKILLS_DIR, file), 'utf8')
78
+ const m = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(md)
79
+ const meta = {}
80
+ for (const line of m[1].split(/\r?\n/)) {
81
+ const i = line.indexOf(':')
82
+ if (i <= 0) continue
83
+ const key = line.slice(0, i).trim()
84
+ if (key) meta[key] = line.slice(i + 1).trim().replace(/^["']|["']$/g, '')
85
+ }
86
+ assert.equal(meta.name, expectName, `${file} name 应为 ${expectName}`)
87
+ assert.ok(meta.description.length > 40, `${file} description 不足`)
88
+ }
89
+ })
90
+
91
+ // ---- loadSkills 一致性:插件能发现且仅发现这两个技能 ----
92
+ t('lib 只能发现拆分的两个技能', async () => {
93
+ const mod = await import(pathToFileURL(path.join(PKG_DIR, '..', 'lib', 'index.js')).href)
94
+ const registered = []
95
+ mod.apply({ skills: { register: (def) => { registered.push(def.name); return () => {} } } })
96
+ assert.deepEqual(registered.sort(), ['windows-enc', 'windows-sys'])
97
+ })
98
+
99
+ if (!process.exitCode) console.log(`\n${passed} 项全部通过`)
100
+ else console.log(`\n${passed} 项通过,存在失败项`)
@@ -0,0 +1,37 @@
1
+ // dsh-plugin-windows-guard 服务端冒烟:假 ctx 走注册全链路(纯数据插件)。
2
+ import assert from 'node:assert'
3
+ import * as mod from '../lib/index.js'
4
+
5
+ // 假 ctx:收集注册与日志
6
+ const registered = []
7
+ const logs = []
8
+ const ctx = {
9
+ skills: {
10
+ register: (def) => {
11
+ registered.push(def)
12
+ return () => {} // disposer
13
+ }
14
+ },
15
+ }
16
+ const origLog = console.log
17
+ console.log = (...args) => { logs.push(args.join(' ')) }
18
+
19
+ mod.apply(ctx)
20
+
21
+ console.log = origLog
22
+
23
+ // 断言:拆分后的两个技能
24
+ const names = registered.map((s) => s.name).sort()
25
+ assert.deepEqual(names, ['windows-enc', 'windows-sys'], '应注册 windows-enc 与 windows-sys')
26
+ for (const skill of registered) {
27
+ assert.ok(skill.description.includes('Windows'), `${skill.name} description 含触发词`)
28
+ assert.ok(skill.source === 'runtime', `${skill.name} source= runtime`)
29
+ }
30
+ const enc = registered.find((s) => s.name === 'windows-enc')
31
+ assert.ok(enc.content.includes('[System.IO.File]::WriteAllText'), 'windows-enc 正文含编码守则')
32
+ assert.ok(enc.content.includes('三不原则'), 'windows-enc 正文含乱码三不')
33
+ const sys = registered.find((s) => s.name === 'windows-sys')
34
+ assert.ok(sys.content.includes('Stop-Process'), 'windows-sys 正文含进程守则')
35
+ assert.ok(logs.some((l) => l.includes('技能已注册:windows-enc')), '日志有注册记录')
36
+
37
+ console.log(`smoke: 全部通过(注册 ${registered.length} 个技能:${registered.map((s) => s.name).join(', ')})`)
@@ -0,0 +1,66 @@
1
+ ---
2
+ name: windows-enc
3
+ description: Windows 编码与 PowerShell 解析防坑守则:读文件乱码(GBK/ANSI 误解码)、写 JSON/YAML 被 BOM 或 UTF-16 破坏、PowerShell 单引号转义、node -e/python -c 引号地狱、stderr 误判、乱码识别与三不原则。读写文件、中文输出、命令解析失败、出现 鈥?/鎻掍欢/锟斤拷 等乱码时加载。
4
+ whenToUse: 读/写文本与配置文件、处理中文输出、命令引号转义、JSON/YAML 解析失败、乱码排查时
5
+ ---
6
+
7
+ # Windows 编码与 PowerShell 解析守则(windows-enc)
8
+
9
+ 本守则针对 Windows 上「命令/文件内容出错」类问题:读了文件变乱码、写了文件解析
10
+ 失败、引号转义写错、stderr 误判。目标:一次写对,不要出错。
11
+
12
+ ## 1. 环境事实卡(先记住,避免误判)
13
+
14
+ - dsh 的 `pwsh` 工具在 Windows 上运行 **PowerShell 7(若安装)或 5.1 兜底**;命令经
15
+ `-Command` 作为**单个 argv** 传入,**没有** bash 风格 shell 转义层——不要在命令上
16
+ 叠加 `\"` / `\'` 等 bash 转义。
17
+ - dsh 已在每条命令前自动执行 `[Console]::OutputEncoding = UTF8` 与
18
+ `$OutputEncoding = UTF8`(pwsh 7 默认即 UTF-8)——**命令输出本身不会乱码**;
19
+ 乱码几乎都来自「读文件」与「写文件」环节,不是管道。
20
+ - 判断成败只看 **`[exit code: N]` / `$LASTEXITCODE`**;`[stderr]` 有内容 ≠ 失败
21
+ (PowerShell 会把不少正常信息写 stderr,git 进度 `To https://...` 还会被渲染成
22
+ 红色「错误」)。Exit code 0 = 成功;非零才排查。
23
+
24
+ ## 2. 编码三铁律(最高频坑,务必逐条执行)
25
+
26
+ | 场景 | ❌ 错 | ✅ 对 |
27
+ |---|---|---|
28
+ | 读文本文件 | `Get-Content foo.json`(5.1 按 ANSI/GBK 解码,中文变 `鈥?`/`鎻掍欢`) | `Get-Content -Encoding UTF8 foo.json` |
29
+ | 写 JSON/YAML/MD | `Set-Content -Path a.json -Value $x` / `$x > a.json`(5.1 写 UTF-16LE 或 BOM) | `[System.IO.File]::WriteAllText("a.json", $x, [System.Text.UTF8Encoding]::new($false))` |
30
+ | pwsh 7 写文件 | `Out-File -Encoding utf8`(5.1 下 = 带 BOM;7 下无 BOM) | `Out-File -Encoding utf8NoBOM`(仅 pwsh 7) |
31
+
32
+ 补充:
33
+ - **查文件编码**(前 3 字节嗅探):`Format-Hex -Path a.json -Count 3`——
34
+ `EF BB BF` = UTF-8 BOM(解析器可能拒绝);`FF FE` = UTF-16LE(必然炸 JSON);
35
+ 无 BOM 且含 `FF FD` 替换符 = 文件本来就被写坏了。
36
+ - **GBK→UTF-8 转换**(确证文件是 GBK 时,PowerShell 无直接 cmdlet,用 .NET):
37
+ `[System.IO.File]::WriteAllText($p, [System.IO.File]::ReadAllText($p, [System.Text.Encoding]::GetEncoding(936)), [System.Text.UTF8Encoding]::new($false))`
38
+ - **只读不改写**:`dsh` 的 `write`/`edit` 工具写 UTF-8 无 BOM,改配置优先用它
39
+ 而不是 PowerShell 重定向。
40
+
41
+ ## 3. 引号转义速查(解析失败的根源)
42
+
43
+ - PowerShell 单引号字符串里,单引号用**两个单引号**转义:`'it''s fine'`;
44
+ **反斜杠转义在 PowerShell 里无效**(`\'` 会被当普通字符)。
45
+ - 需要变量插值才用双引号:`"$env:USERPROFILE\path"`(`$` 会插值;不想要插值用单引号)。
46
+ - 传参给外部程序(node.exe / python.exe)有引号嵌套时,**不要逐层转义**,改为一层:
47
+ - 首选:`& node.exe "C:\path\script.mjs" "arg with space"`(参数各自加引号)
48
+ - 复杂脚本:**写临时文件** `node C:\Users\...\AppData\Local\Temp\script.mjs` 再执行,
49
+ 避免 `node -e "..."` 里再嵌 `"` 与 `'` 的地狱(历史实录:
50
+ `SyntaxError: unterminated string literal` 即由此而来)。
51
+ - `cmd /c` 更糟:cmd 有自己的引号规则,能用 PowerShell 就不用 cmd。
52
+
53
+ ## 4. 输出判断:见到乱码怎么办(三不原则)
54
+
55
+ 常见乱码特征与成因:
56
+
57
+ | 症状 | 成因 | 处理 |
58
+ |---|---|---|
59
+ | `鈥?` `锟斤拷` `鎻掍欢` `鏋勮瑙勮寖` | UTF-8 字节被按 GBK 解码(读了文件没带 `-Encoding`,或文件本就是 GBK) | 按第 2 节:带 `-Encoding UTF8` 重读;`Format-Hex` 查文件真编码 |
60
+ | `??OK??` `??? 中文` | 中文在某环节被转成 ASCII(UTF-8→ANSI 丢失,无法恢复) | 回源头重新获取原文;不要靠猜 |
61
+ | `\uFFFD\uFFFD` | 解码失败替换符 | 同上,先转码再读 |
62
+
63
+ **三不原则**:
64
+ 1. **不猜**——乱码文本语义不可信,别试图「意思理解一下」继续用;
65
+ 2. **不引用**——不要把它当答案、文件名、配置值传给下游;
66
+ 3. **先重读**——回到源头(原文件原命令)按守则重新读/重新执行。
@@ -0,0 +1,59 @@
1
+ ---
2
+ name: windows-sys
3
+ description: Windows 系统环境防坑守则:路径与文件系统(MAX_PATH 长路径、空格/中文路径、文件占用 EACCES/EBUSY、junction 与 link: 安装、路径格式)、进程与端口(netstat 查端口、Stop-Process/taskkill、先查后杀)、跨平台杂项(CRLF 换行、.cmd 脚本宿主/ExecutionPolicy、时区、大小写不敏感)。排查路径报错、文件被占用、端口/进程问题时加载。
4
+ whenToUse: 路径/文件系统报错、文件占用、进程管理、端口冲突、CRLF/换行、跨平台脚本、环境排查时
5
+ ---
6
+
7
+ # Windows 系统环境守则(windows-sys)
8
+
9
+ 本守则针对 Windows 「系统环境」类问题:路径、文件占用、进程/端口、换行与跨平台。
10
+ 与 windows-enc(编码/转义/乱码)互补;判断成败仍看 `[exit code: N]`(真相唯一权威)。
11
+
12
+ ## 1. 路径与文件系统
13
+
14
+ - **长路径**:Windows MAX_PATH 260 字符(未开长路径支持时)。Node 处理
15
+ `C:\Users\...\AppData\Local\npm-cache\_npx\**\node_modules\...` 这类深层路径已接近
16
+ 极限;报 ENOENT/EPERM 且路径很长时,先缩短路径(`subst` / 浅层目录)再查真正原因。
17
+ - **空格/中文路径**:Node 已自动处理,但 PowerShell 里输路径用引号包住:
18
+ `Get-ChildItem "D:\my project\file.json"`。
19
+ - **文件占用/权限**(`EACCES`/`Access is denied`/`EBUSY`):Windows 上被进程打开的文件
20
+ 无法删除/重写;先停占用进程(见第 2 节)再操作。别名 `realpath '...sock'` 等
21
+ 对 socket/管道文件报 EACCES 是正常保护,不是出 bug,别据此怀疑权限。
22
+ - **符号链接 & `link:`**:dsh 插件以 `link:` 装入 profile,Node 按 **realpath** 解析——
23
+ 插件里引用的外部依赖必须装在插件自己的 node_modules,不能假设能从 profile 解析。
24
+ 移动含 junction 的目录用 robocopy 或先解除链接,别用普通复制。
25
+ - **路径格式**:Windows 下用原生日志 `C:\...`;向 Node/JSON 传路径时双反斜杠
26
+ (`"C:\\dsh"`)或在 JS 里用 String.raw。
27
+
28
+ ## 2. 进程与端口(先查后杀)
29
+
30
+ ```powershell
31
+ # 查端口占用(3080 = dsh web,39090 = launcher 启动页)
32
+ netstat -ano | findstr :3080
33
+
34
+ # 查进程
35
+ Get-Process -Id 12345 | Format-List Id, ProcessName, Path
36
+ tasklist | findstr /i "node"
37
+
38
+ # 杀进程(Force 必要;确认 PID 是目标再杀)
39
+ Stop-Process -Id 12345 -Force
40
+ taskkill /F /PID 12345
41
+ ```
42
+
43
+ - 杀进程**前先确认** PID(任务名/命令行参数核对),Windows 上杀错进程没有
44
+ UNDO;残留孤儿进程是「端口被占/文件被锁」的头号原因。
45
+ - `dsh` 相关:后端是 node;launcher 是 DshLauncher.exe。改 profile 后先
46
+ `node D:\dsh\scripts\check-profile.mjs` 自检再重启。
47
+
48
+ ## 3. 跨平台与杂项
49
+
50
+ - **换行符**:写脚本/配置文件统一 **LF**;PowerShell 的 `"..."` 字符串含 `\r\n` 时会
51
+ 悄悄变成 CRLF。检查:`Format-Hex -Path f | Select-Object -First 1`(0D 0A = CRLF)。
52
+ git 里 `core.autocrlf` 会改写,跨团队项目先约定(本机推荐
53
+ `git config core.autocrlf false` + `.gitattributes` 显式声明)。
54
+ - **脚本宿主**:npm/pnpm 在 Windows 的 bin 是 `.cmd`;PowerShell 执行 `.cmd`/`.bat`
55
+ 常见 `-ExecutionPolicy Bypass` 需求。跨平台脚本优先用 Node/Python 直跑,不经 shell。
56
+ - **时区**:日志/文件时间常是 UTC 或本地混用,比对时间先断言时区
57
+ (`Get-Date` vs `Get-Date -AsUTC`)。
58
+ - **大小写**:Windows 路径大小写不敏感,但 git/Node 比较敏感——重命名只改大小写
59
+ 时要 git mv 两步走,否则文件不识别。