DeepSeek Harness 打包后 Office 转 PDF 失败:DSH plugin 长路径越限排查

故障排查发布于 2026-10-03作者: DeepSeek Plugin 插件市场
DeepSeek HarnessDSHWindows桌面端打包LibreOfficeKitMAX_PATH长路径Office 转 PDF
Windows 出桌面端包时,prepared runtime 的 Office 转换能过,packaged runtime 必报 Unknown LibreOfficeKit exception。两份文件逐字节一致,真凶是打包布局把最深资源路径推过 260。本文给 A–F 隔离、Junction 判定与浅目录修法。

在 Windows 上本地出桌面包时,prepared runtime 的 Office 转换能正常通过(DOCX, XLSX, PPTX to PDF passed),但 packaged runtime 转 DOCX 必然失败,抛 OfficeToPdfError: LibreOffice conversion failed / [cause]: ConversionError: LibreOffice native conversion failed: Unknown LibreOfficeKit exception。 这个错误会把你引向「原生包损坏」或「asar 路径传错」两个错误方向——而报告人已经逐一排除:两份 native package 文件数、大小、逐文件 SHA-256 全部相同,传给 helper 的也确实是 app.asar.unpacked 下的真实物理路径(#7485)。真正的变量只有一个:打包布局让 LibreOffice 自身最深层的资源文件路径越过了 Windows 的 MAX_PATH(260);把同一份 engine 用 Junction 映射到 C:\lo-test 后,一切都恢复正常。本文按「先分诊 → A–F 收敛变量 → 排除三项 → 定位到最深文件 → Junction 判定 → 三种修法」展开,并在最后说明为什么这个缺陷只在开发者机器上出现。

先分诊:为什么这个错误信息会把你带偏

一句话:DSH 层面把一切未知的底层转换错误折叠成了同一个 'failed',Unknown LibreOfficeKit exception 正好落进那一支,原始信息在映射时就丢了——所以从错误文本反推真因是徒劳的,必须靠外部量(这里是路径长度)。

映射点在 packages/document/office-to-pdf/src/index.ts:265-271:已知 code 原样抛出,未知一律折成 'failed'。于是:

  1. 你看到的顶层错误是 OfficeToPdfError,它只告诉你「转换失败」;
  2. [cause] 里的 ConversionError: … Unknown LibreOfficeKit exception 是 native helper 给出的兜底类型,不含具体失败对象;
  3. 失败位置 @deepseek-ai/libreoffice-kit runNative() 只说明「在原生转换这一步」,不说明是哪条资源路径没打开。

所以正确的排查姿势不是盯着这段文本,而是把它当作「未知原因」处理,然后用外部量把候选空间砍掉。这也顺带说明:如果 code 映射那一支能带上底层异常的类型或文本,这类报告会好查很多(这是改进建议,不是本文结论)。

定位:A–F 隔离、排除误判与 Junction 实验

第一步:用 A–F 隔离把变量收敛到「dsh root」

报告人最有价值的一手,是做了六组交叉组合,把决定成败的变量压到了一个上:dsh root 是 prepared 还是 packaged。

环境与命令:

text
OS: Windows
tag: dsh-v0.1.7-alpha.1
desktop target: win32-x64
Electron: 44.0.0
electron-builder: 26.15.3
bundled Node: 24.18.1
bundled pnpm: 11.7.0
源码目录: C:\PROJETS-PERSO\deepseek-harness

pnpm.cmd run package:desktop:win:x64:unsigned

六组结果:

组合dsh rootElectronruntime结果
ApreparedpreparedpreparedPASS
BpackagedpackagedpackagedFAIL
CpackagedpreparedpackagedFAIL
DpreparedpackagedpreparedPASS
EpackagedpreparedpreparedFAIL
FpreparedpreparedpackagedPASS

规律非常干净:只要 dsh root 是 packaged 就失败,是 prepared 就通过。于是可以一次性排除两个变量:

  • Electron executable 不是决定因素(A/C 对照、D/B 对照都不改变结论);
  • bundled runtime 不是决定因素(A/F 都不改变结论)。

这一步的意义在于:它把「猜打包哪里不一样」变成「dsh root 这一层一定有个物理差异」。

第二步:排除三项最常见误判

在往「路径长度」上走之前,先把三条更朴素的假设明确判死,否则结论会一直被人质疑。

1. native package 内容差异 —— 排除

逐项比较 prepared 与 packaged 两处的 engine:

text
Prepared: …\targets\win-x64\dsh\node_modules\@deepseek-ai\libreoffice-kit-win32-x64
Packaged: …\unsigned-artifacts\win-unpacked\resources\app.asar.unpacked\dsh\node_modules\@deepseek-ai\libreoffice-kit-win32-x64

file count identical
file size identical
per-file SHA-256 identical

因此「electron-builder 漏文件」「打包改坏了 native package」「包内容损坏」三种解释全部出局。

2. app.asar 虚拟路径被直接传给 native helper —— 排除

原本怀疑 require.resolve() 在 Electron 里返回了 …\resources\app.asar\dsh\node_modules\… 这种虚拟路径。实际打印出来的是:

text
executable =
…\unsigned-artifacts\win-unpacked\resources\app.asar.unpacked\dsh\node_modules\@deepseek-ai\libreoffice-kit-win32-x64\bin\libreoffice-kit.exe

programDirectory =
…\unsigned-artifacts\win-unpacked\resources\app.asar.unpacked\dsh\node_modules\@deepseek-ai\libreoffice-kit-win32-x64\program\program

root =
…\unsigned-artifacts\win-unpacked\resources\app.asar.unpacked\dsh\node_modules\@deepseek-ai\libreoffice-kit-win32-x64

已经是 app.asar.unpacked 对应的真实物理路径,不是 app.asar 虚拟路径。而且 libreoffice-kit.exe 本身能正常启动——所以也不是「可执行文件路径访问不到」。

3. Electron / bundled runtime —— A–F 已排除

见上一节的对照表,不再重复。

第三步:真正越限的是「树内最深的那个文件」,不是 engine 根

这里是整条排查里最容易出错的一步:按 engine 根算长度,两份布局都不超过 260,会让你得出「与 MAX_PATH 无关」的错误结论;必须递归统计树内每个文件的绝对路径。

先看按每个文件递归统计的结果(同一份 win-x64 构建,源码根 D:\WorksPlace\deepseek-harness = 30 字符):

目录树文件数最长路径>260 的文件
prepared(targets\win-x64\dsh\node_modules\…)20502150
packaged(…\unsigned-artifacts\win-unpacked\resources\app.asar.unpacked\…)2050275385

最深的一条是:

text
…\program\share\config\soffice.cfg\modules\simpress\popupmenu\pagepanecanvasmaster.xml

所以不需要引入「被转换文档的路径」或「临时/profile 目录」来解释——打包布局让 LibreOffice 自身的资源文件越过了阈值。报告人提出的判定实验(量输入/输出文档路径)因此已有答案:越限的是 runNative() 拿到的 programDirectory 之下的资源路径,而不是文档路径。

两处测量的口径要对齐

原始报告给的是 prepared ~218 / packaged ~278,且说 >= 260 的文件约 706 个。第二位复现者的源码根短 3 个字符,得到 215 / 275、>260 计 385 个。对照关系是:

text
215 + 3 = 218
275 + 3 = 278
差 60 字符 = unsigned-artifacts\win-unpacked\resources\app.asar.unpacked\

两条完全对齐,说明量的是同一个量,只是 >=260 与 >260 的计数口径不同。 这也是为什么报告里务必写清「比较运算符」和「量的哪一层」——它们能决定结论是「无关」还是「就是它」。

第四步:Junction 判定实验——同一份 engine,只换路径

决定性的 A/B 做法:不改 packaged engine 的任何一个文件,只用 Windows Junction 把它映射到短路径,其余条件全部保持不变。

powershell
New-Item -ItemType Junction `
  -Path 'C:\lo-test' `
  -Target 'C:\PROJETS-PERSO\deepseek-harness\apps\desktop\.desktop-build\targets\win-x64\unsigned-artifacts\win-unpacked\resources\app.asar.unpacked\dsh\node_modules\@deepseek-ai\libreoffice-kit-win32-x64'

然后让 runtime 使用:

text
executable:       C:\lo-test\bin\libreoffice-kit.exe
programDirectory: C:\lo-test\program\program

保持不变的量:

text
same packaged engine
same files
same SHA-256
same runtime
same Electron
same smoke test
same input/output/profile logic

结果:

text
Long physical path -> FAIL
C:\lo-test         -> PASS
desktop runtime: DOCX, XLSX, PPTX to PDF passed

为什么 Junction 有效? 因为 MAX_PATH 约束的是每个文件的完整路径,短路径 Junction 等于把每一个文件的公共前缀一起变短——对最深那个文件同样生效。所以「Junction 能过」与「这是路径长度问题」并不矛盾,反而是同一件事的两面。

机制:长路径的两个条件与一个很容易踩的坑

长路径需要两个条件:开注册表不够

一句话:Windows 上突破 MAX_PATH 要同时满足 LongPathsEnabled=1 与进程清单声明 longPathAware;后者缺失时注册表形同虚设,所以「让用户开长路径」不是可行的修法。

  • 在 apps/desktop 全目录搜 longPathAware 与 LongPathsEnabled —— 无命中。
  • 第二位复现者的机器上 HKLM\SYSTEM\CurrentControlSet\Control\FileSystem\LongPathsEnabled = 1,打包后的 smoke 依然失败。

两者合起来正好解释:即使运行环境开了注册表,打包出来的进程仍按 MAX_PATH 行为走。LibreOffice 及其依赖组件未必全部使用支持 long path 的 Windows API,所以这条路从一开始就不该指望。

一个必须避开的坑:Junction 做在仓库根上无效

这一条是复现者踩过并写进结论的,照搬错方向会浪费大量时间:

  • 无效用法:把仓库根做成 Junction,再用短路径调用构建 —— 失败,错误信息里的路径仍是展开后的长路径。原因是 Node 的 realpathSync()(以及 import.meta.url)会把 junction 展开回真实路径。
  • 有效用法:把 Junction 做在 engine 目录上,且真正传给 helper 的就是那个短路径字符串(C:\lo-test\bin\libreoffice-kit.exe)。

结论一句话:起决定作用的是「实际交给原生进程的那个路径字符串」,而不是文件系统里存不存在一个短路径别名。

修法:浅目录 / 缩短源码根 / 打包前 preflight 三条路径

修法一(框架侧,推荐):把 native engine 放到浅目录

一句话:把 LibreOfficeKit 的物理位置从 resources\app.asar.unpacked\dsh\node_modules\@deepseek-ai\libreoffice-kit-win32-x64 挪到 resources\lok 这类浅目录,构建期与运行期同时获得余量。

目标结构:

text
resources\
  lok\
    bin\
    program\

而不是:

text
resources\
  app.asar.unpacked\
    dsh\
      node_modules\
        @deepseek-ai\
          libreoffice-kit-win32-x64\

然后让 @deepseek-ai/libreoffice-kit 或 Desktop runtime 显式使用这个 native engine 的物理路径。

为什么浅目录是「有效且充分」的——三段路径预算

按实测:engine 根 190 字符、最深文件 275、相对深度 85 字符。代入三种情形:

text
当前安装版      resources(69) + 75 + 85        = 229   < 260   ✅  本来就安全
改 resources\lok 后安装版   73 + 85            = 158   < 260   ✅
改 resources\lok 后构建期   114 + 14 + 85      = 213   < 260   ✅

这张表同时解释了一个反直觉的事实:当前安装后的布局本来就是安全的(229),失败只发生在构建期那个更深的输出目录上(275)。所以这个缺陷只在开发者的机器上出现,终端用户从未遇到——浅目录改造的价值是让两条路径都具备余量,而不只是救某一个已经出事的产品。

修法二(不改代码,当下可用):缩短源码根

一句话:把整个仓库复制到短路径下重新跑完整流水线,实测即可通过——代价是你要接受一个很短的源码根。

复现者把仓库放到 D:\dsh(6 字符),重跑完整打包流水线:

text
源码根 D:\dsh  (6 字符)
  → 最长文件路径 251,>260 的文件 0 个
  → 两次 smoke 全部通过(prepare 阶段 + packaged 阶段)
  → 产出安装器,并成功安装、启动、运行

由此得到一个可操作的约束:

text
win-x64 unsigned 打包要求源码根路径 ≲ 15 字符(最长后缀约 245,260 − 245)

这条约束还有一层来源差异值得注意:正式发布路径用 artifacts/,本地 unsigned 测试用 unsigned-artifacts/(长 6 个字符)。两者预算不同且未在任何地方说明——这意味着该问题可能在本地测试时暴露、在正式发布时恰好被掩盖。

修法三(护栏):打包前做路径预算 preflight

一句话:在 package-target.ts 的准备阶段之前先算一遍最深资源文件的绝对路径长度,超限就在耗时工作开始前失败,并直接告诉你「需要多短的源码根」。

这个护栏要解决的是排查成本:现在的情况是跑了二十分钟,最后抛出一条指向 LibreOffice 的错误,而真因在路径长度上。preflight 只做一件事——把「20 分钟后一条误导性错误」换成「一开始就说清差多少」。它同时也是修法一二都没做时的临时保险。

两个流程观察:为什么这个约束一直没人发现

这两条不是技术机制,但解释了「为什么它至今还在这里」:

  1. 桌面打包未接入 CI。 核对 .github/workflows/ 下全部 19 个 workflow,没有任何 desktop job——这条路径没有 CI 信号。
  2. 约束未被记录。 在 apps/desktop/、docs/、.agents/notes/ 搜索 MAX_PATH / 260 / long path 等关键词,无命中。

因此「本地 unsigned 打包对源码根长度有硬约束」这件事,既没有自动化检查、也没有文字记录,只能靠人踩坑发现。把约束写进文档、把检查接进 CI,和修代码同等重要。

排查注意事项

  1. 先看错误是不是被折叠过的。 OfficeToPdfError 把未知 code 一律折成 'failed',遇到 Unknown LibreOfficeKit exception 就别再从文本反推,改用外部量排查。
  2. 做隔离测试时一次只动一个变量。 A–F 六组把 dsh root / Electron / runtime 三个变量穷举清楚,才有资格说「决定因素是 X」。
  3. 比较两份产物先比内容,再比布局。 SHA-256 全同就直接排除「打包改坏文件」,把注意力转到物理路径上。
  4. 量长度要量对那一层。 engine 根可能都在 260 以内,越限的往往是树内最深的那个资源文件(如 program\share\config\...)。写报告时把「按什么量」「用 > 还是 >=」都写明。
  5. 报告里给出「超出多少」。 只给计数只能让人接受「有问题」,给出 276 (+16) 这样的数字才能判断是「差一点」还是「差很多」。
  6. 别指望打开 Windows 长路径开关。 它需要 LongPathsEnabled=1 加 进程清单 longPathAware 两个条件;仓库内搜不到后者,实测开注册表也无效。
  7. Junction 要用在正确的位置。 做在仓库根上会被 realpathSync / import.meta.url 展开而失效;做在 engine 目录并让 runtime 显式使用短路径字符串才有效。
  8. 记住决定性的是「交给原生进程的路径字符串」,不是别名是否存在。 这条决定了所有绕行方案的写法。
  9. 区分构建期与运行期预算。 当前安装后布局(229)本就安全,构建期输出目录(275)才越限——修法要能同时覆盖两者。
  10. 对短根约束做文档与 CI 双保险。 本地 unsigned-artifacts 比正式 artifacts 长 6 个字符,预算不同且未说明,极易出现「本地炸、正式过」或反过来。

这条报告最值得带走的不是「LibreOffice 有长路径 bug」,而是两套测量口径的差别:按 engine 根量,两份布局都不越限、结论会是「与 MAX_PATH 无关」;按树内最深文件量,才看到 385 个文件越限、真相才浮出来。 如果你也在 Windows 上做桌面端二次分发或本地出包,建议现在就做两件事:把源码根换成短路径(D:\dsh 这类,实测可过)以避免被这条约束浪费半天,以及在打包脚本里加一个路径长度 preflight,让下一次这类问题在开始构建时就报出来,而不是二十分钟后伪装成一条 LibreOffice 错误。把插件装卸、更新确认与系统日志集中到 DSH Plugin Hub 里,本地出包这条链上的环境问题会更好对照。

DSH Plugin Hub · 确认安装

来源:Discussion #7485。

常见问题

为什么 prepared runtime 能过、packaged runtime 必失败,两者不是同一份 LibreOffice 吗?

是同一份,而且逐字节一致——报告人比较过两处的 libreoffice-kit-win32-x64,文件数、文件大小、每个文件的 SHA-256 完全相同。变的是**物理绝对路径**:packaged 布局多出 unsigned-artifacts\win-unpacked\resources\app.asar.unpacked\ 这一段,比 prepared 深 60 个字符。LibreOfficeKit 内部访问它自己的 program\share\... 资源时,最深那个文件因此越过了 260 的 Windows 传统路径上限。

我在系统里已经打开长路径支持了,为什么还是失败?

因为 Windows 长路径要**同时**满足两个条件:注册表 HKLM\SYSTEM\CurrentControlSet\Control\FileSystem\LongPathsEnabled = 1,**以及进程清单里声明 longPathAware**。后者缺失时,注册表开了对相关进程也不生效。报告人实测在 LongPathsEnabled=1 的机器上,打包后的 smoke 依然失败;在仓库里搜 longPathAware 也搜不到。所以「让用户去开注册表」不是可行的修复方向。

听说把仓库根目录做成 Junction 指向短路径可以绕开,为什么我试了没用?

因为 Node 的 realpathSync() 与 import.meta.url 会把 Junction 展开回真实路径,于是交给构建的还是长路径。有效的是另一种用法:**把 Junction 做在 LibreOffice engine 目录上**,并让 runtime 显式使用短路径字符串(C:\lo-test\bin\libreoffice-kit.exe)。起决定作用的是**实际交给原生进程的那个路径字符串**,而不是文件系统里是否存在一个短路径别名。

这个缺陷正式发布的安装包会中招吗?

基本不会。按实测数字反推:当前安装后的布局深度约 229,本来就在 260 以内;失败只发生在**构建期**那个更深的输出目录上(约 275)。这也解释了为什么这条报告一直出自开发者的机器、终端用户从未复现。修复的价值在于让构建期和运行期**同时**留出余量,而不是救一个已经出事的产品。

官方改了吗?不改代码我能怎么办?

按该讨论的进展,浅目录方案属于建议方向,尚未见到已合并的改动。不改代码的当下做法是把源码根换到短路径上重跑(实测 D:\dsh 这个 6 字符的根能让最长文件路径降到 251、超限文件归零,两次 smoke 全过、安装器产出并成功安装启动)。换算下来,win-x64 unsigned 打包大约要求源码根 ≲ 15 字符。

相关术语

MAX_PATH
Windows 传统的 260 字符路径上限(完全限定路径)。它约束的是**每一个文件的完整路径**,而不是某个目录根的路径——这决定了「量哪一层的长度」这件事本身就能决定结论对错:按 engine 根量往往都在限内,按树内最深文件量才会暴露越限。— https://github.com/deepseek-ai/deepseek-harness/discussions/7485
longPathAware(进程清单声明)
Windows 允许进程突破 MAX_PATH 的第二个前提条件。仅在注册表里打开 `LongPathsEnabled=1` 不够:可执行文件的应用清单还必须声明该进程是长路径感知的。仓库内搜索 `longPathAware` / `LongPathsEnabled` 均无命中,这正是「开注册表也无效」的原因。— https://github.com/deepseek-ai/deepseek-harness/discussions/7485
prepared / packaged(两种桌面产物布局)
prepared 是构建期直接落盘的 runtime 目录(`targets\win-x64\dsh\node_modules\...`);packaged 是 electron-builder 输出的解包目录(`...\unsigned-artifacts\win-unpacked\resources\app.asar.unpacked\dsh\node_modules\...`)。两者内容一致,但后者多一层 `unsigned-artifacts\win-unpacked\resources\app.asar.unpacked\`,即 60 个字符的物理路径深度差。— https://github.com/deepseek-ai/deepseek-harness/discussions/7485
路径预算 preflight(建议护栏)
在 `package-target.ts` 的准备阶段之前,先按目标布局计算最深资源文件的绝对路径长度,超限时**在耗时工作开始前**就失败,并给出所需的最短源码根长度。目的是把「20 分钟后抛出一条指向 LibreOffice 的错误」换成「一开始就说清楚差多少」。— https://github.com/deepseek-ai/deepseek-harness/discussions/7485

来源