├── .gitignore ├── LICENSE ├── README.md ├── README_EN.md ├── app.js ├── botType ├── chatHandler.js ├── completionHandler.js ├── utils.js └── workflowHandler.js ├── config └── logger.js ├── docs ├── template.zh.mdx ├── template_advanced_chat.zh.mdx ├── template_chat.zh.mdx └── template_workflow.zh.mdx ├── ecosystem.config.cjs ├── nodemon.json ├── package.json ├── project_flow.md ├── public ├── index.html └── logs.html └── vercel.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # 应用日志 11 | logs/ 12 | *.log 13 | combined-*.log 14 | error-*.log 15 | 16 | # Diagnostic reports (https://nodejs.org/api/report.html) 17 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 18 | 19 | # Runtime data 20 | pids 21 | *.pid 22 | *.seed 23 | *.pid.lock 24 | 25 | # Directory for instrumented libs generated by jscoverage/JSCover 26 | lib-cov 27 | 28 | # Coverage directory used by tools like istanbul 29 | coverage 30 | *.lcov 31 | 32 | # nyc test coverage 33 | .nyc_output 34 | 35 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 36 | .grunt 37 | 38 | # Bower dependency directory (https://bower.io/) 39 | bower_components 40 | 41 | # node-waf configuration 42 | .lock-wscript 43 | 44 | # Compiled binary addons (https://nodejs.org/api/addons.html) 45 | build/Release 46 | 47 | # Dependency directories 48 | node_modules/ 49 | jspm_packages/ 50 | 51 | # Snowpack dependency directory (https://snowpack.dev/) 52 | web_modules/ 53 | 54 | # TypeScript cache 55 | *.tsbuildinfo 56 | 57 | # Optional npm cache directory 58 | .npm 59 | 60 | # Optional eslint cache 61 | .eslintcache 62 | 63 | # Optional stylelint cache 64 | .stylelintcache 65 | 66 | # Microbundle cache 67 | .rpt2_cache/ 68 | .rts2_cache_cjs/ 69 | .rts2_cache_es/ 70 | .rts2_cache_umd/ 71 | 72 | # Optional REPL history 73 | .node_repl_history 74 | 75 | # Output of 'npm pack' 76 | *.tgz 77 | 78 | # Yarn Integrity file 79 | .yarn-integrity 80 | 81 | # dotenv environment variable files 82 | .env 83 | .env.development.local 84 | .env.test.local 85 | .env.production.local 86 | .env.local 87 | 88 | # parcel-bundler cache (https://parceljs.org/) 89 | .cache 90 | .parcel-cache 91 | 92 | # Next.js build output 93 | .next 94 | out 95 | 96 | # Nuxt.js build / generate output 97 | .nuxt 98 | dist 99 | 100 | # Gatsby files 101 | .cache/ 102 | # Comment in the public line in if your project uses Gatsby and not Next.js 103 | # https://nextjs.org/blog/next-9-1#public-directory-support 104 | # public 105 | 106 | # vuepress build output 107 | .vuepress/dist 108 | 109 | # vuepress v2.x temp and cache directory 110 | .temp 111 | .cache 112 | 113 | # Docusaurus cache and generated files 114 | .docusaurus 115 | 116 | # Serverless directories 117 | .serverless/ 118 | 119 | # FuseBox cache 120 | .fusebox/ 121 | 122 | # DynamoDB Local files 123 | .dynamodb/ 124 | 125 | # TernJS port file 126 | .tern-port 127 | 128 | # Stores VSCode versions used for testing VSCode extensions 129 | .vscode-test 130 | 131 | # yarn v2 132 | .yarn/cache 133 | .yarn/unplugged 134 | .yarn/build-state.yml 135 | .yarn/install-state.gz 136 | .pnp.* 137 | 138 | # pnpm 139 | .pnpm-lock.yaml 140 | 141 | # IDE 142 | .idea/ 143 | .vscode/ 144 | *.swp 145 | *.swo 146 | 147 | # OS 148 | .DS_Store 149 | Thumbs.db -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 NOV 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![banner](https://io.onenov.cn/file/202412240230660.webp) 2 | 3 | # Dify2OpenAI Gateway 4 | 5 | [![爱发电](https://afdian.moeci.com/13/badge.svg)](https://afdian.com/@orence) 6 | 简体中文版自述文件 7 | README in English 8 | 9 | **Dify2OpenAI** 是一个将 Dify 应用程序转换为 OpenAI API 接口的网关服务,使您可以使用 OpenAI API 兼容的方式访问 Dify 的 LLM、知识库、工具和工作流程。 10 | 11 | --- 12 | 13 | ## 特征 14 | 15 | - 将 Dify API 转换为 OpenAI API 16 | - 支持流式传输和阻止 17 | - 在 dify 上支持 Chat、Completion、Agent 和 Workflow bots API 18 | 19 | ## 支持 20 | 21 | - 图像支持 22 | - 变量支持 23 | - 持续对话 24 | - Workflow Bot 25 | - Streaming & Blocking 26 | - Agent & Chat bots 27 | 28 | --- 29 | 30 | ## 安装与启动 31 | 32 | ### 安装依赖 33 | 34 | ```bash 35 | git clone https://github.com/onenov/Dify2OpenAI.git 36 | cd Dify2OpenAI 37 | npm install 38 | ``` 39 | 40 | ### 启动服务 41 | 42 | 使用 PM2 启动(推荐): 43 | 44 | ```bash 45 | # 直接使用PM2命令 46 | pm2 start ecosystem.config.cjs 47 | 48 | # 或使用npm脚本 49 | npm run pm2:start 50 | ``` 51 | 52 | 或者使用普通方式启动: 53 | 54 | ```bash 55 | npm run start 56 | ``` 57 | 58 | 默认服务会在 `http://localhost:3099` 运行。 59 | 60 | ### PM2 常用命令 61 | 62 | 使用PM2直接管理: 63 | 64 | ```bash 65 | # 查看应用状态 66 | pm2 list 67 | 68 | # 查看日志 69 | pm2 logs 70 | 71 | # 重启应用 72 | pm2 restart dify2openai 73 | 74 | # 停止应用 75 | pm2 stop dify2openai 76 | 77 | # 删除应用 78 | pm2 delete dify2openai 79 | 80 | # 监控应用 81 | pm2 monit 82 | ``` 83 | 84 | 使用npm脚本管理: 85 | 86 | ```bash 87 | # 启动应用 88 | npm run pm2:start 89 | 90 | # 查看日志 91 | npm run pm2:logs 92 | 93 | # 重启应用 94 | npm run pm2:restart 95 | 96 | # 停止应用 97 | npm run pm2:stop 98 | 99 | # 删除应用 100 | npm run pm2:delete 101 | 102 | # 监控应用 103 | npm run pm2:monit 104 | ``` 105 | 106 | --- 107 | 108 | ## 一键部署 109 | 110 | ### Vercel 部署 111 | 112 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fonenov%2FDify2OpenAI) 113 | 114 | 1. 点击上方按钮跳转至 Vercel 115 | 2. 创建并导入项目 116 | 3. 直接部署即可,无需配置环境变量 117 | 4. 部署完成后,可以通过以下三种方式访问: 118 | - 在 Authorization Header 中传递所有配置 119 | - 在 Authorization Header 中传递 API_KEY,其他配置通过 model 参数传递 120 | - 在 Authorization Header 中传递 DIFY_API_URL,其他配置通过 model 参数传递 121 | 122 | 注意:Vercel 的无服务器函数有 10 秒的超时限制。 123 | 124 | --- 125 | 126 | ## 接入方式 127 | 128 | ### 接入方式一:所有配置在 Authorization Header 中 129 | 130 | **Authorization Header 格式:** 131 | 132 | ``` 133 | Authorization: Bearer DIFY_API_URL|API_KEY|BOT_TYPE|INPUT_VARIABLE|OUTPUT_VARIABLE 134 | ``` 135 | 136 | - 所有配置信息都通过 Authorization Header 传递。 137 | - `model` 参数设置为 `dify`。 138 | 139 | **示例:** 140 | 141 | ```bash 142 | Authorization: Bearer https://cloud.dify.ai/v1|app-xxxx|Chat 143 | ``` 144 | 145 | ### 接入方式二:Authorization Header 传递 API_KEY,model 参数传递其他配置 146 | 147 | **Authorization Header 格式:** 148 | 149 | ``` 150 | Authorization: Bearer API_KEY 151 | ``` 152 | 153 | **`model` 参数格式:** 154 | 155 | ``` 156 | "model": "dify|BOT_TYPE|DIFY_API_URL|INPUT_VARIABLE|OUTPUT_VARIABLE" 157 | ``` 158 | 159 | - Authorization Header 中只包含 `API_KEY`。 160 | - 其他配置信息通过请求体中的 `model` 参数传递。 161 | 162 | **示例:** 163 | 164 | ```bash 165 | Authorization: Bearer app-xxxx 166 | ``` 167 | 168 | ```json 169 | "model": "dify|Chat|https://cloud.dify.ai/v1" 170 | ``` 171 | 172 | ### 接入方式三:Authorization Header 传递 DIFY_API_URL,model 参数传递其他配置 173 | 174 | **Authorization Header 格式:** 175 | 176 | ``` 177 | Authorization: Bearer DIFY_API_URL 178 | ``` 179 | 180 | **`model` 参数格式:** 181 | 182 | ``` 183 | "model": "dify|API_KEY|BOT_TYPE|INPUT_VARIABLE|OUTPUT_VARIABLE" 184 | ``` 185 | 186 | - Authorization Header 中只包含 `DIFY_API_URL`。 187 | - 其他配置信息通过请求体中的 `model` 参数传递。 188 | 189 | **示例:** 190 | 191 | ```bash 192 | Authorization: Bearer https://cloud.dify.ai/v1 193 | ``` 194 | 195 | ```json 196 | "model": "dify|app-xxxx|Chat" 197 | ``` 198 | 199 | --- 200 | 201 | ## 开发指南 202 | 203 | ### 目录结构 204 | 205 | ``` 206 | . 207 | ├── app.js # 应用入口文件 208 | ├── botType/ # 机器人类型处理器 209 | │ ├── chatHandler.js # 聊天处理器 210 | │ ├── completionHandler.js # 补全处理器 211 | │ ├── utils.js # 工具函数 212 | │ └── workflowHandler.js # 工作流处理器 213 | ├── config/ # 配置文件目录 214 | │ └── logger.js # 日志配置 215 | ├── public/ # 静态文件目录 216 | │ └── index.html # API 文档页面 217 | ├── ecosystem.config.cjs # PM2 配置文件 218 | ├── nodemon.json # Nodemon 配置文件 219 | └── package.json # 项目配置文件 220 | ``` 221 | 222 | ### 开发模式配置 223 | 224 | 项目使用 nodemon 进行开发模式的热重载,配置如下: 225 | 226 | ```json 227 | { 228 | "watch": ["*.js", "botType/*.js", "config/*.js"], 229 | "ext": "js,json,env", 230 | "ignore": [ 231 | "node_modules/", 232 | "*.test.js", 233 | "logs/*", 234 | ".git", 235 | "public/*" 236 | ], 237 | "delay": "500", 238 | "verbose": true 239 | } 240 | ``` 241 | 242 | - `watch`: 监控的文件和目录 243 | - `ext`: 监控的文件扩展名 244 | - `ignore`: 忽略的文件和目录 245 | - `delay`: 延迟重启时间(毫秒) 246 | - `verbose`: 显示详细日志 247 | 248 | ### 开发流程 249 | 250 | 1. 克隆项目 251 | 252 | ```bash 253 | git clone https://github.com/onenov/Dify2OpenAI.git 254 | cd Dify2OpenAI 255 | ``` 256 | 257 | 2. 安装依赖 258 | 259 | ```bash 260 | npm install 261 | ``` 262 | 263 | 3. 启动开发服务器 264 | 265 | ```bash 266 | npm run dev 267 | ``` 268 | 269 | 4. 生产环境部署 270 | 271 | ```bash 272 | npm start 273 | # 或使用 PM2 274 | pm2 start ecosystem.config.cjs 275 | ``` 276 | 277 | ### 代码风格 278 | 279 | - 使用 ES Modules 导入导出 280 | - 异步操作使用 async/await 281 | - 错误处理使用 try/catch 282 | - 使用 winston 进行日志记录 283 | 284 | --- 285 | 286 | ## 日志系统 287 | 288 | ### 日志配置 289 | 290 | 默认情况下: 291 | 292 | - 生产环境(`npm start`):只记录错误级别日志,仅在控制台显示 293 | - 开发环境(`npm run dev`):记录所有级别日志,同时输出到控制台和文件 294 | 295 | 日志文件存储在 `logs` 目录下: 296 | 297 | - `combined-%DATE%.log`: 所有级别的日志 298 | - `error-%DATE%.log`: 仅错误级别的日志 299 | 300 | ### 日志级别 301 | 302 | 支持以下日志级别(按严重程度排序): 303 | 304 | - `error`: 错误信息 305 | - `warn`: 警告信息 306 | - `info`: 一般信息 307 | - `debug`: 调试信息 308 | 309 | ### 日志格式 310 | 311 | 每条日志包含以下信息: 312 | 313 | - 时间戳 314 | - 日志级别 315 | - 详细信息 316 | - 元数据(如果有) 317 | 318 | 示例: 319 | 320 | ```json 321 | { 322 | "level": "info", 323 | "message": "服务器启动成功", 324 | "timestamp": "2024-12-24T01:51:10+08:00", 325 | "port": 3099 326 | } 327 | ``` 328 | 329 | ### 日志轮转 330 | 331 | 日志文件按以下规则自动轮转: 332 | 333 | - 按日期轮转(每天一个新文件) 334 | - 单个文件最大 20MB 335 | - 保留最近 14 天的日志 336 | - 超出限制的日志文件会被自动删除 337 | 338 | ### 性能优化 339 | 340 | 为了优化性能,日志系统采用以下策略: 341 | 342 | - 使用缓冲写入,减少 I/O 操作 343 | - 异步写入,不阻塞主线程 344 | - 自动清理过期日志,控制磁盘占用 345 | 346 | --- 347 | 348 | ## 示例 349 | 350 | ### 基础对话 351 | 352 | #### 接入方式一 353 | 354 | ```bash 355 | curl http://localhost:3099/v1/chat/completions \ 356 | -H "Content-Type: application/json" \ 357 | -H "Authorization: Bearer https://cloud.dify.ai/v1|app-xxxx|Chat" \ 358 | -X POST \ 359 | -d '{ 360 | "model": "dify", 361 | "stream": true, 362 | "messages": [ 363 | { 364 | "role": "system", 365 | "content": "You are a helpful assistant." 366 | }, 367 | { 368 | "role": "user", 369 | "content": "你好" 370 | } 371 | ] 372 | }' 373 | ``` 374 | 375 | #### 接入方式二 376 | 377 | ```bash 378 | curl http://localhost:3099/v1/chat/completions \ 379 | -H "Content-Type: application/json" \ 380 | -H "Authorization: Bearer app-xxxx" \ 381 | -X POST \ 382 | -d '{ 383 | "model": "dify|Chat|https://cloud.dify.ai/v1", 384 | "stream": true, 385 | "messages": [ 386 | { 387 | "role": "system", 388 | "content": "You are a helpful assistant." 389 | }, 390 | { 391 | "role": "user", 392 | "content": "你好" 393 | } 394 | ] 395 | }' 396 | ``` 397 | 398 | #### 接入方式三 399 | 400 | ```bash 401 | curl http://localhost:3099/v1/chat/completions \ 402 | -H "Content-Type: application/json" \ 403 | -H "Authorization: Bearer https://cloud.dify.ai/v1" \ 404 | -X POST \ 405 | -d '{ 406 | "model": "dify|app-xxxx|Chat", 407 | "stream": true, 408 | "messages": [ 409 | { 410 | "role": "system", 411 | "content": "You are a helpful assistant." 412 | }, 413 | { 414 | "role": "user", 415 | "content": "你好" 416 | } 417 | ] 418 | }' 419 | ``` 420 | 421 | ### 带图片的对话 422 | 423 | #### 接入方式一 424 | 425 | ```bash 426 | curl http://localhost:3099/v1/chat/completions \ 427 | -H "Content-Type: application/json" \ 428 | -H "Authorization: Bearer https://cloud.dify.ai/v1|app-xxxx|Chat" \ 429 | -X POST \ 430 | -d '{ 431 | "model": "dify", 432 | "stream": true, 433 | "messages": [ 434 | { 435 | "role": "user", 436 | "content": [ 437 | "请分析这张图片。", 438 | { 439 | "type": "image_url", 440 | "image_url": { 441 | "url": "https://example.com/image.jpg" 442 | } 443 | } 444 | ] 445 | } 446 | ] 447 | }' 448 | ``` 449 | 450 | #### 接入方式二 451 | 452 | ```bash 453 | curl http://localhost:3099/v1/chat/completions \ 454 | -H "Content-Type: application/json" \ 455 | -H "Authorization: Bearer app-xxxx" \ 456 | -X POST \ 457 | -d '{ 458 | "model": "dify|Chat|https://cloud.dify.ai/v1", 459 | "stream": true, 460 | "messages": [ 461 | { 462 | "role": "user", 463 | "content": [ 464 | "请分析这张图片。", 465 | { 466 | "type": "image_url", 467 | "image_url": { 468 | "url": "https://example.com/image.jpg" 469 | } 470 | } 471 | ] 472 | } 473 | ] 474 | }' 475 | ``` 476 | 477 | #### 接入方式三 478 | 479 | ```bash 480 | curl http://localhost:3099/v1/chat/completions \ 481 | -H "Content-Type: application/json" \ 482 | -H "Authorization: Bearer https://cloud.dify.ai/v1" \ 483 | -X POST \ 484 | -d '{ 485 | "model": "dify|app-xxxx|Chat", 486 | "stream": true, 487 | "messages": [ 488 | { 489 | "role": "user", 490 | "content": [ 491 | "请分析这张图片。", 492 | { 493 | "type": "image_url", 494 | "image_url": { 495 | "url": "https://example.com/image.jpg" 496 | } 497 | } 498 | ] 499 | } 500 | ] 501 | }' 502 | ``` 503 | 504 | --- 505 | 506 | ## 注意事项 507 | 508 | - **参数替换**:请将示例中的 `https://cloud.dify.ai/v1`、`app-xxxx`、`BOT_TYPE` 等参数替换为您实际的值。 509 | - **`BOT_TYPE`**:可选值为 `Chat`、`Completion` 或 `Workflow`,请根据您的应用类型选择。 510 | - **`INPUT_VARIABLE` 和 `OUTPUT_VARIABLE`**:主要用于 `Workflow` 类型的应用,如果不需要可省略。 511 | - **`stream` 参数**:如果需要流式返回,请将 `stream` 设置为 `true`,否则可以省略或设置为 `false`。 512 | - **安全性**:请妥善保管您的 `API_KEY`,不要泄露给无关人员。 513 | 514 | --- 515 | 516 | ## 联系 517 | 518 | WeChat:**`AOKIEO`** | Mail: **`dev@orence.ai`** 519 | 520 | ## License 521 | 522 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 523 | 524 | --- 525 | 526 | ## 更新日志 527 | 528 | ### 2025-04-05更新 529 | 530 | #### 已修复的问题 531 | 532 | 1. **Base64图像处理问题** 533 | - **问题**:无法正确处理base64编码的图像数据,错误地将其作为URL处理 534 | - **修复**:添加了对以"data:"开头的数据的检测,并通过`uploadFileToDify`函数上传到Dify服务器 535 | - **效果**:现在可以正确处理base64图像数据,获取文件ID并使用`local_file`方式引用 536 | 537 | 2. **OpenAI标准格式消息处理** 538 | - **问题**:不支持OpenAI标准格式的字符串类型内容处理 539 | - **修复**:增加了`typeof content === "string"`的判断逻辑 540 | - **效果**:可以处理多种类型的消息格式,包括字符串和对象混合的内容数组 541 | 542 | 3. **文件类型自动识别** 543 | - **问题**:所有文件都被错误地识别为"image"类型,导致PDF等文件处理失败 544 | - **修复**:创建了`getFileExtension()`和`getFileType()`函数,根据扩展名识别文件类型 545 | - **效果**:正确区分document、image、audio、video等不同类型文件,确保Dify能正确处理 546 | 547 | 4. **多消息图片处理** 548 | - **问题**:只处理最后一条消息中的图片,忽略之前消息中的图片内容 549 | - **修复**:重构了消息处理逻辑,先扫描所有消息找图片,再从最后一条提取文本 550 | - **效果**:能够处理多条消息中的图片,不会遗漏任何消息中的图像内容 551 | 552 | 5. **PM2脚本便捷命令** 553 | - **改进**:添加了PM2管理的npm脚本命令 554 | - **效果**:可以通过`npm run pm2:*`命令更方便地管理应用 555 | 556 | --- 557 | 558 | **感谢您使用 Dify2OpenAI!如果您在使用过程中遇到任何问题,欢迎提问,我们将尽快协助您解决。** 559 | -------------------------------------------------------------------------------- /README_EN.md: -------------------------------------------------------------------------------- 1 | ![banner](https://io.onenov.cn/file/202412240230660.webp) 2 | 3 | # Dify2OpenAI Gateway 4 | 5 | [![爱发电](https://afdian.moeci.com/13/badge.svg)](https://afdian.com/@orence) 6 | 简体中文版自述文件 7 | README in English 8 | 9 | **Dify2OpenAI** is a gateway service that transforms Dify applications into OpenAI API-compatible interfaces, allowing you to access Dify's LLM, Knowledge Base, Tools, and Workflows using OpenAI API-compatible methods. 10 | 11 | --- 12 | 13 | ## Features 14 | 15 | - Convert Dify API to OpenAI API 16 | - Support streaming and blocking 17 | - Support Chat, Completion, Agent, and Workflow bots API on Dify 18 | 19 | ## Support 20 | 21 | - Image Support 22 | - Variable Support 23 | - Continuous Conversation 24 | - Workflow Bot 25 | - Streaming & Blocking 26 | - Agent & Chat bots 27 | 28 | --- 29 | 30 | ## Quick Start 31 | 32 | ### Installation & Startup 33 | 34 | ```bash 35 | git clone https://github.com/onenov/Dify2OpenAI.git 36 | cd Dify2OpenAI 37 | npm install 38 | ``` 39 | 40 | ### Start Service 41 | 42 | Using PM2 (Recommended): 43 | 44 | ```bash 45 | # Directly using PM2 command 46 | pm2 start ecosystem.config.cjs 47 | 48 | # Or using npm scripts 49 | npm run pm2:start 50 | ``` 51 | 52 | Or start normally: 53 | 54 | ```bash 55 | npm run start 56 | ``` 57 | 58 | The service will run on `http://localhost:3099` by default. 59 | 60 | ### PM2 Common Commands 61 | 62 | Manage directly with PM2: 63 | 64 | ```bash 65 | # View application status 66 | pm2 list 67 | 68 | # View logs 69 | pm2 logs 70 | 71 | # Restart application 72 | pm2 restart dify2openai 73 | 74 | # Stop application 75 | pm2 stop dify2openai 76 | 77 | # Delete application 78 | pm2 delete dify2openai 79 | 80 | # Monitor application 81 | pm2 monit 82 | ``` 83 | 84 | Manage using npm scripts: 85 | 86 | ```bash 87 | # Start application 88 | npm run pm2:start 89 | 90 | # View logs 91 | npm run pm2:logs 92 | 93 | # Restart application 94 | npm run pm2:restart 95 | 96 | # Stop application 97 | npm run pm2:stop 98 | 99 | # Delete application 100 | npm run pm2:delete 101 | 102 | # Monitor application 103 | npm run pm2:monit 104 | ``` 105 | 106 | --- 107 | 108 | ## One-Click Deploy 109 | 110 | ### Deploy on Vercel 111 | 112 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fonenov%2FDify2OpenAI) 113 | 114 | 1. Click the button above to go to Vercel 115 | 2. Create and import the project 116 | 3. Deploy directly, no environment variables needed 117 | 4. After deployment, you can access it in three ways: 118 | - Pass all configurations in the Authorization Header 119 | - Pass API_KEY in the Authorization Header, other configurations through the model parameter 120 | - Pass DIFY_API_URL in the Authorization Header, other configurations through the model parameter 121 | 122 | Note: Vercel serverless functions have a 10-second timeout limit. 123 | 124 | --- 125 | 126 | ## Access Methods 127 | 128 | ### Method One: All Configurations in Authorization Header 129 | 130 | **Authorization Header Format:** 131 | 132 | ``` 133 | Authorization: Bearer DIFY_API_URL|API_KEY|BOT_TYPE|INPUT_VARIABLE|OUTPUT_VARIABLE 134 | ``` 135 | 136 | Example: 137 | 138 | ``` 139 | Authorization: Bearer https://cloud.dify.ai/v1|app-xxxx|Chat 140 | ``` 141 | 142 | Set `model` parameter to `dify` 143 | 144 | ### Method Two: API_KEY in Authorization Header 145 | 146 | **Authorization Header Format:** 147 | 148 | ``` 149 | Authorization: Bearer API_KEY 150 | ``` 151 | 152 | **Model Parameter Format:** 153 | 154 | ``` 155 | "model": "dify|BOT_TYPE|DIFY_API_URL|INPUT_VARIABLE|OUTPUT_VARIABLE" 156 | ``` 157 | 158 | Example: 159 | 160 | ``` 161 | Authorization: Bearer app-xxxx 162 | "model": "dify|Chat|https://cloud.dify.ai/v1" 163 | ``` 164 | 165 | ### Method Three: DIFY_API_URL in Authorization Header 166 | 167 | **Authorization Header Format:** 168 | 169 | ``` 170 | Authorization: Bearer DIFY_API_URL 171 | ``` 172 | 173 | **Model Parameter Format:** 174 | 175 | ``` 176 | "model": "dify|API_KEY|BOT_TYPE|INPUT_VARIABLE|OUTPUT_VARIABLE" 177 | ``` 178 | 179 | Example: 180 | 181 | ``` 182 | Authorization: Bearer https://cloud.dify.ai/v1 183 | "model": "dify|app-xxxx|Chat" 184 | ``` 185 | 186 | ## Examples 187 | 188 | ### Basic Chat Examples 189 | 190 | #### Method One 191 | 192 | ```bash 193 | curl http://localhost:3099/v1/chat/completions \ 194 | -H "Content-Type: application/json" \ 195 | -H "Authorization: Bearer https://cloud.dify.ai/v1|app-xxxx|Chat" \ 196 | -X POST \ 197 | -d '{ 198 | "model": "dify", 199 | "stream": true, 200 | "messages": [ 201 | { 202 | "role": "system", 203 | "content": "You are a helpful assistant." 204 | }, 205 | { 206 | "role": "user", 207 | "content": "Hello" 208 | } 209 | ] 210 | }' 211 | ``` 212 | 213 | #### Method Two 214 | 215 | ```bash 216 | curl http://localhost:3099/v1/chat/completions \ 217 | -H "Content-Type: application/json" \ 218 | -H "Authorization: Bearer app-xxxx" \ 219 | -X POST \ 220 | -d '{ 221 | "model": "dify|Chat|https://cloud.dify.ai/v1", 222 | "stream": true, 223 | "messages": [ 224 | { 225 | "role": "system", 226 | "content": "You are a helpful assistant." 227 | }, 228 | { 229 | "role": "user", 230 | "content": "Hello" 231 | } 232 | ] 233 | }' 234 | ``` 235 | 236 | #### Method Three 237 | 238 | ```bash 239 | curl http://localhost:3099/v1/chat/completions \ 240 | -H "Content-Type: application/json" \ 241 | -H "Authorization: Bearer https://cloud.dify.ai/v1" \ 242 | -X POST \ 243 | -d '{ 244 | "model": "dify|app-xxxx|Chat", 245 | "stream": true, 246 | "messages": [ 247 | { 248 | "role": "system", 249 | "content": "You are a helpful assistant." 250 | }, 251 | { 252 | "role": "user", 253 | "content": "Hello" 254 | } 255 | ] 256 | }' 257 | ``` 258 | 259 | ### Image Chat Examples 260 | 261 | #### Method One 262 | 263 | ```bash 264 | curl http://localhost:3099/v1/chat/completions \ 265 | -H "Content-Type: application/json" \ 266 | -H "Authorization: Bearer https://cloud.dify.ai/v1|app-xxxx|Chat" \ 267 | -X POST \ 268 | -d '{ 269 | "model": "dify", 270 | "stream": true, 271 | "messages": [ 272 | { 273 | "role": "user", 274 | "content": [ 275 | "Please analyze this image.", 276 | { 277 | "type": "image_url", 278 | "image_url": { 279 | "url": "https://example.com/image.jpg" 280 | } 281 | } 282 | ] 283 | } 284 | ] 285 | }' 286 | ``` 287 | 288 | #### Method Two 289 | 290 | ```bash 291 | curl http://localhost:3099/v1/chat/completions \ 292 | -H "Content-Type: application/json" \ 293 | -H "Authorization: Bearer app-xxxx" \ 294 | -X POST \ 295 | -d '{ 296 | "model": "dify|Chat|https://cloud.dify.ai/v1", 297 | "stream": true, 298 | "messages": [ 299 | { 300 | "role": "user", 301 | "content": [ 302 | "Please analyze this image.", 303 | { 304 | "type": "image_url", 305 | "image_url": { 306 | "url": "https://example.com/image.jpg" 307 | } 308 | } 309 | ] 310 | } 311 | ] 312 | }' 313 | ``` 314 | 315 | #### Method Three 316 | 317 | ```bash 318 | curl http://localhost:3099/v1/chat/completions \ 319 | -H "Content-Type: application/json" \ 320 | -H "Authorization: Bearer https://cloud.dify.ai/v1" \ 321 | -X POST \ 322 | -d '{ 323 | "model": "dify|app-xxxx|Chat", 324 | "stream": true, 325 | "messages": [ 326 | { 327 | "role": "user", 328 | "content": [ 329 | "Please analyze this image.", 330 | { 331 | "type": "image_url", 332 | "image_url": { 333 | "url": "https://example.com/image.jpg" 334 | } 335 | } 336 | ] 337 | } 338 | ] 339 | }' 340 | ``` 341 | 342 | --- 343 | 344 | ## Notes 345 | 346 | - **Parameter Replacement**: Please replace parameters such as `https://cloud.dify.ai/v1`, `app-xxxx`, `BOT_TYPE`, etc. with your actual values. 347 | - **`BOT_TYPE`**: Available values are `Chat`, `Completion`, or `Workflow`, choose according to your application type. 348 | - **`INPUT_VARIABLE` and `OUTPUT_VARIABLE`**: Mainly used for `Workflow` type applications, can be omitted if not needed. 349 | - **`stream` Parameter**: If you need streaming responses, set `stream` to `true`, otherwise you can omit it or set it to `false`. 350 | - **Security**: Keep your `API_KEY` secure and do not share it with unauthorized individuals. 351 | 352 | --- 353 | 354 | ## Development Guide 355 | 356 | ### Directory Structure 357 | 358 | ``` 359 | . 360 | ├── app.js # Application entry file 361 | ├── botType/ # Bot type handlers 362 | │ ├── chatHandler.js # Chat handler 363 | │ ├── completionHandler.js # Completion handler 364 | │ ├── utils.js # Utility functions 365 | │ └── workflowHandler.js # Workflow handler 366 | ├── config/ # Configuration files 367 | │ └── logger.js # Logger configuration 368 | ├── public/ # Static files directory 369 | │ └── index.html # API documentation page 370 | ├── ecosystem.config.cjs # PM2 configuration file 371 | ├── nodemon.json # Nodemon configuration file 372 | └── package.json # Project configuration file 373 | ``` 374 | 375 | ### Development Mode Configuration 376 | 377 | The project uses nodemon for hot reloading in development mode: 378 | 379 | ```json 380 | { 381 | "watch": ["*.js", "botType/*.js", "config/*.js"], 382 | "ext": "js,json,env", 383 | "ignore": [ 384 | "node_modules/", 385 | "*.test.js", 386 | "logs/*", 387 | ".git", 388 | "public/*" 389 | ], 390 | "delay": "500", 391 | "verbose": true 392 | } 393 | ``` 394 | 395 | - `watch`: Files and directories to monitor 396 | - `ext`: File extensions to monitor 397 | - `ignore`: Files and directories to ignore 398 | - `delay`: Restart delay in milliseconds 399 | - `verbose`: Show detailed logs 400 | 401 | ### Development Process 402 | 403 | 1. Clone the project 404 | 405 | ```bash 406 | git clone https://github.com/onenov/Dify2OpenAI.git 407 | cd Dify2OpenAI 408 | ``` 409 | 410 | 2. Install dependencies 411 | 412 | ```bash 413 | npm install 414 | ``` 415 | 416 | 3. Start development server 417 | 418 | ```bash 419 | npm run dev 420 | ``` 421 | 422 | 4. Production deployment 423 | 424 | ```bash 425 | npm start 426 | # or using PM2 427 | pm2 start ecosystem.config.cjs 428 | ``` 429 | 430 | ### Code Style 431 | 432 | - Use ES Modules for imports/exports 433 | - Use async/await for asynchronous operations 434 | - Use try/catch for error handling 435 | - Use winston for logging 436 | 437 | --- 438 | 439 | ## Logging System 440 | 441 | ### Log Configuration 442 | 443 | By default: 444 | 445 | - Production environment (`npm start`): Only logs error level, console output only 446 | - Development environment (`npm run dev`): Logs all levels, outputs to both console and file 447 | 448 | Log files are stored in the `logs` directory: 449 | 450 | - `combined-%DATE%.log`: Logs of all levels 451 | - `error-%DATE%.log`: Error level logs only 452 | 453 | ### Log Levels 454 | 455 | Supports the following log levels (in order of severity): 456 | 457 | - `error`: Error messages 458 | - `warn`: Warning messages 459 | - `info`: General information 460 | - `debug`: Debug information 461 | 462 | ### Log Format 463 | 464 | Each log entry contains: 465 | 466 | - Timestamp 467 | - Log level 468 | - Detailed message 469 | - Metadata (if any) 470 | 471 | Example: 472 | 473 | ```json 474 | { 475 | "level": "info", 476 | "message": "Server started successfully", 477 | "timestamp": "2024-12-24T01:51:10+08:00", 478 | "port": 3099 479 | } 480 | ``` 481 | 482 | ### Log Rotation 483 | 484 | Log files are automatically rotated according to: 485 | 486 | - Daily rotation (new file each day) 487 | - Maximum file size of 20MB 488 | - Keep logs for the last 14 days 489 | - Automatically delete logs exceeding limits 490 | 491 | ### Performance Optimization 492 | 493 | For performance, the logging system: 494 | 495 | - Uses buffered writing to reduce I/O operations 496 | - Writes asynchronously to avoid blocking the main thread 497 | - Automatically cleans up expired logs to control disk usage 498 | 499 | --- 500 | 501 | ## Support 502 | 503 | WeChat:**`AOKIEO`** | Mail: **`dev@orence.ai`** 504 | 505 | ## License 506 | 507 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 508 | 509 | --- 510 | 511 | **Thank you for using Dify2OpenAI! If you encounter any problems during use, please feel free to ask and we will assist you as soon as possible.** -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | // app.js 2 | 3 | import express from "express"; 4 | import bodyParser from "body-parser"; 5 | import fetch from "node-fetch"; 6 | import FormData from "form-data"; 7 | import { PassThrough } from "stream"; 8 | import { log } from './config/logger.js'; 9 | import fs from 'fs'; 10 | import path from 'path'; 11 | import { fileURLToPath } from 'url'; 12 | import { WebSocketServer } from 'ws'; 13 | import { Tail } from 'tail'; 14 | import http from 'http'; 15 | 16 | // 引入各 bot 类型的处理器 17 | import chatHandler from "./botType/chatHandler.js"; 18 | import completionHandler from "./botType/completionHandler.js"; 19 | import workflowHandler from "./botType/workflowHandler.js"; 20 | 21 | // 从 utils.js 中导入工具函数 22 | import { 23 | sanitizeLog, 24 | logRequest, 25 | logResponse, 26 | logApiCall, 27 | generateId, 28 | } from "./botType/utils.js"; 29 | 30 | // 定义 parseConfig 函数 31 | function parseConfig(authHeader, modelParam) { 32 | log("debug", "开始解析配置", { 33 | authHeader: authHeader ? authHeader.substring(0, 20) + "..." : "No Auth Header", 34 | modelParam, 35 | }); 36 | 37 | let config = {}; 38 | 39 | // 从 Authorization header 获取信息 40 | if (!authHeader || !authHeader.startsWith("Bearer ")) { 41 | log("error", "缺少或无效的 Authorization header"); 42 | throw new Error("Missing or invalid Authorization header"); 43 | } 44 | 45 | const [_, token] = authHeader.split("Bearer "); 46 | const tokenParts = token.split("|"); 47 | 48 | // 方式一:所有信息都在 Authorization header 中 49 | if (tokenParts.length >= 3) { 50 | const [difyApiUrl, apiKey, botType, inputVariable, outputVariable] = tokenParts; 51 | config = { 52 | DIFY_API_URL: difyApiUrl, 53 | API_KEY: apiKey, 54 | BOT_TYPE: botType, 55 | INPUT_VARIABLE: inputVariable || "", 56 | OUTPUT_VARIABLE: outputVariable || "", 57 | }; 58 | log("info", "配置解析成功 - 方式一", config); 59 | return config; 60 | } 61 | 62 | // 方式二和方式三的处理 63 | if (tokenParts.length === 1) { 64 | const singleValue = tokenParts[0].trim(); 65 | 66 | // 解析 model 参数 67 | if (!modelParam) { 68 | log("error", "缺少 model 参数"); 69 | throw new Error("Missing model parameter"); 70 | } 71 | 72 | const modelParts = modelParam.split("|"); 73 | if (modelParts[0] !== "dify" || modelParts.length < 3) { 74 | log("error", "无效的 model 参数格式"); 75 | throw new Error("Invalid model parameter format"); 76 | } 77 | 78 | // 方式二:Authorization 是 API_KEY 79 | if (singleValue.length > 0 && !singleValue.includes("http")) { 80 | config.API_KEY = singleValue; 81 | const [_, botType, difyApiUrl, inputVariable, outputVariable] = modelParts; 82 | config.DIFY_API_URL = difyApiUrl; 83 | config.BOT_TYPE = botType; 84 | config.INPUT_VARIABLE = inputVariable || ""; 85 | config.OUTPUT_VARIABLE = outputVariable || ""; 86 | log("info", "配置解析成功 - 方式二", config); 87 | } 88 | // 方式三:Authorization 是 DIFY_API_URL 89 | else { 90 | config.DIFY_API_URL = singleValue; 91 | const [_, apiKey, botType, inputVariable, outputVariable] = modelParts; 92 | config.API_KEY = apiKey; 93 | config.BOT_TYPE = botType; 94 | config.INPUT_VARIABLE = inputVariable || ""; 95 | config.OUTPUT_VARIABLE = outputVariable || ""; 96 | log("info", "配置解析成功 - 方式三", config); 97 | } 98 | } 99 | 100 | // 验证必要的配置参数 101 | if (!config.DIFY_API_URL || !config.API_KEY || !config.BOT_TYPE) { 102 | log("error", "缺少必要的配置参数", { 103 | DIFY_API_URL: !!config.DIFY_API_URL, 104 | API_KEY: !!config.API_KEY, 105 | BOT_TYPE: !!config.BOT_TYPE, 106 | config 107 | }); 108 | throw new Error("Missing required configuration parameters"); 109 | } 110 | 111 | return config; 112 | } 113 | 114 | const app = express(); 115 | 116 | // 配置 CORS 中间件,允许所有跨域请求 117 | app.use((req, res, next) => { 118 | res.header("Access-Control-Allow-Origin", "*"); 119 | res.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS"); 120 | res.header("Access-Control-Allow-Headers", "*"); 121 | res.header("Access-Control-Allow-Credentials", true); 122 | res.header("Access-Control-Max-Age", "86400"); 123 | 124 | if (req.method === "OPTIONS") { 125 | return res.sendStatus(200); 126 | } 127 | next(); 128 | }); 129 | 130 | // 配置静态文件服务 131 | app.use(express.static('public')); 132 | 133 | // 配置请求体解析 134 | app.use(express.json({ limit: "100mb" })); 135 | app.use(express.urlencoded({ limit: "100mb", extended: true })); 136 | app.use(express.raw({ limit: "100mb" })); 137 | 138 | // 添加请求体日志(仅开发环境) 139 | app.use((req, res, next) => { 140 | if (process.env.NODE_ENV === 'development' && req.method === "POST") { 141 | log('debug', 'Raw request body', { body: req.body }); 142 | } 143 | next(); 144 | }); 145 | 146 | app.use((req, res, next) => { 147 | if (process.env.NODE_ENV === 'development') { 148 | log('info', 'Incoming request', { 149 | method: req.method, 150 | path: req.path 151 | }); 152 | } 153 | next(); 154 | }); 155 | 156 | // 根路径 157 | app.get("/", (req, res) => { 158 | res.sendFile(path.join(__dirname, 'public/index.html')); 159 | }); 160 | 161 | // 获取模型列表 162 | app.get("/v1/models", (req, res) => { 163 | const models = { 164 | object: "list", 165 | data: [ 166 | { 167 | id: "dify", 168 | object: "model", 169 | owned_by: "dify", 170 | permission: null, 171 | capabilities: { 172 | vision: true, 173 | file_processing: true, 174 | }, 175 | }, 176 | ], 177 | }; 178 | res.json(models); 179 | }); 180 | 181 | // 处理 /v1/chat/completions 请求 182 | app.post("/v1/chat/completions", async (req, res) => { 183 | const requestId = generateId(); 184 | const startTime = Date.now(); 185 | 186 | // 记录请求详情 187 | logRequest(req, requestId); 188 | 189 | const authHeader = req.headers.authorization; 190 | if (!authHeader) { 191 | const error = new Error("Missing Authorization header"); 192 | log("error", "缺少 Authorization header", { 193 | requestId, 194 | error: error.message, 195 | stack: error.stack, 196 | }); 197 | return res.status(401).json({ error: "Missing Authorization header" }); 198 | } 199 | 200 | try { 201 | // 解析配置 202 | const config = parseConfig(authHeader, req.body.model); 203 | const botType = config.BOT_TYPE; 204 | 205 | log("debug", "请求参数处理", { 206 | requestId, 207 | botType, 208 | }); 209 | 210 | // 根据 botType 分发请求 211 | if (botType === "Chat") { 212 | await chatHandler.handleRequest(req, res, config, requestId, startTime); 213 | } else if (botType === "Completion") { 214 | await completionHandler.handleRequest( 215 | req, 216 | res, 217 | config, 218 | requestId, 219 | startTime 220 | ); 221 | } else if (botType === "Workflow") { 222 | await workflowHandler.handleRequest( 223 | req, 224 | res, 225 | config, 226 | requestId, 227 | startTime 228 | ); 229 | } else { 230 | log("error", "无效的 bot 类型", { botType }); 231 | throw new Error("Invalid bot type in configuration."); 232 | } 233 | } catch (error) { 234 | // 详细记录错误信息 235 | log("error", "处理请求时发生错误", { 236 | requestId, 237 | error: { 238 | message: error.message, 239 | stack: error.stack, 240 | name: error.name, 241 | }, 242 | timestamp: new Date().toISOString(), 243 | }); 244 | res.status(500).json({ error: error.message }); 245 | } 246 | }); 247 | 248 | const server = http.createServer(app); 249 | 250 | server.listen(process.env.PORT || 3099, () => { 251 | log('info', '服务器启动成功', { 252 | port: process.env.PORT || 3099, 253 | env: process.env.NODE_ENV || 'development' 254 | }); 255 | }); 256 | -------------------------------------------------------------------------------- /botType/chatHandler.js: -------------------------------------------------------------------------------- 1 | // chatHandler.js 2 | 3 | import fetch from "node-fetch"; 4 | import { PassThrough } from "stream"; 5 | import FormData from "form-data"; 6 | import { log } from '../config/logger.js'; 7 | import { logApiCall, generateId, getFileExtension, getFileType } from "./utils.js"; 8 | 9 | // 导入实用工具函数(假设定义在 utils.js 中) 10 | 11 | // 上传文件到 Dify 并获取文件 ID 12 | async function uploadFileToDify(base64Data, config, userId) { 13 | try { 14 | // 解析 base64 数据 URL,提取 contentType 和 base64 字符串 15 | const matches = base64Data.match(/^data:(.+);base64,(.+)$/); 16 | if (!matches || matches.length !== 3) { 17 | throw new Error("Invalid base64 data"); 18 | } 19 | let contentType = matches[1]; 20 | const base64String = matches[2]; 21 | let fileData = Buffer.from(base64String, "base64"); 22 | 23 | // 如果 contentType 是 'image/jpg',将其调整为 'image/jpeg' 24 | if (contentType === "image/jpg") { 25 | contentType = "image/jpeg"; 26 | } 27 | 28 | // 从 contentType 确定文件扩展名 29 | const fileExtension = contentType.split("/")[1]; // 例如 'jpeg'、'png'、'gif' 30 | 31 | // 使用扩展名创建文件名 32 | const filename = `image.${fileExtension}`; 33 | 34 | // 创建 FormData 并包含 'user' 字段 35 | const form = new FormData(); 36 | form.append("file", fileData, { 37 | filename: filename, 38 | contentType: contentType, 39 | }); 40 | form.append("user", userId); // 使用提供的用户标识符 41 | 42 | // 记录文件上传请求的详细信息 43 | log("info", "正在上传文件到 Dify", { 44 | url: `${config.DIFY_API_URL}/files/upload`, 45 | headers: { 46 | Authorization: `Bearer ${config.API_KEY}`, 47 | ...form.getHeaders(), 48 | }, 49 | formData: "<>", // 出于安全考虑,不记录实际文件数据 50 | }); 51 | 52 | // 发送上传请求 53 | const response = await fetch(`${config.DIFY_API_URL}/files/upload`, { 54 | method: "POST", 55 | headers: { 56 | Authorization: `Bearer ${config.API_KEY}`, 57 | ...form.getHeaders(), 58 | }, 59 | body: form, 60 | }); 61 | 62 | // 记录文件上传响应的详细信息 63 | log("info", "文件上传响应", { 64 | status: response.status, 65 | statusText: response.statusText, 66 | }); 67 | 68 | if (!response.ok) { 69 | const errorBody = await response.text(); 70 | log("error", "文件上传失败", { 71 | status: response.status, 72 | statusText: response.statusText, 73 | errorBody: errorBody, 74 | }); 75 | throw new Error( 76 | `文件上传失败: ${response.status} ${response.statusText}: ${errorBody}` 77 | ); 78 | } 79 | 80 | const result = await response.json(); 81 | log("info", "文件上传成功", { fileId: result.id }); 82 | return result.id; // 返回文件 ID 83 | } catch (error) { 84 | console.error("上传文件出错:", error); 85 | throw error; 86 | } 87 | } 88 | 89 | // 处理 Chat 类型的请求 90 | async function handleRequest(req, res, config, requestId, startTime) { 91 | try { 92 | const apiPath = "/chat-messages"; 93 | const data = req.body; 94 | const messages = data.messages; 95 | let queryString = ""; 96 | let files = []; 97 | 98 | // 记录收到的请求头和请求体 99 | log("info", "收到请求", { 100 | requestId, 101 | headers: req.headers, 102 | body: data, 103 | }); 104 | 105 | const userId = "apiuser"; // 如果可用,替换为实际的用户 ID 106 | const lastMessage = messages[messages.length - 1]; 107 | 108 | // 第一步:先扫描所有消息中的图片内容 109 | log("info", "开始扫描所有消息中的图片", { requestId, messageCount: messages.length }); 110 | for (const message of messages) { 111 | if (Array.isArray(message.content)) { 112 | for (const content of message.content) { 113 | if (content.type === "image_url" && content.image_url && content.image_url.url) { 114 | const imageUrl = content.image_url.url; 115 | 116 | // 检查URL是否为base64数据 117 | if (imageUrl.startsWith('data:')) { 118 | // 是base64数据,需要上传 119 | const fileExt = getFileExtension(imageUrl); 120 | const fileType = getFileType(fileExt); 121 | log("info", "检测到base64数据,准备上传", { requestId, fileType, fileExt }); 122 | const fileId = await uploadFileToDify( 123 | imageUrl, 124 | config, 125 | userId 126 | ); 127 | files.push({ 128 | type: fileType, 129 | transfer_method: "local_file", 130 | upload_file_id: fileId, 131 | }); 132 | } else { 133 | // 是真正的URL,直接使用remote_url方式 134 | const fileExt = getFileExtension(imageUrl); 135 | const fileType = getFileType(fileExt); 136 | log("info", "检测到远程文件URL", { requestId, url: imageUrl.substring(0, 30) + '...', fileType, fileExt }); 137 | files.push({ 138 | type: fileType, 139 | transfer_method: "remote_url", 140 | url: imageUrl, 141 | }); 142 | } 143 | } 144 | } 145 | } 146 | } 147 | 148 | // 第二步:从最后一条消息中提取查询文本 149 | if (Array.isArray(lastMessage.content)) { 150 | for (const content of lastMessage.content) { 151 | // 处理字符串类型的内容(OpenAI格式) 152 | if (typeof content === "string") { 153 | queryString += content + "\n"; 154 | } 155 | // 处理对象类型的内容 156 | else if (content.type === "text") { 157 | queryString += content.text + "\n"; 158 | } 159 | // 注意:这里不再重复处理image_url,因为已经在上面处理过了 160 | } 161 | queryString = queryString.trim(); // 去除末尾的换行符 162 | } else { 163 | queryString = lastMessage.content; 164 | } 165 | 166 | // 构建对话历史,不包括最后一条消息 167 | const history = messages 168 | .slice(0, -1) 169 | .map((message) => { 170 | // 处理可能为数组的消息内容 171 | let contentText = ""; 172 | if (Array.isArray(message.content)) { 173 | for (const content of message.content) { 174 | if (content.type === "text") { 175 | contentText += content.text + "\n"; 176 | } 177 | // 注意:如果需要,可以不同地处理 'image_url' 178 | } 179 | contentText = contentText.trim(); 180 | } else { 181 | contentText = message.content; 182 | } 183 | return `${message.role}: ${contentText}`; 184 | }) 185 | .join("\n"); 186 | 187 | // 如果存在历史记录,将其包含在 queryString 中 188 | if (history) { 189 | queryString = `Here is our talk history:\n'''\n${history}\n'''\n\nHere is my question:\n${queryString}`; 190 | } 191 | 192 | // 记录消息处理 193 | log("info", "处理 Chat 类型消息", { 194 | requestId, 195 | messageCount: messages.length, 196 | lastMessageRole: lastMessage.role, 197 | hasFiles: files.length > 0, 198 | queryString, 199 | files, 200 | }); 201 | 202 | const stream = data.stream !== undefined ? data.stream : false; 203 | 204 | // 为 Dify 准备请求体 205 | const requestBody = { 206 | inputs: {}, 207 | query: queryString, 208 | response_mode: "streaming", 209 | conversation_id: "", // 如果可用,使用现有的 conversation_id 210 | user: userId, // 确保一致的 'user' 标识符 211 | auto_generate_name: false, 212 | files: files, 213 | }; 214 | 215 | // 记录将要发送到 Dify 的请求载荷 216 | log("info", "发送请求到 Dify", { 217 | requestId, 218 | url: config.DIFY_API_URL + apiPath, 219 | method: "POST", 220 | headers: { 221 | "Content-Type": "application/json", 222 | Authorization: `Bearer ${config.API_KEY}`, 223 | }, 224 | body: requestBody, 225 | }); 226 | 227 | // 发送请求到 Dify 228 | const resp = await fetch(config.DIFY_API_URL + apiPath, { 229 | method: "POST", 230 | headers: { 231 | "Content-Type": "application/json", 232 | Authorization: `Bearer ${config.API_KEY}`, 233 | }, 234 | body: JSON.stringify(requestBody), 235 | }); 236 | 237 | // 记录 API 调用的持续时间 238 | const apiCallDuration = Date.now() - startTime; 239 | logApiCall(requestId, config, apiPath, apiCallDuration); 240 | 241 | // 记录 Dify 的响应状态 242 | log("info", "收到 Dify 响应", { 243 | requestId, 244 | status: resp.status, 245 | statusText: resp.statusText, 246 | }); 247 | 248 | if (!resp.ok) { 249 | const errorBody = await resp.text(); 250 | log("error", "Dify API 请求失败", { 251 | requestId, 252 | status: resp.status, 253 | statusText: resp.statusText, 254 | errorBody: errorBody, 255 | }); 256 | res.status(resp.status).send(errorBody); 257 | return; 258 | } 259 | 260 | let isResponseEnded = false; 261 | 262 | if (stream) { 263 | res.setHeader("Content-Type", "text/event-stream"); 264 | let buffer = ""; 265 | const responseStream = resp.body 266 | .pipe(new PassThrough()) 267 | .on("data", (chunk) => { 268 | buffer += chunk.toString(); 269 | let lines = buffer.split("\n"); 270 | 271 | for (let i = 0; i < lines.length - 1; i++) { 272 | let line = lines[i].trim(); 273 | 274 | if (!line.startsWith("data:")) continue; 275 | line = line.slice(5).trim(); 276 | let chunkObj; 277 | try { 278 | if (line.startsWith("{")) { 279 | chunkObj = JSON.parse(line); 280 | } else { 281 | continue; 282 | } 283 | } catch (error) { 284 | console.error("解析 chunk 出错:", error); 285 | continue; 286 | } 287 | 288 | // 记录每个 chunk 的内容 289 | // log('debug', '处理 chunk', { 290 | // requestId, 291 | // chunkObj, 292 | // }); 293 | 294 | if ( 295 | chunkObj.event === "message" || 296 | chunkObj.event === "agent_message" || 297 | chunkObj.event === "text_chunk" 298 | ) { 299 | let chunkContent; 300 | if (chunkObj.event === "text_chunk") { 301 | chunkContent = chunkObj.data.text; 302 | } else { 303 | chunkContent = chunkObj.answer; 304 | } 305 | 306 | if (chunkContent !== "") { 307 | const chunkId = `chatcmpl-${Date.now()}`; 308 | const chunkCreated = chunkObj.created_at; 309 | 310 | if (!isResponseEnded) { 311 | res.write( 312 | "data: " + 313 | JSON.stringify({ 314 | id: chunkId, 315 | object: "chat.completion.chunk", 316 | created: chunkCreated, 317 | model: data.model, 318 | choices: [ 319 | { 320 | index: 0, 321 | delta: { 322 | content: chunkContent, 323 | }, 324 | finish_reason: null, 325 | }, 326 | ], 327 | }) + 328 | "\n\n" 329 | ); 330 | } 331 | } 332 | } else if ( 333 | chunkObj.event === "workflow_finished" || 334 | chunkObj.event === "message_end" 335 | ) { 336 | const chunkId = `chatcmpl-${Date.now()}`; 337 | const chunkCreated = chunkObj.created_at; 338 | if (!isResponseEnded) { 339 | res.write( 340 | "data: " + 341 | JSON.stringify({ 342 | id: chunkId, 343 | object: "chat.completion.chunk", 344 | created: chunkCreated, 345 | model: data.model, 346 | choices: [ 347 | { 348 | index: 0, 349 | delta: {}, 350 | finish_reason: "stop", 351 | }, 352 | ], 353 | }) + 354 | "\n\n" 355 | ); 356 | } 357 | if (!isResponseEnded) { 358 | res.write("data: [DONE]\n\n"); 359 | } 360 | 361 | res.end(); 362 | isResponseEnded = true; 363 | } else if (chunkObj.event === "agent_thought") { 364 | // 如果需要,处理 agent_thought 事件 365 | } else if (chunkObj.event === "ping") { 366 | // 如果需要,处理 ping 事件 367 | } else if (chunkObj.event === "error") { 368 | console.error(`Error: ${chunkObj.code}, ${chunkObj.message}`); 369 | res 370 | .status(500) 371 | .write( 372 | `data: ${JSON.stringify({ error: chunkObj.message })}\n\n` 373 | ); 374 | 375 | if (!isResponseEnded) { 376 | res.write("data: [DONE]\n\n"); 377 | } 378 | 379 | res.end(); 380 | isResponseEnded = true; 381 | } 382 | } 383 | 384 | buffer = lines[lines.length - 1]; 385 | }); 386 | 387 | // 记录响应结束 388 | responseStream.on("end", () => { 389 | log("info", "响应结束", { requestId }); 390 | }); 391 | } else { 392 | let result = ""; 393 | let usageData = ""; 394 | let buffer = ""; 395 | let hasError = false; 396 | 397 | // 记录普通响应的开始 398 | log("info", "开始处理普通响应", { 399 | requestId, 400 | timestamp: new Date().toISOString(), 401 | }); 402 | 403 | const responseStream = resp.body; 404 | responseStream.on("data", (chunk) => { 405 | buffer += chunk.toString(); 406 | let lines = buffer.split("\n"); 407 | 408 | for (let i = 0; i < lines.length - 1; i++) { 409 | const line = lines[i].trim(); 410 | if (line === "") continue; 411 | let chunkObj; 412 | try { 413 | const cleanedLine = line.replace(/^data: /, "").trim(); 414 | if (cleanedLine.startsWith("{") && cleanedLine.endsWith("}")) { 415 | chunkObj = JSON.parse(cleanedLine); 416 | } else { 417 | continue; 418 | } 419 | } catch (error) { 420 | console.error("解析 JSON 出错:", error); 421 | continue; 422 | } 423 | 424 | // // 记录每个 chunk 的内容 425 | // log('debug', '处理 chunk', { 426 | // requestId, 427 | // chunkObj, 428 | // }); 429 | 430 | if ( 431 | chunkObj.event === "message" || 432 | chunkObj.event === "agent_message" 433 | ) { 434 | result += chunkObj.answer; 435 | } else if (chunkObj.event === "message_end") { 436 | usageData = { 437 | prompt_tokens: chunkObj.metadata.usage.prompt_tokens || 100, 438 | completion_tokens: 439 | chunkObj.metadata.usage.completion_tokens || 10, 440 | total_tokens: chunkObj.metadata.usage.total_tokens || 110, 441 | }; 442 | } else if (chunkObj.event === "workflow_finished") { 443 | const outputs = chunkObj.data.outputs; 444 | if (config.OUTPUT_VARIABLE) { 445 | result = outputs[config.OUTPUT_VARIABLE]; 446 | } else { 447 | result = outputs; 448 | } 449 | result = String(result); 450 | usageData = { 451 | prompt_tokens: chunkObj.metadata?.usage?.prompt_tokens || 100, 452 | completion_tokens: 453 | chunkObj.metadata?.usage?.completion_tokens || 10, 454 | total_tokens: chunkObj.data.total_tokens || 110, 455 | }; 456 | } else if (chunkObj.event === "agent_thought") { 457 | // 如果需要,处理 agent_thought 事件 458 | } else if (chunkObj.event === "ping") { 459 | // 如果需要,处理 ping 事件 460 | } else if (chunkObj.event === "error") { 461 | hasError = true; 462 | console.error(`Error: ${chunkObj.code}, ${chunkObj.message}`); 463 | break; 464 | } 465 | } 466 | 467 | buffer = lines[lines.length - 1]; 468 | }); 469 | 470 | responseStream.on("end", () => { 471 | if (hasError) { 472 | res 473 | .status(500) 474 | .json({ error: "An error occurred while processing the request." }); 475 | } else { 476 | const formattedResponse = { 477 | id: `chatcmpl-${generateId()}`, 478 | object: "chat.completion", 479 | created: Math.floor(Date.now() / 1000), 480 | model: data.model, 481 | choices: [ 482 | { 483 | index: 0, 484 | message: { 485 | role: "assistant", 486 | content: result.trim(), 487 | }, 488 | logprobs: null, 489 | finish_reason: "stop", 490 | }, 491 | ], 492 | usage: usageData, 493 | system_fingerprint: "fp_2f57f81c11", 494 | }; 495 | const jsonResponse = JSON.stringify(formattedResponse, null, 2); 496 | 497 | // 记录发送的响应 498 | log("info", "发送响应", { 499 | requestId, 500 | response: formattedResponse, 501 | }); 502 | 503 | res.set("Content-Type", "application/json"); 504 | res.send(jsonResponse); 505 | } 506 | }); 507 | } 508 | } catch (error) { 509 | console.error("处理 Chat 请求时发生错误:", error); 510 | 511 | // 记录错误 512 | log("error", "处理 Chat 请求时发生错误", { 513 | requestId, 514 | error: error.message, 515 | stack: error.stack, 516 | }); 517 | 518 | res.status(500).json({ error: error.message }); 519 | } 520 | } 521 | 522 | export default { 523 | handleRequest, 524 | }; 525 | -------------------------------------------------------------------------------- /botType/completionHandler.js: -------------------------------------------------------------------------------- 1 | // completionHandler.js 2 | 3 | import fetch from "node-fetch"; 4 | import { PassThrough } from "stream"; 5 | import { log } from '../config/logger.js'; 6 | import { logApiCall, generateId, getFileExtension, getFileType } from "./utils.js"; 7 | 8 | // 上传文件到 Dify,获取文件 ID 9 | async function uploadFileToDify(base64Data, config, userId) { 10 | try { 11 | // 解析 base64 数据 URL,提取 contentType 和 base64 字符串 12 | const matches = base64Data.match(/^data:(.+);base64,(.+)$/); 13 | if (!matches || matches.length !== 3) { 14 | throw new Error("Invalid base64 data"); 15 | } 16 | let contentType = matches[1]; 17 | const base64String = matches[2]; 18 | let fileData = Buffer.from(base64String, "base64"); 19 | 20 | // 如果 contentType 是 'image/jpg',将其调整为 'image/jpeg' 21 | if (contentType === "image/jpg") { 22 | contentType = "image/jpeg"; 23 | } 24 | 25 | // 从 contentType 确定文件扩展名 26 | const fileExtension = contentType.split("/")[1]; // 例如 'jpeg'、'png'、'gif' 27 | 28 | // 使用扩展名创建文件名 29 | const filename = `image.${fileExtension}`; 30 | 31 | // 创建 FormData 并包含 'user' 字段 32 | const form = new FormData(); 33 | form.append("file", fileData, { 34 | filename: filename, 35 | contentType: contentType, 36 | }); 37 | form.append("user", userId); // 使用提供的用户标识符 38 | 39 | // 记录文件上传请求的详细信息 40 | log("info", "正在上传文件到 Dify", { 41 | url: `${config.DIFY_API_URL}/files/upload`, 42 | headers: { 43 | Authorization: `Bearer ${config.API_KEY}`, 44 | ...form.getHeaders(), 45 | }, 46 | formData: "<>", // 出于安全考虑,不记录实际文件数据 47 | }); 48 | 49 | // 发送上传请求 50 | const response = await fetch(`${config.DIFY_API_URL}/files/upload`, { 51 | method: "POST", 52 | headers: { 53 | Authorization: `Bearer ${config.API_KEY}`, 54 | ...form.getHeaders(), 55 | }, 56 | body: form, 57 | }); 58 | 59 | // 记录文件上传响应的详细信息 60 | log("info", "文件上传响应", { 61 | status: response.status, 62 | statusText: response.statusText, 63 | }); 64 | 65 | if (!response.ok) { 66 | const errorBody = await response.text(); 67 | log("error", "文件上传失败", { 68 | status: response.status, 69 | statusText: response.statusText, 70 | errorBody: errorBody, 71 | }); 72 | throw new Error( 73 | `文件上传失败: ${response.status} ${response.statusText}: ${errorBody}` 74 | ); 75 | } 76 | 77 | const result = await response.json(); 78 | log("info", "文件上传成功", { fileId: result.id }); 79 | return result.id; // 返回文件 ID 80 | } catch (error) { 81 | console.error("上传文件出错:", error); 82 | throw error; 83 | } 84 | } 85 | 86 | // 处理 Completion 类型请求 87 | async function handleRequest(req, res, config, requestId, startTime) { 88 | try { 89 | const apiPath = "/completion-messages"; 90 | const data = req.body; 91 | const messages = data.messages; 92 | let queryString = ""; 93 | let files = []; 94 | 95 | // 记录收到的请求头和请求体 96 | log("info", "收到请求", { 97 | requestId, 98 | headers: req.headers, 99 | body: data, 100 | }); 101 | 102 | const userId = "apiuser"; // 如果可用,替换为实际的用户 ID 103 | 104 | // 第一步:先扫描所有消息中的图片内容 105 | log("info", "开始扫描所有消息中的图片", { requestId, messageCount: messages.length }); 106 | for (const message of messages) { 107 | if (Array.isArray(message.content)) { 108 | for (const content of message.content) { 109 | if (content.type === "image_url" && content.image_url && content.image_url.url) { 110 | const imageUrl = content.image_url.url; 111 | 112 | // 检查URL是否为base64数据 113 | if (imageUrl.startsWith('data:')) { 114 | // 是base64数据,需要上传 115 | const fileExt = getFileExtension(imageUrl); 116 | const fileType = getFileType(fileExt); 117 | log("info", "检测到base64数据,准备上传", { requestId, fileType, fileExt }); 118 | const fileId = await uploadFileToDify( 119 | imageUrl, 120 | config, 121 | userId 122 | ); 123 | files.push({ 124 | type: fileType, 125 | transfer_method: "local_file", 126 | upload_file_id: fileId, 127 | }); 128 | } else { 129 | // 是真正的URL,直接使用remote_url方式 130 | const fileExt = getFileExtension(imageUrl); 131 | const fileType = getFileType(fileExt); 132 | log("info", "检测到远程文件URL", { requestId, url: imageUrl.substring(0, 30) + '...', fileType, fileExt }); 133 | files.push({ 134 | type: fileType, 135 | transfer_method: "remote_url", 136 | url: imageUrl, 137 | }); 138 | } 139 | } 140 | } 141 | } 142 | } 143 | 144 | // 第二步:从最后一条消息中提取查询文本 145 | const lastMessage = messages[messages.length - 1]; 146 | if (Array.isArray(lastMessage.content)) { 147 | for (const content of lastMessage.content) { 148 | // 处理字符串类型的内容(OpenAI格式) 149 | if (typeof content === "string") { 150 | queryString += content + "\n"; 151 | } 152 | // 处理对象类型的内容 153 | else if (content.type === "text") { 154 | queryString += content.text + "\n"; 155 | } 156 | // 注意:这里不再重复处理image_url,因为已经在上面处理过了 157 | } 158 | queryString = queryString.trim(); // 去除末尾的换行符 159 | } else { 160 | queryString = lastMessage.content; 161 | } 162 | 163 | // 日志记录 164 | log("info", "处理 Completion 类型消息", { 165 | requestId, 166 | contentLength: queryString.length, 167 | queryString, 168 | files, 169 | }); 170 | 171 | const stream = data.stream !== undefined ? data.stream : false; 172 | let requestBody; 173 | 174 | // 如果指定了 INPUT_VARIABLE,则使用它作为键,否则默认使用 'query' 175 | const inputKey = config.INPUT_VARIABLE || "query"; 176 | 177 | // 构建请求体 178 | requestBody = { 179 | inputs: { [inputKey]: queryString }, 180 | response_mode: "streaming", 181 | user: userId, 182 | files: files, 183 | }; 184 | 185 | // 记录将要发送到 Dify 的请求载荷 186 | log("info", "发送请求到 Dify", { 187 | requestId, 188 | url: config.DIFY_API_URL + apiPath, 189 | method: "POST", 190 | headers: { 191 | "Content-Type": "application/json", 192 | Authorization: `Bearer ${config.API_KEY}`, 193 | }, 194 | body: requestBody, 195 | }); 196 | 197 | // 发送请求到 Dify 198 | const resp = await fetch(config.DIFY_API_URL + apiPath, { 199 | method: "POST", 200 | headers: { 201 | "Content-Type": "application/json", 202 | Authorization: `Bearer ${config.API_KEY}`, 203 | }, 204 | body: JSON.stringify(requestBody), 205 | }); 206 | 207 | // 记录 API 调用时间 208 | const apiCallDuration = Date.now() - startTime; 209 | logApiCall(requestId, config, apiPath, apiCallDuration); 210 | 211 | // 记录 Dify 的响应状态 212 | log("info", "收到 Dify 响应", { 213 | requestId, 214 | status: resp.status, 215 | statusText: resp.statusText, 216 | }); 217 | 218 | if (!resp.ok) { 219 | const errorBody = await resp.text(); 220 | log("error", "Dify API 请求失败", { 221 | requestId, 222 | status: resp.status, 223 | statusText: resp.statusText, 224 | errorBody: errorBody, 225 | }); 226 | res.status(resp.status).send(errorBody); 227 | return; 228 | } 229 | 230 | let isResponseEnded = false; 231 | 232 | if (stream) { 233 | res.setHeader("Content-Type", "text/event-stream"); 234 | let buffer = ""; 235 | const responseStream = resp.body 236 | .pipe(new PassThrough()) 237 | .on("data", (chunk) => { 238 | buffer += chunk.toString(); 239 | let lines = buffer.split("\n"); 240 | 241 | for (let i = 0; i < lines.length - 1; i++) { 242 | let line = lines[i].trim(); 243 | 244 | if (!line.startsWith("data:")) continue; 245 | line = line.slice(5).trim(); 246 | let chunkObj; 247 | try { 248 | if (line.startsWith("{")) { 249 | chunkObj = JSON.parse(line); 250 | } else { 251 | continue; 252 | } 253 | } catch (error) { 254 | console.error("解析 chunk 出错:", error); 255 | continue; 256 | } 257 | 258 | // 记录每个 chunk 的内容 259 | log("debug", "处理 chunk", { 260 | requestId, 261 | chunkObj, 262 | }); 263 | 264 | if ( 265 | chunkObj.event === "message" || 266 | chunkObj.event === "agent_message" || 267 | chunkObj.event === "text_chunk" 268 | ) { 269 | let chunkContent; 270 | if (chunkObj.event === "text_chunk") { 271 | chunkContent = chunkObj.data.text; 272 | } else { 273 | chunkContent = chunkObj.answer; 274 | } 275 | 276 | if (chunkContent !== "") { 277 | const chunkId = `chatcmpl-${Date.now()}`; 278 | const chunkCreated = chunkObj.created_at; 279 | 280 | if (!isResponseEnded) { 281 | res.write( 282 | "data: " + 283 | JSON.stringify({ 284 | id: chunkId, 285 | object: "chat.completion.chunk", 286 | created: chunkCreated, 287 | model: data.model, 288 | choices: [ 289 | { 290 | index: 0, 291 | delta: { 292 | content: chunkContent, 293 | }, 294 | finish_reason: null, 295 | }, 296 | ], 297 | }) + 298 | "\n\n" 299 | ); 300 | } 301 | } 302 | } else if ( 303 | chunkObj.event === "workflow_finished" || 304 | chunkObj.event === "message_end" 305 | ) { 306 | const chunkId = `chatcmpl-${Date.now()}`; 307 | const chunkCreated = chunkObj.created_at; 308 | if (!isResponseEnded) { 309 | res.write( 310 | "data: " + 311 | JSON.stringify({ 312 | id: chunkId, 313 | object: "chat.completion.chunk", 314 | created: chunkCreated, 315 | model: data.model, 316 | choices: [ 317 | { 318 | index: 0, 319 | delta: {}, 320 | finish_reason: "stop", 321 | }, 322 | ], 323 | }) + 324 | "\n\n" 325 | ); 326 | } 327 | if (!isResponseEnded) { 328 | res.write("data: [DONE]\n\n"); 329 | } 330 | 331 | res.end(); 332 | isResponseEnded = true; 333 | } else if (chunkObj.event === "agent_thought") { 334 | // 如果需要,处理 agent_thought 事件 335 | } else if (chunkObj.event === "ping") { 336 | // 如果需要,处理 ping 事件 337 | } else if (chunkObj.event === "error") { 338 | console.error(`Error: ${chunkObj.code}, ${chunkObj.message}`); 339 | res 340 | .status(500) 341 | .write( 342 | `data: ${JSON.stringify({ error: chunkObj.message })}\n\n` 343 | ); 344 | 345 | if (!isResponseEnded) { 346 | res.write("data: [DONE]\n\n"); 347 | } 348 | 349 | res.end(); 350 | isResponseEnded = true; 351 | } 352 | } 353 | 354 | buffer = lines[lines.length - 1]; 355 | }); 356 | 357 | // 记录响应结束 358 | responseStream.on("end", () => { 359 | log("info", "响应结束", { requestId }); 360 | }); 361 | } else { 362 | let result = ""; 363 | let usageData = ""; 364 | let buffer = ""; 365 | let hasError = false; 366 | 367 | // 记录普通响应开始 368 | log("info", "开始处理普通响应", { 369 | requestId, 370 | timestamp: new Date().toISOString(), 371 | }); 372 | 373 | const responseStream = resp.body; 374 | responseStream.on("data", (chunk) => { 375 | buffer += chunk.toString(); 376 | let lines = buffer.split("\n"); 377 | 378 | for (let i = 0; i < lines.length - 1; i++) { 379 | const line = lines[i].trim(); 380 | if (line === "") continue; 381 | let chunkObj; 382 | try { 383 | const cleanedLine = line.replace(/^data: /, "").trim(); 384 | if (cleanedLine.startsWith("{") && cleanedLine.endsWith("}")) { 385 | chunkObj = JSON.parse(cleanedLine); 386 | } else { 387 | continue; 388 | } 389 | } catch (error) { 390 | console.error("解析 JSON 出错:", error); 391 | continue; 392 | } 393 | 394 | // 记录每个 chunk 的内容 395 | log("debug", "处理 chunk", { 396 | requestId, 397 | chunkObj, 398 | }); 399 | 400 | if ( 401 | chunkObj.event === "message" || 402 | chunkObj.event === "agent_message" 403 | ) { 404 | result += chunkObj.answer; 405 | } else if (chunkObj.event === "message_end") { 406 | usageData = { 407 | prompt_tokens: chunkObj.metadata.usage.prompt_tokens || 100, 408 | completion_tokens: 409 | chunkObj.metadata.usage.completion_tokens || 10, 410 | total_tokens: chunkObj.metadata.usage.total_tokens || 110, 411 | }; 412 | } else if (chunkObj.event === "workflow_finished") { 413 | const outputs = chunkObj.data.outputs; 414 | if (config.OUTPUT_VARIABLE) { 415 | result = outputs[config.OUTPUT_VARIABLE]; 416 | } else { 417 | result = outputs; 418 | } 419 | result = String(result); 420 | usageData = { 421 | prompt_tokens: chunkObj.metadata?.usage?.prompt_tokens || 100, 422 | completion_tokens: 423 | chunkObj.metadata?.usage?.completion_tokens || 10, 424 | total_tokens: chunkObj.data.total_tokens || 110, 425 | }; 426 | } else if (chunkObj.event === "agent_thought") { 427 | // 如果需要,处理 agent_thought 事件 428 | } else if (chunkObj.event === "ping") { 429 | // 如果需要,处理 ping 事件 430 | } else if (chunkObj.event === "error") { 431 | hasError = true; 432 | console.error(`Error: ${chunkObj.code}, ${chunkObj.message}`); 433 | break; 434 | } 435 | } 436 | 437 | buffer = lines[lines.length - 1]; 438 | }); 439 | 440 | responseStream.on("end", () => { 441 | if (hasError) { 442 | res 443 | .status(500) 444 | .json({ error: "An error occurred while processing the request." }); 445 | } else { 446 | const formattedResponse = { 447 | id: `chatcmpl-${generateId()}`, 448 | object: "chat.completion", 449 | created: Math.floor(Date.now() / 1000), 450 | model: data.model, 451 | choices: [ 452 | { 453 | index: 0, 454 | message: { 455 | role: "assistant", 456 | content: result.trim(), 457 | }, 458 | logprobs: null, 459 | finish_reason: "stop", 460 | }, 461 | ], 462 | usage: usageData, 463 | system_fingerprint: "fp_2f57f81c11", 464 | }; 465 | const jsonResponse = JSON.stringify(formattedResponse, null, 2); 466 | 467 | // 记录发送的响应 468 | log("info", "发送响应", { 469 | requestId, 470 | response: formattedResponse, 471 | }); 472 | 473 | res.set("Content-Type", "application/json"); 474 | res.send(jsonResponse); 475 | } 476 | }); 477 | } 478 | } catch (error) { 479 | console.error("处理 Completion 请求时发生错误:", error); 480 | 481 | // 记录错误 482 | log("error", "处理 Completion 请求时发生错误", { 483 | requestId, 484 | error: error.message, 485 | stack: error.stack, 486 | }); 487 | 488 | res.status(500).json({ error: error.message }); 489 | } 490 | } 491 | 492 | export default { 493 | handleRequest, 494 | }; 495 | -------------------------------------------------------------------------------- /botType/utils.js: -------------------------------------------------------------------------------- 1 | // utils.js 2 | import { log } from '../config/logger.js'; 3 | 4 | // 安全地记录对象(移除敏感信息) 5 | export function sanitizeLog(obj) { 6 | if (!obj) return obj; 7 | const sanitized = JSON.parse(JSON.stringify(obj)); 8 | 9 | // 隐藏敏感字段 10 | if (sanitized.headers && sanitized.headers.authorization) { 11 | sanitized.headers.authorization = '******'; 12 | } 13 | if (sanitized.API_KEY) { 14 | sanitized.API_KEY = '******'; 15 | } 16 | 17 | return sanitized; 18 | } 19 | 20 | // 记录请求详情 21 | export function logRequest(req, requestId) { 22 | log('info', '收到新请求', { 23 | requestId, 24 | method: req.method, 25 | url: req.url, 26 | headers: sanitizeLog(req.headers), 27 | body: sanitizeLog(req.body), 28 | query: req.query 29 | }); 30 | } 31 | 32 | // 记录响应详情 33 | export function logResponse(requestId, status, data) { 34 | log('info', '发送响应', { 35 | requestId, 36 | status, 37 | response: sanitizeLog(data) 38 | }); 39 | } 40 | 41 | // 记录API调用详情 42 | export function logApiCall(requestId, config, apiPath, duration) { 43 | log('info', 'Dify API调用完成', { 44 | requestId, 45 | apiPath, 46 | botType: config.BOT_TYPE, 47 | durationMs: duration 48 | }); 49 | } 50 | 51 | // 生成唯一的请求ID 52 | export function generateId() { 53 | let result = ""; 54 | const characters = 55 | "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; 56 | for (let i = 0; i < 29; i++) { 57 | result += characters.charAt(Math.floor(Math.random() * characters.length)); 58 | } 59 | return result; 60 | } 61 | 62 | // 从 URL 中提取文件扩展名 63 | export function getFileExtension(url) { 64 | // 如果是 base64 数据,从 MIME 类型提取 65 | if (url.startsWith('data:')) { 66 | const mimeMatch = url.match(/data:([^;]+)/); 67 | if (mimeMatch && mimeMatch[1]) { 68 | const mime = mimeMatch[1]; 69 | // 常见 MIME 类型映射到扩展名 70 | const mimeToExt = { 71 | 'image/jpeg': 'jpg', 72 | 'image/png': 'png', 73 | 'image/gif': 'gif', 74 | 'image/webp': 'webp', 75 | 'image/svg+xml': 'svg', 76 | 'application/pdf': 'pdf', 77 | 'text/plain': 'txt', 78 | 'text/html': 'html', 79 | 'audio/mpeg': 'mp3', 80 | 'video/mp4': 'mp4' 81 | }; 82 | return mimeToExt[mime] || 'bin'; // 默认二进制文件 83 | } 84 | return 'bin'; // 默认二进制文件 85 | } 86 | 87 | // 如果是 URL,清除参数并提取扩展名 88 | try { 89 | // 移除 URL 参数 90 | const cleanUrl = url.split('?')[0]; 91 | // 获取最后一部分并提取扩展名 92 | const parts = cleanUrl.split('/'); 93 | const filename = parts[parts.length - 1]; 94 | const ext = filename.split('.').pop().toLowerCase(); 95 | return ext || 'bin'; // 如果没有扩展名,返回默认值 96 | } catch (error) { 97 | log('warn', '无法从 URL 提取文件扩展名', { url: url.substring(0, 30) + '...', error }); 98 | return 'bin'; 99 | } 100 | } 101 | 102 | // 根据扩展名判断文件类型 103 | export function getFileType(extension) { 104 | // 根据 Dify API 文档中的类型分类 105 | const documentExts = ['txt', 'md', 'markdown', 'pdf', 'html', 'xlsx', 'xls', 'docx', 'csv', 'eml', 'msg', 'pptx', 'ppt', 'xml', 'epub']; 106 | const imageExts = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg']; 107 | const audioExts = ['mp3', 'm4a', 'wav', 'webm', 'amr']; 108 | const videoExts = ['mp4', 'mov', 'mpeg', 'mpga']; 109 | 110 | // 将扩展名转为小写进行比较 111 | const ext = extension.toLowerCase(); 112 | 113 | if (documentExts.includes(ext)) return 'document'; 114 | if (imageExts.includes(ext)) return 'image'; 115 | if (audioExts.includes(ext)) return 'audio'; 116 | if (videoExts.includes(ext)) return 'video'; 117 | 118 | // 默认作为自定义类型 119 | return 'custom'; 120 | } 121 | 122 | // 导出日志函数,以便其他模块直接使用 123 | export { log }; -------------------------------------------------------------------------------- /botType/workflowHandler.js: -------------------------------------------------------------------------------- 1 | // workflowHandler.js 2 | 3 | import fetch from "node-fetch"; 4 | import { PassThrough } from "stream"; 5 | import { log } from '../config/logger.js'; 6 | import { logApiCall, generateId, getFileExtension, getFileType } from "./utils.js"; 7 | 8 | // 上传文件到 Dify,获取文件 ID 9 | async function uploadFileToDify(base64Data, config, userId) { 10 | try { 11 | // 解析 base64 数据 URL,提取 contentType 和 base64 字符串 12 | const matches = base64Data.match(/^data:(.+);base64,(.+)$/); 13 | if (!matches || matches.length !== 3) { 14 | throw new Error("Invalid base64 data"); 15 | } 16 | let contentType = matches[1]; 17 | const base64String = matches[2]; 18 | let fileData = Buffer.from(base64String, "base64"); 19 | 20 | // 如果 contentType 是 'image/jpg',将其调整为 'image/jpeg' 21 | if (contentType === "image/jpg") { 22 | contentType = "image/jpeg"; 23 | } 24 | 25 | // 从 contentType 确定文件扩展名 26 | const fileExtension = contentType.split("/")[1]; // 例如 'jpeg'、'png'、'gif' 27 | 28 | // 创建文件名 29 | const filename = `file.${fileExtension}`; 30 | 31 | // 创建 FormData 并包含 'user' 字段 32 | const form = new FormData(); 33 | form.append("file", fileData, { 34 | filename: filename, 35 | contentType: contentType, 36 | }); 37 | form.append("user", userId); // 使用提供的用户标识符 38 | 39 | // 记录文件上传请求的详细信息 40 | log("info", "正在上传文件到 Dify", { 41 | url: `${config.DIFY_API_URL}/files/upload`, 42 | headers: { 43 | Authorization: `Bearer ${config.API_KEY}`, 44 | ...form.getHeaders(), 45 | }, 46 | formData: "<>", // 出于安全考虑,不记录实际文件数据 47 | }); 48 | 49 | // 发送上传请求 50 | const response = await fetch(`${config.DIFY_API_URL}/files/upload`, { 51 | method: "POST", 52 | headers: { 53 | Authorization: `Bearer ${config.API_KEY}`, 54 | ...form.getHeaders(), 55 | }, 56 | body: form, 57 | }); 58 | 59 | // 记录文件上传响应的详细信息 60 | log("info", "文件上传响应", { 61 | status: response.status, 62 | statusText: response.statusText, 63 | }); 64 | 65 | if (!response.ok) { 66 | const errorBody = await response.text(); 67 | log("error", "文件上传失败", { 68 | status: response.status, 69 | statusText: response.statusText, 70 | errorBody: errorBody, 71 | }); 72 | throw new Error( 73 | `文件上传失败: ${response.status} ${response.statusText}: ${errorBody}` 74 | ); 75 | } 76 | 77 | const result = await response.json(); 78 | log("info", "文件上传成功", { fileId: result.id }); 79 | return result.id; // 返回文件 ID 80 | } catch (error) { 81 | console.error("上传文件出错:", error); 82 | throw error; 83 | } 84 | } 85 | 86 | // 处理 Workflow 类型请求 87 | async function handleRequest(req, res, config, requestId, startTime) { 88 | try { 89 | const apiPath = "/workflows/run"; 90 | const data = req.body; 91 | const messages = data.messages; 92 | let inputs = {}; 93 | let files = []; 94 | 95 | // 记录收到的请求头和请求体 96 | log("info", "收到请求", { 97 | requestId, 98 | headers: req.headers, 99 | body: data, 100 | }); 101 | 102 | const userId = "apiuser"; // 如果可用,替换为实际的用户 ID 103 | const lastMessage = messages[messages.length - 1]; 104 | 105 | // 第一步:先扫描所有消息中的图片内容 106 | log("info", "开始扫描所有消息中的图片", { requestId, messageCount: messages.length }); 107 | for (const message of messages) { 108 | if (Array.isArray(message.content)) { 109 | for (const content of message.content) { 110 | if (content.type === "image_url" && content.image_url && content.image_url.url) { 111 | const imageUrl = content.image_url.url; 112 | 113 | // 检查URL是否为base64数据 114 | if (imageUrl.startsWith('data:')) { 115 | // 是base64数据,需要上传 116 | const fileExt = getFileExtension(imageUrl); 117 | const fileType = getFileType(fileExt); 118 | log("info", "检测到base64数据,准备上传", { requestId, fileType, fileExt }); 119 | const fileId = await uploadFileToDify( 120 | imageUrl, 121 | config, 122 | userId 123 | ); 124 | // 构建输入格式 125 | const fileInput = { 126 | transfer_method: "local_file", 127 | upload_file_id: fileId, 128 | type: fileType, 129 | }; 130 | inputs["file_input"] = fileInput; 131 | } else { 132 | // 是真正的URL,直接使用remote_url方式 133 | const fileExt = getFileExtension(imageUrl); 134 | const fileType = getFileType(fileExt); 135 | log("info", "检测到远程文件URL", { requestId, url: imageUrl.substring(0, 30) + '...', fileType, fileExt }); 136 | const fileInput = { 137 | transfer_method: "remote_url", 138 | url: imageUrl, 139 | type: fileType, 140 | }; 141 | inputs["file_input"] = fileInput; 142 | } 143 | } 144 | } 145 | } 146 | } 147 | 148 | // 第二步:从最后一条消息中提取查询文本 149 | if (Array.isArray(lastMessage.content)) { 150 | for (const content of lastMessage.content) { 151 | // 处理字符串类型的内容(OpenAI格式) 152 | if (typeof content === "string") { 153 | // 将字符串类型的内容设置为输入变量 154 | inputs["text_input"] = content; 155 | } 156 | // 处理对象类型的内容 157 | else if (content.type === "text") { 158 | // 假设文本内容是输入变量,需要根据您的应用逻辑调整 159 | inputs["text_input"] = content.text; 160 | } 161 | // 注意:这里不再重复处理image_url,因为已经在上面处理过了 162 | } 163 | } else { 164 | // 假设消息内容是输入变量,需要根据您的应用逻辑调整 165 | inputs["text_input"] = lastMessage.content; 166 | } 167 | 168 | // 日志记录 169 | log("info", "处理 Workflow 类型消息", { 170 | requestId, 171 | inputs, 172 | files, 173 | }); 174 | 175 | const stream = data.stream !== undefined ? data.stream : false; 176 | 177 | // 构建请求体 178 | const requestBody = { 179 | inputs: inputs, 180 | response_mode: "streaming", 181 | user: userId, 182 | files: files, // 如果需要,可以将 files 数组添加到请求体中 183 | }; 184 | 185 | // 记录将要发送到 Dify 的请求载荷 186 | log("info", "发送请求到 Dify", { 187 | requestId, 188 | url: config.DIFY_API_URL + apiPath, 189 | method: "POST", 190 | headers: { 191 | "Content-Type": "application/json", 192 | Authorization: `Bearer ${config.API_KEY}`, 193 | }, 194 | body: requestBody, 195 | }); 196 | 197 | // 发送请求到 Dify 198 | const resp = await fetch(config.DIFY_API_URL + apiPath, { 199 | method: "POST", 200 | headers: { 201 | "Content-Type": "application/json", 202 | Authorization: `Bearer ${config.API_KEY}`, 203 | }, 204 | body: JSON.stringify(requestBody), 205 | }); 206 | 207 | // 记录 API 调用时间 208 | const apiCallDuration = Date.now() - startTime; 209 | logApiCall(requestId, config, apiPath, apiCallDuration); 210 | 211 | // 记录 Dify 的响应状态 212 | log("info", "收到 Dify 响应", { 213 | requestId, 214 | status: resp.status, 215 | statusText: resp.statusText, 216 | }); 217 | 218 | if (!resp.ok) { 219 | const errorBody = await resp.text(); 220 | log("error", "Dify API 请求失败", { 221 | requestId, 222 | status: resp.status, 223 | statusText: resp.statusText, 224 | errorBody: errorBody, 225 | }); 226 | res.status(resp.status).send(errorBody); 227 | return; 228 | } 229 | 230 | let isResponseEnded = false; 231 | 232 | if (stream) { 233 | res.setHeader("Content-Type", "text/event-stream"); 234 | let buffer = ""; 235 | const responseStream = resp.body 236 | .pipe(new PassThrough()) 237 | .on("data", (chunk) => { 238 | buffer += chunk.toString(); 239 | let lines = buffer.split("\n"); 240 | 241 | for (let i = 0; i < lines.length - 1; i++) { 242 | let line = lines[i].trim(); 243 | 244 | if (!line.startsWith("data:")) continue; 245 | line = line.slice(5).trim(); 246 | let chunkObj; 247 | try { 248 | if (line.startsWith("{")) { 249 | chunkObj = JSON.parse(line); 250 | } else { 251 | continue; 252 | } 253 | } catch (error) { 254 | console.error("解析 chunk 出错:", error); 255 | continue; 256 | } 257 | 258 | // 记录每个 chunk 的内容 259 | log("debug", "处理 chunk", { 260 | requestId, 261 | chunkObj, 262 | }); 263 | 264 | if (chunkObj.event === "workflow_started") { 265 | // 处理 workflow_started 事件 266 | } else if (chunkObj.event === "node_started") { 267 | // 处理 node_started 事件 268 | } else if (chunkObj.event === "node_finished") { 269 | // 处理 node_finished 事件 270 | } else if (chunkObj.event === "workflow_finished") { 271 | const outputs = chunkObj.data.outputs; 272 | let result; 273 | if (config.OUTPUT_VARIABLE) { 274 | result = outputs[config.OUTPUT_VARIABLE]; 275 | } else { 276 | result = outputs; 277 | } 278 | 279 | const chunkId = `chatcmpl-${Date.now()}`; 280 | const chunkCreated = chunkObj.created_at; 281 | if (!isResponseEnded) { 282 | res.write( 283 | "data: " + 284 | JSON.stringify({ 285 | id: chunkId, 286 | object: "chat.completion.chunk", 287 | created: chunkCreated, 288 | model: data.model, 289 | choices: [ 290 | { 291 | index: 0, 292 | delta: { 293 | content: result, 294 | }, 295 | finish_reason: "stop", 296 | }, 297 | ], 298 | }) + 299 | "\n\n" 300 | ); 301 | res.write("data: [DONE]\n\n"); 302 | res.end(); 303 | isResponseEnded = true; 304 | } 305 | } else if (chunkObj.event === "ping") { 306 | // 处理 ping 事件 307 | } else if (chunkObj.event === "error") { 308 | console.error(`Error: ${chunkObj.code}, ${chunkObj.message}`); 309 | res 310 | .status(500) 311 | .write( 312 | `data: ${JSON.stringify({ error: chunkObj.message })}\n\n` 313 | ); 314 | 315 | if (!isResponseEnded) { 316 | res.write("data: [DONE]\n\n"); 317 | } 318 | 319 | res.end(); 320 | isResponseEnded = true; 321 | } 322 | } 323 | 324 | buffer = lines[lines.length - 1]; 325 | }); 326 | 327 | // 记录响应结束 328 | responseStream.on("end", () => { 329 | log("info", "响应结束", { requestId }); 330 | }); 331 | } else { 332 | let result = ""; 333 | let usageData = ""; 334 | let buffer = ""; 335 | let hasError = false; 336 | 337 | // 记录普通响应开始 338 | log("info", "开始处理普通响应", { 339 | requestId, 340 | timestamp: new Date().toISOString(), 341 | }); 342 | 343 | const responseStream = resp.body; 344 | responseStream.on("data", (chunk) => { 345 | buffer += chunk.toString(); 346 | let lines = buffer.split("\n"); 347 | 348 | for (let i = 0; i < lines.length - 1; i++) { 349 | const line = lines[i].trim(); 350 | if (line === "") continue; 351 | let chunkObj; 352 | try { 353 | const cleanedLine = line.replace(/^data: /, "").trim(); 354 | if (cleanedLine.startsWith("{") && cleanedLine.endsWith("}")) { 355 | chunkObj = JSON.parse(cleanedLine); 356 | } else { 357 | continue; 358 | } 359 | } catch (error) { 360 | console.error("解析 JSON 出错:", error); 361 | continue; 362 | } 363 | 364 | // 记录每个 chunk 的内容 365 | log("debug", "处理 chunk", { 366 | requestId, 367 | chunkObj, 368 | }); 369 | 370 | if (chunkObj.event === "workflow_finished") { 371 | const outputs = chunkObj.data.outputs; 372 | if (config.OUTPUT_VARIABLE) { 373 | result = outputs[config.OUTPUT_VARIABLE]; 374 | } else { 375 | result = outputs; 376 | } 377 | usageData = { 378 | total_tokens: chunkObj.data.total_tokens || 110, 379 | }; 380 | } else if (chunkObj.event === "ping") { 381 | // 处理 ping 事件 382 | } else if (chunkObj.event === "error") { 383 | hasError = true; 384 | console.error(`Error: ${chunkObj.code}, ${chunkObj.message}`); 385 | break; 386 | } 387 | } 388 | 389 | buffer = lines[lines.length - 1]; 390 | }); 391 | 392 | responseStream.on("end", () => { 393 | if (hasError) { 394 | res 395 | .status(500) 396 | .json({ error: "An error occurred while processing the request." }); 397 | } else { 398 | const formattedResponse = { 399 | id: `chatcmpl-${generateId()}`, 400 | object: "chat.completion", 401 | created: Math.floor(Date.now() / 1000), 402 | model: data.model, 403 | choices: [ 404 | { 405 | index: 0, 406 | message: { 407 | role: "assistant", 408 | content: result, 409 | }, 410 | logprobs: null, 411 | finish_reason: "stop", 412 | }, 413 | ], 414 | usage: usageData, 415 | system_fingerprint: "fp_2f57f81c11", 416 | }; 417 | const jsonResponse = JSON.stringify(formattedResponse, null, 2); 418 | 419 | // 记录发送的响应 420 | log("info", "发送响应", { 421 | requestId, 422 | response: formattedResponse, 423 | }); 424 | 425 | res.set("Content-Type", "application/json"); 426 | res.send(jsonResponse); 427 | } 428 | }); 429 | } 430 | } catch (error) { 431 | console.error("处理 Workflow 请求时发生错误:", error); 432 | 433 | // 记录错误 434 | log("error", "处理 Workflow 请求时发生错误", { 435 | requestId, 436 | error: error.message, 437 | stack: error.stack, 438 | }); 439 | 440 | res.status(500).json({ error: error.message }); 441 | } 442 | } 443 | 444 | export default { 445 | handleRequest, 446 | }; 447 | -------------------------------------------------------------------------------- /config/logger.js: -------------------------------------------------------------------------------- 1 | import winston from 'winston'; 2 | import 'winston-daily-rotate-file'; 3 | import path from 'path'; 4 | import { fileURLToPath } from 'url'; 5 | 6 | const __filename = fileURLToPath(import.meta.url); 7 | const __dirname = path.dirname(__filename); 8 | 9 | // 定义日志级别 10 | const levels = { 11 | error: 0, 12 | warn: 1, 13 | info: 2, 14 | debug: 3, 15 | }; 16 | 17 | // 定义日志颜色 18 | const colors = { 19 | error: 'red', 20 | warn: 'yellow', 21 | info: 'green', 22 | debug: 'blue', 23 | }; 24 | 25 | // 添加颜色支持 26 | winston.addColors(colors); 27 | 28 | // 创建格式化器 29 | const format = winston.format.combine( 30 | winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), 31 | winston.format.json(), 32 | winston.format.prettyPrint() 33 | ); 34 | 35 | // 根据环境创建不同的传输配置 36 | function getTransports() { 37 | const isDev = process.env.NODE_ENV === 'development'; 38 | 39 | // 生产环境只在控制台输出错误 40 | if (!isDev) { 41 | return [ 42 | new winston.transports.Console({ 43 | level: 'error', 44 | format: winston.format.simple() 45 | }) 46 | ]; 47 | } 48 | 49 | // 开发环境完整日志 50 | return [ 51 | new winston.transports.Console({ 52 | format: winston.format.combine( 53 | winston.format.colorize(), 54 | winston.format.simple() 55 | ) 56 | }), 57 | new winston.transports.DailyRotateFile({ 58 | filename: path.join(__dirname, '../logs/error-%DATE%.log'), 59 | datePattern: 'YYYY-MM-DD', 60 | level: 'error', 61 | maxSize: '20m', 62 | maxFiles: '14d' 63 | }), 64 | new winston.transports.DailyRotateFile({ 65 | filename: path.join(__dirname, '../logs/combined-%DATE%.log'), 66 | datePattern: 'YYYY-MM-DD', 67 | maxSize: '20m', 68 | maxFiles: '14d' 69 | }) 70 | ]; 71 | } 72 | 73 | // 创建 Winston logger 74 | const logger = winston.createLogger({ 75 | levels, 76 | format, 77 | transports: getTransports() 78 | }); 79 | 80 | // 导出日志函数 81 | export const log = (level, message, meta = {}) => { 82 | // 非开发环境且不是错误,则不记录日志 83 | if (process.env.NODE_ENV !== 'development' && level !== 'error') { 84 | return; 85 | } 86 | 87 | logger.log(level, message, { 88 | timestamp: new Date().toISOString(), 89 | ...meta, 90 | }); 91 | }; 92 | 93 | export default logger; 94 | -------------------------------------------------------------------------------- /docs/template.zh.mdx: -------------------------------------------------------------------------------- 1 | import { CodeGroup } from '../code.tsx' 2 | import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx' 3 | 4 | # 文本生成型应用 API 5 | 6 | 文本生成应用无会话支持,适合用于翻译/文章写作/总结 AI 等等。 7 | 8 |
9 | ### 基础 URL 10 | 11 | ```javascript 12 | ``` 13 | 14 | 15 | ### 鉴权 16 | 17 | 18 | Dify Service API 使用 `API-Key` 进行鉴权。 19 | **强烈建议开发者把 `API-Key` 放在后端存储,而非分享或者放在客户端存储,以免 `API-Key` 泄露,导致财产损失。** 20 | 所有 API 请求都应在 **`Authorization`** HTTP Header 中包含您的 `API-Key`,如下所示: 21 | 22 | 23 | ```javascript 24 | Authorization: Bearer {API_KEY} 25 | ``` 26 | 27 |
28 | 29 | --- 30 | 31 | 37 | 38 | 39 | 发送请求给文本生成型应用。 40 | 41 | ### Request Body 42 | 43 | 44 | 45 | (选填)允许传入 App 定义的各变量值。 46 | inputs 参数包含了多组键值对(Key/Value pairs),每组的键对应一个特定变量,每组的值则是该变量的具体值。 47 | 文本生成型应用要求至少传入一组键值对。 48 | - `query` (string) 必填 49 | 用户输入的文本内容。 50 | 51 | 52 | - `streaming` 流式模式(推荐)。基于 SSE(**[Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events)**)实现类似打字机输出方式的流式返回。 53 | - `blocking` 阻塞模式,等待执行完毕后返回结果。(请求若流程较长可能会被中断)。 54 | 由于 Cloudflare 限制,请求会在 100 秒超时无返回后中断。 55 | 56 | 57 | 用户标识,用于定义终端用户的身份,方便检索、统计。 58 | 由开发者定义规则,需保证用户标识在应用内唯一。 59 | 60 | 61 | 上传的文件。 62 | - `type` (string) 支持类型:图片 `image`(目前仅支持图片格式) 。 63 | - `transfer_method` (string) 传递方式: 64 | - `remote_url`: 图片地址。 65 | - `local_file`: 上传文件。 66 | - `url` 图片地址。(仅当传递方式为 `remote_url` 时)。 67 | - `upload_file_id` 上传文件 ID。(仅当传递方式为 `local_file `时)。 68 | 69 | 70 | 71 | ### Response 72 | 73 | 当 `response_mode` 为 `blocking` 时,返回 ChatCompletionResponse object。 74 | 当 `response_mode` 为 `streaming`时,返回 ChunkChatCompletionResponse object 流式序列。 75 | 76 | ### ChatCompletionResponse 77 | 返回完整的 App 结果,`Content-Type` 为 `application/json`。 78 | - `message_id` (string) 消息唯一 ID 79 | - `mode` (string) App 模式,固定为 chat 80 | - `answer` (string) 完整回复内容 81 | - `metadata` (object) 元数据 82 | - `usage` (Usage) 模型用量信息 83 | - `retriever_resources` (array[RetrieverResource]) 引用和归属分段列表 84 | - `created_at` (int) 消息创建时间戳,如:1705395332 85 | 86 | ### ChunkChatCompletionResponse 87 | 返回 App 输出的流式块,`Content-Type` 为 `text/event-stream`。 88 | 每个流式块均为 data: 开头,块之间以 `\n\n` 即两个换行符分隔,如下所示: 89 | 90 | ```streaming {{ title: 'Response' }} 91 | data: {"event": "message", "task_id": "900bbd43-dc0b-4383-a372-aa6e6c414227", "id": "663c5084-a254-4040-8ad3-51f2a3c1a77c", "answer": "Hi", "created_at": 1705398420}\n\n 92 | ``` 93 | 94 | 流式块中根据 `event` 不同,结构也不同: 95 | - `event: message` LLM 返回文本块事件,即:完整的文本以分块的方式输出。 96 | - `task_id` (string) 任务 ID,用于请求跟踪和下方的停止响应接口 97 | - `message_id` (string) 消息唯一 ID 98 | - `answer` (string) LLM 返回文本块内容 99 | - `created_at` (int) 创建时间戳,如:1705395332 100 | - `event: message_end` 消息结束事件,收到此事件则代表文本流式返回结束。 101 | - `task_id` (string) 任务 ID,用于请求跟踪和下方的停止响应接口 102 | - `message_id` (string) 消息唯一 ID 103 | - `metadata` (object) 元数据 104 | - `usage` (Usage) 模型用量信息 105 | - `retriever_resources` (array[RetrieverResource]) 引用和归属分段列表 106 | - `event: tts_message` TTS 音频流事件,即:语音合成输出。内容是Mp3格式的音频块,使用 base64 编码后的字符串,播放的时候直接解码即可。(开启自动播放才有此消息) 107 | - `task_id` (string) 任务 ID,用于请求跟踪和下方的停止响应接口 108 | - `message_id` (string) 消息唯一 ID 109 | - `audio` (string) 语音合成之后的音频块使用 Base64 编码之后的文本内容,播放的时候直接 base64 解码送入播放器即可 110 | - `created_at` (int) 创建时间戳,如:1705395332 111 | - `event: tts_message_end` TTS 音频流结束事件,收到这个事件表示音频流返回结束。 112 | - `task_id` (string) 任务 ID,用于请求跟踪和下方的停止响应接口 113 | - `message_id` (string) 消息唯一 ID 114 | - `audio` (string) 结束事件是没有音频的,所以这里是空字符串 115 | - `created_at` (int) 创建时间戳,如:1705395332 116 | - `event: message_replace` 消息内容替换事件。 117 | 开启内容审查和审查输出内容时,若命中了审查条件,则会通过此事件替换消息内容为预设回复。 118 | - `task_id` (string) 任务 ID,用于请求跟踪和下方的停止响应接口 119 | - `message_id` (string) 消息唯一 ID 120 | - `answer` (string) 替换内容(直接替换 LLM 所有回复文本) 121 | - `created_at` (int) 创建时间戳,如:1705395332 122 | - `event: error` 123 | 流式输出过程中出现的异常会以 stream event 形式输出,收到异常事件后即结束。 124 | - `task_id` (string) 任务 ID,用于请求跟踪和下方的停止响应接口 125 | - `message_id` (string) 消息唯一 ID 126 | - `status` (int) HTTP 状态码 127 | - `code` (string) 错误码 128 | - `message` (string) 错误消息 129 | - `event: ping` 每 10s 一次的 ping 事件,保持连接存活。 130 | 131 | ### Errors 132 | - 404,对话不存在 133 | - 400,`invalid_param`,传入参数异常 134 | - 400,`app_unavailable`,App 配置不可用 135 | - 400,`provider_not_initialize`,无可用模型凭据配置 136 | - 400,`provider_quota_exceeded`,模型调用额度不足 137 | - 400,`model_currently_not_support`,当前模型不可用 138 | - 400,`completion_request_error`,文本生成失败 139 | - 500,服务内部异常 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | ```bash {{ title: 'cURL' }} 148 | curl -X POST '${props.appDetail.api_base_url}/completion-messages' \ 149 | --header 'Authorization: Bearer {api_key}' \ 150 | --header 'Content-Type: application/json' \ 151 | --data-raw '{ 152 | "inputs": { 153 | "query": "Hello, world!" 154 | }, 155 | "response_mode": "streaming", 156 | "user": "abc-123" 157 | }' 158 | ``` 159 | 160 | ### blocking 161 | 162 | ```json {{ title: 'Response' }} 163 | { 164 | "id": "0b089b9a-24d9-48cc-94f8-762677276261", 165 | "answer": "how are you?", 166 | "created_at": 1679586667 167 | } 168 | ``` 169 | 170 | ### streaming 171 | 172 | ```streaming {{ title: 'Response' }} 173 | data: {"id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "answer": " I", "created_at": 1679586595} 174 | data: {"id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "answer": " I", "created_at": 1679586595} 175 | data: {"event": "tts_message", "conversation_id": "23dd85f3-1a41-4ea0-b7a9-062734ccfaf9", "message_id": "a8bdc41c-13b2-4c18-bfd9-054b9803038c", "created_at": 1721205487, "task_id": "3bf8a0bb-e73b-4690-9e66-4e429bad8ee7", "audio": "qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq"} 176 | data: {"event": "tts_message_end", "conversation_id": "23dd85f3-1a41-4ea0-b7a9-062734ccfaf9", "message_id": "a8bdc41c-13b2-4c18-bfd9-054b9803038c", "created_at": 1721205487, "task_id": "3bf8a0bb-e73b-4690-9e66-4e429bad8ee7", "audio": ""} 177 | ``` 178 | 179 | 180 | 181 | 182 | --- 183 | 189 | 190 | 191 | 上传文件(目前仅支持图片)并在发送消息时使用,可实现图文多模态理解。 192 | 支持 png, jpg, jpeg, webp, gif 格式。 193 | 上传的文件仅供当前终端用户使用。 194 | 195 | ### Request Body 196 | 该接口需使用 `multipart/form-data` 进行请求。 197 | 198 | 199 | 要上传的文件。 200 | 201 | 202 | 用户标识,用于定义终端用户的身份,必须和发送消息接口传入 user 保持一致。 203 | 204 | 205 | 206 | ### Response 207 | 成功上传后,服务器会返回文件的 ID 和相关信息。 208 | - `id` (uuid) ID 209 | - `name` (string) 文件名 210 | - `size` (int) 文件大小(byte) 211 | - `extension` (string) 文件后缀 212 | - `mime_type` (string) 文件 mime-type 213 | - `created_by` (uuid) 上传人 ID 214 | - `created_at` (timestamp) 上传时间 215 | 216 | ### Errors 217 | - 400,`no_file_uploaded`,必须提供文件 218 | - 400,`too_many_files`,目前只接受一个文件 219 | - 400,`unsupported_preview`,该文件不支持预览 220 | - 400,`unsupported_estimate`,该文件不支持估算 221 | - 413,`file_too_large`,文件太大 222 | - 415,`unsupported_file_type`,不支持的扩展名,当前只接受文档类文件 223 | - 503,`s3_connection_failed`,无法连接到 S3 服务 224 | - 503,`s3_permission_denied`,无权限上传文件到 S3 225 | - 503,`s3_file_too_large`,文件超出 S3 大小限制 226 | 227 | 228 | 229 | 230 | 231 | ```bash {{ title: 'cURL' }} 232 | curl -X POST '${props.appDetail.api_base_url}/files/upload' \ 233 | --header 'Authorization: Bearer {api_key}' \ 234 | --form 'file=@"/path/to/file"' 235 | ``` 236 | 237 | 238 | 239 | 240 | ```json {{ title: 'Response' }} 241 | { 242 | "id": "72fa9618-8f89-4a37-9b33-7e1178a24a67", 243 | "name": "example.png", 244 | "size": 1024, 245 | "extension": "png", 246 | "mime_type": "image/png", 247 | "created_by": 123, 248 | "created_at": 1577836800, 249 | } 250 | ``` 251 | 252 | 253 | 254 | --- 255 | 261 | 262 | 263 | 仅支持流式模式。 264 | ### Path 265 | - `task_id` (string) 任务 ID,可在流式返回 Chunk 中获取 266 | 267 | ### Request Body 268 | - `user` (string) Required 269 | 用户标识,用于定义终端用户的身份,必须和发送消息接口传入 user 保持一致。 270 | ### Response 271 | - `result` (string) 固定返回 success 272 | 273 | 274 | 275 | ```bash {{ title: 'cURL' }} 276 | curl -X POST '${props.appDetail.api_base_url}/completion-messages/:task_id/stop' \ 277 | -H 'Authorization: Bearer {api_key}' \ 278 | -H 'Content-Type: application/json' \ 279 | --data-raw '{ 280 | "user": "abc-123" 281 | }' 282 | ``` 283 | 284 | 285 | 286 | 287 | ```json {{ title: 'Response' }} 288 | { 289 | "result": "success" 290 | } 291 | ``` 292 | 293 | 294 | 295 | --- 296 | 297 | 303 | 304 | 305 | 消息终端用户反馈、点赞,方便应用开发者优化输出预期。 306 | 307 | ### Path Params 308 | 309 | 310 | 消息 ID 311 | 312 | 313 | 314 | ### Request Body 315 | 316 | 317 | 318 | 点赞 like, 点踩 dislike, 撤销点赞 null 319 | 320 | 321 | 用户标识,由开发者定义规则,需保证用户标识在应用内唯一。 322 | 323 | 324 | 消息反馈的具体信息。 325 | 326 | 327 | 328 | ### Response 329 | - `result` (string) 固定返回 success 330 | 331 | 332 | 333 | 334 | 335 | ```bash {{ title: 'cURL' }} 336 | curl -X POST '${props.appDetail.api_base_url}/messages/:message_id/feedbacks' \ 337 | --header 'Authorization: Bearer {api_key}' \ 338 | --header 'Content-Type: application/json' \ 339 | --data-raw '{ 340 | "rating": "like", 341 | "user": "abc-123", 342 | "content": "message feedback information" 343 | }' 344 | ``` 345 | 346 | 347 | 348 | 349 | ```json {{ title: 'Response' }} 350 | { 351 | "result": "success" 352 | } 353 | ``` 354 | 355 | 356 | 357 | 358 | --- 359 | 360 | 366 | 367 | 368 | 文字转语音。 369 | 370 | ### Request Body 371 | 372 | 373 | 374 | Dify 生成的文本消息,那么直接传递生成的message-id 即可,后台会通过 message_id 查找相应的内容直接合成语音信息。如果同时传 message_id 和 text,优先使用 message_id。 375 | 376 | 377 | 语音生成内容。如果没有传 message-id的话,则会使用这个字段的内容 378 | 379 | 380 | 用户标识,由开发者定义规则,需保证用户标识在应用内唯一。 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | ```bash {{ title: 'cURL' }} 389 | curl -o text-to-audio.mp3 -X POST '${props.appDetail.api_base_url}/text-to-audio' \ 390 | --header 'Authorization: Bearer {api_key}' \ 391 | --header 'Content-Type: application/json' \ 392 | --data-raw '{ 393 | "message_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", 394 | "text": "你好Dify", 395 | "user": "abc-123", 396 | "streaming": false 397 | }' 398 | ``` 399 | 400 | 401 | 402 | 403 | ```json {{ title: 'headers' }} 404 | { 405 | "Content-Type": "audio/wav" 406 | } 407 | ``` 408 | 409 | 410 | 411 | --- 412 | 413 | 419 | 420 | 421 | 用于获取应用的基本信息 422 | ### Response 423 | - `name` (string) 应用名称 424 | - `description` (string) 应用描述 425 | - `tags` (array[string]) 应用标签 426 | 427 | 428 | 429 | ```bash {{ title: 'cURL' }} 430 | curl -X GET '${props.appDetail.api_base_url}/info' \ 431 | -H 'Authorization: Bearer {api_key}' 432 | ``` 433 | 434 | 435 | ```json {{ title: 'Response' }} 436 | { 437 | "name": "My App", 438 | "description": "This is my app.", 439 | "tags": [ 440 | "tag1", 441 | "tag2" 442 | ] 443 | } 444 | ``` 445 | 446 | 447 | 448 | 449 | --- 450 | 451 | 457 | 458 | 459 | 用于进入页面一开始,获取功能开关、输入参数名称、类型及默认值等使用。 460 | 461 | ### Response 462 | - `opening_statement` (string) 开场白 463 | - `suggested_questions` (array[string]) 开场推荐问题列表 464 | - `suggested_questions_after_answer` (object) 启用回答后给出推荐问题。 465 | - `enabled` (bool) 是否开启 466 | - `speech_to_text` (object) 语音转文本 467 | - `enabled` (bool) 是否开启 468 | - `retriever_resource` (object) 引用和归属 469 | - `enabled` (bool) 是否开启 470 | - `annotation_reply` (object) 标记回复 471 | - `enabled` (bool) 是否开启 472 | - `user_input_form` (array[object]) 用户输入表单配置 473 | - `text-input` (object) 文本输入控件 474 | - `label` (string) 控件展示标签名 475 | - `variable` (string) 控件 ID 476 | - `required` (bool) 是否必填 477 | - `default` (string) 默认值 478 | - `paragraph` (object) 段落文本输入控件 479 | - `label` (string) 控件展示标签名 480 | - `variable` (string) 控件 ID 481 | - `required` (bool) 是否必填 482 | - `default` (string) 默认值 483 | - `select` (object) 下拉控件 484 | - `label` (string) 控件展示标签名 485 | - `variable` (string) 控件 ID 486 | - `required` (bool) 是否必填 487 | - `default` (string) 默认值 488 | - `options` (array[string]) 选项值 489 | - `file_upload` (object) 文件上传配置 490 | - `image` (object) 图片设置 491 | 当前仅支持图片类型:`png`, `jpg`, `jpeg`, `webp`, `gif` 492 | - `enabled` (bool) 是否开启 493 | - `number_limits` (int) 图片数量限制,默认 3 494 | - `transfer_methods` (array[string]) 传递方式列表,remote_url , local_file,必选一个 495 | - `system_parameters` (object) 系统参数 496 | - `file_size_limit` (int) 文档上传大小限制 (MB) 497 | - `image_file_size_limit` (int) 图片文件上传大小限制(MB) 498 | - `audio_file_size_limit` (int) 音频文件上传大小限制 (MB) 499 | - `video_file_size_limit` (int) 视频文件上传大小限制 (MB) 500 | 501 | 502 | 503 | 504 | 505 | ```bash {{ title: 'cURL' }} 506 | curl -X GET '${props.appDetail.api_base_url}/parameters' \ 507 | --header 'Authorization: Bearer {api_key}' 508 | ``` 509 | 510 | 511 | 512 | 513 | ```json {{ title: 'Response' }} 514 | { 515 | "introduction": "nice to meet you", 516 | "user_input_form": [ 517 | { 518 | "text-input": { 519 | "label": "a", 520 | "variable": "a", 521 | "required": true, 522 | "max_length": 48, 523 | "default": "" 524 | } 525 | }, 526 | { 527 | // ... 528 | } 529 | ], 530 | "file_upload": { 531 | "image": { 532 | "enabled": true, 533 | "number_limits": 3, 534 | "transfer_methods": [ 535 | "remote_url", 536 | "local_file" 537 | ] 538 | } 539 | }, 540 | "system_parameters": { 541 | "file_size_limit": 15, 542 | "image_file_size_limit": 10, 543 | "audio_file_size_limit": 50, 544 | "video_file_size_limit": 100 545 | } 546 | } 547 | ``` 548 | 549 | 550 | 551 | -------------------------------------------------------------------------------- /docs/template_workflow.zh.mdx: -------------------------------------------------------------------------------- 1 | import { CodeGroup } from '../code.tsx' 2 | import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from '../md.tsx' 3 | 4 | # Workflow 应用 API 5 | 6 | Workflow 应用无会话支持,适合用于翻译/文章写作/总结 AI 等等。 7 | 8 |
9 | ### Base URL 10 | 11 | ```javascript 12 | ``` 13 | 14 | 15 | ### Authentication 16 | 17 | Dify Service API 使用 `API-Key` 进行鉴权。 18 | **强烈建议开发者把 `API-Key` 放在后端存储,而非分享或者放在客户端存储,以免 `API-Key` 泄露,导致财产损失。** 19 | 所有 API 请求都应在 **`Authorization`** HTTP Header 中包含您的 `API-Key`,如下所示: 20 | 21 | 22 | ```javascript 23 | Authorization: Bearer {API_KEY} 24 | 25 | ``` 26 | 27 |
28 | 29 | --- 30 | 31 | 37 | 38 | 39 | 执行 workflow,没有已发布的 workflow,不可执行。 40 | 41 | ### Request Body 42 | - `inputs` (object) Required 43 | 允许传入 App 定义的各变量值。 44 | inputs 参数包含了多组键值对(Key/Value pairs),每组的键对应一个特定变量,每组的值则是该变量的具体值。变量可以是文件列表类型。 45 | 文件列表类型变量适用于传入文件结合文本理解并回答问题,仅当模型支持该类型文件解析能力时可用。如果该变量是文件列表类型,该变量对应的值应是列表格式,其中每个元素应包含以下内容: 46 | - `type` (string) 支持类型: 47 | - `document` 具体类型包含:'TXT', 'MD', 'MARKDOWN', 'PDF', 'HTML', 'XLSX', 'XLS', 'DOCX', 'CSV', 'EML', 'MSG', 'PPTX', 'PPT', 'XML', 'EPUB' 48 | - `image` 具体类型包含:'JPG', 'JPEG', 'PNG', 'GIF', 'WEBP', 'SVG' 49 | - `audio` 具体类型包含:'MP3', 'M4A', 'WAV', 'WEBM', 'AMR' 50 | - `video` 具体类型包含:'MP4', 'MOV', 'MPEG', 'MPGA' 51 | - `custom` 具体类型包含:其他文件类型 52 | - `transfer_method` (string) 传递方式,`remote_url` 图片地址 / `local_file` 上传文件 53 | - `url` (string) 图片地址(仅当传递方式为 `remote_url` 时) 54 | - `upload_file_id` (string) 上传文件 ID(仅当传递方式为 `local_file` 时) 55 | - `response_mode` (string) Required 56 | 返回响应模式,支持: 57 | - `streaming` 流式模式(推荐)。基于 SSE(**[Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events)**)实现类似打字机输出方式的流式返回。 58 | - `blocking` 阻塞模式,等待执行完毕后返回结果。(请求若流程较长可能会被中断)。 59 | 由于 Cloudflare 限制,请求会在 100 秒超时无返回后中断。 60 | - `user` (string) Required 61 | 用户标识,用于定义终端用户的身份,方便检索、统计。 62 | 由开发者定义规则,需保证用户标识在应用内唯一。 63 | 64 | 65 | ### Response 66 | 当 `response_mode` 为 `blocking` 时,返回 CompletionResponse object。 67 | 当 `response_mode` 为 `streaming`时,返回 ChunkCompletionResponse object 流式序列。 68 | 69 | ### CompletionResponse 70 | 返回完整的 App 结果,`Content-Type` 为 `application/json` 。 71 | - `workflow_run_id` (string) workflow 执行 ID 72 | - `task_id` (string) 任务 ID,用于请求跟踪和下方的停止响应接口 73 | - `data` (object) 详细内容 74 | - `id` (string) workflow 执行 ID 75 | - `workflow_id` (string) 关联 Workflow ID 76 | - `status` (string) 执行状态, `running` / `succeeded` / `failed` / `stopped` 77 | - `outputs` (json) Optional 输出内容 78 | - `error` (string) Optional 错误原因 79 | - `elapsed_time` (float) Optional 耗时(s) 80 | - `total_tokens` (int) Optional 总使用 tokens 81 | - `total_steps` (int) 总步数(冗余),默认 0 82 | - `created_at` (timestamp) 开始时间 83 | - `finished_at` (timestamp) 结束时间 84 | 85 | ### ChunkCompletionResponse 86 | 返回 App 输出的流式块,`Content-Type` 为 `text/event-stream`。 87 | 每个流式块均为 data: 开头,块之间以 `\n\n` 即两个换行符分隔,如下所示: 88 | 89 | ```streaming {{ title: 'Response' }} 90 | data: {"event": "message", "task_id": "900bbd43-dc0b-4383-a372-aa6e6c414227", "id": "663c5084-a254-4040-8ad3-51f2a3c1a77c", "answer": "Hi", "created_at": 1705398420}\n\n 91 | ``` 92 | 93 | 流式块中根据 `event` 不同,结构也不同,包含以下类型: 94 | - `event: workflow_started` workflow 开始执行 95 | - `task_id` (string) 任务 ID,用于请求跟踪和下方的停止响应接口 96 | - `workflow_run_id` (string) workflow 执行 ID 97 | - `event` (string) 固定为 `workflow_started` 98 | - `data` (object) 详细内容 99 | - `id` (string) workflow 执行 ID 100 | - `workflow_id` (string) 关联 Workflow ID 101 | - `sequence_number` (int) 自增序号,App 内自增,从 1 开始 102 | - `created_at` (timestamp) 开始时间 103 | - `event: node_started` node 开始执行 104 | - `task_id` (string) 任务 ID,用于请求跟踪和下方的停止响应接口 105 | - `workflow_run_id` (string) workflow 执行 ID 106 | - `event` (string) 固定为 `node_started` 107 | - `data` (object) 详细内容 108 | - `id` (string) workflow 执行 ID 109 | - `node_id` (string) 节点 ID 110 | - `node_type` (string) 节点类型 111 | - `title` (string) 节点名称 112 | - `index` (int) 执行序号,用于展示 Tracing Node 顺序 113 | - `predecessor_node_id` (string) 前置节点 ID,用于画布展示执行路径 114 | - `inputs` (object) 节点中所有使用到的前置节点变量内容 115 | - `created_at` (timestamp) 开始时间 116 | - `event: node_finished` node 执行结束,成功失败同一事件中不同状态 117 | - `task_id` (string) 任务 ID,用于请求跟踪和下方的停止响应接口 118 | - `workflow_run_id` (string) workflow 执行 ID 119 | - `event` (string) 固定为 `node_finished` 120 | - `data` (object) 详细内容 121 | - `id` (string) node 执行 ID 122 | - `node_id` (string) 节点 ID 123 | - `index` (int) 执行序号,用于展示 Tracing Node 顺序 124 | - `predecessor_node_id` (string) optional 前置节点 ID,用于画布展示执行路径 125 | - `inputs` (object) 节点中所有使用到的前置节点变量内容 126 | - `process_data` (json) Optional 节点过程数据 127 | - `outputs` (json) Optional 输出内容 128 | - `status` (string) 执行状态 `running` / `succeeded` / `failed` / `stopped` 129 | - `error` (string) Optional 错误原因 130 | - `elapsed_time` (float) Optional 耗时(s) 131 | - `execution_metadata` (json) 元数据 132 | - `total_tokens` (int) optional 总使用 tokens 133 | - `total_price` (decimal) optional 总费用 134 | - `currency` (string) optional 货币,如 `USD` / `RMB` 135 | - `created_at` (timestamp) 开始时间 136 | - `event: workflow_finished` workflow 执行结束,成功失败同一事件中不同状态 137 | - `task_id` (string) 任务 ID,用于请求跟踪和下方的停止响应接口 138 | - `workflow_run_id` (string) workflow 执行 ID 139 | - `event` (string) 固定为 `workflow_finished` 140 | - `data` (object) 详细内容 141 | - `id` (string) workflow 执行 ID 142 | - `workflow_id` (string) 关联 Workflow ID 143 | - `status` (string) 执行状态 `running` / `succeeded` / `failed` / `stopped` 144 | - `outputs` (json) Optional 输出内容 145 | - `error` (string) Optional 错误原因 146 | - `elapsed_time` (float) Optional 耗时(s) 147 | - `total_tokens` (int) Optional 总使用 tokens 148 | - `total_steps` (int) 总步数(冗余),默认 0 149 | - `created_at` (timestamp) 开始时间 150 | - `finished_at` (timestamp) 结束时间 151 | - `event: tts_message` TTS 音频流事件,即:语音合成输出。内容是Mp3格式的音频块,使用 base64 编码后的字符串,播放的时候直接解码即可。(开启自动播放才有此消息) 152 | - `task_id` (string) 任务 ID,用于请求跟踪和下方的停止响应接口 153 | - `message_id` (string) 消息唯一 ID 154 | - `audio` (string) 语音合成之后的音频块使用 Base64 编码之后的文本内容,播放的时候直接 base64 解码送入播放器即可 155 | - `created_at` (int) 创建时间戳,如:1705395332 156 | - `event: tts_message_end` TTS 音频流结束事件,收到这个事件表示音频流返回结束。 157 | - `task_id` (string) 任务 ID,用于请求跟踪和下方的停止响应接口 158 | - `message_id` (string) 消息唯一 ID 159 | - `audio` (string) 结束事件是没有音频的,所以这里是空字符串 160 | - `created_at` (int) 创建时间戳,如:1705395332 161 | - `event: ping` 每 10s 一次的 ping 事件,保持连接存活。 162 | 163 | ### Errors 164 | - 400,`invalid_param`,传入参数异常 165 | - 400,`app_unavailable`,App 配置不可用 166 | - 400,`provider_not_initialize`,无可用模型凭据配置 167 | - 400,`provider_quota_exceeded`,模型调用额度不足 168 | - 400,`model_currently_not_support`,当前模型不可用 169 | - 400,`workflow_request_error`,workflow 执行失败 170 | - 500,服务内部异常 171 | 172 | 173 | 174 | 175 | ```bash {{ title: 'cURL' }} 176 | curl -X POST '${props.appDetail.api_base_url}/workflows/run' \ 177 | --header 'Authorization: Bearer {api_key}' \ 178 | --header 'Content-Type: application/json' \ 179 | --data-raw '{ 180 | "inputs": {}, 181 | "response_mode": "streaming", 182 | "user": "abc-123" 183 | }' 184 | ``` 185 | 186 | 187 | ```json {{ title: 'File variable example' }} 188 | { 189 | "inputs": { 190 | "{variable_name}": 191 | [ 192 | { 193 | "transfer_method": "local_file", 194 | "upload_file_id": "{upload_file_id}", 195 | "type": "{document_type}" 196 | } 197 | ] 198 | } 199 | } 200 | ``` 201 | 202 | ### Blocking Mode 203 | 204 | ```json {{ title: 'Response' }} 205 | { 206 | "workflow_run_id": "djflajgkldjgd", 207 | "task_id": "9da23599-e713-473b-982c-4328d4f5c78a", 208 | "data": { 209 | "id": "fdlsjfjejkghjda", 210 | "workflow_id": "fldjaslkfjlsda", 211 | "status": "succeeded", 212 | "outputs": { 213 | "text": "Nice to meet you." 214 | }, 215 | "error": null, 216 | "elapsed_time": 0.875, 217 | "total_tokens": 3562, 218 | "total_steps": 8, 219 | "created_at": 1705407629, 220 | "finished_at": 1727807631 221 | } 222 | } 223 | ``` 224 | 225 | ### Streaming Mode 226 | 227 | ```streaming {{ title: 'Response' }} 228 | data: {"event": "workflow_started", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "workflow_id": "dfjasklfjdslag", "sequence_number": 1, "created_at": 1679586595}} 229 | data: {"event": "node_started", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "node_id": "dfjasklfjdslag", "node_type": "start", "title": "Start", "index": 0, "predecessor_node_id": "fdljewklfklgejlglsd", "inputs": {}, "created_at": 1679586595}} 230 | data: {"event": "node_finished", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "node_id": "dfjasklfjdslag", "node_type": "start", "title": "Start", "index": 0, "predecessor_node_id": "fdljewklfklgejlglsd", "inputs": {}, "outputs": {}, "status": "succeeded", "elapsed_time": 0.324, "execution_metadata": {"total_tokens": 63127864, "total_price": 2.378, "currency": "USD"}, "created_at": 1679586595}} 231 | data: {"event": "workflow_finished", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "workflow_id": "dfjasklfjdslag", "outputs": {}, "status": "succeeded", "elapsed_time": 0.324, "total_tokens": 63127864, "total_steps": "1", "created_at": 1679586595, "finished_at": 1679976595}} 232 | data: {"event": "tts_message", "conversation_id": "23dd85f3-1a41-4ea0-b7a9-062734ccfaf9", "message_id": "a8bdc41c-13b2-4c18-bfd9-054b9803038c", "created_at": 1721205487, "task_id": "3bf8a0bb-e73b-4690-9e66-4e429bad8ee7", "audio": "qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq"} 233 | data: {"event": "tts_message_end", "conversation_id": "23dd85f3-1a41-4ea0-b7a9-062734ccfaf9", "message_id": "a8bdc41c-13b2-4c18-bfd9-054b9803038c", "created_at": 1721205487, "task_id": "3bf8a0bb-e73b-4690-9e66-4e429bad8ee7", "audio": ""} 234 | ``` 235 | 236 | 237 | ```json {{ title: 'File upload sample code' }} 238 | import requests 239 | import json 240 | 241 | def upload_file(file_path, user): 242 | upload_url = "https://api.dify.ai/v1/files/upload" 243 | headers = { 244 | "Authorization": "Bearer app-xxxxxxxx", 245 | } 246 | 247 | try: 248 | print("上传文件中...") 249 | with open(file_path, 'rb') as file: 250 | files = { 251 | 'file': (file_path, file, 'text/plain') # 确保文件以适当的MIME类型上传 252 | } 253 | data = { 254 | "user": user, 255 | "type": "TXT" # 设置文件类型为TXT 256 | } 257 | 258 | response = requests.post(upload_url, headers=headers, files=files, data=data) 259 | if response.status_code == 201: # 201 表示创建成功 260 | print("文件上传成功") 261 | return response.json().get("id") # 获取上传的文件 ID 262 | else: 263 | print(f"文件上传失败,状态码: {response.status_code}") 264 | return None 265 | except Exception as e: 266 | print(f"发生错误: {str(e)}") 267 | return None 268 | 269 | def run_workflow(file_id, user, response_mode="blocking"): 270 | workflow_url = "https://api.dify.ai/v1/workflows/run" 271 | headers = { 272 | "Authorization": "Bearer app-xxxxxxxxx", 273 | "Content-Type": "application/json" 274 | } 275 | 276 | data = { 277 | "inputs": { 278 | "orig_mail": [{ 279 | "transfer_method": "local_file", 280 | "upload_file_id": file_id, 281 | "type": "document" 282 | }] 283 | }, 284 | "response_mode": response_mode, 285 | "user": user 286 | } 287 | 288 | try: 289 | print("运行工作流...") 290 | response = requests.post(workflow_url, headers=headers, json=data) 291 | if response.status_code == 200: 292 | print("工作流执行成功") 293 | return response.json() 294 | else: 295 | print(f"工作流执行失败,状态码: {response.status_code}") 296 | return {"status": "error", "message": f"Failed to execute workflow, status code: {response.status_code}"} 297 | except Exception as e: 298 | print(f"发生错误: {str(e)}") 299 | return {"status": "error", "message": str(e)} 300 | 301 | # 使用示例 302 | file_path = "{your_file_path}" 303 | user = "difyuser" 304 | 305 | # 上传文件 306 | file_id = upload_file(file_path, user) 307 | if file_id: 308 | # 文件上传成功,继续运行工作流 309 | result = run_workflow(file_id, user) 310 | print(result) 311 | else: 312 | print("文件上传失败,无法执行工作流") 313 | ``` 314 | 315 | 316 | 317 | 318 | --- 319 | 320 | 326 | 327 | 328 | 根据 workflow 执行 ID 获取 workflow 任务当前执行结果 329 | ### Path 330 | - `workflow_run_id` (string) workflow_run_id,可在流式返回 Chunk 中获取 331 | ### Response 332 | - `id` (string) workflow 执行 ID 333 | - `workflow_id` (string) 关联的 Workflow ID 334 | - `status` (string) 执行状态 `running` / `succeeded` / `failed` / `stopped` 335 | - `inputs` (json) 任务输入内容 336 | - `outputs` (json) 任务输出内容 337 | - `error` (string) 错误原因 338 | - `total_steps` (int) 任务执行总步数 339 | - `total_tokens` (int) 任务执行总 tokens 340 | - `created_at` (timestamp) 任务开始时间 341 | - `finished_at` (timestamp) 任务结束时间 342 | - `elapsed_time` (float) 耗时(s) 343 | 344 | 345 | ### Request Example 346 | 347 | ```bash {{ title: 'cURL' }} 348 | curl -X GET '${props.appDetail.api_base_url}/workflows/run/:workflow_run_id' \ 349 | -H 'Authorization: Bearer {api_key}' \ 350 | -H 'Content-Type: application/json' 351 | ``` 352 | 353 | 354 | ### Response Example 355 | 356 | ```json {{ title: 'Response' }} 357 | { 358 | "id": "b1ad3277-089e-42c6-9dff-6820d94fbc76", 359 | "workflow_id": "19eff89f-ec03-4f75-b0fc-897e7effea02", 360 | "status": "succeeded", 361 | "inputs": "{\"sys.files\": [], \"sys.user_id\": \"abc-123\"}", 362 | "outputs": null, 363 | "error": null, 364 | "total_steps": 3, 365 | "total_tokens": 0, 366 | "created_at": 1705407629, 367 | "finished_at": 1727807631, 368 | "elapsed_time": 30.098514399956912 369 | } 370 | ``` 371 | 372 | 373 | 374 | 375 | --- 376 | 377 | 383 | 384 | 385 | 仅支持流式模式。 386 | ### Path 387 | - `task_id` (string) 任务 ID,可在流式返回 Chunk 中获取 388 | ### Request Body 389 | - `user` (string) Required 390 | 用户标识,用于定义终端用户的身份,必须和发送消息接口传入 user 保持一致。 391 | ### Response 392 | - `result` (string) 固定返回 "success" 393 | 394 | 395 | ### Request Example 396 | 397 | ```bash {{ title: 'cURL' }} 398 | curl -X POST '${props.appDetail.api_base_url}/workflows/tasks/:task_id/stop' \ 399 | -H 'Authorization: Bearer {api_key}' \ 400 | -H 'Content-Type: application/json' \ 401 | --data-raw '{ 402 | "user": "abc-123" 403 | }' 404 | ``` 405 | 406 | 407 | ### Response Example 408 | 409 | ```json {{ title: 'Response' }} 410 | { 411 | "result": "success" 412 | } 413 | ``` 414 | 415 | 416 | 417 | 418 | --- 419 | 420 | 426 | 427 | 428 | 上传文件并在发送消息时使用,可实现图文多模态理解。 429 | 支持您的工作流程所支持的任何格式。 430 | 上传的文件仅供当前终端用户使用。 431 | 432 | ### Request Body 433 | 该接口需使用 `multipart/form-data` 进行请求。 434 | 435 | 436 | 要上传的文件。 437 | 438 | 439 | 用户标识,用于定义终端用户的身份,必须和发送消息接口传入 user 保持一致。 440 | 441 | 442 | 443 | ### Response 444 | 成功上传后,服务器会返回文件的 ID 和相关信息。 445 | - `id` (uuid) ID 446 | - `name` (string) 文件名 447 | - `size` (int) 文件大小(byte) 448 | - `extension` (string) 文件后缀 449 | - `mime_type` (string) 文件 mime-type 450 | - `created_by` (uuid) 上传人 ID 451 | - `created_at` (timestamp) 上传时间 452 | 453 | ### Errors 454 | - 400,`no_file_uploaded`,必须提供文件 455 | - 400,`too_many_files`,目前只接受一个文件 456 | - 400,`unsupported_preview`,该文件不支持预览 457 | - 400,`unsupported_estimate`,该文件不支持估算 458 | - 413,`file_too_large`,文件太大 459 | - 415,`unsupported_file_type`,不支持的扩展名,当前只接受文档类文件 460 | - 503,`s3_connection_failed`,无法连接到 S3 服务 461 | - 503,`s3_permission_denied`,无权限上传文件到 S3 462 | - 503,`s3_file_too_large`,文件超出 S3 大小限制 463 | 464 | 465 | 466 | 467 | 468 | ```bash {{ title: 'cURL' }} 469 | curl -X POST '${props.appDetail.api_base_url}/files/upload' \ 470 | --header 'Authorization: Bearer {api_key}' \ 471 | --form 'file=@"/path/to/file"' 472 | ``` 473 | 474 | 475 | 476 | 477 | ```json {{ title: 'Response' }} 478 | { 479 | "id": "72fa9618-8f89-4a37-9b33-7e1178a24a67", 480 | "name": "example.png", 481 | "size": 1024, 482 | "extension": "png", 483 | "mime_type": "image/png", 484 | "created_by": 123, 485 | "created_at": 1577836800, 486 | } 487 | ``` 488 | 489 | 490 | 491 | --- 492 | 493 | 499 | 500 | 501 | 倒序返回workflow日志 502 | 503 | ### Query 504 | 505 | 506 | 507 | 关键字 508 | 509 | 510 | 执行状态 succeeded/failed/stopped 511 | 512 | 513 | 当前页码, 默认1. 514 | 515 | 516 | 每页条数, 默认20. 517 | 518 | 519 | 520 | ### Response 521 | - `page` (int) 当前页码 522 | - `limit` (int) 每页条数 523 | - `total` (int) 总条数 524 | - `has_more` (bool) 是否还有更多数据 525 | - `data` (array[object]) 当前页码的数据 526 | - `id` (string) 标识 527 | - `workflow_run` (object) Workflow 执行日志 528 | - `id` (string) 标识 529 | - `version` (string) 版本 530 | - `status` (string) 执行状态, `running` / `succeeded` / `failed` / `stopped` 531 | - `error` (string) (可选) 错误 532 | - `elapsed_time` (float) 耗时,单位秒 533 | - `total_tokens` (int) 消耗的token数量 534 | - `total_steps` (int) 执行步骤长度 535 | - `created_at` (timestamp) 开始时间 536 | - `finished_at` (timestamp) 结束时间 537 | - `created_from` (string) 来源 538 | - `created_by_role` (string) 角色 539 | - `created_by_account` (string) (可选) 帐号 540 | - `created_by_end_user` (object) 用户 541 | - `id` (string) 标识 542 | - `type` (string) 类型 543 | - `is_anonymous` (bool) 是否匿名 544 | - `session_id` (string) 会话标识 545 | - `created_at` (timestamp) 创建时间 546 | 547 | 548 | 549 | 550 | 551 | ```bash {{ title: 'cURL' }} 552 | curl -X GET '${props.appDetail.api_base_url}/workflows/logs?limit=1' 553 | --header 'Authorization: Bearer {api_key}' 554 | ``` 555 | 556 | 557 | ### Response Example 558 | 559 | ```json {{ title: 'Response' }} 560 | { 561 | "page": 1, 562 | "limit": 1, 563 | "total": 7, 564 | "has_more": true, 565 | "data": [ 566 | { 567 | "id": "e41b93f1-7ca2-40fd-b3a8-999aeb499cc0", 568 | "workflow_run": { 569 | "id": "c0640fc8-03ef-4481-a96c-8a13b732a36e", 570 | "version": "2024-08-01 12:17:09.771832", 571 | "status": "succeeded", 572 | "error": null, 573 | "elapsed_time": 1.3588523610014818, 574 | "total_tokens": 0, 575 | "total_steps": 3, 576 | "created_at": 1726139643, 577 | "finished_at": 1726139644 578 | }, 579 | "created_from": "service-api", 580 | "created_by_role": "end_user", 581 | "created_by_account": null, 582 | "created_by_end_user": { 583 | "id": "7f7d9117-dd9d-441d-8970-87e5e7e687a3", 584 | "type": "service_api", 585 | "is_anonymous": false, 586 | "session_id": "abc-123" 587 | }, 588 | "created_at": 1726139644 589 | } 590 | ] 591 | } 592 | ``` 593 | 594 | 595 | 596 | --- 597 | 598 | 604 | 605 | 606 | 用于获取应用的基本信息 607 | ### Response 608 | - `name` (string) 应用名称 609 | - `description` (string) 应用描述 610 | - `tags` (array[string]) 应用标签 611 | 612 | 613 | 614 | ```bash {{ title: 'cURL' }} 615 | curl -X GET '${props.appDetail.api_base_url}/info' \ 616 | -H 'Authorization: Bearer {api_key}' 617 | ``` 618 | 619 | 620 | ```json {{ title: 'Response' }} 621 | { 622 | "name": "My App", 623 | "description": "This is my app.", 624 | "tags": [ 625 | "tag1", 626 | "tag2" 627 | ] 628 | } 629 | ``` 630 | 631 | 632 | 633 | 634 | --- 635 | 636 | 642 | 643 | 644 | 用于进入页面一开始,获取功能开关、输入参数名称、类型及默认值等使用。 645 | 646 | ### Response 647 | - `user_input_form` (array[object]) 用户输入表单配置 648 | - `text-input` (object) 文本输入控件 649 | - `label` (string) 控件展示标签名 650 | - `variable` (string) 控件 ID 651 | - `required` (bool) 是否必填 652 | - `default` (string) 默认值 653 | - `paragraph` (object) 段落文本输入控件 654 | - `label` (string) 控件展示标签名 655 | - `variable` (string) 控件 ID 656 | - `required` (bool) 是否必填 657 | - `default` (string) 默认值 658 | - `select` (object) 下拉控件 659 | - `label` (string) 控件展示标签名 660 | - `variable` (string) 控件 ID 661 | - `required` (bool) 是否必填 662 | - `default` (string) 默认值 663 | - `options` (array[string]) 选项值 664 | - `file_upload` (object) 文件上传配置 665 | - `image` (object) 图片设置 666 | 当前仅支持图片类型:`png`, `jpg`, `jpeg`, `webp`, `gif` 667 | - `enabled` (bool) 是否开启 668 | - `number_limits` (int) 图片数量限制,默认 3 669 | - `transfer_methods` (array[string]) 传递方式列表,remote_url , local_file,必选一个 670 | - `system_parameters` (object) 系统参数 671 | - `file_size_limit` (int) 文档上传大小限制 (MB) 672 | - `image_file_size_limit` (int) 图片文件上传大小限制(MB) 673 | - `audio_file_size_limit` (int) 音频文件上传大小限制 (MB) 674 | - `video_file_size_limit` (int) 视频文件上传大小限制 (MB) 675 | 676 | 677 | 678 | 679 | 680 | 681 | ```bash {{ title: 'cURL' }} 682 | curl -X GET '${props.appDetail.api_base_url}/parameters' \ 683 | --header 'Authorization: Bearer {api_key}' 684 | ``` 685 | 686 | 687 | 688 | 689 | ```json {{ title: 'Response' }} 690 | { 691 | "user_input_form": [ 692 | { 693 | "paragraph": { 694 | "label": "Query", 695 | "variable": "query", 696 | "required": true, 697 | "default": "" 698 | } 699 | } 700 | ], 701 | "file_upload": { 702 | "image": { 703 | "enabled": false, 704 | "number_limits": 3, 705 | "detail": "high", 706 | "transfer_methods": [ 707 | "remote_url", 708 | "local_file" 709 | ] 710 | } 711 | }, 712 | "system_parameters": { 713 | "file_size_limit": 15, 714 | "image_file_size_limit": 10, 715 | "audio_file_size_limit": 50, 716 | "video_file_size_limit": 100 717 | } 718 | } 719 | ``` 720 | 721 | 722 | 723 | -------------------------------------------------------------------------------- /ecosystem.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | apps: [{ 3 | // 应用名称 4 | name: "dify2openai", 5 | 6 | // 入口文件 7 | script: "app.js", 8 | 9 | // 实例数量 10 | instances: 1, 11 | 12 | // 自动重启 13 | autorestart: true, 14 | 15 | // 监控变化 16 | watch: false 17 | }] 18 | } 19 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["*.js", "botType/*.js", "config/*.js"], 3 | "ext": "js,json,env", 4 | "ignore": [ 5 | "node_modules/", 6 | "*.test.js", 7 | "logs/*", 8 | ".git", 9 | "public/*" 10 | ], 11 | "delay": "500", 12 | "verbose": true 13 | } 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dify2openai", 3 | "version": "1.0.0", 4 | "description": "turn dify api into openai", 5 | "main": "app.js", 6 | "type": "module", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "start": "NODE_ENV=production node app.js", 10 | "dev": "NODE_ENV=development nodemon app.js", 11 | "pm2:start": "pm2 start ecosystem.config.cjs", 12 | "pm2:stop": "pm2 stop dify2openai", 13 | "pm2:restart": "pm2 restart dify2openai", 14 | "pm2:delete": "pm2 delete dify2openai", 15 | "pm2:logs": "pm2 logs dify2openai", 16 | "pm2:monit": "pm2 monit" 17 | }, 18 | "keywords": [], 19 | "author": "orence", 20 | "license": "MIT", 21 | "dependencies": { 22 | "body-parser": "^2.2.0", 23 | "dotenv": "^16.4.7", 24 | "express": "^5.1.0", 25 | "form-data": "^4.0.2", 26 | "form-data-encoder": "^4.0.2", 27 | "formdata-node": "^6.0.3", 28 | "node-fetch": "^3.3.2", 29 | "tail": "^2.2.6", 30 | "uuid": "^11.1.0", 31 | "winston": "^3.17.0", 32 | "winston-daily-rotate-file": "^5.0.0", 33 | "ws": "^8.18.1" 34 | }, 35 | "devDependencies": { 36 | "nodemon": "^3.1.9" 37 | }, 38 | "packageManager": "pnpm@9.15.1+sha512.1acb565e6193efbebda772702950469150cf12bcc764262e7587e71d19dc98a423dff9536e57ea44c49bdf790ff694e83c27be5faa23d67e0c033b583be4bfcf" 39 | } 40 | -------------------------------------------------------------------------------- /project_flow.md: -------------------------------------------------------------------------------- 1 | # Dify2OpenAI 运行流程详解 2 | 3 | ## 项目概述 4 | 5 | Dify2OpenAI是一个网关服务,将Dify应用程序转换为OpenAI API兼容的接口,使开发者能够使用熟悉的OpenAI API格式来访问Dify的LLM、知识库、工具和工作流程能力。 6 | 7 | ## 整体架构 8 | 9 | ```mermaid 10 | flowchart TB 11 | Client([客户端])<-->|OpenAI格式请求/响应|Gateway 12 | 13 | subgraph Gateway [Dify2OpenAI网关服务] 14 | direction TB 15 | style Gateway fill:#f5f5f5,stroke:#333,stroke-width:1px 16 | Parser([配置解析器]):::component 17 | ChatHandler([聊天处理器]):::component 18 | CompletionHandler([补全处理器]):::component 19 | WorkflowHandler([工作流处理器]):::component 20 | ImageProcessor([图像处理器]):::component 21 | Logger([日志系统]):::component 22 | end 23 | 24 | Gateway<-->|Dify格式请求/响应|DifyAPI([Dify API服务]) 25 | 26 | classDef component fill:#e1f5fe,stroke:#01579b,stroke-width:1px,color:#01579b 27 | classDef external fill:#f0f4c3,stroke:#827717,stroke-width:1px,color:#827717 28 | 29 | class Client,DifyAPI external 30 | ``` 31 | 32 | ## 请求处理流程 33 | 34 | ```mermaid 35 | sequenceDiagram 36 | participant client as 客户端 37 | participant gateway as Dify2OpenAI网关 38 | participant parser as 配置解析器 39 | participant selector as 处理器选择器 40 | participant chat as 聊天处理器 41 | participant completion as 补全处理器 42 | participant workflow as 工作流处理器 43 | participant image as 图像处理器 44 | participant dify as Dify API 45 | 46 | rect rgb(240, 248, 255) 47 | note right of client: 请求处理阶段 48 | client->>+gateway: 发送OpenAI格式请求 49 | gateway->>+parser: 解析Authorization和model参数 50 | parser-->>-gateway: 返回解析后的配置 51 | gateway->>+selector: 根据BOT_TYPE选择处理器 52 | end 53 | 54 | rect rgb(255, 248, 240) 55 | note right of selector: 分发到不同处理器 56 | alt 聊天机器人(Chat) 57 | selector->>+chat: 处理请求 58 | chat->>+image: 处理图像内容(如有) 59 | image-->>-chat: 返回处理后的图像 60 | chat->>dify: 发送转换后的请求 61 | deactivate chat 62 | else 补全机器人(Completion) 63 | selector->>+completion: 处理请求 64 | completion->>+image: 处理图像内容(如有) 65 | image-->>-completion: 返回处理后的图像 66 | completion->>dify: 发送转换后的请求 67 | deactivate completion 68 | else 工作流机器人(Workflow) 69 | selector->>+workflow: 处理请求 70 | workflow->>+image: 处理图像内容(如有) 71 | image-->>-workflow: 返回处理后的图像 72 | workflow->>dify: 发送转换后的请求 73 | deactivate workflow 74 | end 75 | deactivate selector 76 | end 77 | 78 | rect rgb(240, 255, 240) 79 | note right of dify: 响应处理阶段 80 | dify-->>gateway: 返回处理结果 81 | gateway->>-client: 转换为OpenAI格式并返回 82 | end 83 | ``` 84 | 85 | ## 配置解析逻辑 86 | 87 | 系统支持三种不同的配置模式,用于灵活适应不同的集成场景: 88 | 89 | ```mermaid 90 | flowchart TD 91 | Start([开始解析]):::start --> CheckAuth{检查Authorization Header} 92 | 93 | CheckAuth -->|格式无效| Error([返回错误]):::error 94 | 95 | CheckAuth -->|包含多个参数| Method1[方式一:
全部在Authorization中]:::method 96 | Method1 --> Extract1[提取DIFY_API_URL、
API_KEY、BOT_TYPE等]:::process 97 | Extract1 --> Validate 98 | 99 | CheckAuth -->|单一参数| CheckParam{检查model参数} 100 | CheckParam -->|无效| Error 101 | 102 | CheckParam -->|有效| CheckValue{检查Authorization值} 103 | 104 | CheckValue -->|不含http| Method2[方式二:
Authorization是API_KEY]:::method 105 | Method2 --> Extract2[从model中提取其他参数]:::process 106 | Extract2 --> Validate 107 | 108 | CheckValue -->|含http| Method3[方式三:
Authorization是DIFY_API_URL]:::method 109 | Method3 --> Extract3[从model中提取其他参数]:::process 110 | Extract3 --> Validate 111 | 112 | Validate[验证必要参数]:::process --> Return([返回解析后的配置]):::end 113 | 114 | classDef start fill:#d4edda,stroke:#28a745,stroke-width:2px,color:#28a745 115 | classDef process fill:#e8f4f8,stroke:#4b8bb3,stroke-width:1px 116 | classDef method fill:#fff3cd,stroke:#856404,stroke-width:1px,color:#856404 117 | classDef error fill:#f8d7da,stroke:#dc3545,stroke-width:2px,color:#dc3545 118 | classDef end fill:#d4edda,stroke:#28a745,stroke-width:2px,color:#28a745 119 | ``` 120 | 121 | ## 三种不同的接入方式 122 | 123 | ### 方式一:所有配置在Authorization Header中 124 | 125 | 使用方式: 126 | 127 | ```bash 128 | Authorization: Bearer DIFY_API_URL|API_KEY|BOT_TYPE|INPUT_VARIABLE|OUTPUT_VARIABLE 129 | ``` 130 | 131 | ### 方式二:Authorization Header传递API_KEY 132 | 133 | 使用方式: 134 | 135 | ```bash 136 | Authorization: Bearer API_KEY 137 | "model": "dify|BOT_TYPE|DIFY_API_URL|INPUT_VARIABLE|OUTPUT_VARIABLE" 138 | ``` 139 | 140 | ### 方式三:Authorization Header传递DIFY_API_URL 141 | 142 | 使用方式: 143 | 144 | ```bash 145 | Authorization: Bearer DIFY_API_URL 146 | "model": "dify|API_KEY|BOT_TYPE|INPUT_VARIABLE|OUTPUT_VARIABLE" 147 | ``` 148 | 149 | ## 消息处理流程 150 | 151 | ```mermaid 152 | flowchart TB 153 | Start([接收请求]):::start --> Parse[解析消息结构]:::process 154 | Parse --> ScanImages[扫描图像内容]:::process 155 | 156 | ScanImages --> CheckType{图像类型检查} 157 | CheckType -->|Base64数据| Upload[上传到Dify]:::upload 158 | CheckType -->|远程URL| Direct[直接使用URL]:::url 159 | 160 | Upload --> BuildReq[构建Dify请求]:::process 161 | Direct --> BuildReq 162 | 163 | Parse --> ExtractText[提取文本内容]:::process 164 | ExtractText --> BuildReq 165 | 166 | BuildReq --> CheckMode{是否流式模式} 167 | CheckMode -->|是| StreamMode[设置流式响应]:::stream 168 | CheckMode -->|否| BlockMode[设置阻塞响应]:::block 169 | 170 | StreamMode --> SendReq[发送到Dify]:::send 171 | BlockMode --> SendReq 172 | 173 | SendReq --> ReceiveResp[接收Dify响应]:::receive 174 | ReceiveResp --> Convert[转换为OpenAI格式]:::process 175 | Convert --> Return([返回给客户端]):::end 176 | 177 | classDef start fill:#d4edda,stroke:#28a745,stroke-width:2px,color:#28a745 178 | classDef process fill:#e8f4f8,stroke:#4b8bb3,stroke-width:1px 179 | classDef upload fill:#e6c3e6,stroke:#8e44ad,stroke-width:1px,color:#8e44ad 180 | classDef url fill:#c3e6cb,stroke:#155724,stroke-width:1px,color:#155724 181 | classDef stream fill:#b8daff,stroke:#004085,stroke-width:1px,color:#004085 182 | classDef block fill:#ffeeba,stroke:#856404,stroke-width:1px,color:#856404 183 | classDef send fill:#d6d8d9,stroke:#1b1e21,stroke-width:1px,color:#1b1e21 184 | classDef receive fill:#f5c6cb,stroke:#721c24,stroke-width:1px,color:#721c24 185 | classDef end fill:#d4edda,stroke:#28a745,stroke-width:2px,color:#28a745 186 | ``` 187 | 188 | ## 图像处理逻辑 189 | 190 | ```mermaid 191 | flowchart TB 192 | Start([检测到图像]):::start --> CheckType{图像类型?} 193 | 194 | subgraph Base64处理 [Base64图像处理流程] 195 | direction TB 196 | style Base64处理 fill:#f0f0ff,stroke:#8080ff,stroke-width:1px 197 | ParseData[解析base64数据]:::process --> ExtractMime[提取MIME类型]:::process 198 | ExtractMime --> CreateBuffer[创建文件Buffer]:::process 199 | CreateBuffer --> CreateForm[创建FormData]:::process 200 | CreateForm --> Upload[上传到Dify]:::upload 201 | Upload --> GetFileId[获取文件ID]:::process 202 | end 203 | 204 | subgraph URL处理 [URL图像处理流程] 205 | direction TB 206 | style URL处理 fill:#f0fff0,stroke:#80ff80,stroke-width:1px 207 | ExtractExt[提取文件扩展名]:::process --> DetermineType[确定文件类型]:::process 208 | end 209 | 210 | CheckType -->|Base64| Base64处理 211 | CheckType -->|URL| URL处理 212 | 213 | GetFileId --> AddToReq([添加到请求中]):::end 214 | DetermineType --> AddToReq 215 | 216 | classDef start fill:#d4edda,stroke:#28a745,stroke-width:2px,color:#28a745 217 | classDef process fill:#e8f4f8,stroke:#4b8bb3,stroke-width:1px 218 | classDef upload fill:#e6c3e6,stroke:#8e44ad,stroke-width:1px,color:#8e44ad 219 | classDef end fill:#d4edda,stroke:#28a745,stroke-width:2px,color:#28a745 220 | ``` 221 | 222 | ## 响应处理流程 223 | 224 | ```mermaid 225 | sequenceDiagram 226 | participant handler as 处理器 227 | participant dify as Dify API 228 | participant client as 客户端 229 | 230 | rect rgb(240, 248, 255) 231 | handler->>dify: 发送请求 232 | end 233 | 234 | alt 流式模式 (stream=true) 235 | rect rgb(240, 255, 240) 236 | dify-->>handler: 返回数据流 237 | handler->>handler: 创建PassThrough流 238 | loop 每个数据块 239 | handler->>handler: 转换为OpenAI格式 240 | handler->>client: 发送数据块 241 | end 242 | handler->>client: 发送结束标记 (data: [DONE]) 243 | end 244 | else 阻塞模式 (stream=false) 245 | rect rgb(255, 248, 240) 246 | dify-->>handler: 返回完整响应 247 | handler->>handler: 转换为OpenAI格式 248 | handler->>client: 发送完整响应 249 | end 250 | end 251 | ``` 252 | 253 | ## 日志系统 254 | 255 | ```mermaid 256 | flowchart LR 257 | ReqLog([请求日志]):::logtype --> Logger 258 | RespLog([响应日志]):::logtype --> Logger 259 | ApiLog([API调用日志]):::logtype --> Logger 260 | ErrorLog([错误日志]):::logtype --> Logger 261 | 262 | subgraph LogSystem [日志处理系统] 263 | direction TB 264 | style LogSystem fill:#f8f9fa,stroke:#6c757d,stroke-width:1px 265 | Logger([日志系统]):::component --> Sanitize[敏感信息处理]:::process 266 | Sanitize --> Output[日志输出]:::process 267 | end 268 | 269 | Output --> Console([控制台输出]):::output 270 | Output --> Files([文件存储]):::output 271 | 272 | Files --> Rotation[日志轮转]:::process 273 | Rotation --> Cleanup[过期日志清理]:::process 274 | 275 | classDef logtype fill:#e2f0d9,stroke:#548235,stroke-width:1px,color:#548235 276 | classDef component fill:#deebf7,stroke:#4472c4,stroke-width:1px,color:#4472c4 277 | classDef process fill:#e8f4f8,stroke:#4b8bb3,stroke-width:1px 278 | classDef output fill:#fff2cc,stroke:#ed7d31,stroke-width:1px,color:#ed7d31 279 | ``` 280 | 281 | ## 部署架构 282 | 283 | ```mermaid 284 | flowchart TB 285 | classDef deploy fill:#ede7f6,stroke:#673ab7,stroke-width:1px,color:#673ab7 286 | classDef server fill:#e3f2fd,stroke:#2196f3,stroke-width:1px,color:#2196f3 287 | classDef component fill:#e8f5e9,stroke:#4caf50,stroke-width:1px,color:#4caf50 288 | classDef external fill:#fff3e0,stroke:#ff9800,stroke-width:1px,color:#ff9800 289 | 290 | Local([本地部署]):::deploy --> PM2([PM2进程管理]):::server 291 | PM2 --> Server([Express服务器]):::server 292 | 293 | Cloud([云部署]):::deploy --> Vercel([Vercel平台]):::server 294 | Vercel --> ServerlessFunc([无服务器函数]):::server 295 | 296 | subgraph 处理层 297 | direction TB 298 | style 处理层 fill:#f5f5f5,stroke:#333,stroke-width:1px 299 | Server --> Handler([请求处理器]):::component 300 | ServerlessFunc --> Handler 301 | end 302 | 303 | Handler --> DifyAPI([Dify API]):::external 304 | ``` 305 | 306 | ## 总结 307 | 308 | Dify2OpenAI网关服务通过巧妙的转换机制,使得开发者能够用统一的OpenAI API格式来调用Dify的各种能力,极大地简化了开发流程和集成难度。系统支持多种配置方式和机器人类型,能够灵活适应不同的应用场景需求。 309 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Dify2OpenAI Gateway 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 32 | 82 | 83 | 84 | 85 |
86 |
87 |
88 | 89 | Dify2OpenAI 90 |
91 | 98 |
99 | 100 |
101 | 102 |
103 |
104 |

