# 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: ' -H 'X-Secret-Key: ' # 从中提取:路径、方法、参数 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', '', '', 'active', CURRENT_TIMESTAMP); INSERT INTO tenant_account_joins (id, tenant_id, account_id, role, current) SELECT uuid_generate_v4(), '', 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": "", "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='' 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_id": "", "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//workflows/draft # ② POST 提交(graph + features + hash + 环境变量) POST /console/api/apps//workflows/draft # payload: {"graph": {...}, "features": {...}, "hash": "<刚拿的hash>", "environment_variables": [], "conversation_variables": []} # ③ 发布 POST /console/api/apps//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 分钟(坑都写在上面了)。