实战日期:2026-08-15 | 目标:企微对话 → AI 调用 CordysCRM 售后 API 本文按「从头到尾」的顺序记录每一步操作、命令、踩坑和修复,下次接新工具照此文档复制执行。
企业微信消息
→ 回调服务(aliyun2:18790,wecom-callback)
→ Dify「企业微信」Chatflow 应用(Agent 节点,记忆窗口 10)
→ AI 判断意图 → 调用自定义工具(OpenAPI 定义)
→ 认证代理(aliyun2:8766,systemd cordys-api-proxy)
→ 目标 API(CordysCRM:8081)
→ 数据返回 → AI 格式化回答 → 推回企微
关键设计:AI 负责决策,代理负责认证安全,接口系统只做数据读写。
为什么需要代理:Dify 自定义工具只支持配「一个」认证 Header,但 CordysCRM 要两个(X-Access-Key + X-Secret-Key)。代理接收 Dify 的单 Token 请求,校验后注入双密钥转发。
/data/cordys_api_proxy.py(systemd cordys-api-proxy,端口 8766):
X-Proxy-Token(不对就 403)X-Access-Key / X-Secret-Key日志全部 flush=True(否则 journald 里看不到)
# systemd 服务
cat > /etc/systemd/system/cordys-api-proxy.service << 'EOF'
[Unit]
Description=CordysCRM API Proxy for Dify
After=network.target
[Service]
Type=simple
ExecStart=/usr/bin/python3 /data/cordys_api_proxy.py
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload && systemctl enable --now cordys-api-proxy
172.20.0.1:8766(docker inspect dify_v1110_chns-api-1 看 Gateway)# 带 token:200
curl -s 'http://127.0.0.1:8766/custom-form/list' -H 'X-Proxy-Token: zilan-crm-proxy-2026'
# 无 token:403
curl -s -o /dev/null -w '%{http_code}' 'http://127.0.0.1:8766/custom-form/list'
# 从 Dify 容器内测试(模拟真实调用路径)
docker exec dify_v1110_chns-api-1 curl -s 'http://172.20.0.1:8766/custom-form/list' -H 'X-Proxy-Token: zilan-crm-proxy-2026'
# 用 api-docs 看接口全貌
curl -s 'http://127.0.0.1:8081/v3/api-docs' \
-H 'X-Access-Key: <key>' -H 'X-Secret-Key: <secret>'
# 从中提取:路径、方法、参数 schema、必填字段
AI 填下拉框要填选项 ID 不是文字,所以 description 里必须写全映射:
公司=178642865321400000(子兰=...001/焕境=...002)
问题处理阶段=...(未处理=...001/安排中=...002/处理中=...003/已处理=.../推迟处理=...)
{
"openapi": "3.0.1",
"info": {"title": "工具名", "version": "1.0.0"},
"servers": [{"url": "http://172.20.0.1:8766"}],
"security": [{"proxyTokenAuth": []}],
"paths": {
"/custom-form/data/page": {
"post": {
"operationId": "queryRecords",
"summary": "查询售后记录",
"description": "keyword 模糊匹配客户姓名。customFormId 固定 436092742804709376。",
"requestBody": {"content": {"application/json": {"schema": {
"type": "object",
"properties": {
"customFormId": {"type": "string", "default": "436092742804709376"},
"keyword": {"type": "string", "description": "客户姓名"}
}
}}}}
}
}
},
"components": {"securitySchemes": {"proxyTokenAuth": {"type": "apiKey", "in": "header", "name": "X-Proxy-Token"}}}
}
⚠️ servers.url 用 docker 网关地址(http://172.20.0.1:8766),因为请求是从 Dify api 容器发起的,容器里 127.0.0.1 是它自己。
Dify 后台 → 工作室 → 企业微信 → 编辑 → 左下「工具」→ 自定义 → 创建 → OpenAPI schema 粘贴 → 认证选 API Key(Header X-Proxy-Token,值 zilan-crm-proxy-2026)。
Step 1:建临时管理员账号(不改用户现有账号,用完即删)
-- Dify 密码哈希 = pbkdf2_hmac sha256 10000轮;password列 = base64(hexlify(dk));salt列 = base64(salt)
INSERT INTO accounts (id, name, email, password, password_salt, status, initialized_at)
VALUES (uuid_generate_v4(), 'temp-admin', 'temp-admin@zilan.local', '<hash>', '<salt>', 'active', CURRENT_TIMESTAMP);
INSERT INTO tenant_account_joins (id, tenant_id, account_id, role, current)
SELECT uuid_generate_v4(), '<tenant_id>', id, 'owner', true FROM accounts WHERE email='temp-admin@zilan.local';
⚠️ 哈希格式坑:hash_password 返回 hexlify(dk)(hex 字节串),存入时还要 base64 一层。直接 base64(dk) 是错的,登录会「Invalid email or password」。
Step 2:登录(password 字段必须先 base64 编码!)
PWD64=$(echo -n '密码' | base64)
curl -s -c /tmp/dify_cj.txt -X POST 'http://127.0.0.1:8088/console/api/login' \
-H 'Content-Type: application/json' \
-d "{\"email\":\"temp-admin@zilan.local\",\"password\":\"$PWD64\",\"language\":\"zh-Hans\"}"
⚠️ 登录接口的 password 加密就是 base64(libs/encryption.py 源码确认),不是 RSA。
Step 3:创建工具 provider
CSRF=$(grep csrf_token /tmp/dify_cj.txt | awk '{print $7}')
curl -s -b /tmp/dify_cj.txt -H "X-CSRF-Token: $CSRF" \
-X POST 'http://127.0.0.1:8088/console/api/workspaces/current/tool-provider/api/add' \
-H 'Content-Type: application/json' \
-d '{
"provider": "工具名",
"icon": {"type": "emoji", "content": "🔧"},
"credentials": {"auth_type": "api_key_header", "api_key_header": "X-Proxy-Token", "api_key_value": "zilan-crm-proxy-2026"},
"schema_type": "openapi",
"schema": "<openapi json 字符串>",
"privacy_policy": "", "custom_disclaimer": "", "labels": []
}'
⚠️ CSRF 坑:登录 cookie 里有 csrf_token,请求头必须带 X-CSRF-Token: <同值>,否则 401。
⚠️ auth_type 值:api_key_header(不是旧的 api_key)。
Step 4:拿到 provider id
SELECT id, name FROM tool_api_providers; -- 记下 UUID,Agent 节点要用
# 看当前图结构
SELECT id, type, version FROM workflows WHERE app_id='<app_id>' ORDER BY updated_at DESC;
# draft = 编辑版本;version 是时间戳的 = 已发布版本
{
"type": "agent",
"title": "AI助手",
"desc": "",
"version": "1",
"model": {"provider": "langgenius/deepseek/deepseek", "model": "deepseek-v4-flash", "model_type": "llm", "mode": "chat", "completion_params": {"temperature": 0.7}},
"agent_strategy_provider_name": "langgenius/agent/agent",
"agent_strategy_name": "function_calling",
"agent_strategy_label": "FunctionCalling",
"memory": {"window": {"enabled": true, "size": 10}, "query_prompt_template": "{{#sys.query#}}", "role_prefix": {"user": "", "assistant": ""}},
"tool_node_version": "2",
"agent_parameters": {
"model": {"value": {"provider": "langgenius/deepseek/deepseek", "model": "deepseek-v4-flash", "model_type": "llm", "mode": "chat", "completion_params": {"temperature": 0.7}}, "type": "mixed"},
"tools": {"value": [{
"provider_name": "<provider的UUID>",
"provider_id": "<provider的UUID>",
"type": "api",
"tool_name": "queryRecords",
"tool_label": "queryRecords",
"tool_configurations": {},
"enabled": true
}], "type": "mixed"},
"instruction": {"value": "<系统提示词>", "type": "constant"},
"query": {"value": "{{#sys.query#}}", "type": "mixed"},
"maximum_iterations": {"value": 3, "type": "constant"}
}
}
| # | 报错 | 根因 | 修法 |
|---|---|---|---|
| 1 | '' is not a valid ModelType |
模型参数缺 model_type 字段,且模型名要放 model 键(不是 name) |
按 4.2 补全 |
| 2 | Invalid plugin id <工具名> |
工具类型字段名是 type(值 api),我写成了 provider_type → 后端默认当 builtin |
改 "type": "api" |
| 3 | invalid input syntax for type uuid: "售后问题跟进表" |
后端把工具 dict 的 provider_name 字段当 provider_id(UUID) 用 |
provider_name 和 provider_id 都填 UUID |
⚠️ 核心教训:后端源码才是真相。三个坑都是读 agent_node.py 源码定位的:
tool.get("type") → 类型字段tool.get("provider_name") → 被塞进 AgentToolEntity.provider_id_fetch_model 读 value["provider"]/value["model"]/value["model_type"]# ① GET 拿当前 draft 的 hash
GET /console/api/apps/<app_id>/workflows/draft
# ② POST 提交(graph + features + hash + 环境变量)
POST /console/api/apps/<app_id>/workflows/draft
# payload: {"graph": {...}, "features": {...}, "hash": "<刚拿的hash>", "environment_variables": [], "conversation_variables": []}
# ③ 发布
POST /console/api/apps/<app_id>/workflows/publish
{"marked_name": "", "marked_comment": ""}
⚠️ hash 坑:每次 sync 后 hash 会变,下一次 sync 前必须重新 GET。否则报 draft_workflow_not_sync。
你是XX助手(原有身份约束)
【工具使用规则】
1. 用户询问某客户的售后记录时,调用 queryRecords 工具(用客户姓名做 keyword)
2. 用户要求新增记录且信息齐全时,先复述确认,用户明确同意后再调用 addRecord
3. 用户要求修改记录时,先 queryRecords 查出记录 id,再调用 updateRecord
4. 查询不到就如实回答"未查到相关记录",禁止编造数据
5. 新增/修改成功后回复"已录入/已更新",并简述内容
POST http://127.0.0.1:8088/v1/chat-messages
Header: Authorization: Bearer app-wecom-chat-20260815
Body: {"inputs": {}, "query": "帮我查一下刘丹的售后记录", "response_mode": "blocking", "user": "test_u"}
实测通过:AI 自动调 queryRecords,返回刘丹 3 条售后记录(地址/电话/问题类别/处理阶段/处理人/备注全部正确)。
测试期间的排障路径(以后遇到照这个顺序查):
docker logs dify_v1110_chns-api-1 → 看 Agent/工具执行错误(最快定位)8766 字样 → 请求已到代理,查 journalctl -u cordys-api-proxy→ HTTP 500 → 目标 API 报错,看代理记录的 body,多半是 AI 传错参数UPDATE tool_api_providers SET user_id='<真实账号id>' WHERE user_id='<临时账号id>'; ⚠️ 否则删临时账号后 Dify 界面报 user is None for api provider,工具全部不显示DELETE FROM accounts WHERE email='temp-admin@...')| 要改的 | 位置 |
|---|---|
| 目标 API 地址 | OpenAPI 的 servers.url + 代理脚本 CRM_BASE |
| 密钥 | 代理脚本 ACCESS_KEY/SECRET_KEY |
| 接口定义 | OpenAPI 的 paths + 字段映射 |
| 工具名 | OpenAPI operationId + Agent 节点 tools 数组 |
| 提示词 | instruction 的 value |
| 参数修正逻辑 | 代理 _forward 里的钩子 |
全流程耗时参考:无坑首接约 1 小时;复用本文档约 30 分钟(坑都写在上面了)。