105 | 106 | Dify2OpenAI Gateway 107 |

108 |

109 | Dify2OpenAI 是一个将 Dify 应用程序转换为 OpenAI API 接口的网关服务,使您可以使用 OpenAI API 兼容的方式与 Dify 应用进行交互。 110 |

111 | 121 |
122 |
123 | 124 | 125 |
126 |

127 | 配置参数 128 |

129 |
130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 145 | 146 | 147 | 148 | 149 | 152 | 153 | 154 | 155 | 156 | 159 | 160 | 161 | 162 | 163 | 166 | 167 | 168 | 169 | 170 | 173 | 174 | 175 |
参数说明示例
DIFY_API_URLDify 服务的 API 基础 URL 143 | https://cloud.dify.ai/v1 144 |
API_KEYDify 应用的 API 密钥 150 | app-xxxx 151 |
BOT_TYPE应用类型(Chat、Completion、Workflow) 157 | Chat 158 |
INPUT_VARIABLE输入变量名称(仅 Workflow 类型需要) 164 | input 165 |
OUTPUT_VARIABLE输出变量名称(仅 Workflow 类型需要) 171 | output 172 |
176 |
177 |
178 | 179 | 180 |
181 |

182 | 接入方式 183 |

184 | 185 | 186 |
187 |

