|
@@ -0,0 +1,310 @@
|
|
|
|
|
+# Dify AI 工具接入完整部署记录(照着做就能成功)
|
|
|
|
|
+
|
|
|
|
|
+> 实战日期: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 负责决策,代理负责认证安全,接口系统只做数据读写。**
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## 一、部署认证代理(10 分钟)
|
|
|
|
|
+
|
|
|
|
|
+**为什么需要代理**:Dify 自定义工具只支持配「一个」认证 Header,但 CordysCRM 要两个(X-Access-Key + X-Secret-Key)。代理接收 Dify 的单 Token 请求,校验后注入双密钥转发。
|
|
|
|
|
+
|
|
|
|
|
+### 1.1 代理脚本要点
|
|
|
|
|
+
|
|
|
|
|
+`/data/cordys_api_proxy.py`(systemd `cordys-api-proxy`,端口 8766):
|
|
|
|
|
+
|
|
|
|
|
+- 校验 `X-Proxy-Token`(不对就 403)
|
|
|
|
|
+- 转发时注入 `X-Access-Key` / `X-Secret-Key`
|
|
|
|
|
+- **支持 chunked 传输**(Dify 的 SSRF 代理用 chunked,不支持就读不到 body)
|
|
|
|
|
+- **参数修正钩子**:可强制替换 AI 传错的参数(如 customFormId)
|
|
|
|
|
+- 日志全部 `flush=True`(否则 journald 里看不到)
|
|
|
|
|
+
|
|
|
|
|
+```bash
|
|
|
|
|
+# 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
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 1.2 安全要点
|
|
|
|
|
+
|
|
|
|
|
+- firewalld **不开** 8766 → 外网访问不到,仅 Docker 内网可用
|
|
|
|
|
+- Dify api 容器访问宿主机的地址是 docker 网关:`172.20.0.1:8766`(`docker inspect dify_v1110_chns-api-1` 看 Gateway)
|
|
|
|
|
+
|
|
|
|
|
+### 1.3 验证
|
|
|
|
|
+
|
|
|
|
|
+```bash
|
|
|
|
|
+# 带 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'
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## 二、编写 OpenAPI 工具定义(30 分钟)
|
|
|
|
|
+
|
|
|
|
|
+### 2.1 先摸清接口(别猜)
|
|
|
|
|
+
|
|
|
|
|
+```bash
|
|
|
|
|
+# 用 api-docs 看接口全貌
|
|
|
|
|
+curl -s 'http://127.0.0.1:8081/v3/api-docs' \
|
|
|
|
|
+ -H 'X-Access-Key: <key>' -H 'X-Secret-Key: <secret>'
|
|
|
|
|
+# 从中提取:路径、方法、参数 schema、必填字段
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 2.2 查业务字段(下拉选项的 ID 映射)
|
|
|
|
|
+
|
|
|
|
|
+AI 填下拉框要填选项 ID 不是文字,所以 description 里必须写全映射:
|
|
|
|
|
+
|
|
|
|
|
+```
|
|
|
|
|
+公司=178642865321400000(子兰=...001/焕境=...002)
|
|
|
|
|
+问题处理阶段=...(未处理=...001/安排中=...002/处理中=...003/已处理=.../推迟处理=...)
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 2.3 OpenAPI JSON 结构
|
|
|
|
|
+
|
|
|
|
|
+```json
|
|
|
|
|
+{
|
|
|
|
|
+ "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 工具(10 分钟)
|
|
|
|
|
+
|
|
|
|
|
+### 3.1 UI 方式(手动)
|
|
|
|
|
+
|
|
|
|
|
+Dify 后台 → 工作室 → 企业微信 → 编辑 → 左下「工具」→ 自定义 → 创建 → OpenAPI schema 粘贴 → 认证选 API Key(Header `X-Proxy-Token`,值 `zilan-crm-proxy-2026`)。
|
|
|
|
|
+
|
|
|
|
|
+### 3.2 控制台 API 方式(自动化,本战采用)
|
|
|
|
|
+
|
|
|
|
|
+**Step 1:建临时管理员账号**(不改用户现有账号,用完即删)
|
|
|
|
|
+
|
|
|
|
|
+```sql
|
|
|
|
|
+-- 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 编码!)
|
|
|
|
|
+
|
|
|
|
|
+```bash
|
|
|
|
|
+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**
|
|
|
|
|
+
|
|
|
|
|
+```bash
|
|
|
|
|
+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**
|
|
|
|
|
+
|
|
|
|
|
+```sql
|
|
|
|
|
+SELECT id, name FROM tool_api_providers; -- 记下 UUID,Agent 节点要用
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## 四、改造 Chatflow:LLM → Agent 节点(40 分钟,本次踩坑重灾区)
|
|
|
|
|
+
|
|
|
|
|
+### 4.1 准备工作
|
|
|
|
|
+
|
|
|
|
|
+```bash
|
|
|
|
|
+# 看当前图结构
|
|
|
|
|
+SELECT id, type, version FROM workflows WHERE app_id='<app_id>' ORDER BY updated_at DESC;
|
|
|
|
|
+# draft = 编辑版本;version 是时间戳的 = 已发布版本
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 4.2 Agent 节点完整 JSON(正确版)
|
|
|
|
|
+
|
|
|
|
|
+```json
|
|
|
|
|
+{
|
|
|
|
|
+ "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"}
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 4.3 三个致命坑(每个都实测踩过)
|
|
|
|
|
+
|
|
|
|
|
+| # | 报错 | 根因 | 修法 |
|
|
|
|
|
+|---|------|------|------|
|
|
|
|
|
+| 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"]`
|
|
|
|
|
+
|
|
|
|
|
+### 4.4 同步 draft + 发布(hash 机制)
|
|
|
|
|
+
|
|
|
|
|
+```bash
|
|
|
|
|
+# ① 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`。
|
|
|
|
|
+
|
|
|
|
|
+### 4.5 提示词模板
|
|
|
|
|
+
|
|
|
|
|
+```
|
|
|
|
|
+你是XX助手(原有身份约束)
|
|
|
|
|
+
|
|
|
|
|
+【工具使用规则】
|
|
|
|
|
+1. 用户询问某客户的售后记录时,调用 queryRecords 工具(用客户姓名做 keyword)
|
|
|
|
|
+2. 用户要求新增记录且信息齐全时,先复述确认,用户明确同意后再调用 addRecord
|
|
|
|
|
+3. 用户要求修改记录时,先 queryRecords 查出记录 id,再调用 updateRecord
|
|
|
|
|
+4. 查询不到就如实回答"未查到相关记录",禁止编造数据
|
|
|
|
|
+5. 新增/修改成功后回复"已录入/已更新",并简述内容
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## 五、测试(真实数据验证)
|
|
|
|
|
+
|
|
|
|
|
+```python
|
|
|
|
|
+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 条售后记录(地址/电话/问题类别/处理阶段/处理人/备注全部正确)。
|
|
|
|
|
+
|
|
|
|
|
+**测试期间的排障路径**(以后遇到照这个顺序查):
|
|
|
|
|
+
|
|
|
|
|
+1. `docker logs dify_v1110_chns-api-1` → 看 Agent/工具执行错误(最快定位)
|
|
|
|
|
+2. 日志里看到 `8766` 字样 → 请求已到代理,查 `journalctl -u cordys-api-proxy`
|
|
|
|
|
+3. 代理日志显示 `→ HTTP 500` → 目标 API 报错,看代理记录的 body,多半是 AI 传错参数
|
|
|
|
|
+4. AI 传错参数(如把表单名当 ID)→ 在代理加**强制替换逻辑**(比教 AI 靠谱)
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## 六、收尾清单
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **转移工具 owner**:`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@...'`)
|
|
|
|
|
+- [ ] 更新 CLAUDE.md(3.11 节:新 Dify Key、代理服务、工具清单)
|
|
|
|
|
+- [ ] 更新 Obsidian(本文件 + 企业微信集成方案.md)
|
|
|
|
|
+- [ ] 桌面回溯记录(含回滚方法)
|
|
|
|
|
+- [ ] 企微实测一轮真实对话
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## 七、复用到其他系统(改哪里)
|
|
|
|
|
+
|
|
|
|
|
+| 要改的 | 位置 |
|
|
|
|
|
+|--------|------|
|
|
|
|
|
+| 目标 API 地址 | OpenAPI 的 servers.url + 代理脚本 CRM_BASE |
|
|
|
|
|
+| 密钥 | 代理脚本 ACCESS_KEY/SECRET_KEY |
|
|
|
|
|
+| 接口定义 | OpenAPI 的 paths + 字段映射 |
|
|
|
|
|
+| 工具名 | OpenAPI operationId + Agent 节点 tools 数组 |
|
|
|
|
|
+| 提示词 | instruction 的 value |
|
|
|
|
|
+| 参数修正逻辑 | 代理 _forward 里的钩子 |
|
|
|
|
|
+
|
|
|
|
|
+**全流程耗时参考**:无坑首接约 1 小时;复用本文档约 30 分钟(坑都写在上面了)。
|