# 桌面端系统提示词注入 · 工程规格书 > **给谁看**:把这份文件连同目标应用的信息一起交给 AI,它能据此完成注入;也给人看,用来判断 AI 做得对不对。 > > **给 AI 的读法**:不要读完就动手。先做第 2 章的探针,把八项事实采集齐,再对照第 3 章判断需要打哪几项补丁。**七项补丁是一个事务,不是七个可选步骤。** > > **不含**:任何具体路径、哈希值、版本号。这些必须由你在目标机器上现场采集。 --- ## 0. 这份文档解决什么问题 改一个桌面 AI 应用的系统提示词,表面上是"换一个模板文件",实际上是**七处协同修改**。少打任何一处,要么不生效,要么应用起不来。 新手(和 AI)最常犯的三个错: 1. 以为改完模板就完事,不知道还要重定向路由 2. 打了路由补丁,忘了注入 `variant`,结果三个模式退化成同一个 3. 所有补丁都对,忘了重算校验值,应用闪退,然后误判成"改坏了" 这份文档把七项补丁逐条写成规格:**定位锚点 → 目标状态 → 实现规则 → 验证方法 → 失败症状**。照着做,或者拿着检查别人的做法。 --- ## 1. 路线判断:三种情况,工作量差十倍 动手前先分类。**分类错了,后面全白做。** | 情况 | 判据 | 工作量 | |---|---|---| | **A. 模板在归档外面** | 资源目录下有 `xxx.asar.unpacked/`,且模板文件在里面 | 改文件 + 重启。**不需要任何补丁** | | **B. 模板在归档里,无完整性校验** | 归档内能找到模板,但替换后直接能启动 | 补丁 1–5,共五项 | | **C. 模板在归档里,有完整性校验** | 替换后闪退,或提示文件损坏 | 补丁 1–7,全部七项 | 判断方法: ``` 1. 在资源目录下找 *.tpl / *.njk / *.hbs / *.mustache → 在 unpacked/ 里:情况 A,停在这里 2. 没有 unpacked/,或模板在 asar 里 → 解包 asar 看目录结构 → 找到模板:进情况 B/C 3. 先按 B 打 1–5,启动测试 → 能起:B 完成 → 闪退/报损坏:C,补 6、7 ``` > **不要一上来就全打七项。** 补丁 7(改写可执行文件)风险最高,能不打就不打。 --- ## 2. 探针:动手前必须采集的八项事实 这八项不齐,第 3 章的判断就是猜的。每项都给判定标准。 | # | 事实 | 怎么取 | 合格判定 | |---|---|---|---| | 1 | 应用安装根目录 | 从运行中进程的路径反查,不要猜 | 目录下确实有 `resources/app.asar` | | 2 | 应用版本 | 安装目录的 `package.json` 或清单文件 | 拿到确切版本号,用于备份目录命名 | | 3 | 归档大小与头部长度 | 读 asar 前 8 字节 + 解析头部 JSON | 头部 JSON 能完整解析,不报错 | | 4 | 模板文件在归档内的路径 | 遍历头部 JSON 的目录树,按扩展名筛 | 找到 1–3 个模板文件 | | 5 | 模板路由表位置 | 在入口 JS 里搜模板文件名字符串 | 找到一张 `{kind: '文件名'}` 形式的映射 | | 6 | 渲染上下文构造点 | 搜模板渲染调用,往上看参数组装 | 找到传入模板变量的那个对象字面量 | | 7 | 截断上限常量 | 搜截断函数,看第二个参数 | 找到形如 `10000` 的常量 | | 8 | 是否启用完整性校验 | 先不动,注入后试启动 | 能启动 = 无校验;闪退 = 有校验 | ### 2.1 怎么解析 asar 头部 asar 不是压缩包,是**一个头部 + 顺序排列的文件内容**: ``` 偏移 0 4 字节 固定值 4 偏移 4 4 字节 header 总长度 偏移 8 4 字节 JSON 长度 偏移 12 N 字节 JSON 目录树(每个文件记 offset / size) 然后 ... 文件内容,按 4 字节对齐依次排列 ``` 目录树长这样: ```json { "files": { "main": { "files": { "index.js": { "size": 1234, "offset": "5678" } } } } } ``` `offset` 是相对文件内容区起点的字符串化数字,不是绝对偏移。 **解析成功的标志**:拿到完整 JSON,且每个 `offset + size` 都不超过归档总长。这两个条件不满足,说明你对格式的理解有偏差,先停下,别改。 ### 2.2 怎么找到路由表 在入口 JS 里搜模板文件名(含扩展名)。命中的位置通常长这样: ```js { normal: "xxx-prompt.tpl", "normal-work": "xxx-prompt.tpl", "normal-code": "xxx-craft-code-prompt.tpl", "normal-design": "xxx-craft-design-prompt.tpl" } ``` 看到 `normal` / `-work` / `-code` / `-design` 这类后缀成组出现,就是它。**这张表就是补丁 1 的作用对象。** --- ## 3. 补丁清单:七项,逐条规格 | # | 补丁 | 解决的机制 | 必需性 | 风险 | |---|---|---|---|---| | 1 | 模板路由重定向 | 让多个模式共用同一模板 | B/C 必打 | 低 | | 2 | 渲染上下文注入 `variant` | 让合并后的模板知道当前是哪个模式 | 打了 1 就必打 | 低 | | 3 | 截断上限抬升 | 防止长身份文件被静默截断 | 视身份文件大小 | 低 | | 4 | 模板文件部署 | 内容本体落地 | B/C 必打 | 低 | | 5 | 安装清单同步 | 清单与实际文件数必须一致 | B/C 必打 | 中 | | 6 | 完整性校验值 | 摘要必须与内容匹配 | C 必打 | 中 | | 7 | 启动开关(fuse) | 关闭"是否校验"的编译期开关 | C 且 6 不够时 | **高** | --- ### 补丁 1 · 模板路由重定向 **解决什么**:应用按"模式"选模板,三个模式对应三个文件。要让它们全部指向你的合并模板,否则你只改了一个模式。 **定位锚点**:第 2.2 节找到的那张 `{kind: 文件名}` 映射表。 **目标状态**:四条(或更多)映射的**值**全部指向同一个文件名。 **实现规则**: - 只改字符串字面量,不改表的结构、键名、顺序 - 新文件名长度若与原文件不同,需要整套等长处理(见 6.1) - 改完,归档内不应再有任何代码引用被替换掉的旧模板名 **验证**:`--dry` 应报告"路由 N/N → 你的模板名"。 **失败症状**:注入"成功"但行为完全没变 —— 说明改的是没被调用的那份表,或者被后面的赋值覆盖了。**这是最常见的一次性错误。** --- ### 补丁 2 · 渲染上下文注入 `variant` **解决什么**:合并成一个模板文件后,模板内部要靠 `{% if variant == 'code' %}` 这类判断选分支。没有这个变量,所有分支判断都走 else,三个模式退化成一个。 **定位锚点**:模板渲染调用点,往上看它的参数怎么组装的(形如 `render(template, {...ctx})`)。 **目标状态**:传给模板的上下文对象里,多一个 `variant` 键,值等于当前模式的名字。 **实现规则**: - 在对象字面量里插入 `variant: <当前kind变量>` - 必须能在渲染作用域里访问到 kind 值;访问不到就从路由表的键反查 - 插入位置要在对象闭合之前,语法不能破 **验证**:`--dry` 报告"variant 已注入";行为验证时三个模式的表现应当**不同**。 **失败症状**:三个模式输出完全一致 —— 大概率这个补丁没生效,或者 `variant` 传了空值。模板对未定义变量静默处理,不报错。 --- ### 补丁 3 · 截断上限抬升 **解决什么**:身份类文件(如 `SOUL.md`)注入时有字符上限,超出部分被静默截断,只在末尾加一句提示,不报错。 **定位锚点**:截断函数的调用处,第二个参数通常是一个形如 `10000` 的常量。 **目标状态**:上限抬到能容纳你的文件,例如 `30000`。 **实现规则**: - 只改数字,不动函数逻辑 - 全文件搜一遍同一个数值,确认没有第二处需要同步改(有些实现里上限被引用两次) - 改完核算:你的身份文件字符数 × 1.2 < 新上限 **验证**:`--dry` 报告新的上限值;注入后行为里,身份文件尾部的内容能被复述。 **失败症状**:人格描述"说一半没了"、尾部规则不生效 —— 且**没有任何报错**。这是最难自查的一项,因为失效是静默的。 --- ### 补丁 4 · 模板文件部署 **解决什么**:内容本体要落到应用读取的目录。 **目标状态**:模板目录里存在你的文件,文件名与补丁 1 里写的完全一致。 **实现规则**: - **文件名必须逐字符匹配**补丁 1 的目标值,扩展名也算 - 换行符与原厂保持同一种(通常是 CRLF)。混用会让模板引擎的空白控制符失常,输出偏移 - 保留文件末尾换行 - 原厂模板**不要删**。留着,应用的其他代码路径可能还在引用;删除会导致清单和路由双重不一致 - 模板本身不能含 `{{`、`{%`、`{#` 之外的裸花括号,字符串里出现这些要转义 **验证**:源文件与部署文件 SHA-256 一致;`--dry` 报告"与母本一致"。 **失败症状**:渲染成空串(文件找不到)、启动白屏(语法错误)、输出多/少一行(换行符不对)。 --- ### 补丁 5 · 安装清单同步 **解决什么**:清单文件记录了归档内每个文件的条目。**条目数与实际文件数不一致,应用按清单读取时会读到错位数据。** **定位锚点**:安装目录下的清单 JSON(名字通常含 `manifest` 或 `integrity`),里面有一组文件条目。 **目标状态**:清单条目集合 == 归档内实际文件集合,一个不多一个不少。 **实现规则**: - **新增文件必须入清单**:你的模板如果归档里原来没有,就要加一条,字段格式照抄同目录其他条目 - **删除文件必须出清单**:如果你移除了原厂模板,对应条目要删 - **保留原厂的必须回填**:如果之前用同名占位文件替换过原厂模板,现在要换回真文件并确认条目指向它 - 条目顺序一般不影响,但字段名、类型(数字 vs 字符串)必须完全一致 **验证**:写个脚本遍历归档目录树,与清单条目做集合比对,差集必须为空。 **失败症状**:启动即报"文件损坏"或直接闪退;日志里可能出现条目数不符。 --- ### 补丁 6 · 完整性校验值 **解决什么**:应用启动时计算归档摘要,与记录的摘要比对。内容改了摘要没改 = 判定被篡改 = 拒绝启动。 **定位锚点**:两处,都要查—— - 清单文件里的哈希字段(形如 `sha256` / `integrity` / `checksum`) - 可执行文件内嵌资源(部分实现把摘要编译进 exe) **目标状态**:记录的摘要 == 修改后归档的实际摘要。 **实现规则**: - **顺序不能错**:改完所有内容 → 再算摘要 → 再写摘要。反过来算的是旧内容的摘要,等于没改 - 算法与官方一致,通常是 SHA-256,对整个归档文件计算 - 十六进制大小写、是否带 `sha256:` 前缀,照原值的格式写 - 如果摘要嵌在 exe 里,改写时**长度必须不变**(等长替换),否则会破坏 PE 结构 **验证**:`--dry` 报告"integrity 一致";启动成功。 **失败症状**:双击无反应、闪退无窗口、或弹"应用已损坏"。**这一项失败时用户往往误判成"注入方式错了",白折腾很久。** --- ### 补丁 7 · 启动开关(fuse) **解决什么**:Electron 把"是否校验归档完整性"编译成可执行文件里的一位开关。补丁 6 改不动(或改不全)时,直接把这一位关掉。 **定位锚点**:可执行文件里一段连续的 fuse 字节序列,每一位对应一个开关,顺序固定但**位置随版本变化**。 **目标状态**:关闭"嵌入式归档完整性校验"那一位。 **实现规则**: - **先备份整个可执行文件**。改错一位,应用彻底不启动,只能靠备份救 - 只改目标那一位,其余字节一个不动 - 放在**最后一步**执行:前面所有补丁都验证通过之后再动它 - 改完必须同时确认补丁 6 的摘要已更新(有些实现是"摘要对 + 开关开"双重校验) **验证**:应用能启动且能发消息。 **失败症状**:完全无法启动,任务管理器里进程闪一下就消失。**此时唯一出路是还原可执行文件。** **决策**:先不打。补丁 6 生效就不用打;补丁 6 打了还闪退,再打这一项。 --- ## 4. 通用方法:换一个应用,怎么重新发现补丁点 上面七项是结构,不是坐标。换应用时坐标全变,但发现方法不变: ``` 第 1 步 解包看结构 用 asar 解包工具(或按 2.1 手写解析)导出完整目录树 → 得到:模板在哪、入口 JS 是哪几个 第 2 步 顺藤摸瓜找路由 在入口 JS 里搜模板文件名 → 拿到映射表 → 就是补丁 1 第 3 步 找上下文 搜模板渲染调用 → 往上看参数组装 → 就是补丁 2 第 4 步 找上限 搜截断函数 → 看第二个参数 → 就是补丁 3 第 5 步 看清单 安装目录下找 manifest → 比对条目数与实际文件数 → 就是补丁 5 第 6 步 试启动 先只打 1–5 → 启动 → 闪退再加 6 → 还闪退再加 7 ``` **原则:从外往里改,一层一层验证。** 每加一层,启动测试一次。一次全打完再启动,出问题无法定位是哪一层。 --- ## 5. 执行顺序与原子性 ### 5.1 顺序 ``` ① 完全退出应用(含托盘、helper、GPU/渲染子进程) ② 探针采集(第 2 章八项) ③ 备份 + 演练还原 ④ 补丁 1 路由重定向 ⑤ 补丁 2 variant 注入 ⑥ 补丁 3 上限抬升 ⑦ 补丁 4 模板部署 ⑧ 补丁 5 清单同步 ⑨ 补丁 6 校验值重算 ← 必须在 4–8 全部改完之后 ⑩ --dry 复检(应全绿) ⑪ 启动测试 ⑫ 补丁 7 fuse ← 仅在第 ⑪ 步失败时执行 ``` ### 5.2 原子性:七项是一个事务 脚本必须做到:**任何一步失败,立即回到注入前状态**,不能停在"改了一半"的中间态。 实现方式:动手前把要改的每个文件读成 buffer 存在内存里(归档头部、入口内容、清单、可执行文件),失败时逐字节写回。**不要靠"再跑一次反向操作"来还原**——反向操作本身也可能失败。 ### 5.3 幂等 脚本第二次运行必须识别出"已经是目标状态"并跳过,而不是再改一遍。 判据:拿现状与目标逐项比对,全部相同就打印"无需改动"退出。**重复注入是新手把应用搞坏的第二大原因**(第一是没备份)。 --- ## 6. 三个必须懂的技术点 ### 6.1 等长替换 归档头部记录每个文件的 `offset` 与 `size`。改任何一个文件的内容长度,后面所有文件的偏移全部错位。 两种解法,新手一律用第一种: | 方法 | 做法 | 风险 | |---|---|---| | **等长替换** | 新内容压到与原内容字节数完全相同,短的补空格(`0x20`),长的删注释/压缩 | 低。头部不动 | | 重算偏移 | 重写整个 JSON 目录树,所有 offset 重算 | 高。算错一处全盘崩 | ```js const oldBuf = fs.readFileSync(entryPath); const newBuf = Buffer.from(newSource, 'utf8'); if (newBuf.length > oldBuf.length) throw new Error('超长,先压缩'); // 补 0x20 不是 0x00:补零会破坏 JS 的字符串解析 const out = Buffer.concat([newBuf, Buffer.alloc(oldBuf.length - newBuf.length, 0x20)]); if (out.length !== oldBuf.length) throw new Error('长度不等'); ``` > 等长只解决"偏移不错位",**不解决"摘要不匹配"**。补丁 6 照样要做。 ### 6.2 校验值的计算时机 ``` 改内容 → 算摘要 → 写摘要 ``` 顺序反了就是白改。写摘要时还要注意格式:十六进制大小写、有无前缀、嵌在 exe 里时长度是否与原值一致。 ### 6.3 缓存:为什么"改完没效果" 两个独立缓存层,都要过: | 层 | 位置 | 什么时候失效 | |---|---|---| | 模板引擎缓存 | 进程内存 | 进程退出才失效 | | 系统提示词缓存 | 会话级 | 新会话才重新渲染 | **行为验证的正确姿势:完全退出 → 重启 → 新建会话 → 测试。** 少一步都可能测出假阴性。 --- ## 7. 验证:三层 | 层 | 检查项 | 通过的含义 | |---|---|---| | 静态 | `--dry` 全绿、源文件与部署文件哈希一致、归档头部可解析、清单条目匹配 | 没写坏 | | 启动 | 应用起来、能建会话、能收发消息 | 没把应用弄坏 | | **行为** | 只有新提示词才会出现的行为 | **真的生效了** | 行为验证要挑一个**旧提示词不可能产生**的行为:让它复述你的自定义身份、触发你新加的格式约束、或用你定义的拒绝话术。差异越具体越好。 前两层过、第三层不过 → 99% 是缓存,不是注入失败。 --- ## 8. 回滚 ### 8.1 五步还原 ``` 入口 → 模板 → 清单 → 校验值 → 可执行文件 ``` 五步全做。只还原一部分,比重来还糟——你不知道现在处于哪个状态。 ### 8.2 逐字节校验 ```js if (sha256(restored) !== sha256(original)) throw new Error('还原不完整'); ``` **只比大小是无效校验。** 一个字节的差异就能让应用起不来,而大小完全一样。 ### 8.3 还原救不了的情况 | 场景 | 出路 | |---|---| | 应用已自动更新,备份对应旧版本 | 按新版本重新注入,或重装 | | 归档损坏且没有原始备份 | 重装 | | 可执行文件被改乱且没备份 exe | 重装 | | 备份是"改过的版本"(备份时机错) | 重装 | **备份的完整性决定退路。** 只备归档不备 exe,改 fuse 失败时就没有退路。 --- ## 9. 故障排查 | 症状 | 最可能原因 | 处理 | |---|---|---| | 双击无反应 / 进程闪退 | 补丁 6 没做,或 exe 被改坏 | 还原;先排除 6 再考虑 7 | | 启动报"文件损坏" | 补丁 5 清单与文件不匹配 | 还原清单与归档 | | 白屏 | 补丁 4 模板语法错误 | 还原模板,单独测语法 | | 能启动,行为完全没变 | 补丁 1 改错表;或缓存没清 | 先重启+新会话,再查路由表 | | 三个模式表现一模一样 | 补丁 2 没生效(`variant` 为空) | 查上下文对象 | | 人格/规则"说一半就断" | 补丁 3 上限不够 | 抬上限,核算字符数 | | 输出多一行或少一行 | 补丁 4 换行符不一致 | 统一 CRLF | | 重复跑脚本后应用坏了 | 脚本不幂等 | 还原,补幂等判断 | | 更新后又变回原样 | 归档被覆盖 | 重新注入,纳入升级后例检 | ### 诊断顺序 ``` 1. 还能启动吗 → 不能:立即还原,别试 2. --dry 报什么 → 报错项就是问题 3. 哈希对吗 → 不对:写入环节有问题 4. 缓存清了吗 → 没清:先清再判断 5. 换回原模板能恢复吗 → 能:模板内容问题;不能:注入问题 ``` --- ## 10. 边界 | 场景 | 结果 | |---|---| | 应用更新 | 归档被替换,**七项补丁全部失效**。必须重跑 | | 服务端强制升级 | 本地改动被覆盖 | | 重装 / 换机器 | 全部重做;脚本可复用,备份不可复用(版本与路径都变) | | 应用启动时自检归档 | 除上面七项外还需处理自检逻辑,超出本文范围 | ### 操作纪律 1. 应用运行中不改文件(文件锁 + 签名校验,改一半就崩) 2. 不在安装目录里放自己的东西(更新会清) 3. 一次只加一层补丁,每层启动一次 4. 改完立即验证,别攒到最后 5. 永远保留一份能用的旧模板 ### 合法性 修改自己设备上已安装软件的行为,属本地配置层面。前提:设备属于你或已获授权、不用于规避付费、不分发修改后的程序、遵守许可协议中关于修改的条款。**分发注入后的程序、帮他人绕过授权,性质不同,不在本文范围。** --- ## 11. 一页速查 ``` 路线:模板在 unpacked/ 外? → 改文件,零补丁 → 在 asar 里:先打 1–5,闪退再补 6、7 七项补丁 1 路由重定向 映射表的值 → 同一个模板名 2 variant 注入 渲染上下文加一个键 3 上限抬升 截断常量 1e4 → 3e4 4 模板部署 文件名逐字符匹配,换行符统一,原厂模板别删 5 清单同步 条目数 == 实际文件数 6 校验值 改完内容再算,算法格式照原值 7 fuse 最后一步,先备份 exe,只改一位 事务性:任一失败立即回滚到注入前;二次运行必须幂等 验证三层:静态 → 启动 → 行为(必须新会话) 回滚五步:入口 · 模板 · 清单 · 校验 · exe,逐字节比对 ``` --- *本文描述工作流与补丁规格,不含任何具体路径、哈希、版本号。实际实施前,第 2 章的八项探针必须现场采集。*