188 | 方式一:所有配置在 Authorization Header 中 189 |

190 |
191 |

✔️ Authorization Header 格式:

192 |
193 | Authorization: Bearer DIFY_API_URL|API_KEY|BOT_TYPE|INPUT_VARIABLE|OUTPUT_VARIABLE 194 |
195 |
196 |
197 |

✔️ 示例:

198 |
199 | Authorization: Bearer https://cloud.dify.ai/v1|app-xxxx|Chat 200 |
201 |
202 |
203 |

model 参数设置为 dify

205 |
206 |
207 | 208 |
209 |
211 |

基础对话示例

212 | 方式一 213 |
214 |
215 |
curl http://localhost:3099/v1/chat/completions \
216 |   -H "Content-Type: application/json" \
217 |   -H "Authorization: Bearer https://cloud.dify.ai/v1|app-xxxx|Chat" \
218 |   -X POST \
219 |   -d '{
220 |     "model": "dify",
221 |     "stream": true,
222 |     "messages": [
223 |       {
224 |         "role": "system",
225 |         "content": "You are a helpful assistant."
226 |       },
227 |       {
228 |         "role": "user",
229 |         "content": "你好"
230 |       }
231 |     ]
232 |   }'
233 |
234 |
235 | 236 |
237 |
239 |

