企业微信 NPC 机器人 — 全流程 Skill v2
架构全景
用户在企微群/私聊 @机器人
→ 企微 WebSocket 长连接推送消息到 Node.js 客户端
→ Node.js 构造 systemPrompt + userPrompt
→ 调用 CNB API 触发 NPC 流水线(POST /{repo}/-/build/start)
→ NPC 在 CNB 沙箱中处理(cnbcool/default-npc:latest)
→ Node.js 轮询构建状态,提取 NPC agent 输出
→ 通过企微流式回复(stream)推回用户
前置条件
- [ ] 企业微信管理员权限(创建/配置智能机器人)
- [ ] CNB Token(有
repo-cnb-trigger:rwscope) - [ ] Node.js >= 18 的服务器
- [ ] npm 包
ws(WebSocket 客户端)
快速检查清单
| # | 检查项 | 确认方式 |
|---|---|---|
| 1 | Bot ID + Secret | 企业微信后台 → 应用管理 → 智能机器人 |
| 2 | 长连接模式 | 机器人设置页 → API模式 → 选择「长连接」 |
| 3 | 仅限创建者 | 机器人设置页 → 关闭此限制 |
完整工作流(7 步)
Step 1: 企业微信后台配置
- 登录 企业微信管理后台
- 应用管理 → 创建智能机器人
- 选择「API模式」→「长连接」(不是 Webhook 回调)
- 记录 Bot ID 和 Secret
- 可见范围设为全员(或目标部门)
- 关闭「仅限创建者使用」
> ⚠️ 必须选「长连接」模式,否则 WebSocket 认证会返回 853000。
Step 2: 创建 CNB NPC 仓库
curl -X POST "$CNB_API/{org}/-/repos" \
-H "Accept: application/vnd.cnb.api+json" \
-H "Authorization: Bearer $CNB_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"wechat-npc","description":"企业微信NPC机器人"}'
Step 3: 配置 NPC 角色(.cnb/settings.yml)
npc:
defaultRole: 小爪
roles:
- name: 小爪
prompt: |
你用"小爪"自称,叫用户"朋友"。
口头禅是"收到,马上处理!"
你是鸿爪派公众号的 AI 助手,擅长 AI 资讯、技术解读、编程辅助。
回复风格:热情友好,简洁有力,不超过 500 字。
如果用户给了对话历史,结合上下文连贯回答。
结束语:任务完成,随时待命!还有其他问题么?
注意:
- 不要输出 ANSI 颜色码
- 不要输出 markdown 表情语法(如 :emoji:)
- 直接输出纯文本内容
角色定制要点:
- name:角色名,通过 env.role 传入,必须与 settings.yml 一致
- prompt:NPC 的核心人设,决定了回复风格
- 建议包含:自称、称呼用户、口头禅、擅长领域、字数限制、格式要求
DeepSeek 推理风格示例:
- name: Reasonix-pro
slogan: 'Reasonix × DeepSeek 深度推理'
prompt: |
你用"小 R"自称,叫用户"朋友"。
口头禅是"让我深度推理一下!"
你是基于 DeepSeek 深度推理的 AI 编程智能体。
擅长:深度推理、代码生成、技术解读、编程辅助、复杂问题分析。
回复风格:先给出结论,再展开详细分析,不超过 500 字。
直接输出纯文本,用 Unicode 表情,不要用 markdown 表情语法。
结束回答时加一句俏皮的结束语。
Step 4: 配置流水线(.cnb.yml)
$:
api_trigger_npc:
- docker:
image: cnbcool/default-npc:latest
sandbox: true
stages:
- name: npc go
type: npc:go
options:
role: $role
systemPrompt: $systemPrompt
userPrompt: $userPrompt
> ⚠️ 铁律:api_trigger_npc 必须放在 $: 下面,不能放顶层!
> 放顶层会报 CONFIG_EVENT_EMPTY。
Step 5: 推送并验证
git clone https://cnb:${CNB_TOKEN}@cnb.cool/{org}/wechat-npc.git
cd wechat-npc
# 添加 .cnb/settings.yml 和 .cnb.yml
git add -A && git commit -m "init: NPC bot" && git push origin main
# 等 CNB 索引(约 5-10 秒)
sleep 8
# 验证触发
curl -X POST "$CNB_API/{org}/wechat-npc/-/build/start" \
-H "Accept: application/vnd.cnb.api+json" \
-H "Authorization: Bearer $CNB_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"event": "api_trigger_npc",
"env": {
"role": "小爪",
"systemPrompt": "你是小爪",
"userPrompt": "你好"
}
}'
# 期望: {"success":true, "sn":"cnb-xxx-xxx", ...}
Step 6: 部署长连接客户端
将模板文件复制到服务器:
mkdir -p ~/wecom-npc-bot && cd ~/wecom-npc-bot
cp /path/to/templates/index.js .
cp /path/to/templates/package.json .
npm install # 自动安装 ws 依赖
# 创建环境变量文件(从模板复制后填入真实值)
cp /path/to/templates/.env.example .env
chmod 600 .env
# 编辑 .env 填入 WECOM_BOT_ID / WECOM_BOT_SECRET / CNB_TOKEN
# 启动(开发调试)
node index.js
Step 7: systemd 常驻(推荐)
# 复制 service 模板(使用 EnvironmentFile 引用 .env,不暴露密钥)
sudo cp wecom-npc-bot.service /etc/systemd/system/wecom-npc-bot.service
# 确认 WorkingDirectory 和 EnvironmentFile 路径正确
sudo systemctl daemon-reload
sudo systemctl enable --now wecom-npc-bot
sudo journalctl -u wecom-npc-bot -f # 查看日志
> ⚠️ service 模板通过 EnvironmentFile= 读取 .env 文件,不在 service 中明文写密钥。
> 确保用户通过 systemd 部署时 .env 文件权限为 600。
企业微信 WebSocket 协议详解
帧格式
所有帧为 JSON,结构:
{
"cmd": "aibot_xxx",
"headers": { "req_id": "前缀_时间戳_序号" },
"body": { ... },
"errcode": 0
}
协议要点
| 协议项 | 值 | 备注 |
|---|---|---|
| WebSocket 地址 | wss://openws.work.weixin.qq.com |
— |
| 认证帧 cmd | aibot_subscribe |
body: {bot_id, secret} |
| 认证成功 | 回复帧 errcode=0 |
⚠️ 无 cmd 字段,靠 req_id 前缀 auth_ 判断 |
| 认证失败 | errcode=853000 |
凭证错/未设长连接/被限流 |
| 收消息 cmd | aibot_msg_callback |
body.msgtype=text/image/voice/file |
| 流式回复 cmd | aibot_respond_msg |
body: {msgtype:"stream", stream:{id,finish,content}} |
| 欢迎语 cmd | aibot_respond_welcome_msg |
用户进入会话时触发 |
| 心跳 | 每 30 秒发 cmd:"ping" |
pong 帧无 cmd,靠 req_id 前缀 ping_ 判断 |
| 事件回调 cmd | aibot_event_callback |
enter_chat/subscribe 等 |
> ⚠️ 核心坑点:认证回复和 pong 帧都没有 cmd 字段!必须通过 req_id 前缀判断。
req_id 前缀约定
| 前缀 | 用途 | 判断逻辑 |
|---|---|---|
auth_ |
认证 | 收到后检查 errcode |
ping_ |
心跳 | 收到后忽略(静默) |
stream_ |
流式回复 | 发送给用户的流 ID |
动态 systemPrompt 模板
index.js 中构建的 systemPrompt 格式:
你是{角色名}的AI小助手,名叫{角色名}。
当前时间:{动态时间}
对话场景:企业微信{群聊/私聊}
用户名称:{动态用户名}
{角色设定:自称、称呼、口头禅、擅长领域、风格}
【对话历史】(如有上下文)
用户:xxx
{角色名}:xxx
通过 env.systemPrompt 传入;NPC 角色通过 env.role 指定(对应 settings.yml 中的角色名)。
NPC 结果提取流程
1. POST /{repo}/-/build/start → 拿到 sn
2. 轮询 GET /{repo}/-/build/status/{sn}(3s→5s→8s 递增间隔)
→ 直到 status=success/error/cancel(超时 120s)
3. 从 status 取 pipelineId 和 stage 名("npc go")
4. GET /{repo}/-/build/logs/stage/{sn}/{pipelineId}/{stageId}
5. 从日志中匹配 Master[...][agent][N] 后的内容
日志提取策略
NPC 日志格式示例:
Master[xxx][agent] Skills: ...
Master[xxx][agent] Resolving role: 小爪
Master[xxx][agent] Starting: 你好
Master[xxx][agent][1] 朋友你好呀!🤖 ← 这是回复
Master[xxx][agent][1] Usage: in=1044 ... ← 跳过
提取策略(三层 fallback,适配 NPC 镜像格式变化):
- 策略 1 — 标准匹配:匹配
Master[...][agent][N]开头的行,过滤Usage:、Skills:、Resolving元数据行,连续 agent 输出拼接取最长段 - 策略 2 — 宽松匹配:过滤所有元数据关键词(Runner/Finished/Usage/Skills 等),收集剩余内容行
- 策略 3 — 尾部 fallback:取日志尾部 >20 字符的非元数据行
快捷指令
| 指令 | 行为 |
|---|---|
/help |
显示使用指南 |
/about |
显示机器人信息 |
/clear |
清除当前用户的对话上下文 |
多机器人部署
同一份 index.js 代码,通过环境变量区分不同机器人:
# 机器人 1 — 小爪(通用助手)
WECOM_BOT_ID="bot_id_1" WECOM_BOT_SECRET="secret_1" \
CNB_REPO="cnbno/wechat-npc" NPC_ROLE="小爪" \
node index.js
# 机器人 2 — 小 R(DeepSeek 推理风格)
WECOM_BOT_ID="bot_id_2" WECOM_BOT_SECRET="secret_2" \
CNB_REPO="cnbno/reasonix-bot" NPC_ROLE="Reasonix-pro" \
node index.js
每个机器人对应独立的 CNB 仓库(独立 settings.yml 定义不同角色)。
环境变量清单
| 变量 | 必需 | 说明 | 默认值 |
|---|---|---|---|
| WECOM_BOT_ID | ✅ | 企微机器人 Bot ID | — |
| WECOM_BOT_SECRET | ✅ | 企微机器人 Secret | — |
| CNB_TOKEN | ✅ | CNB 令牌(需 repo-cnb-trigger:rw scope) | — |
| CNB_API_ENDPOINT | ✅ | CNB API 地址 | https://api.cnb.cool |
| CNB_REPO | ✅ | NPC 仓库路径 | cnbno/wechat-npc |
| NPC_ROLE | ❌ | NPC 角色名 | 小爪 |
| BOT_NAME | ❌ | 对外显示名 | 取 NPC_ROLE |
| WS_URL | ❌ | WebSocket 地址 | wss://openws.work.weixin.qq.com |
| CONTEXT_DIR | ❌ | 会话上下文持久化目录 | ./context |
踩坑速查表
| # | 错误 | 原因 | 解决 |
|---|---|---|---|
| 1 | CONFIG_EVENT_EMPTY |
api_trigger_npc 放在 .cnb.yml 顶层 |
移到 $: 下面 |
| 2 | errcode=853000 |
认证失败 | ① 检查 Bot ID/Secret ② 确认长连接模式 ③ 等几分钟(限流) |
| 3 | user is not logged in |
自定义 NPC 镜像缺环境变量 | 用 cnbcool/default-npc:latest |
| 4 | NPC 日志无 agent 输出 | 提取正则不匹配 | 检查 Master[...][agent][N] 格式 |
| 5 | 消息收不到 | 「仅限创建者」未关闭 | 后台关闭此限制 |
| 6 | 断线频繁重连 | 多个进程同 Bot ID 竞争 | 确保每个 Bot ID 只有一个进程 |
| 7 | 重连雪崩 | 无退避策略 | 已内置指数退避:3s→4.5s→6.7s...最长 60s |
| 8 | NPC 回复含 ANSI 码 | NPC 输出未清洗 | cleanOutput 已处理 |
| 9 | errcode:5 (CNB API) | 路径/参数错误 | 查 Skill 文档核对,不直接认定 Token 权限不够 |
API 接口速查
| 操作 | 方法 | 路径 | Scope |
|---|---|---|---|
| 创建仓库 | POST | /{org}/-/repos |
repo-manage:rw |
| 触发 NPC | POST | /{repo}/-/build/start |
repo-cnb-trigger:rw |
| 查构建状态 | GET | /{repo}/-/build/status/{sn} |
repo-cnb-trigger:r |
| 查 Stage 日志 | GET | /{repo}/-/build/logs/stage/{sn}/{pipelineId}/{stageId} |
repo-cnb-trigger:r |
> 所有接口 Accept: application/vnd.cnb.api+json
提示词完整记录
小爪(通用助手)— settings.yml 中的 prompt
你用"小爪"自称,叫用户"朋友"。
你的口头禅是"收到,马上处理!"
你是「鸿爪派」公众号的 AI 助手,擅长 AI 资讯、技术解读、编程辅助。
回复风格:热情友好,简洁有力,不超过 500 字。
如果用户给了对话历史,结合上下文连贯回答。
结束语:任务完成,随时待命!还有其他问题么?
注意:
- 不要输出 ANSI 颜色码
- 不要输出 markdown 表情语法(如 :emoji:)
- 直接输出纯文本内容
小 R(DeepSeek 推理风格)— settings.yml 中的 prompt
你用"小 R"自称,叫用户"朋友"。
你的口头禅是"让我深度推理一下!"
你是基于 DeepSeek 深度推理的 AI 编程智能体。
擅长:深度推理、代码生成、技术解读、编程辅助、复杂问题分析。
回复风格:先给出结论,再展开详细分析,不超过 500 字。
直接输出纯文本,用 Unicode 表情,不要用 markdown 表情语法。
结束回答时加一句俏皮的结束语。
index.js 中的动态 systemPrompt(v4:只拼运行时上下文)
[动态上下文 — 由系统自动注入]
当前时间:{动态}
对话场景:企业微信{群聊/私聊}
用户名称:{动态}
机器人名称:{角色名}
【对话历史】(如果有上下文)
用户:xxx
{角色名}:xxx
> 角色人设(自称、口头禅、风格等)统一由 settings.yml 的 prompt 字段管理,不在 index.js 中重复定义。
文件清单
| 文件 | 作用 |
|---|---|
SKILL.md |
本文档 — 完整工作流 |
templates/index.js |
Node.js 长连接客户端 v4(通用模板) |
templates/package.json |
依赖声明(ws) |
templates/.cnb.yml |
NPC 流水线配置 |
templates/.cnb-settings.yml |
NPC 角色定义模板 |
templates/wecom-npc-bot.service |
systemd 服务模板(EnvironmentFile 方式) |
templates/.env.example |
环境变量模板(复制为 .env 后填值) |