面向一次线下培训:交付一个可双击运行的工作台程序,现场接真实企微监听,用浏览器打开界面,同时让核心 Node 逻辑不再以源码形式散落在培训机上。
本文既是方案说明,也是打包操作手册。打包侧的代码准备与 bun 安装均已完成。
执行状态:技术链路已通,培训交付仍暂缓。 session-first 架构已回流本仓库(开关
QIWEI_AGENT_CONVERSATION_MODE=session,培训入口默认打开)。现场交付仍卡 Fmode 登录席位。详见docs/specs/session-first-port-plan.md。
| 项目 | 结论 |
|---|---|
| 交付形态 | 一个文件夹: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 去翻它。也就是说,最值钱的话术资产在新架构下天然无法被编译保护。打包仍能挡住「拷走源码改一改就跑」,但别指望它保护话术。
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 接口清单)走内联,不落盘。
原有设计是工作台与监听分两个进程,这样刷新页面不会中断监听。培训包保留这个结构,只是两个进程变成同一个 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 + 子命令」。
以下改动已全部落地,且在 Node 路径下验证零回归。
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 不再自行推导包根,改为复用统一来源。
新增 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 — 启动时拉起 Runtimeruntime/callback-service/src/personal-polling.mjs — 好友轮询 WorkerBun 的打包器无法分析 require(path.join(PACKAGE_ROOT, ...)) 这种运行时拼路径。三个文件已改为静态相对路径,Node 下解析结果完全等价:
runtime/callback-service/src/runtime-state.mjsruntime/callback-service/src/processor-bridge.mjsruntime/callback-service/src/config-loader.mjsmcp/src/dashboard/server.js 的静态目录支持 QIWEI_DASHBOARD_STATIC_DIR,编译后指向 exe 同级 web/mcp/src/core/api-catalog.js 改为「磁盘优先、内联兜底」,源码运行读文件,编译后用内联 JSON@ffmpeg-installer/ffmpeg 在找不到平台二进制时会在 require 阶段直接抛错,而 @binsee/wx-voice 依赖它。原先三处顶层引入,意味着编译后工作台会直接起不来。已改为用到语音时才加载:
mcp/src/core/voice-clone-service.jsmcp/src/core/inbound-media.jsmcp/src/dashboard/agent-service.js改动后,未部署语音组件的环境仍可正常收发文字与图片消息。
bin/qiwei-training.js — 培训入口,负责注入环境、分发子命令、拉起监听、打开浏览器scripts/build-training-package.mjs — 前置检查、编译、复制资产、生成现场手册package.json 新增 training:check / training:build / training:start| 条件 | 状态 |
|---|---|
| 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/bin、D:\bun\bin、C:\bun\bin,因此刚装完不必重启终端。
npm run training:check
该命令会逐项校验上表并给出结论。
前置检查通过后,一条命令完成:
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 外置,因为培训不演示语音,且它们携带平台二进制。
在打包机先自测一遍,再拷到培训机。
冒烟验收
cd dist/qiwei-training && .\qiwei-workbench.exe --no-open,确认进程不退出、无异常堆栈http://127.0.0.1:4320/#agent,页面正常渲染(验证 web/ 外置生效)http://127.0.0.1:4320/api/health,返回 JSON.\qiwei-workbench.exe runtime status,alive 为 true(验证 exe 自举成功)outputs/ 下已生成 runtime/qiwei-runtime.json 与 SQLite 三件套(.db / .db-wal / .db-shm).\qiwei-workbench.exe runtime stop,状态转为 stopped现场全链路验收
.env.local,页面完成企微扫码登录,账号显示在线第 5 步是本方案最关键的技术闸门,详见下节 G1。
| 编号 | 风险 | 判断方式 | 应对 |
|---|---|---|---|
| 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,现场只剩一个消息收件箱。这条从「可接受的降级」变成了「必须满足的硬前置」。
所有改动都是「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 脚本。
改动前后跑同一组 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 的历史结构,属于既有待修问题。
outputs/ 会积累真实客户会话,培训结束后随包删除.env.local 含可用凭据,不要放进共享盘或版本库runtime stop