带图片的对话示例

240 | 图像支持 241 |
242 |
243 |
curl http://localhost:3099/v1/chat/completions \
244 |   -H "Content-Type: application/json" \
245 |   -H "Authorization: Bearer https://cloud.dify.ai/v1|app-xxxx|Chat" \
246 |   -X POST \
247 |   -d '{
248 |     "model": "dify",
249 |     "stream": true,
250 |     "messages": [
251 |       {
252 |         "role": "user",
253 |         "content": [
254 |           "请分析这张图片。",
255 |           {
256 |             "type": "image_url",
257 |             "image_url": {
258 |               "url": "https://example.com/image.jpg"
259 |             }
260 |           }
261 |         ]
262 |       }
263 |     ]
264 |   }'
265 |
266 |
267 | 268 | 269 |
270 |

271 | 方式二:Authorization Header 传递 API_KEY,model 参数传递其他配置 272 |

273 |
274 |

✔️ Authorization Header 格式:

275 |
276 | Authorization: Bearer API_KEY 277 |
278 |
279 |
280 |

✔️ model 参数格式:

281 |
282 | "model": "dify|BOT_TYPE|DIFY_API_URL|INPUT_VARIABLE|OUTPUT_VARIABLE" 283 |
284 |
285 |
286 | 287 |
288 |
290 |

