@wwlocal/aibot-plugin-node 20260409.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/README.md +489 -0
  2. package/config.example.json +169 -0
  3. package/dist/cjs/index.js +76 -0
  4. package/dist/cjs/src/adapters/anthropic-adapter.js +534 -0
  5. package/dist/cjs/src/adapters/base-adapter.js +176 -0
  6. package/dist/cjs/src/adapters/deepseek-adapter.js +328 -0
  7. package/dist/cjs/src/adapters/dify-adapter.js +636 -0
  8. package/dist/cjs/src/adapters/index.js +131 -0
  9. package/dist/cjs/src/adapters/openai-adapter.js +361 -0
  10. package/dist/cjs/src/adapters/webhook-adapter.js +260 -0
  11. package/dist/cjs/src/agent-forwarder.js +87 -0
  12. package/dist/cjs/src/ca-cert.js +162 -0
  13. package/dist/cjs/src/config.js +169 -0
  14. package/dist/cjs/src/const.js +124 -0
  15. package/dist/cjs/src/conversation-manager.js +147 -0
  16. package/dist/cjs/src/dm-policy.js +46 -0
  17. package/dist/cjs/src/group-policy.js +95 -0
  18. package/dist/cjs/src/media-handler.js +136 -0
  19. package/dist/cjs/src/media-loader.js +271 -0
  20. package/dist/cjs/src/media-storage.js +165 -0
  21. package/dist/cjs/src/media-uploader.js +203 -0
  22. package/dist/cjs/src/message-parser.js +133 -0
  23. package/dist/cjs/src/message-sender.js +87 -0
  24. package/dist/cjs/src/monitor.js +849 -0
  25. package/dist/cjs/src/reqid-store.js +87 -0
  26. package/dist/cjs/src/server.js +72 -0
  27. package/dist/cjs/src/service-manager.js +135 -0
  28. package/dist/cjs/src/state-manager.js +143 -0
  29. package/dist/cjs/src/template-card-parser.js +498 -0
  30. package/dist/cjs/src/timeout.js +41 -0
  31. package/dist/cjs/src/version.js +25 -0
  32. package/dist/esm/index.js +74 -0
  33. package/dist/esm/src/adapters/anthropic-adapter.js +512 -0
  34. package/dist/esm/src/adapters/base-adapter.js +174 -0
  35. package/dist/esm/src/adapters/deepseek-adapter.js +326 -0
  36. package/dist/esm/src/adapters/dify-adapter.js +634 -0
  37. package/dist/esm/src/adapters/index.js +123 -0
  38. package/dist/esm/src/adapters/openai-adapter.js +339 -0
  39. package/dist/esm/src/adapters/webhook-adapter.js +258 -0
  40. package/dist/esm/src/agent-forwarder.js +84 -0
  41. package/dist/esm/src/ca-cert.js +136 -0
  42. package/dist/esm/src/config.js +145 -0
  43. package/dist/esm/src/const.js +100 -0
  44. package/dist/esm/src/conversation-manager.js +144 -0
  45. package/dist/esm/src/dm-policy.js +44 -0
  46. package/dist/esm/src/group-policy.js +92 -0
  47. package/dist/esm/src/media-handler.js +133 -0
  48. package/dist/esm/src/media-loader.js +246 -0
  49. package/dist/esm/src/media-storage.js +143 -0
  50. package/dist/esm/src/media-uploader.js +198 -0
  51. package/dist/esm/src/message-parser.js +131 -0
  52. package/dist/esm/src/message-sender.js +83 -0
  53. package/dist/esm/src/monitor.js +841 -0
  54. package/dist/esm/src/reqid-store.js +85 -0
  55. package/dist/esm/src/server.js +69 -0
  56. package/dist/esm/src/service-manager.js +133 -0
  57. package/dist/esm/src/state-manager.js +134 -0
  58. package/dist/esm/src/template-card-parser.js +495 -0
  59. package/dist/esm/src/timeout.js +38 -0
  60. package/dist/esm/src/version.js +22 -0
  61. package/dist/esm/types/index.d.ts +14 -0
  62. package/dist/esm/types/src/adapters/anthropic-adapter.d.ts +93 -0
  63. package/dist/esm/types/src/adapters/base-adapter.d.ts +76 -0
  64. package/dist/esm/types/src/adapters/deepseek-adapter.d.ts +87 -0
  65. package/dist/esm/types/src/adapters/dify-adapter.d.ts +100 -0
  66. package/dist/esm/types/src/adapters/index.d.ts +60 -0
  67. package/dist/esm/types/src/adapters/openai-adapter.d.ts +82 -0
  68. package/dist/esm/types/src/adapters/types.d.ts +373 -0
  69. package/dist/esm/types/src/adapters/webhook-adapter.d.ts +54 -0
  70. package/dist/esm/types/src/agent-forwarder.d.ts +32 -0
  71. package/dist/esm/types/src/ca-cert.d.ts +53 -0
  72. package/dist/esm/types/src/config.d.ts +29 -0
  73. package/dist/esm/types/src/const.d.ts +74 -0
  74. package/dist/esm/types/src/conversation-manager.d.ts +81 -0
  75. package/dist/esm/types/src/dm-policy.d.ts +27 -0
  76. package/dist/esm/types/src/group-policy.d.ts +28 -0
  77. package/dist/esm/types/src/interface.d.ts +332 -0
  78. package/dist/esm/types/src/media-handler.d.ts +36 -0
  79. package/dist/esm/types/src/media-loader.d.ts +47 -0
  80. package/dist/esm/types/src/media-storage.d.ts +35 -0
  81. package/dist/esm/types/src/media-uploader.d.ts +65 -0
  82. package/dist/esm/types/src/message-parser.d.ts +89 -0
  83. package/dist/esm/types/src/message-sender.d.ts +34 -0
  84. package/dist/esm/types/src/monitor.d.ts +30 -0
  85. package/dist/esm/types/src/reqid-store.d.ts +23 -0
  86. package/dist/esm/types/src/server.d.ts +23 -0
  87. package/dist/esm/types/src/service-manager.d.ts +52 -0
  88. package/dist/esm/types/src/state-manager.d.ts +76 -0
  89. package/dist/esm/types/src/template-card-parser.d.ts +18 -0
  90. package/dist/esm/types/src/timeout.d.ts +20 -0
  91. package/dist/esm/types/src/version.d.ts +2 -0
  92. package/dist/index.d.ts +2 -0
  93. package/package.json +51 -0
