Procházet zdrojové kódy

vault backup: 2026-08-18 19:59:14

zhensolid před 2 týdny
rodič
revize
dd8c97387c

+ 310 - 0
子兰工作/Dify AI工具接入指南.md

@@ -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 分钟(坑都写在上面了)。

+ 6 - 0
子兰工作/企业微信集成方案.md

@@ -130,6 +130,12 @@ send_msg 主动推送回复到企微
 - **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 字节