基础对话示例

291 | 方式二 292 |
293 |
294 |
curl http://localhost:3099/v1/chat/completions \
295 |   -H "Content-Type: application/json" \
296 |   -H "Authorization: Bearer app-xxxx" \
297 |   -X POST \
298 |   -d '{  
299 |     "model": "dify|Chat|https://cloud.dify.ai/v1",
300 |     "stream": true,
301 |     "messages": [
302 |       {
303 |         "role": "system",
304 |         "content": "You are a helpful assistant."
305 |       },
306 |       {
307 |         "role": "user",
308 |         "content": "你好"
309 |       }
310 |     ]
311 |   }'
312 |
313 |
314 | 315 |
316 |
318 |

带图片的对话示例

319 | 图像支持 320 |
321 |
322 |
curl http://localhost:3099/v1/chat/completions \
323 |   -H "Content-Type: application/json" \
324 |   -H "Authorization: Bearer app-xxxx" \
325 |   -X POST \
326 |   -d '{  
327 |     "model": "dify|Chat|https://cloud.dify.ai/v1",
328 |     "stream": true,
329 |     "messages": [
330 |       {
331 |         "role": "user",
332 |         "content": [
333 |           "请分析这张图片。",
334 |           {
335 |             "type": "image_url",
336 |             "image_url": {
337 |               "url": "https://example.com/image.jpg"
338 |             }
339 |           }
340 |         ]
341 |       }
342 |     ]
343 |   }'
344 |
345 |
346 | 347 | 348 |
349 |

