bun-training-package-plan.md 13 KB

培训版 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. 交付物结构

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 的不同子命令。

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.jsPACKAGE_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,指定安装盘符:

$env:BUN_INSTALL='D:\bun'; irm bun.sh/install.ps1 | iex

构建脚本除 PATH 外还会探测 BUN_INSTALL~/.bun/binD:\bun\binC:\bun\bin,因此刚装完不必重启终端。

npm run training:check

该命令会逐项校验上表并给出结论。

6. 打包执行

前置检查通过后,一条命令完成:

npm run training:build

脚本会依次执行:清空 dist/qiwei-training/bun build --compile 生成 exe → 复制 web/knowledge/、配置模板 → 生成 .env.local 模板与现场 README.md → 打印产物体积。

可选参数:

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 statusalivetrue(验证 exe 自举成功)
  5. 确认 outputs/ 下已生成 runtime/qiwei-runtime.json 与 SQLite 三件套(.db / .db-wal / .db-shm
  6. .\qiwei-workbench.exe runtime stop,状态转为 stopped

现场全链路验收

  1. 填写 .env.local,页面完成企微扫码登录,账号显示在线
  2. 配置测试白名单,开启 AI 监听
  3. 用另一台手机发文字消息,确认进入工作台会话列表
  4. 发一张图片,确认显示为图片消息且不触发自动回复

第 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.logCannot 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.jsmcp/src/core/runtime-launcher.jsscripts/build-training-package.mjs 及三条 npm 脚本。

10. 回归基线

改动前后跑同一组 smoke,结果完全一致,未引入新失败。

通过:product-modemessage-archivesession-reply-deliveryagent-inbound-mediaagent-knowledgeagent-response-human-styleagent-intake-policyagent-response-qualityagent-generation-concurrencyoutbound-callbackcustomer-masterresponse-monitorsmoke-test,以及 npm run check 全量语法校验。

改动前即失败,与本方案无关:account-switchagent-consolecallback-relaystartup-preview。前三者断言的是 mcp/src/dashboard/app.js 的历史结构,属于既有待修问题。

11. 现场纪律

  • outputs/ 会积累真实客户会话,培训结束后随包删除
  • .env.local 含可用凭据,不要放进共享盘或版本库
  • 关闭工作台窗口不会停止监听,需执行 runtime stop
  • 培训包与正式技能包是两条独立交付线,不要用培训包覆盖客户环境