@@ -0,0 +1,636 @@
1
+ 'use strict';
2
+
3
+ var baseAdapter = require('./base-adapter.js');
4
+
5
+ /**
6
+ * Dify 适配器
7
+ *
8
+ * 适用于:Dify 平台的 Chat 应用(ChatBot / Chatflow)
9
+ *
10
+ * 与 OpenAI 的核心差异:
11
+ * - 接口路径:/v1/chat-messages(而非 /v1/chat/completions)
12
+ * - 请求格式:{ query, user, conversation_id, response_mode, inputs, files }
13
+ * - 流式标识:response_mode: "streaming"
14
+ * - SSE 事件类型:
15
+ * - ChatBot 模式:message(answer 字段)
16
+ * - Chatflow 模式:text_chunk(text 字段)+ workflow/node/iteration/loop 事件
17
+ * - 会话管理:conversation_id(有状态多轮对话)
18
+ * - 文件上传:/v1/files/upload(multipart form-data)
19
+ */
20
+ /**
21
+ * 会话 ID 缓存(chatId -> conversation_id)
22
+ * 带 TTL 清理,防止内存泄漏
23
+ */
24
+ class ConversationCache {
25
+ constructor(ttlMs = 24 * 60 * 60 * 1000) {
26
+ this.cache = new Map();
27
+ // 默认 24 小时
28
+ this.ttlMs = ttlMs;
29
+ }
30
+ get(chatId) {
31
+ const entry = this.cache.get(chatId);
32
+ if (!entry)
33
+ return undefined;
34
+ // 检查 TTL
35
+ if (Date.now() - entry.timestamp > this.ttlMs) {
36
+ this.cache.delete(chatId);
37
+ return undefined;
38
+ }
39
+ return entry.conversationId;
40
+ }
41
+ set(chatId, conversationId) {
42
+ this.cache.set(chatId, {
43
+ conversationId,
44
+ timestamp: Date.now(),
45
+ });
46
+ // 定期清理过期条目(简单实现)
47
+ if (this.cache.size > 1000) {
48
+ this.cleanup();
49
+ }
50
+ }
51
+ cleanup() {
52
+ const now = Date.now();
53
+ for (const [key, entry] of this.cache.entries()) {
54
+ if (now - entry.timestamp > this.ttlMs) {
55
+ this.cache.delete(key);
56
+ }
57
+ }
58
+ }
59
+ }
60
+ /**
61
+ * Dify 适配器
62
+ */
63
+ class DifyAdapter extends baseAdapter.BaseAdapter {
64
+ constructor() {
65
+ super(...arguments);
66
+ this.name = "dify";
67
+ this.displayName = "Dify";
68
+ }
69
+ /**
70
+ * 获取 Dify 配置选项
71
+ */
72
+ getOptions(endpoint) {
73
+ return endpoint.providerOptions || {};
74
+ }
75
+ /**
76
+ * 转发请求到 Dify API
77
+ */
78
+ async forward(request, endpoint, callbacks) {
79
+ const { text, mediaPaths, abortSignal, runtime, context } = request;
80
+ const { deliver, onReplyStart, onError } = callbacks;
81
+ const options = this.getOptions(endpoint);
82
+ this.log(runtime, `Forwarding to Dify: url=${endpoint.url}, appType=${options.difyAppType || "chat"}`);
83
+ // 获取 chatId(用于会话管理)
84
+ const chatId = context?.chatId || "default";
85
+ const userId = options.difyUser || context?.userId || "wecom-user";
86
+ // 构建会话隔离 key:accountId + endpointName + chatId
87
+ // 防止不同账号/不同 Dify 应用共用 conversation_id
88
+ const accountId = context?.accountId || "default";
89
+ const conversationKey = `${accountId}:${endpoint.name}:${chatId}`;
90
+ // 获取已有的 conversation_id
91
+ const existingConversationId = DifyAdapter.conversationCache.get(conversationKey);
92
+ // 处理文件(如果有 mediaPaths)
93
+ // - 远程 URL:直接使用 transfer_method: "remote_url",无需上传
94
+ // - 本地文件:先上传到 Dify /v1/files/upload,再用 transfer_method: "local_file"
95
+ let difyFiles;
96
+ if (mediaPaths && mediaPaths.length > 0) {
97
+ difyFiles = await this.prepareDifyFiles({
98
+ mediaPaths,
99
+ baseUrl: endpoint.url,
100
+ apiKey: endpoint.apiKey || "",
101
+ user: userId,
102
+ runtime,
103
+ });
104
+ }
105
+ // 构建请求体
106
+ const requestBody = this.buildDifyRequest({
107
+ query: text,
108
+ user: userId,
109
+ conversationId: existingConversationId,
110
+ inputs: options.difyInputs,
111
+ stream: endpoint.stream !== false,
112
+ files: difyFiles,
113
+ });
114
+ // 构建请求头
115
+ const headers = this.buildHeaders(endpoint);
116
+ // 拼接 chat-messages 路径
117
+ const chatMessagesUrl = this.buildChatMessagesUrl(endpoint.url);
118
+ // 超时控制
119
+ const timeoutMs = endpoint.timeoutMs || 300000;
120
+ const { controller, timeoutId } = this.createTimeoutController(timeoutMs, abortSignal);
121
+ try {
122
+ const response = await this.fetchWithErrorHandling(chatMessagesUrl, {
123
+ method: "POST",
124
+ headers,
125
+ body: JSON.stringify(requestBody),
126
+ signal: controller.signal,
127
+ }, runtime);
128
+ let finalText;
129
+ if (requestBody.response_mode === "streaming") {
130
+ // 流式响应
131
+ finalText = await this.handleStreamResponse({
132
+ response,
133
+ chatId: conversationKey,
134
+ baseUrl: endpoint.url,
135
+ apiKey: endpoint.apiKey || "",
136
+ deliver,
137
+ onReplyStart,
138
+ onError,
139
+ runtime,
140
+ showWorkflowProgress: options.showWorkflowProgress,
141
+ });
142
+ }
143
+ else {
144
+ // 非流式响应
145
+ finalText = await this.handleBlockingResponse({
146
+ response,
147
+ chatId: conversationKey,
148
+ deliver,
149
+ onReplyStart,
150
+ onError,
151
+ runtime,
152
+ });
153
+ }
154
+ // 最终 deliver
155
+ if (finalText) {
156
+ try {
157
+ await deliver({ text: finalText }, { kind: "final" });
158
+ }
159
+ catch (e) {
160
+ onError?.(e instanceof Error ? e : new Error(String(e)), { kind: "final" });
161
+ }
162
+ }
163
+ this.log(runtime, `Response complete: textLength=${finalText.length}`);
164
+ return finalText;
165
+ }
166
+ catch (err) {
167
+ if (err?.name === "AbortError") {
168
+ const error = new Error(`Request timed out after ${timeoutMs}ms`);
169
+ this.logError(runtime, error.message);
170
+ onError?.(error, { kind: "timeout" });
171
+ throw error;
172
+ }
173
+ throw err;
174
+ }
175
+ finally {
176
+ clearTimeout(timeoutId);
177
+ }
178
+ }
179
+ /**
180
+ * 构建 Dify 请求体
181
+ */
182
+ buildDifyRequest(params) {
183
+ const request = {
184
+ query: params.query,
185
+ user: params.user,
186
+ response_mode: params.stream ? "streaming" : "blocking",
187
+ // Dify 文档要求 inputs 必传,即使为空也需传 {}
188
+ inputs: params.inputs && Object.keys(params.inputs).length > 0
189
+ ? params.inputs
190
+ : {},
191
+ };
192
+ if (params.conversationId) {
193
+ request.conversation_id = params.conversationId;
194
+ }
195
+ if (params.files && params.files.length > 0) {
196
+ request.files = params.files;
197
+ }
198
+ return request;
199
+ }
200
+ /**
201
+ * 处理流式响应
202
+ */
203
+ async handleStreamResponse(params) {
204
+ const { response, chatId, baseUrl, deliver, onReplyStart, onError, runtime, showWorkflowProgress } = params;
205
+ const body = response.body;
206
+ if (!body) {
207
+ throw new Error("Response has no body for streaming");
208
+ }
209
+ const reader = body.getReader();
210
+ let accumulatedText = "";
211
+ let replyStarted = false;
212
+ let currentEvent;
213
+ try {
214
+ for await (const { event, data } of this.readSSEStream(reader, runtime)) {
215
+ // 更新当前事件类型
216
+ if (event) {
217
+ currentEvent = event;
218
+ }
219
+ // 跳过 ping 事件
220
+ if (currentEvent === "ping" || data === "") {
221
+ continue;
222
+ }
223
+ // 解析事件数据
224
+ let eventData;
225
+ try {
226
+ eventData = JSON.parse(data);
227
+ }
228
+ catch {
229
+ this.log(runtime, `Failed to parse Dify SSE JSON: ${data.slice(0, 200)}`);
230
+ continue;
231
+ }
232
+ // 处理不同事件类型
233
+ const eventType = eventData.event || currentEvent;
234
+ switch (eventType) {
235
+ case "message":
236
+ // ChatBot 模式:文本增量(answer 字段)
237
+ if (eventData.answer) {
238
+ if (!replyStarted) {
239
+ replyStarted = true;
240
+ try {
241
+ await onReplyStart?.();
242
+ }
243
+ catch (e) {
244
+ this.logError(runtime, `onReplyStart error: ${String(e)}`);
245
+ }
246
+ }
247
+ accumulatedText += eventData.answer;
248
+ try {
249
+ await deliver({ text: accumulatedText }, { kind: "block" });
250
+ }
251
+ catch (e) {
252
+ onError?.(e instanceof Error ? e : new Error(String(e)), { kind: "block" });
253
+ }
254
+ }
255
+ // 保存 conversation_id
256
+ if (eventData.conversation_id) {
257
+ DifyAdapter.conversationCache.set(chatId, eventData.conversation_id);
258
+ }
259
+ break;
260
+ case "text_chunk":
261
+ // Chatflow 模式:文本片段(text 字段,非 answer)
262
+ if (eventData.text) {
263
+ if (!replyStarted) {
264
+ replyStarted = true;
265
+ try {
266
+ await onReplyStart?.();
267
+ }
268
+ catch (e) {
269
+ this.logError(runtime, `onReplyStart error: ${String(e)}`);
270
+ }
271
+ }
272
+ accumulatedText += eventData.text;
273
+ try {
274
+ await deliver({ text: accumulatedText }, { kind: "block" });
275
+ }
276
+ catch (e) {
277
+ onError?.(e instanceof Error ? e : new Error(String(e)), { kind: "block" });
278
+ }
279
+ }
280
+ // 保存 conversation_id
281
+ if (eventData.conversation_id) {
282
+ DifyAdapter.conversationCache.set(chatId, eventData.conversation_id);
283
+ }
284
+ break;
285
+ case "message_end":
286
+ // 消息结束
287
+ this.log(runtime, "Dify message_end received");
288
+ if (eventData.conversation_id) {
289
+ DifyAdapter.conversationCache.set(chatId, eventData.conversation_id);
290
+ }
291
+ break;
292
+ case "message_file":
293
+ // 文件消息:优先使用 url,如果是相对路径则拼接完整 URL
294
+ if (eventData.url) {
295
+ const fileUrl = this.resolveFileUrl(eventData.url, baseUrl);
296
+ this.log(runtime, `Dify file received: type=${eventData.type || "unknown"}, url=${fileUrl}`);
297
+ try {
298
+ await deliver({ mediaUrl: fileUrl }, { kind: "block" });
299
+ }
300
+ catch (e) {
301
+ onError?.(e instanceof Error ? e : new Error(String(e)), { kind: "block" });
302
+ }
303
+ }
304
+ break;
305
+ case "message_replace":
306
+ // 替换消息(用新内容覆盖)—— ChatBot 模式
307
+ if (eventData.answer !== undefined) {
308
+ accumulatedText = eventData.answer;
309
+ try {
310
+ await deliver({ text: accumulatedText }, { kind: "block" });
311
+ }
312
+ catch (e) {
313
+ onError?.(e instanceof Error ? e : new Error(String(e)), { kind: "block" });
314
+ }
315
+ }
316
+ break;
317
+ case "text_replace":
318
+ // 替换文本(用新内容覆盖)—— Chatflow 模式
319
+ if (eventData.text !== undefined) {
320
+ accumulatedText = eventData.text;
321
+ try {
322
+ await deliver({ text: accumulatedText }, { kind: "block" });
323
+ }
324
+ catch (e) {
325
+ onError?.(e instanceof Error ? e : new Error(String(e)), { kind: "block" });
326
+ }
327
+ }
328
+ break;
329
+ case "workflow_started":
330
+ if (showWorkflowProgress) {
331
+ this.log(runtime, "Dify workflow started");
332
+ }
333
+ // 保存 conversation_id
334
+ if (eventData.conversation_id) {
335
+ DifyAdapter.conversationCache.set(chatId, eventData.conversation_id);
336
+ }
337
+ break;
338
+ case "workflow_finished":
339
+ if (showWorkflowProgress) {
340
+ this.log(runtime, `Dify workflow finished: status=${eventData.data?.status}`);
341
+ }
342
+ break;
343
+ case "node_started":
344
+ if (showWorkflowProgress && eventData.data?.title) {
345
+ this.log(runtime, `Dify node started: ${eventData.data.title}`);
346
+ }
347
+ break;
348
+ case "node_finished":
349
+ if (showWorkflowProgress && eventData.data?.title) {
350
+ this.log(runtime, `Dify node finished: ${eventData.data.title}`);
351
+ }
352
+ break;
353
+ case "node_retry":
354
+ if (showWorkflowProgress && eventData.data?.title) {
355
+ this.log(runtime, `Dify node retry: ${eventData.data.title}, retryIndex=${eventData.data.retry_index}`);
356
+ }
357
+ break;
358
+ case "iteration_started":
359
+ case "iteration_next":
360
+ case "iteration_completed":
361
+ if (showWorkflowProgress) {
362
+ this.log(runtime, `Dify ${eventType}: index=${eventData.data?.index}`);
363
+ }
364
+ break;
365
+ case "loop_started":
366
+ case "loop_next":
367
+ case "loop_completed":
368
+ if (showWorkflowProgress) {
369
+ this.log(runtime, `Dify ${eventType}: index=${eventData.data?.index}`);
370
+ }
371
+ break;
372
+ case "workflow_paused":
373
+ this.log(runtime, "Dify workflow paused (waiting for human input)");
374
+ break;
375
+ case "agent_log":
376
+ if (showWorkflowProgress) {
377
+ this.log(runtime, `Dify agent_log: ${data.slice(0, 200)}`);
378
+ }
379
+ break;
380
+ case "error":
381
+ // 错误事件
382
+ const errorMsg = eventData.message || "Unknown Dify error";
383
+ this.logError(runtime, `Dify error: code=${eventData.code}, status=${eventData.status}, message=${errorMsg}`);
384
+ onError?.(new Error(errorMsg), { kind: "dify-error" });
385
+ break;
386
+ default:
387
+ // 未知事件类型,记录日志
388
+ this.log(runtime, `Unknown Dify event: ${eventType}`);
389
+ }
390
+ }
391
+ }
392
+ catch (err) {
393
+ this.logError(runtime, `SSE stream error: ${String(err)}`);
394
+ onError?.(err instanceof Error ? err : new Error(String(err)), { kind: "stream" });
395
+ }
396
+ return accumulatedText;
397
+ }
398
+ /**
399
+ * 处理非流式(blocking)响应
400
+ */
401
+ async handleBlockingResponse(params) {
402
+ const { response, chatId, deliver, onReplyStart, onError, runtime } = params;
403
+ let data;
404
+ try {
405
+ data = await response.json();
406
+ }
407
+ catch (err) {
408
+ const error = new Error(`Failed to parse Dify response JSON: ${String(err)}`);
409
+ onError?.(error, { kind: "parse" });
410
+ throw error;
411
+ }
412
+ // 保存 conversation_id
413
+ if (data.conversation_id) {
414
+ DifyAdapter.conversationCache.set(chatId, data.conversation_id);
415
+ }
416
+ const answer = data.answer || "";
417
+ if (answer) {
418
+ try {
419
+ await onReplyStart?.();
420
+ }
421
+ catch (e) {
422
+ this.logError(runtime, `onReplyStart error: ${String(e)}`);
423
+ }
424
+ try {
425
+ await deliver({ text: answer }, { kind: "block" });
426
+ }
427
+ catch (e) {
428
+ onError?.(e instanceof Error ? e : new Error(String(e)), { kind: "block" });
429
+ }
430
+ }
431
+ return answer;
432
+ }
433
+ // ==========================================================================
434
+ // 文件上传
435
+ // ==========================================================================
436
+ /**
437
+ * 准备 Dify 文件参数
438
+ *
439
+ * 根据 Dify 文档,文件有两种传输方式:
440
+ * - remote_url:直接传 URL,Dify 自行下载(适用于远程 URL)
441
+ * - local_file:先通过 /v1/files/upload 上传,再传 upload_file_id(适用于本地文件)
442
+ *
443
+ * @returns DifyFile 列表(处理失败的跳过并记录日志)
444
+ */
445
+ async prepareDifyFiles(params) {
446
+ const { mediaPaths, baseUrl, apiKey, user, runtime } = params;
447
+ const results = [];
448
+ for (const media of mediaPaths) {
449
+ try {
450
+ if (media.path.startsWith("http://") || media.path.startsWith("https://")) {
451
+ // 远程 URL:直接使用 remote_url 方式,无需上传
452
+ const contentType = media.contentType || this.guessContentTypeFromUrl(media.path);
453
+ const difyType = this.inferDifyFileType(contentType);
454
+ this.log(runtime, `Using remote_url for Dify file: type=${difyType}, url=${media.path}`);
455
+ results.push({
456
+ type: difyType,
457
+ transfer_method: "remote_url",
458
+ url: media.path,
459
+ });
460
+ }
461
+ else {
462
+ // 本地文件:先上传到 Dify,再用 local_file 方式
463
+ const uploaded = await this.uploadLocalFile({
464
+ filePath: media.path,
465
+ contentType: media.contentType,
466
+ baseUrl,
467
+ apiKey,
468
+ user,
469
+ runtime,
470
+ });
471
+ if (uploaded) {
472
+ results.push(uploaded);
473
+ }
474
+ }
475
+ }
476
+ catch (err) {
477
+ this.logError(runtime, `Failed to prepare file ${media.path}: ${String(err)}`);
478
+ }
479
+ }
480
+ return results;
481
+ }
482
+ /**
483
+ * 上传本地文件到 Dify(/v1/files/upload)
484
+ *
485
+ * @returns 上传成功的 DifyFile,失败返回 undefined
486
+ */
487
+ async uploadLocalFile(params) {
488
+ const { filePath, contentType: providedContentType, baseUrl, apiKey, user, runtime } = params;
489
+ const uploadUrl = this.buildApiUrl(baseUrl, "/files/upload");
490
+ const { readFile } = await import('node:fs/promises');
491
+ const { basename } = await import('node:path');
492
+ const fileBuffer = await readFile(filePath);
493
+ const filename = basename(filePath);
494
+ const contentType = providedContentType || this.guessContentType(filename);
495
+ const difyType = this.inferDifyFileType(contentType);
496
+ this.log(runtime, `Uploading local file to Dify: name=${filename}, type=${difyType}, size=${fileBuffer.byteLength}`);
497
+ // 构建 multipart/form-data
498
+ const formData = new FormData();
499
+ const blob = new Blob([fileBuffer], { type: contentType });
500
+ formData.append("file", blob, filename);
501
+ formData.append("user", user);
502
+ const response = await fetch(uploadUrl, {
503
+ method: "POST",
504
+ headers: {
505
+ Authorization: `Bearer ${apiKey}`,
506
+ },
507
+ body: formData,
508
+ });
509
+ if (!response.ok) {
510
+ const errorBody = await response.text().catch(() => "(unreadable)");
511
+ this.logError(runtime, `Dify file upload failed: HTTP ${response.status} — ${errorBody.slice(0, 300)}`);
512
+ return undefined;
513
+ }
514
+ const uploadResult = await response.json();
515
+ this.log(runtime, `Dify file uploaded: id=${uploadResult.id}, name=${uploadResult.name}`);
516
+ return {
517
+ type: difyType,
518
+ transfer_method: "local_file",
519
+ upload_file_id: uploadResult.id,
520
+ };
521
+ }
522
+ /**
523
+ * 从 URL 中提取文件名
524
+ */
525
+ extractFilename(url) {
526
+ try {
527
+ const pathname = new URL(url).pathname;
528
+ const parts = pathname.split("/");
529
+ return parts[parts.length - 1] || "file";
530
+ }
531
+ catch {
532
+ return "file";
533
+ }
534
+ }
535
+ /**
536
+ * 根据文件名猜测 Content-Type
537
+ */
538
+ guessContentType(filename) {
539
+ const ext = filename.split(".").pop()?.toLowerCase() || "";
540
+ const mimeMap = {
541
+ jpg: "image/jpeg",
542
+ jpeg: "image/jpeg",
543
+ png: "image/png",
544
+ gif: "image/gif",
545
+ webp: "image/webp",
546
+ svg: "image/svg+xml",
547
+ pdf: "application/pdf",
548
+ doc: "application/msword",
549
+ docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
550
+ xls: "application/vnd.ms-excel",
551
+ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
552
+ ppt: "application/vnd.ms-powerpoint",
553
+ pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
554
+ txt: "text/plain",
555
+ csv: "text/csv",
556
+ mp3: "audio/mpeg",
557
+ wav: "audio/wav",
558
+ mp4: "video/mp4",
559
+ avi: "video/x-msvideo",
560
+ };
561
+ return mimeMap[ext] || "application/octet-stream";
562
+ }
563
+ /**
564
+ * 从 URL 中猜测 Content-Type
565
+ */
566
+ guessContentTypeFromUrl(url) {
567
+ const filename = this.extractFilename(url);
568
+ return this.guessContentType(filename);
569
+ }
570
+ /**
571
+ * 根据 Content-Type 推断 Dify 文件类型
572
+ */
573
+ inferDifyFileType(contentType) {
574
+ if (contentType.startsWith("image/"))
575
+ return "image";
576
+ if (contentType.startsWith("audio/"))
577
+ return "audio";
578
+ if (contentType.startsWith("video/"))
579
+ return "video";
580
+ return "document";
581
+ }
582
+ // ==========================================================================
583
+ // URL 工具
584
+ // ==========================================================================
585
+ /**
586
+ * 拼接 Dify API URL
587
+ *
588
+ * 支持多种 baseUrl 格式:
589
+ * - http://host/v1 → http://host/v1/chat-messages
590
+ * - http://host/v1/ → http://host/v1/chat-messages
591
+ * - http://host → http://host/v1/chat-messages
592
+ * - http://host/v1/chat-messages → http://host/v1/chat-messages (不变)
593
+ */
594
+ buildChatMessagesUrl(baseUrl) {
595
+ // 如果已经包含 chat-messages 路径,直接返回
596
+ if (baseUrl.includes("/chat-messages")) {
597
+ return baseUrl;
598
+ }
599
+ return this.buildApiUrl(baseUrl, "/chat-messages");
600
+ }
601
+ /**
602
+ * 拼接 Dify API 通用路径
603
+ */
604
+ buildApiUrl(baseUrl, path) {
605
+ let base = baseUrl.replace(/\/+$/, "");
606
+ // 如果 baseUrl 不包含 /v1,自动补上
607
+ if (!base.includes("/v1")) {
608
+ base += "/v1";
609
+ }
610
+ return `${base}${path}`;
611
+ }
612
+ /**
613
+ * 解析文件 URL(处理相对路径)
614
+ *
615
+ * Dify 返回的 message_file.url 可能是:
616
+ * - 完整的 HTTP URL → 直接使用
617
+ * - 相对路径(如 /files/xxx/preview) → 拼接 baseUrl
618
+ */
619
+ resolveFileUrl(url, baseUrl) {
620
+ if (url.startsWith("http://") || url.startsWith("https://")) {
621
+ return url;
622
+ }
623
+ // 相对路径:从 baseUrl 提取 origin
624
+ try {
625
+ const origin = new URL(baseUrl).origin;
626
+ return `${origin}${url.startsWith("/") ? url : `/${url}`}`;
627
+ }
628
+ catch {
629
+ return url;
630
+ }
631
+ }
632
+ }
633
+ // 全局会话缓存
634
+ DifyAdapter.conversationCache = new ConversationCache();
635
+
636
+ exports.DifyAdapter = DifyAdapter;