350 | 方式三:Authorization Header 传递 DIFY_API_URL,model 参数传阒其他配置 351 |

352 |
353 |

✔️ Authorization Header 格式:

354 |
355 | Authorization: Bearer DIFY_API_URL 356 |
357 |
358 |
359 |

✔️ model 参数格式:

360 |
361 | "model": "dify|API_KEY|BOT_TYPE|INPUT_VARIABLE|OUTPUT_VARIABLE" 362 |
363 |
364 |
365 | 366 |
367 |
369 |

基础对话示例

370 | 方式三 371 |
372 |
373 |
curl http://localhost:3099/v1/chat/completions \
374 |   -H "Content-Type: application/json" \
375 |   -H "Authorization: Bearer https://cloud.dify.ai/v1" \
376 |   -X POST \
377 |   -d '{  
378 |     "model": "dify|app-xxxx|Chat",
379 |     "stream": true,
380 |     "messages": [
381 |       {
382 |         "role": "system",
383 |         "content": "You are a helpful assistant."
384 |       },
385 |       {
386 |         "role": "user",
387 |         "content": "你好"
388 |       }
389 |     ]
390 |   }'
391 |
392 |
393 | 394 |
395 |
397 |

带图片的对话示例

398 | 图像支持 399 |
400 |
401 |
curl http://localhost:3099/v1/chat/completions \
402 |   -H "Content-Type: application/json" \
403 |   -H "Authorization: Bearer https://cloud.dify.ai/v1" \
404 |   -X POST \
405 |   -d '{  
406 |     "model": "dify|app-xxxx|Chat",
407 |     "stream": true,
408 |     "messages": [
409 |       {
410 |         "role": "user",
411 |         "content": [
412 |           "请分析这张图片。",
413 |           {
414 |             "type": "image_url",
415 |             "image_url": {
416 |               "url": "https://example.com/image.jpg"
417 |             }
418 |           }
419 |         ]
420 |       }
421 |     ]
422 |   }'
423 |
424 |
425 |
426 | 427 | 428 |
429 |
430 |
431 |
432 | 433 |
434 |

