--- tags: [企业微信, 集成, 配置] date: 2026-07-29 --- ## 凭证 | 参数 | 值 | |------|-----| | 企业 ID (CorpID) | `wwa979fc0ebcce602a` | | 应用 ID (AgentID) | `1000008` | | 应用 Secret | `3QIxJQfqFgACBBcybmyc_I-mOoFvicH-Y6hdQ09ldLk` | | Token(回调验证) | `dc56c68beba4c06984ce17c6342cacb1` | | EncodingAESKey(回调加密) | `WngnBCzsdXlPcdNBVG6l7auYMokMvuM94jMunTdSEHM` | | 回调 URL | `http://api.zilanlife.com/wecom/callback` | | 服务端 IP | `59.51.140.250` | | 可信域名 | `api.zilanlife.com` | | DeepSeek API Key | `sk-8a62671966e1470bb1a6b223a9a549fa` | | 回调服务端口 | `18790`(仅 127.0.0.1) | --- ## 一、配置步骤 ### 1. 创建自建应用 登录 [work.weixin.qq.com](https://work.weixin.qq.com) → 应用管理 → 创建应用 ### 2. 创建子域名并配置服务器 **DNS(用户操作)**:A 记录 `api.zilanlife.com` → `47.109.158.218` ⚠️ `api.zilanlife.com` 走 Nginx 80 端口(HTTP),不需要额外开放端口。`/wecom/callback` 路径反代到内网 `127.0.0.1:18790`。 **Nginx(Hermes 在 aliyun2 执行)**: ```bash cat > /www/server/panel/vhost/nginx/api.zilanlife.com.conf << 'EOF' server { listen 80; server_name api.zilanlife.com; root /www/wwwroot/api.zilanlife.com; index index.html; location /wecom/callback { proxy_pass http://127.0.0.1:18790; proxy_set_header Host $host; proxy_read_timeout 60s; } } EOF mkdir -p /www/wwwroot/api.zilanlife.com nginx -t && nginx -s reload ``` ### 3. 验证域名归属 1. 企业微信应用详情页 → 网页授权及JS-SDK → 可信域名 → 输入 `api.zilanlife.com` 2. 下载验证文件 `WW_verify_j5y70gNGnVqhnH4d.txt` 3. 上传到服务器: ```bash echo 'j5y70gNGnVqhnH4d' > /www/wwwroot/api.zilanlife.com/WW_verify_j5y70gNGnVqhnH4d.txt ``` 4. 确认 `http://api.zilanlife.com/WW_verify_j5y70gNGnVqhnH4d.txt` 可访问 5. 点击验证通过 ### 4. 配置 IP 白名单 应用详情页 → 企业可信 IP → 添加 `59.51.140.250` ⚠️ 必须先完成域名验证才能操作。 ### 5. 配置接收消息 应用详情页 → 接收消息 → 设置API接收: | 字段 | 值 | |------|-----| | URL | `http://api.zilanlife.com/wecom/callback` | | Token | `dc56c68beba4c06984ce17c6342cacb1` | | EncodingAESKey | `WngnBCzsdXlPcdNBVG6l7auYMokMvuM94jMunTdSEHM` | 消息事件勾选:用户发送的普通消息 ### 6. 部署回调服务 Python 脚本路径:`/data/wecom_callback.py`(aliyun2) 依赖安装: ```bash pip3 install pycryptodome ``` 启动/重启(systemd 管理,崩溃 5 秒自愈): ```bash systemctl restart wecom-callback journalctl -u wecom-callback -f ``` --- ## 二、回调服务架构 ``` 员工在企微发消息 ↓ 企微 POST 加密 XML → api.zilanlife.com/wecom/callback ↓ Nginx 反代 → 127.0.0.1:18790(Python 服务,systemd 托管) ↓ 1. URL 验证(GET):解密 echostr → 返回明文 2. 接收消息(POST):秒回 200 → 解密消息体 → 后台异步处理 ↓ Dify 工作流「企微AI回复」(多轮会话)→ AI 生成 ↓ send_msg 主动推送回复到企微 ``` ### 核心功能 1. **URL 验证**:GET 请求,解密 `echostr` 参数并返回明文(企业微信首次配置时调用) 2. **消息接收**:POST XML 加密体,解密后得到用户消息,先秒回 200 再异步处理(企微 5 秒超时) 3. **AI 回复**:调 Dify 工作流(`app-6ax5fzZvsOuQvSDiQ0bcCgwa`)生成回复,15 秒未完成先发「正在处理」 4. **主动推送**:`send_msg` 调企微 API 把回复发回用户 ### 多轮会话(2026-08-15 改造 v3) - **方案**:切换到 Dify **Chatflow** 应用(「企业微信」advanced-chat,LLM 节点开记忆窗口 10),走 `/v1/chat-messages` API 原生 `conversation_id` - **踩坑**:原「企微AI回复」是 Workflow 应用,`/v1/workflows/run` 请求模型不含 conversation_id,多轮无效 - **回调脚本**:按 `userid` 维护 conversation_id(互不串扰),**每天凌晨 3:00 全量过期**,存 `/data/wecom_conversations.json` - **API Key**:`app-wecom-chat-20260815`(Dify「企业微信」应用) - **原版备份**:`/data/wecom_callback.py.bak-20260814`;回溯记录见桌面 `aliyun2_企微回调多轮会话_回溯记录.md` ### AI 工具接入(2026-08-15) - 「企业微信」应用已升级为 **Agent 节点** + 4 个 CordysCRM 售后工具(查询/新增/查看/修改) - 认证代理:systemd `cordys-api-proxy`(端口 8766,仅内网) - 完整接入流程 + 踩坑 → 见 [[Dify AI工具接入指南]] ### 加密/解密 - 算法:AES-256-CBC,IV 为 AES Key 前 16 字节 - 明文结构:`16字节随机串 + 4字节消息长度(big-endian) + 消息内容 + Corpid` - PKCS7 填充到 32 字节倍数 --- ## 三、API 参考 ### 主动发消息 ```python POST https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={TOKEN} { "touser": "WuXingdeYeKong", "msgtype": "text", "agentid": 1000008, "text": {"content": "消息内容"} } ``` ### 获取通讯录 ```python GET https://qyapi.weixin.qq.com/cgi-bin/user/simplelist?access_token={TOKEN}&department_id=1&fetch_child=1 ``` - 返回 155 人 - `userid` 如 `WuXingdeYeKong`,不一定是姓名拼音 - 申亮 = `WuXingdeYeKong` ### Access Token - 接口:`GET /cgi-bin/gettoken?corpid=...&corpsecret=...` - 有效期:7200 秒(2 小时) - 需缓存复用 --- ## 四、诗词 API - 端点:`http://47.109.158.218:1279/api/v1/poems/random` - 搜索:`http://47.109.158.218:1279/api/v1/poems/search?q=关键词` - ⚠️ 搜索按内容不按作者,搜"李白"返回提及的诗,非李白所作 - 按作者找:用 random 随机抽取直到命中 --- ## 五、踩坑记录 | 坑 | 现象 | 解决 | |----|------|------| | 未配可信域名 | IP 白名单无法添加 | 先验证域名,再配 IP | | IP 未加白 | 通讯录/user API 返回 60020 | 应用详情 → 企业可信 IP | | `userIds` 参数无效 | user/list 返回所有人 | 拉全量客户端建映射 | | 回调 Key 不一致 | URL 验证失败 | Token/EncodingAESKey 需与后台严格一致 | | DeepSeek Key 错误 | 401 Authorization Required | 使用正确的 sk-xxx key | | 搜索按内容不按作者 | 搜"李白"返回马致远 | random 抽到命中为止 | --- ## 七、消息模板 ### 签单前复盘表 — 设计师通知 **数据来源**:销帮帮 formId=7539701,2026年,模板创建(text_12=ef4570ef...),排除关联客户已删除的记录 **业务术语**: - `模板创建` = 销帮帮 text_12 值为模板创建 UUID - 对外展示为「未填写」(业务语境:记录已创建但未完善) **卡片格式**(手机端适配,分割线不超过 5 个 ━): ``` 📊 {设计师名} — 2026年签单前复盘表 ━━━━━ 📝 未填写 {N} 条 ━━━━━ 请在规定范围时间内登录销帮帮填写完毕! ``` **示例**: ``` 📊 陈叶婷 — 2026年签单前复盘表 ━━━━━ 📝 未填写 5 条 ━━━━━ 请在规定范围时间内登录销帮帮填写完毕! ``` **流程**: 1. 销帮帮 API 拉取 formId=7539701,筛选 text_12=模板创建 2. 过滤 addTime >= 2026-01-01 3. customer/detail 校验每条 text_2,排除 code=100404 4. 按 ownerId 分组统计,id2name 查姓名 5. **姓名映射**(销帮帮→企微)后查 userid,逐人发送 **API 调取条件**: ```json { "corpid": "xbb8abca6c277b846dab5e0c8a52fe9506e", "formId": 7539701, "conditions": [ {"attr": "text_12", "value": ["ef4570ef-5460-7b62-b888-b3295f475e51"], "symbol": "equal"} ], "pageSize": 100, "page": 1 } ``` 然后客户端过滤 `addTime >= 2026-01-01`,再逐条 `customer/detail` 排除 `code=100404`。 **发送规则**: - 按设计师分组,每人一条卡片(只含自己的数量) - 姓名映射转换后查企微 userid - 赵耀跳过(企微无此人) - 每条间隔 ≥ 0.5 秒(避免企微频率限制) **姓名映射**: | 销帮帮 | 企业微信 | userid | |--------|----------|--------| | 胡华玉 | 胡玉 | WeiXiaoXiangYang | | 陈明先 | 陈想 | ChenXiang | | 王飞 | 王习僧 | b5ed7c2b0034f08e6ae8e87d758b39a6 | | 赵耀 | ❌ 不存在 | — | --- ## 八、已验证功能 - ✅ Token 获取 - ✅ 通讯录读取(155 人) - ✅ 主动发送消息 - ✅ 诗词 → 企微发送 - ✅ 接收消息回调 + URL 验证 - ✅ 员工消息 → Dify 工作流 → AI 自动回复 --- ## 九、Dify 工作流集成 ### 9.1 工作流配置 **工作流名称**:企微AI回复 | 参数 | 值 | |------|-----| | API Key | `app-6ax5fzZvsOuQvSDiQ0bcCgwa` | | 端点 | `http://47.109.158.218:8088/v1/workflows/run` | | 输入 | `query`(文本) | | 输出 | `text`(文本) | **节点结构**: 1. 开始节点 → `query` 输入 2. LLM 节点 → 提示词 `{{#start.query#}}`,关掉 Thinking 3. 结束节点 → 输出 `text` ### 9.2 回调服务对接 回调路径:`api.zilanlife.com/wecom/callback` → Nginx → `127.0.0.1:18790` 流程: ``` 员工发消息 → 企微加密 POST → 回调服务解密 → Dify API 发送 query → LLM 处理 → 返回 text → 加密回复 → 员工收到 ``` 代码要点: ```python body = {"inputs": {"query": user_msg}, "response_mode": "blocking", "user": "wecom"} req = Request(DIFY_API_URL, data=json.dumps(body).encode(), headers={"Authorization": f"Bearer {DIFY_API_KEY}"}) # 返回 text = data["data"]["outputs"]["text"] ``` ### 9.3 版本演进 | 版本 | 后端 | 模式 | 状态 | |------|------|:---:|:---:| | v1 | DeepSeek API 直连 | 同步 | ❌ Key 泄露风险 | | v2 | Key 注释 | 同步 | ❌ 不可用 | | v3 | Dify 工作流 | 同步 | ❌ 超时断连 (BrokenPipe) | | v4 | Dify 工作流 | 异步 | ✅ 当前 | ### 9.4 异步架构(解决超时断连) **问题**:同步模式下 Dify 处理 15-30 秒,企微 5 秒超时断连 → BrokenPipeError **方案**: ``` 收到消息 → 立即 200(不等 Dify)→ 后台线程处理 → send_msg 主动推送 ``` **代码**: ```python def do_POST(self): # 先返回 200 self.send_response(200); self.end_headers() # 后台处理 threading.Thread(target=lambda: send_msg(user_id, ai_reply(msg))).start() ``` ### 9.5 三层可靠性保护 | 保护 | 机制 | 效果 | |------|------|------| | 超时兜底 | Dify 15s 无响应 → 先发「正在处理」→ 继续等 45s | 不丢消息,用户有反馈 | | 发送重试 | send_msg 失败自动重试 3 次,间隔 1s | 网络抖动不丢 | | systemd | `Restart=always`,崩溃 5s 自动复活 | 进程挂了秒恢复 | **并发能力**: - Python 线程处理 100+ 人同时提问无压力 - Dify Worker 池自动排队 - 真正瓶颈在企微 API 频率限制,不在架构 **systemd 配置**: ```ini [Service] ExecStart=/usr/bin/python3 -u /data/wecom_callback.py Restart=always RestartSec=5 # 服务器重启后自动拉起 → systemctl enable ``` --- ## 十、定时任务(签单复盘企微推送) | 项目 | 内容 | |------|------| | 任务 ID | `82990ab2bd98` | | 触发 | 每天 5:00 | | 执行 | 本机 `wecom_xbb_push.sh` → SSH aliyun2 → Python 脚本 | | 监控 | `deliver: origin`,结果推回 Hermes | **为什么迁到 aliyun2**: - 本地家宽 IP 会变动(已从 59.51.140.250 变为 1.204.41.232) - 服务器 IP 47.109.158.218 固定,企微白名单稳定 **脚本路径**: - 本地:`~/.hermes/scripts/wecom_xbb_push.sh`(SSH 跳板) - 远程:`/opt/scripts/wecom_xbb_push.py`(实际执行) --- ## 十一、踩坑记录(新增) | 坑 | 现象 | 解决 | |----|------|------| | IP 变动 | 本地出网 IP 从 59.51→1.204,企微 60020 | 脚本迁 aliyun2,用固定 IP | | `systemctl stop` 卡死 | 服务持续 deactivating,超时 | prod.js SIGTERM 处理有 bug,手动 kill 进程 | | WAL 删除致数据丢失 | 部署时删 WAL 文件 → 未刷盘数据永久丢失 | 先用 `wal_checkpoint` 强制刷盘,再停服 | | Dify LLM Thinking 输出 | 回复含 `` 标签 | 关掉 LLM 节点的 Thinking 模式 | | Dify 变量引用错误 | `#sys.query#` not found | 改为 `#start.query#` | | Cron 本地跑不通 | `no_agent` 脚本依赖本地 IP 白名单 | 改 shell 脚本 SSH 远端执行 | --- ## 十二、2026-09-06:企业微信对话建档 → 云装天下 ERP(Dify 全链路打通实录) > 目标:员工在企业微信里发"登记客户 姓名 手机号 楼盘 来源",AI 自动查重→复述→确认→写入云装天下 ERP(信息客户 type2)。本节为**复现级实录**,照做可重演。 > 状态:✅ 已端到端实测成功(见 §12.5)。生产上线前待办见 §12.7。 ### 12.1 最终链路(现状,与售后共用同一入口) ``` 员工在企微「子兰企业助手」(AgentID 1000008) 发文字 → https://api.zilanlife.com/wecom/callback (nginx → 127.0.0.1:18790) → /data/wecom_callback.py(现有售后回调,未改动;解密后 POST 给 Dify) → Dify http://127.0.0.1:8088/v1/chat-messages(Bearer app-wecom-chat-20260815) → Dify「企业微信」Chatflow(deepseek-v4-flash,FunctionCalling) ├─ 建档意图(登记客户/建档/…) → erp_checknumber → 复述待确认 → erp_add_project → 回执 └─ 售后/装修问答 → 原路(Cordys 工具) → 回复经回调脚本加密推送回企微 ``` 分流由 Dify Agent 系统提示词内「客户登记规则」完成,**回调脚本零改动**(本地草稿 wecom_callback_v2_jd.py 已作废)。 ### 12.2 服务器新增物(aliyun2 = 47.109.158.218) | 项 | 内容 | |---|---| | 建档桥 | `/data/erp_bridge.py`,systemd `erp-bridge.service`,监听 `0.0.0.0:8767` | | 桥功能 | md5 签名、from/type=2/source 枚举硬校验、幂等(sqlite 24h)、频控、审计(jsonl);端点 /health /sources /checknumber /add_project /movetospare | | 桥鉴权 | 请求头 `X-Proxy-Token`,兼容 `Basic ` 前缀(Dify api_key_header 固定带 Basic):`t=header.strip(); t=t[6:] if t.startswith("Basic ")` | | 桥配置 | ERP_BASE=https://gzzlsh.cloudcubic.net;ERP_TOKEN=14EDCFA9BA50B0DD4768187EC07D6F59;CHANNEL=云装天下(占位);branch=1;projecttype=2;BUILDING_MODE=free(测试期) | | 启动 | `systemctl start erp-bridge`;健康 `curl -H 'X-Proxy-Token: Basic zilan-jd-proxy-2026' http://127.0.0.1:8767/health` | ### 12.3 Dify 侧改动(容器/DB 层,勿用浏览器画布) 1. 自定义工具:表 `tool_api_providers`「云装ERP建档桥」id=`03333ad2-b2b8-4000-b23a-9a7a389874fe`;schema servers=`http://172.20.0.1:8767`(Dify 容器网络宿主机网关);credentials_str 对齐 Cordys:`{"auth_type":"api_key_header","api_key_header":"X-Proxy-Token","api_key_value":""}`,不带 prefix 键。 2. 线上 Agent 直改 DB:apps.workflow_id=`38326b5d-43d2-43b0-8c24-ea4a43e6e7fb`(草稿 `8ed1dbb8-…` 同步)——agent 节点 `agent_parameters.tools.value` 追加 4 个 erp_* 工具(provider 同上);`instruction.value` 末尾追加「客户登记规则」全文(见本地 agent-追加指令-客户登记.md)。改前备份 /tmp/workflow_*.20260906072931.json。 3. 网络:compose `.env` `SSRF_PROXY_ALLOW_PRIVATE_IPS=172.17.0.0/16,172.20.0.0/16,172.21.0.0/16,172.30.0.0/16,172.31.0.0/16,192.168.0.0/20,127.0.0.0/8`(备份 .env.bak-20260906)→ `docker-compose up -d` 重建;**重建后 api 容器换 IP → `docker restart dify_v1110_chns-nginx-1` 重新解析,否则 502**。 ### 12.4 复现验证命令 ```bash curl -s -m 120 -X POST http://127.0.0.1:8088/v1/chat-messages \ -H 'Authorization: Bearer app-wecom-chat-20260815' -H 'Content-Type: application/json' \ -d '{"inputs":{},"query":"登记客户 张三 13812345678 观山云墅 信息流—小红书","response_mode":"blocking","user":"probe"}' # Agent 应查重(未建档)→复述待确认;同会话发"确认"→真实写入(仅假号测试) ``` ### 12.5 2026-09-06 实测结果 - 企微 WuXingdeYeKong:「登记客户 张三 13700000009 观山云墅 其他」→ Dify 会话 b025f057; - Agent 查重"未建档"→复述待确认→回「确认」→ `erp_add_project` 成功; - 桥审计 `{"mobile":"13700000009","client_id":12771,"project_id":12967,"status":"ok","msg":"成功!"}`;ERP 信息客户池可见(KH 开头编号)。 ### 12.6 本次踩坑(务必看) 1. Dify api_key_header 固定发 `X-Proxy-Token: Basic ` → 桥须剥离 Basic 前缀否则 401; 2. SSRF 私网拦截 + squid 对公网 http 也 MITM 成 https(自签证书失败)→ 最终"私网网关 http + 白名单放行"; 3. docker-compose up 后 api 换 IP → dify nginx 502 → restart nginx; 4. 浏览器画布改 Dify 编排易误删节点/清空指令(已发生)→ 一律容器/DB+备份; 5. 工具凭据落库为 SECRET_KEY+Fernet 加密,直写库须同算法。 ### 12.7 生产上线前待办(非阻塞联调) - ERP 方:正式 from 渠道标识、source 字典口径、楼盘查询接口/清单、员工查询接口; - 我方:企微身份→手机号映射(clerkPhone 归属)、BUILDING_MODE=whitelist、来源对话选项映射、申请测试环境。 ### 12.8 相关文件 - 空间 /Users/shenliang/Documents/云装天下/:erp_bridge.py、erp_bridge_openapi.yaml、agent-追加指令-客户登记.md、cloudcubic_api.py、CLAUDE.md(「Dify 接入进度(2026-09-06)」) ### 12.9 如何新增/复现一个 Dify 自定义工具(Swagger API)并挂到 Agent > 通用流程,不限于建档桥。两种方式任选:A=网页 UI(直观但慢);B=直接写库(快、可脚本化,推荐给 AI 自动执行)。 #### 方式 A:网页 UI(人肉操作路径) 1. Dify 控制台 → **集成 → 工具 → Swagger API 作为工具 → +添加/创建**; 2. 填名称(如「云装ERP建档桥」);Schema 粘贴 OpenAPI yaml/json; - **servers.url 必须写 Dify 容器能访问到的地址**:宿主机桥用 `http://172.20.0.1:8767`(网关,需 SSRF 白名单放行,见 §12.3.3); - security: `X-Proxy-Token` header(apiKey)声明在 components.securitySchemes; 3. **鉴权方法**:选「请求头」→ 键 `X-Proxy-Token` → 值填真实 token(如 `zilan-jd-proxy-2026`)。 - ⚠️ 即使选了 Basic 前缀也无所谓:Dify 运行时恒发 `X-Proxy-Token: Basic `,**后端接口需自己剥掉 Basic 前缀**; 4. 保存 → 工具出现在"工具插件/Swagger API"列表 → 勾选/添加全部工具; 5. 到 Agent(应用编排 → AI助手节点)→ **工具列表 +添加** → 搜索该工具名 →「添加全部」→ 保存/发布。 #### 方式 B:直接写库(AI 推荐路径) 表:`dify.tool_api_providers`,关键列:`id(uuid)`、`name`、`schema`(文本)、`schema_type_str='openapi'`、`credentials_str`、`user_id`、`tenant_id`、`tools_str`、`description`。 - schema 例(省略 paths): ```yaml openapi: 3.0.1 info: {title: 云装ERP建档桥, version: 0.1.0} servers: [{url: http://172.20.0.1:8767}] security: [{ProxyToken: []}] paths: { ... } # operationId 即工具名,如 erp_checknumber components: securitySchemes: {ProxyToken: {type: apiKey, in: header, name: X-Proxy-Token}} ``` - credentials_str(值必须用 Dify SECRET_KEY 加密): ```json {"auth_type":"api_key_header","api_key_header":"X-Proxy-Token","api_key_value":""} ``` - 加密方式(容器内 python,与 Cordys 存储一致): ```python import os, hashlib, base64 from cryptography.fernet import Fernet key = base64.urlsafe_b64encode(hashlib.sha256(os.environ["SECRET_KEY"].encode()).digest()) enc = Fernet(key).encrypt(b"zilan-jd-proxy-2026").decode() ``` - 挂到 Agent(改 `workflows.graph` 的 llm 节点): ```json // nodes[] 里 data.type=="agent" 的节点: "agent_parameters": { "tools": { "value": [ {"provider_name":"03333ad2-b2b8-4000-b23a-9a7a389874fe", "provider_id":"03333ad2-b2b8-4000-b23a-9a7a389874fe","type":"api", "tool_name":"erp_add_project","tool_label":"erp_add_project", "tool_configurations":{},"enabled":true} ]}} ``` - ⚠️ 直写库前先备份行(SELECT→存 json);agent 运行读 apps.workflow_id 绑定的那行,改完即时生效(无需点发布);redis 有缓存则重启 api 或等过期。