DSH plugin 手改 profile package.json 后 web 起不来?BOM 排查
如果你刚手工编辑过 profile 的 package.json(比如装完插件调整依赖),随后 dsh web 一启动就报 SyntaxError: Unexpected token ... is not valid JSON、栈里是 readProfileManifest,那几乎可以确定:文件被保存成了带 BOM 的 UTF-8。 内容本身是合法 JSON,多出来的只是开头 3 个字节 EF BB BF;而 readFileSync(path, 'utf8') 会原样保留 BOM,那个 U+FEFF 被一路交给 JSON.parse,解析必然失败。修法是保存为「UTF-8(无 BOM)」,或用官方建议的一行 strip;dsh-plugin-doctor 的 manifest-bom 检查可以在启动前就把它查出来。
DSH plugin 报错现象:dsh web 启动即崩,且改一次犯一次
症状是「启动即崩、报错看不见原因、还反复复发」——这三条合起来基本就锁定了 BOM。 具体表现:
- 启动立刻失败,栈指向
readProfileManifest:在 Windows 上通过源码仓库跑pnpm dsh web(或npx @deepseek-ai/dsh web),启动立即崩:
SyntaxError: Unexpected token ...
is not valid JSON
at JSON.parse (<anonymous>)
at readProfileManifest (packages/boot/app-boot/src/profile.ts:272:23)
at loadProfile (packages/boot/app-boot/src/profile.ts:385:55)
...
Node.js v24.19.0
- 触发动作极其普通:编辑 profile 的
~/.dsh/profiles/<profile>/package.json(例如装完插件手工调整依赖),然后用带 BOM 的 UTF-8 保存——部分 Windows 编辑器默认就会写 BOM——再启动dsh web,立刻报is not valid JSON(#1842)。 - 报错本身不给线索:
Unexpected token后面跟的那个字符在终端里可能显示成问号或乱码,因为U+FEFF不可见。栈只告诉你崩在JSON.parse,不会说「你这个文件开头多了三个字节」。 - 它是复发型故障:有用户明确记录了这个模式——去掉 BOM 后
dsh web能正常启动;但只要再次编辑该文件(改插件依赖、调 profile 配置),如果保存工具又写了 UTF-8 BOM,启动会立刻报同样的is not valid JSON。所以「一次性把文件改干净」不是终点,得同时改掉保存习惯(#1842)。 - 典型 BOM 来源:Windows PowerShell 5.1 的
Set-Content/Out-File -Encoding utf8(会写 BOM);PowerShell 的>重定向;记事本「另存为 → UTF-8 带 BOM」;以及某些编辑器的「以 UTF-8 保存」选项。VS Code 默认无 BOM,所以用 VS Code 的「通过编码保存 → UTF-8」通常就安全(#1842)。
DeepSeek Harness 机制:profile package.json 的 UTF-8 BOM 直达 JSON.parse
链路只有两步,但两步都「什么都没做错」,所以失败点被藏得很深。 逐层拆解:
- BOM 是字节而不是内容:UTF-8 BOM 就是文件开头的
EF BB BF三个字节,解码后是U+FEFF。JSON 规范不允许它在{之前出现。所以带 BOM 的文件在字节层不合法、在编辑器可见层完全正常——这正是它难诊断的原因(#1842)。 readFileSync保留 BOM:packages/boot/app-boot/src/profile.ts的readProfileManifest在第 267 行执行readFileSync(path, 'utf8')。这个调用只负责按 UTF-8 解码,不做 BOM 剥离——BOM 被当作普通字符原样留在字符串开头(#1842)。JSON.parse拒绝U+FEFF:紧接着的第 272 行直接JSON.parse(raw),于是抛出SyntaxError。一句最小复现就能看清边界:
const raw = Buffer.from([0xEF, 0xBB, 0xBF]).toString() + '{"dependencies":{}}'
JSON.parse(raw) // SyntaxError: Unexpected token '?', "?{"dependencies":{}}" is not valid JSON
JSON.parse(raw.replace(/^\uFEFF/, '')) // works
- 同类隐患不止一处:同一个文件里还有若干
JSON.parse(readFileSync(..., 'utf8'))站点(第 227、247、390 行)属于同一类缺陷。这也是为什么更稳妥的做法不是逐点加 strip,而是抽一个会在解析前剥离前导 BOM 的readJsonFile辅助函数,一次覆盖全部读取点(#1842)。 - 为什么「用户侧多加小心」不足以解决问题:因为写 BOM 的是保存工具而不是用户意图——同一台机器上不同工具、不同版本、不同默认编码设置都会影响结果,而且文件会被反复重写。把兼容放在解析侧(parse 前 strip
\uFEFF)才能让这个坑不再复发(#1842)。
DSH plugin 修复:一行 strip 与 manifest-bom 预检
处置分三层:先让宿主在解析前 strip、再给一个能自查的预检、最后把保存习惯钉死。 具体如下:
- 上游正解:解析前去掉前导 BOM(一行):
- raw = readFileSync(path, 'utf8')
+ raw = readFileSync(path, 'utf8').replace(/^\uFEFF/, '')
或者用更直白的形式:
const text = await fs.readFile(path, 'utf8');
const json = text.charCodeAt(0) === 0xFEFF ? text.slice(1) : text;
return JSON.parse(json);
注意要一并覆盖同文件里其他 manifest 读取点(第 227、247、390 行),或直接抽成共享的 readJsonFile。可用的 cherry-pick 分支是 zoahdev/deepseek-harness @ fix/profile-manifest-bom-strip,直接基于 upstream master(47f9438)构建,改动就是 readProfileManifest 在 JSON.parse 前 strip 前导 \uFEFF,并把同样的修复应用到兄弟 manifest 读取点(#1842)。
2. 应急自救:把文件重存为无 BOM 的 UTF-8:编辑器里操作 VS Code 为「右下角编码 → 通过编码保存 → 选择 UTF-8」;命令行则用一段 Node 脚本直接剥掉前三个字节:
node -e "const fs=require('fs');const p=process.argv[1];const b=fs.readFileSync(p);if(b[0]===0xEF&&b[1]===0xBB&&b[2]===0xBF)fs.writeFileSync(p,b.subarray(3))" "$HOME/.dsh/profiles/web/package.json"
改完不需要重启别的什么——重新启动 dsh web 即可。Windows 上 profile 路径通常是 %USERPROFILE%\.dsh\profiles\<profile>\package.json。
3. 别用会写 BOM 的保存方式:Windows PowerShell 5.1 的 Set-Content / Out-File -Encoding utf8 会写入 BOM;要写配置文件请用 Node,或用 PowerShell 7 的 utf8NoBOM 编码(#1842)。
4. 自查 3 个字节:想看文件到底有没有 BOM,就看开头是不是 EF BB BF。在 macOS / Linux 上可以看一眼十六进制开头:
od -An -tx1 -N3 ~/.dsh/profiles/web/package.json
# 带 BOM 会输出: ef bb bf
- 启动前预检:
dsh-plugin-doctor的manifest-bom:dsh-plugin-doctorv1.6.0 起在--profile下新增manifest-bom检查,把「启动后崩」变成「启动前可诊断」:
npx dsh-plugin-doctor --profile ~/.dsh/profiles/web --json
Windows 上端到端验证过:profile package.json 以 EF BB BF 开头 → manifest-bom: FAIL、退出码 2、消息点名 Discussion #1842;干净 manifest → PASS、退出码 0。该版本测试 15/15,新 fixture 覆盖 BOM / 干净 / 缺失三种情况。注意上游那一行 readProfileManifest strip U+FEFF 仍然才是永久修复,这个检查只是让前置条件可诊断(来源)。
6. 插件管理走正门可以减少「手改 manifest」的次数:这个坑最常见的触发路径就是「装完插件后手工改 profile 的 package.json 依赖」。在 DSH Plugin Hub 里完成插件的安装、卸载与更新,可以让依赖维护由工具代劳,只有在确实需要手工调整时才动那个文件——动完之后顺手做一次上面的 manifest-bom 预检(来源)。
7. 给插件作者的一条经验:任何「读 JSON 配置文件」的实现都应该在解析前剥离前导 U+FEFF,尤其是在 Windows 上是主要使用场景的项目。把 BOM 当作需要兼容的合法输入,而不是让用户去猜为什么合法 JSON 会解析失败(来源)。
DSH plugin 排查注意事项
先记住内容是对的、别去翻 JSON 语法——多出来的只是开头 3 个字节,而且只要保存工具还会写 BOM,它就会复发,所以「改文件」和「改保存习惯」必须一起做。 八条要点:
- 内容是合法的,别去翻 JSON 语法:多出来的只是开头 3 个字节。
- 受影响的是整棵插件树:DSH插件 与 DeepSeek插件 的依赖都记在这份 manifest 里,宿主在读到依赖列表之前就崩了,于是所有插件一起加载失败。
- 报错不会点名 BOM:栈只在
JSON.parse/readProfileManifest上,U+FEFF不可见。时间线(刚编辑过 manifest)才是最快线索。 - 会复发:每次重保存都可能再次带 BOM,光修一次文件不够。
- 两条路一起治:保存时选「UTF-8(无 BOM)」+ 宿主在解析前 strip。
- PowerShell 5.1 是常见元凶:
Set-Content/Out-File -Encoding utf8都会写 BOM。 - 同类站点不止一处:同一文件里其他 manifest 读取同样缺 strip,逐点修不如抽公共助手。
- 预检比事后排查便宜:改过 profile manifest 就跑一次
manifest-bom检查。

来源:Discussion #1842、fix/profile-manifest-bom-strip、dsh-plugin-doctor v1.6.0。
常见问题
看不出 BOM 的原因是这个字符不可见——DeepSeek Harness 崩在 JSON.parse 里的 SyntaxError: Unexpected token ... is not valid JSON,而 BOM 是**不可见字符**:终端里那一行甚至可能显示成一个问号或乱码。栈信息只告诉你崩在 readProfileManifest(packages/boot/app-boot/src/profile.ts:272),不会提示文件开头多了 3 个字节 EF BB BF。所以「刚编辑过 profile 的 package.json,然后就起不来了」这条时间线,才是最快的线索(来源:Discussion #1842)。
那份文件在 DSH plugin 眼里**确实是合法 JSON**——多出来的只是开头 3 个字节(EF BB BF),所以崩溃与内容无关,只与字节有关。JSON 规范不允许在 { 之前出现 U+FEFF,而 readFileSync(path, 'utf8') 会**原样保留** BOM(它只在读取时按 utf8 解码,不做 BOM 剥离),于是这个字符被一路交到 JSON.parse。你自己用编辑器看到的内容当然是对的,问题在字节层(来源:Discussion #1842)。
这是 DSH plugin 的**复发型**故障:profile 的 package.json 每次被编辑器或脚本重新保存时都可能再次带上 BOM。典型来源是 Windows PowerShell 5.1 的 Set-Content / Out-File -Encoding utf8(会写 BOM)以及记事本的「UTF-8 带 BOM」另存选项。所以治本要两件事:保存时显式选「UTF-8(无 BOM)」,以及让宿主在 readProfileManifest 里解析前 strip \uFEFF(来源:Discussion #1842)。
可以在启动前查出来:DSH plugin 的 profile 用 dsh-plugin-doctor 就能在启动前验出 BOM。它从 v1.6.0 起在 --profile 下提供 manifest-bom 检查,profile 的 package.json 以 EF BB BF 开头时报 manifest-bom: FAIL 并以退出码 2 结束、消息里直接点名 Discussion #1842;干净文件则 PASS、退出码 0。跑 npx dsh-plugin-doctor --profile ~/.dsh/profiles/web --json 即可,比等启动崩掉再看栈要快得多(来源:dsh-plugin-doctor v1.6.0)。
相关术语
- UTF-8 BOM
- UTF-8 BOM 是文件开头的三字节标记 `EF BB BF`(解码后是 `U+FEFF`)。JSON 规范不允许它在 `{` 之前出现,因此任何直接 `JSON.parse` 整份文件内容的代码都会失败。它通常由 Windows 编辑器或 PowerShell 5.1 的 `utf8` 编码选项写入。— https://github.com/deepseek-ai/deepseek-harness/discussions/1842
- readProfileManifest
- readProfileManifest 是 DeepSeek Harness 在 `packages/boot/app-boot/src/profile.ts` 里读取 profile manifest 的函数。它在 :267 用 `readFileSync(path, 'utf8')` 读文件、在 :272 直接 `JSON.parse`,中间没有剥离 BOM 的步骤,因此带 BOM 的 `package.json` 会让启动立即失败。— https://github.com/deepseek-ai/deepseek-harness/discussions/1842
- manifest-bom preflight(manifest-bom 预检)
- manifest-bom 预检是 `dsh-plugin-doctor` v1.6.0 加入的检查项:按 `--profile` 检查 profile 的 `package.json` 是否以 `EF BB BF` 开头。它把「启动后崩」变成「启动前可诊断」的前置条件检查,是上游一行修复之外的配套手段。— https://github.com/zoahdev/dsh-plugin-doctor/releases/tag/v1.6.0
来源
- deepseek-harness Discussion #1842:profile package.json with UTF-8 BOM crashes dsh web (Unexpected token ... is not valid JSON)· deepseek-ai(GitHub Discussions)
- 修复分支 zoahdev/deepseek-harness @ fix/profile-manifest-bom-strip(基于 upstream master 47f9438)· GitHub(zoahdev)
- dsh-plugin-doctor v1.6.0:新增 manifest-bom 预检(tests 15/15)· GitHub(zoahdev)