接口转换

435 |

将 Dify API 无缝转换为 OpenAI API,兼容各种 OpenAI 客户端

436 |
437 |
438 |
439 | 440 |
441 |

多模态支持

442 |

支持图像、文档、音频和视频等多种类型文件处理

443 |
444 |
445 |
446 | 447 |
448 |

多种应用类型

449 |

支持 Chat、Completion、Workflow 等不同类型的 Dify 应用

450 |
451 |
452 |
453 | 454 | 455 |
456 |
457 |
458 |

459 | Dify2OpenAI 460 |

461 |

将 Dify 应用程序转换为 OpenAI API 接口的网关服务

462 |
463 |
464 |

快速链接

465 | 471 |
472 |
473 |

资源链接

474 | 482 |
483 |
484 |
485 |

© 2025 Dify2OpenAI

486 |

更新日期: 2025年4月5日

487 |
488 |
489 | 490 | 491 | 493 | 494 | 495 |
496 | 497 | 498 | 499 | 500 | 501 | 786 | 787 | 788 | -------------------------------------------------------------------------------- /public/logs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 实时日志查看器 7 | 78 | 79 | 80 |
81 |
82 |

实时日志查看器

83 |
84 | 87 | 88 | 89 | 90 |
未连接
91 |
92 |
93 |
94 |
95 | 96 | 194 | 195 | 196 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "builds": [ 4 | { 5 | "src": "app.js", 6 | "use": "@vercel/node" 7 | } 8 | ], 9 | "routes": [ 10 | { 11 | "src": "/(.*)", 12 | "dest": "app.js" 13 | } 14 | ] 15 | } 16 | --------------------------------------------------------------------------------