# dsh-kb-rag `install.ps1` 修复报告:中文用户名导致 WinError 123
**受影响版本**:1.6.2 及更早(此报告写于 1.6.2 为最新版的时期;修复已随 1.6.3 发布,当前版本见 `CHANGELOG.md`)
**受影响平台**:Windows + Windows PowerShell 5.1 + 用户名含非 ASCII 字符
**修复成本**:1 行代码
**建议**:合并后发 1.6.3(该修复已随 1.6.3 发布,当前版本见 `CHANGELOG.md`)
---
## 一、摘要
`install.ps1` 在**用户名含中文**的 Windows 上必然失败在第 3 步引擎冒烟测试:
```
== 3/5 引擎冒烟测试 (kb_engine.py stats)
[XX] {"ok": false, "error": "OSError: [WinError 123] 文件名、目录名或卷标语法不正确。:
'C:\\Users\\??\\AppData\\Local\\Temp\\kbrag-smoke-dcb891cf'"}
[XX] 引擎冒烟测试失败
```
路径里的 `??` 不是终端显示问题,**引擎收到的字符串里就是两个问号**。
根因不在引擎,而在 PowerShell 5.1 的管道编码。修复方式是在 `install.ps1` 顶部加一行:
```powershell
$OutputEncoding = New-Object System.Text.UTF8Encoding($false)
```
---
## 二、复现
环境:Windows、Python 3.10.9 (Miniconda3)、用户名含非 ASCII 字符(任何非 ASCII 用户名均可复现)
```powershell
npx --yes --package dsh-kb-rag -c "dsh-kb-rag-install --profile web"
```
或直接跑脚本:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\install.ps1 -SkipPip -SkipNode -SkipDsh
```
第 3 步必失败,退出码 1。
**注意这只在含非 ASCII 字符的 `%TEMP%` 下复现**。把 `TEMP` 指向纯 ASCII 路径(如 `C:\kbragtmp`)
即可绕过 —— 这大概也是它一直没被发现的原因。
---
## 三、根因
### 3.1 触发链
`install.mjs` 用 `spawnSync("powershell", ...)` 拉起的是 **Windows PowerShell 5.1**,
而它的 `$OutputEncoding` 默认值是 `System.Text.ASCIIEncoding`:
```powershell
PS> powershell -NoProfile -Command '$OutputEncoding.GetType().FullName'
System.Text.ASCIIEncoding
```
`install.ps1` 第 3 步:
```powershell
$smokeDir = Join-Path ([System.IO.Path]::GetTempPath()) ("kbrag-smoke-" + ...)
$req = '{"kb_root":"' + ($smokeDir -replace '\\', '/') + '"}'
$out = $req | & $Py $Engine stats 2>&1 | Out-String # ← 整个脚本唯一的管道
```
1. `GetTempPath()` 返回 `C:\Users\<用户名>\AppData\Local\Temp\`,路径含中文;
2. 拼成 JSON 后**经 stdin 管道**送给 `python kb_engine.py stats`;
3. 管道写给原生进程 stdin 的内容按 **ASCII** 编码,**所有非 ASCII 字符静默变成 `?`**;
4. 引擎按 `sys.stdin.buffer.read().decode("utf-8")` 严格解析,拿到的就是字面 `??`;
5. Windows 上 `?` 是非法文件名字符,`mkdir` 抛 `OSError [WinError 123]`。
### 3.2 字节级证据
```powershell
$OutputEncoding 为 ASCII 时,python 从 stdin 读到:
b'{"kb_root":"C:/Users/??/AppData/Local/Temp/kbrag-smoke-dcb891cf"}\r\n'
```
注意:`??` 是**两个字节 0x3F 0x3F**,不是编码错误后的替换字符 —— 是发送端就已经写坏了。
### 3.3 引擎本身没有问题
把 `kb_root` 直接设成中文目录,完全正常:
```powershell
PS> python kb_engine.py stats <<< '{"kb_root":"D:/workspace/文献库-smoke"}'
{"ok": true, "docs": 0, ...} # SQLite 正常建在中文目录里
```
**引擎完全支持中文路径。坏的只有 PowerShell → Python 这一次管道。**
同理,插件**运行期**也没有这个问题:`lib/index.js` 的 `handle.stdin.write(...)` 走 Node 流,
默认 UTF-8。所以这个 bug 只影响**安装阶段**。
---
## 四、修复
在 `scripts/install.ps1` 中,`$ErrorActionPreference = "Continue"`(第 18 行)之后插入 5 行:
```powershell
# [kb-rag-fix] Windows PowerShell 5.1 的 $OutputEncoding 默认是 ASCII:路径里的非 ASCII 字符
# (例如中文用户名 C:\Users\<用户>\AppData\Local\Temp)写进原生进程的 stdin 时会变成 "?",
# 引擎因此拿到非法路径,报 OSError [WinError 123]。这里统一强制 UTF-8(不带 BOM:
# 引擎用 raw.decode("utf-8") 解析 stdin),覆盖本脚本所有通向 python 的管道。
$OutputEncoding = New-Object System.Text.UTF8Encoding($false)
```
### 四个要点
| 要点 | 原因 |
|---|---|
| 放在**脚本顶部**,而不是那一行前面 | `$OutputEncoding` 是「管道写向原生进程」的总开关,放顶部能顺带防住以后新增的管道 |
| 必须是 **UTF-8 不带 BOM** | 引擎用 `raw.decode("utf-8")` 解析 stdin;带 BOM 会让 `json.loads` 直接失败(`Unexpected UTF-8 BOM`) |
| 对 PowerShell 7 无害 | PS7 默认已是 UTF-8 |
| 不污染外部环境 | `powershell -File` 每次都是独立会话 |
### 完整 patch(基于当前 `main`,git 格式)
```diff
diff --git a/npm-package/scripts/install.ps1 b/npm-package/scripts/install.ps1
--- a/npm-package/scripts/install.ps1
+++ b/npm-package/scripts/install.ps1
@@ -16,6 +16,11 @@
)
$ErrorActionPreference = "Continue"
+# [kb-rag-fix] Windows PowerShell 5.1 的 $OutputEncoding 默认是 ASCII:路径里的非 ASCII 字符
+# (例如中文用户名 C:\Users\<用户>\AppData\Local\Temp)写进原生进程的 stdin 时会变成 "?",
+# 引擎因此拿到非法路径,报 OSError [WinError 123]。这里统一强制 UTF-8(不带 BOM:
+# 引擎用 raw.decode("utf-8") 解析 stdin),覆盖本脚本所有通向 python 的管道。
+$OutputEncoding = New-Object System.Text.UTF8Encoding($false)
$PyProbeModules = @("fitz", "numpy", "faiss", "sentence_transformers", "torch")
$PkgOf = @{ fitz = "PyMuPDF"; faiss = "faiss-cpu"; sentence_transformers = "sentence-transformers"; docx = "python-docx" }
if (-not $Mirror -and $env:PIP_INDEX_URL) { $Mirror = $env:PIP_INDEX_URL }
```
> 补丁本身**没有任何其他改动**,净增 5 行。
> 注意:合并时请保持该文件原有的 **UTF-8 BOM** —— PS 5.1 读无 BOM 的 UTF-8 会按系统 ANSI
> 代码页解析,文件里的中文注释会变乱码并可能报语法错。
---
## 五、实测对照
全部在**全新 `powershell -NoProfile` 子进程**里执行(即 `install.mjs` 真正拉起的那种),
`TEMP`/`TMP` 指向真实中文目录:
| 场景 | 结果 |
|---|---|
| 原版 1.6.2 + 中文临时目录 | 失败,退出码 1
`OSError [WinError 123] ... 'D:\\workspace\\????\\kbrag-smoke-9b9bf473'` |
| **修复版** + 中文临时目录 | 通过,退出码 0
`[OK] engine v3.0.0 自检通过` |
| 原版 1.6.2 + 纯 ASCII 临时目录 | 通过,退出码 0(即当前的临时绕行办法) |
修复版完整输出:
```
[OK] found: python (3.10)
== 3/5 引擎冒烟测试 (kb_engine.py stats)
[OK] engine v3.0.0 自检通过
[OK] embed: BAAI/bge-small-zh-v1.5 已缓存
[OK] rerank: BAAI/bge-reranker-base 已缓存
```
另外已确认无害的部分:`Get-HfCacheRoot`、`USERPROFILE` 这些带中文的路径只参与
PowerShell 内部的 `Test-Path`,不经过管道,不受影响;`scripts/install.sh` 也没有这个问题
(bash 直接把字节写进管道,不做编码转换)。
---
## 六、影响面
**所有用户名含中文(或其他非 ASCII 字符)的 Windows 用户**都会在安装时撞上这一步。
中文 Windows 用户名在国内极其普遍(如 `C:\Users\<用户名>`),
所以实际受影响人群不小。用户侧的临时绕行办法是:
```powershell
$env:TEMP = 'C:\kbragtmp'; $env:TMP = 'C:\kbragtmp'
```
---
## 七、建议顺带补一行 README 排错表
| 报错 / 现象 | 原因 | 处理 |
|---|---|---|
| `WinError 123 ... C:\Users\??\...` | 用户名含非 ASCII 字符时,PowerShell 5.1 的 `$OutputEncoding` 默认是 ASCII,会把路径里的中文写成 `?`(1.6.2 及以前) | 升级到修复版;或临时 `$env:TEMP='C:\kbragtmp'` 后重跑 |
---
## 八、附:文件信息
| 项 | 值 |
|---|---|
| 目标文件 | `npm-package/scripts/install.ps1` |
| 上游 `main` 基线 | 11961 bytes / UTF-8 BOM / LF / 253 行 |
| 修复后 | 12445 bytes / UTF-8 BOM / LF / 258 行 |
| 净增 | 5 行(4 行注释 + 1 行代码) |
| npm 发布版差异 | 发布包内该文件为 **CRLF**(main 为 LF),文本内容一致 |