# 培训版 Bun 单文件打包方案 面向一次线下培训:交付一个可双击运行的工作台程序,现场接真实企微监听,用浏览器打开界面,同时让核心 Node 逻辑不再以源码形式散落在培训机上。 本文既是方案说明,也是打包操作手册。打包侧的代码准备与 bun 安装均已完成。 > **执行状态:技术链路已通,培训交付仍暂缓。** session-first 架构已回流本仓库(开关 `QIWEI_AGENT_CONVERSATION_MODE=session`,培训入口默认打开)。现场交付仍卡 Fmode 登录席位。详见 `docs/specs/session-first-port-plan.md`。 ## 1. 目标与边界 | 项目 | 结论 | | --- | --- | | 交付形态 | 一个文件夹:`qiwei-workbench.exe` + 少量必须落盘的资产 | | 界面 | 浏览器打开 `http://127.0.0.1:4320/#agent`,不做 Electron 桌面壳 | | 企微监听 | 现场真实监听,个人版本地轮询 | | 代码保护 | 服务端 Node 逻辑进二进制;前端与知识库仍明文 | | 正式发布 | 不受影响,`npx fmode-qiwei workspace` 与 MCP 技能包保持原样 | 明确不在本次范围:语音克隆、企业微信官方 CLI、MCP 技能安装、Windows 开机自启计划任务。 ### 保护强度的诚实说明 `bun build --compile` 把 JS 与运行时打进一个可执行文件,**它是提高阅读门槛,不是加密**。字符串、提示词、接口路径仍可从二进制中提取。本方案能达到的效果是:培训机上没有可以直接打开、复制、改一改就能跑的 `mcp/src/core/*.js`。要做到真正的机密保护,需要把核心逻辑上收到服务端,那是另一条路线。 **切到 session-first 架构后,这层保护的收益会进一步下降。** 厚闸门时代值钱的资产在代码里(上千处正则、十个改稿点),编译能藏住;薄路径把这些拆掉后,能力转移到 `knowledge/` 话术、十几行 session 提示词和很薄的红线,而 `knowledge/` 必须明文落盘——模型要用 `Read`/`Glob`/`Grep` 去翻它。也就是说,**最值钱的话术资产在新架构下天然无法被编译保护**。打包仍能挡住「拷走源码改一改就跑」,但别指望它保护话术。 ## 2. 交付物结构 ```text qiwei-training/ ├── qiwei-workbench.exe 工作台 + 监听 Runtime + 好友轮询(三合一) ├── web/ 前端资源 index.html / app.js / styles.css ├── knowledge/ 客服知识库,现场可编辑 ├── qiwei.runtime.config.mjs 轮询配置 ├── .env.local 本机凭据模板,现场填写 ├── README.md 现场操作手册 └── outputs/ 首次启动后生成:SQLite、登录态、消息、日志 ``` ### 为什么这些必须留在磁盘上 | 资产 | 原因 | | --- | --- | | `web/` | 浏览器要直接加载,打进二进制也一样能在 DevTools 看到,外置反而降低打包风险 | | `knowledge/` | Agent 按文件切块检索,且现场需要临时改话术 | | `.env.local` | 凭据绝不能编译进程序,否则等于把 token 随包分发 | | `qiwei.runtime.config.mjs` | 现场可能要调轮询间隔 | | `outputs/` | 运行数据,含真实客户会话 | `mcp/catalog/qiwei-endpoints.json`(104 接口清单)走内联,不落盘。 ## 3. 运行架构:一个可执行文件,三种角色 原有设计是工作台与监听分两个进程,这样刷新页面不会中断监听。培训包保留这个结构,只是两个进程变成同一个 exe 的不同子命令。 ```text qiwei-workbench.exe 起工作台 → 拉起监听 → 打开浏览器 qiwei-workbench.exe runtime start 监听 Runtime(由上面自动拉起) qiwei-workbench.exe runtime status 查看监听状态 qiwei-workbench.exe runtime stop 停止监听 qiwei-workbench.exe worker friend 好友轮询 Worker(由 Runtime 自动拉起) qiwei-workbench.exe dashboard 只起工作台,不拉监听 ``` 编译后 `process.execPath` 指向 exe 自身,无法再当 node 解释器执行 `.js` 脚本。因此所有原先 `spawn(process.execPath, [某个脚本])` 的位置,都改为经统一启动器决定用「node + 脚本」还是「exe + 子命令」。 ## 4. 已完成的代码准备 以下改动已全部落地,且在 Node 路径下验证零回归。 ### 4.1 路径根可注入 `mcp/src/core/runtime-context.js` 的 `PACKAGE_ROOT` 支持 `QIWEI_PACKAGE_ROOT` 覆盖。编译后 `__dirname` 指向只读虚拟路径,由启动入口把包根指到 exe 所在目录。`runtime/callback-service/src/config-loader.mjs` 同步对齐。 `mcp/src/core/agent-session-guide.js` 不再自行推导包根,改为复用统一来源。 ### 4.2 子进程启动收口 新增 `mcp/src/core/runtime-launcher.js`,根据 `QIWEI_RUNTIME_LAUNCHER=self` 决定启动方式。以下四处已接入: - `mcp/src/core/listener-runtime-control.js` — 工作台启停监听 - `mcp/src/core/relay-daemon.js` — 守护进程与 Windows 计划任务命令 - `scripts/start-dashboard.js` — 启动时拉起 Runtime - `runtime/callback-service/src/personal-polling.mjs` — 好友轮询 Worker ### 4.3 动态 require 改静态(打包必需) Bun 的打包器无法分析 `require(path.join(PACKAGE_ROOT, ...))` 这种运行时拼路径。三个文件已改为静态相对路径,Node 下解析结果完全等价: - `runtime/callback-service/src/runtime-state.mjs` - `runtime/callback-service/src/processor-bridge.mjs` - `runtime/callback-service/src/config-loader.mjs` ### 4.4 静态资源解析 - `mcp/src/dashboard/server.js` 的静态目录支持 `QIWEI_DASHBOARD_STATIC_DIR`,编译后指向 exe 同级 `web/` - `mcp/src/core/api-catalog.js` 改为「磁盘优先、内联兜底」,源码运行读文件,编译后用内联 JSON ### 4.5 语音依赖惰性化(关键修复) `@ffmpeg-installer/ffmpeg` 在找不到平台二进制时会在 `require` 阶段直接抛错,而 `@binsee/wx-voice` 依赖它。原先三处顶层引入,意味着编译后**工作台会直接起不来**。已改为用到语音时才加载: - `mcp/src/core/voice-clone-service.js` - `mcp/src/core/inbound-media.js` - `mcp/src/dashboard/agent-service.js` 改动后,未部署语音组件的环境仍可正常收发文字与图片消息。 ### 4.6 新增入口与构建脚本 - `bin/qiwei-training.js` — 培训入口,负责注入环境、分发子命令、拉起监听、打开浏览器 - `scripts/build-training-package.mjs` — 前置检查、编译、复制资产、生成现场手册 - `package.json` 新增 `training:check` / `training:build` / `training:start` ## 5. 打包前置条件 | 条件 | 状态 | | --- | --- | | Node ≥ 22.5 | 满足(当前 v24.9.0) | | `node_modules` 已安装 | 满足 | | 培训入口与构建脚本 | 已就绪 | | 前端资源与知识库 | 已就绪 | | 代码改造与回归验证 | 已完成 | | bun 已安装 | 满足(1.3.14,装于 `D:\bun`) | | **session-first 架构已回流本仓库** | **未满足,见 `session-first-port-plan.md`** | | **培训机已装 Claude Code 并验证可调用** | **待确认,新架构下为硬前置** | 打包工具链本身已就绪,`npm run training:check` 通过。但后两项属于交付内容的前提,不在该命令的检查范围内,需人工确认。 若需在其他机器重装 bun,指定安装盘符: ```powershell $env:BUN_INSTALL='D:\bun'; irm bun.sh/install.ps1 | iex ``` 构建脚本除 `PATH` 外还会探测 `BUN_INSTALL`、`~/.bun/bin`、`D:\bun\bin`、`C:\bun\bin`,因此刚装完不必重启终端。 ```powershell npm run training:check ``` 该命令会逐项校验上表并给出结论。 ## 6. 打包执行 前置检查通过后,一条命令完成: ```powershell npm run training:build ``` 脚本会依次执行:清空 `dist/qiwei-training/` → `bun build --compile` 生成 exe → 复制 `web/`、`knowledge/`、配置模板 → 生成 `.env.local` 模板与现场 `README.md` → 打印产物体积。 可选参数: ```powershell node scripts/build-training-package.mjs --outdir dist/my-package node scripts/build-training-package.mjs --target bun-windows-x64 node scripts/build-training-package.mjs --bundle-optional # 尝试把语音依赖也打进去 ``` 默认把 `@ffmpeg-installer/ffmpeg`、`@ffprobe-installer/ffprobe`、`@binsee/wx-voice` 外置,因为培训不演示语音,且它们携带平台二进制。 ## 7. 打包后验收 在打包机先自测一遍,再拷到培训机。 **冒烟验收** 1. `cd dist/qiwei-training && .\qiwei-workbench.exe --no-open`,确认进程不退出、无异常堆栈 2. 浏览器访问 `http://127.0.0.1:4320/#agent`,页面正常渲染(验证 `web/` 外置生效) 3. 访问 `http://127.0.0.1:4320/api/health`,返回 JSON 4. 另开终端 `.\qiwei-workbench.exe runtime status`,`alive` 为 `true`(验证 exe 自举成功) 5. 确认 `outputs/` 下已生成 `runtime/qiwei-runtime.json` 与 SQLite 三件套(`.db` / `.db-wal` / `.db-shm`) 6. `.\qiwei-workbench.exe runtime stop`,状态转为 stopped **现场全链路验收** 7. 填写 `.env.local`,页面完成企微扫码登录,账号显示在线 8. 配置测试白名单,开启 AI 监听 9. 用另一台手机发文字消息,确认进入工作台会话列表 10. 发一张图片,确认显示为图片消息且不触发自动回复 第 5 步是本方案最关键的技术闸门,详见下节 G1。 ## 8. 风险闸门与应对 | 编号 | 风险 | 判断方式 | 应对 | | --- | --- | --- | --- | | G1 | Bun 没有 `node:sqlite` 内置模块 | 2026-08-20 实测:首次启动即报 `No such built-in module: node:sqlite` | **已绕过,不是死路。** 能力用 `scripts/sqlite-engine-probe.mjs` 在 Node 与 Bun 上各跑 16/16:WAL、busy_timeout、CHECK、外键、显式事务、upsert、只读模式全部对等。生产代码经 `mcp/src/core/sqlite-engine.js` 适配:Node 仍走 `DatabaseSync`,编译产物走 `bun:sqlite`。`.get()` 未命中时 Node 返回 `undefined`、Bun 返回 `null`,本仓库调用点全是 `?.` / `\|\| null`,不受影响 | | G2 | 打包器无法解析某个依赖 | `bun build` 直接报错 | 加 `--external` 并确认该依赖不在启动路径上 | | G3 | 编译后仍有模块在启动阶段读取 `node_modules` | 验收第 1 步崩溃 | 按 4.5 的方式改为惰性加载 | | G4 | exe 自举失败,监听拉不起来 | 验收第 4 步 `alive` 为 false | 检查 `QIWEI_RUNTIME_LAUNCHER` 是否为 `self`,查看 `outputs/runtime/runtime.stderr.log` | | G5 | 4320 端口被占用 | 启动报端口冲突 | `--port 4321` | | G6 | 培训机缺少 Claude Code | 薄路径下等于零 Agent 能力,演示直接失败 | **硬前置**,必须提前装好并验证可调用,无替代方案 | | G7 | Windows 的 `PATH` 缺少 `System32`,导致任何以裸名调用 `cmd.exe` 的子进程 ENOENT | 打包机已实测命中:浏览器打不开、外部命令探测全部失败 | 培训入口与构建脚本已改用 `process.env.ComSpec` 定位 `cmd.exe`。若培训机仍异常,用 `--no-open` 启动后手动打开浏览器 | | G8 | ESM 里用 `createRequire(import.meta.url)` 加载 CJS,bundler 不跟踪运行时解析;编译后 `import.meta.url` 指向虚拟路径 | 2026-08-20 实测:Dashboard 能起,Runtime 立刻崩,`runtime.stderr.log` 报 `Cannot find module '../../../mcp/src/core/runtime-context.js' from 'B:\~BUN\root\...'` | **已修复。** `runtime/callback-service/src/` 下六处全部改为 ESM 静态 import。以后凡跨 ESM/CJS 边界加载本仓库模块,禁止 `createRequire` + 相对路径 | 关于 G6:自动回复由 `agent-runtime` 调用本机 `claude.exe` 完成,不在 exe 内。厚闸门时代缺它只是降级——消息仍进工作台,只是不生成草稿。但 session-first 架构把整个回复能力都交给了模型侧,缺它就是**完全没有 Agent**,现场只剩一个消息收件箱。这条从「可接受的降级」变成了「必须满足的硬前置」。 ## 9. 回滚 所有改动都是「Node 路径行为不变、编译路径新增分支」的形式,不需要专门回滚: - 不设置 `QIWEI_RUNTIME_LAUNCHER` 时,所有子进程仍按原方式用 node 启动脚本 - 不设置 `QIWEI_PACKAGE_ROOT` / `QIWEI_DASHBOARD_STATIC_DIR` 时,路径解析与改动前一致 - 培训包产物在 `dist/`,已被 git 忽略,不影响正式发布 若要完全撤回,只需还原第 4 节列出的文件,并删除 `bin/qiwei-training.js`、`mcp/src/core/runtime-launcher.js`、`scripts/build-training-package.mjs` 及三条 npm 脚本。 ## 10. 回归基线 改动前后跑同一组 smoke,结果完全一致,未引入新失败。 通过:`product-mode`、`message-archive`、`session-reply-delivery`、`agent-inbound-media`、`agent-knowledge`、`agent-response-human-style`、`agent-intake-policy`、`agent-response-quality`、`agent-generation-concurrency`、`outbound-callback`、`customer-master`、`response-monitor`、`smoke-test`,以及 `npm run check` 全量语法校验。 改动前即失败,与本方案无关:`account-switch`、`agent-console`、`callback-relay`、`startup-preview`。前三者断言的是 `mcp/src/dashboard/app.js` 的历史结构,属于既有待修问题。 ## 11. 现场纪律 - `outputs/` 会积累真实客户会话,培训结束后随包删除 - `.env.local` 含可用凭据,不要放进共享盘或版本库 - 关闭工作台窗口不会停止监听,需执行 `runtime stop` - 培训包与正式技能包是两条独立交付线,不要用培训包覆盖客户环境