@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,512 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { BaseAdapter } from './base-adapter.js';
4
+
5
+ /**
6
+ * Anthropic 适配器
7
+ *
8
+ * 适用于:Anthropic Claude 原生 API,以及阿里百炼、腾讯混元、MiniMax、
9
+ * AWS Bedrock、Google Vertex AI 等提供 Anthropic Messages API 兼容接口的平台
10
+ *
11
+ * 与 OpenAI 的核心差异:
12
+ * - 接口路径:POST /v1/messages(而非 /v1/chat/completions)
13
+ * - 系统提示词:顶层 system 字段(而非 messages[0].role = "system")
14
+ * - 认证方式:x-api-key 头(而非 Authorization: Bearer)
15
+ * - 响应结构:content[] 块数组(而非 choices[0].message.content 字符串)
16
+ * - 流式格式:多事件类型(message_start → content_block_delta → message_stop)
17
+ * - 思维链:原生 type: "thinking" 块(而非私有 reasoning_content 字段)
18
+ * - 必填字段:max_tokens(OpenAI 可选)
19
+ *
20
+ * 推理内容折叠展示格式(collapse 模式,复用 DeepSeek 策略):
21
+ * <think>
22
+ * {thinking}
23
+ * </think>
24
+ *
25
+ * {content}
26
+ */
27
+ /**
28
+ * Anthropic 适配器
29
+ */
30
+ class AnthropicAdapter extends BaseAdapter {
31
+ constructor() {
32
+ super(...arguments);
33
+ this.name = "anthropic";
34
+ this.displayName = "Anthropic";
35
+ }
36
+ /**
37
+ * 获取 Anthropic 配置选项
38
+ */
39
+ getOptions(endpoint) {
40
+ return endpoint.providerOptions || {};
41
+ }
42
+ /**
43
+ * 获取推理内容展示模式
44
+ */
45
+ getReasoningDisplayMode(endpoint) {
46
+ return this.getOptions(endpoint).reasoningDisplay || "collapse";
47
+ }
48
+ // ==========================================================================
49
+ // URL 规范化
50
+ // ==========================================================================
51
+ /**
52
+ * 规范化 Anthropic API URL
53
+ *
54
+ * 用户可能配置为 base URL(如 https://api.minimaxi.com/anthropic)
55
+ * 或完整路径(如 https://api.anthropic.com/v1/messages)。
56
+ * 如果 URL 不以 /v1/messages 结尾,则自动拼接。
57
+ */
58
+ normalizeUrl(url) {
59
+ const trimmed = url.replace(/\/+$/, ""); // 去掉尾部斜杠
60
+ if (trimmed.endsWith("/v1/messages")) {
61
+ return trimmed;
62
+ }
63
+ return `${trimmed}/v1/messages`;
64
+ }
65
+ // ==========================================================================
66
+ // 请求头构建(覆写:支持 x-api-key 认证方式)
67
+ // ==========================================================================
68
+ /**
69
+ * 构建 Anthropic 请求头
70
+ *
71
+ * 认证方式:
72
+ * - useXApiKey=true(默认):使用 x-api-key 头(Anthropic 原生)
73
+ * - useXApiKey=false:使用 Authorization: Bearer(第三方兼容平台)
74
+ */
75
+ buildHeaders(endpoint) {
76
+ const options = this.getOptions(endpoint);
77
+ const headers = {
78
+ "Content-Type": "application/json",
79
+ ...(endpoint.headers || {}),
80
+ };
81
+ // 认证方式
82
+ const useXApiKey = options.useXApiKey !== false; // 默认 true
83
+ if (endpoint.apiKey) {
84
+ if (useXApiKey) {
85
+ headers["x-api-key"] = endpoint.apiKey;
86
+ }
87
+ else {
88
+ headers["Authorization"] = `Bearer ${endpoint.apiKey}`;
89
+ }
90
+ }
91
+ // Anthropic API 版本头
92
+ const anthropicVersion = options.anthropicVersion || "2023-06-01";
93
+ headers["anthropic-version"] = anthropicVersion;
94
+ return headers;
95
+ }
96
+ // ==========================================================================
97
+ // 转发主流程
98
+ // ==========================================================================
99
+ /**
100
+ * 转发请求到 Anthropic Messages API
101
+ */
102
+ async forward(request, endpoint, callbacks) {
103
+ const { text, mediaPaths, quoteContent, abortSignal, runtime, historyMessages } = request;
104
+ const { deliver, onReplyStart, onError } = callbacks;
105
+ const options = this.getOptions(endpoint);
106
+ // 规范化 URL:自动补全 /v1/messages 路径
107
+ const url = this.normalizeUrl(endpoint.url);
108
+ this.log(runtime, `Forwarding to Anthropic: url=${url}, model=${endpoint.model}`);
109
+ // 构建消息
110
+ const messages = this.buildMessages({
111
+ text,
112
+ mediaPaths,
113
+ quoteContent,
114
+ historyMessages,
115
+ });
116
+ // 构建请求体
117
+ const requestBody = this.buildRequestBody(messages, endpoint, options);
118
+ // 调试日志:记录请求体关键信息(不记录完整 messages 避免泄露用户内容)
119
+ this.log(runtime, `Request params: model=${requestBody.model}, stream=${requestBody.stream}, max_tokens=${requestBody.max_tokens}, hasSystem=${!!requestBody.system}, messagesCount=${requestBody.messages.length}`);
120
+ // 构建请求头
121
+ const headers = this.buildHeaders(endpoint);
122
+ // 超时控制
123
+ const timeoutMs = endpoint.timeoutMs || 300000;
124
+ const { controller, timeoutId } = this.createTimeoutController(timeoutMs, abortSignal);
125
+ try {
126
+ const response = await this.fetchWithErrorHandling(url, {
127
+ method: "POST",
128
+ headers,
129
+ body: JSON.stringify(requestBody),
130
+ signal: controller.signal,
131
+ }, runtime);
132
+ let finalText;
133
+ const displayMode = this.getReasoningDisplayMode(endpoint);
134
+ if (requestBody.stream) {
135
+ // 流式响应
136
+ finalText = await this.handleStreamResponse({
137
+ response,
138
+ deliver,
139
+ onReplyStart,
140
+ onError,
141
+ runtime,
142
+ displayMode,
143
+ });
144
+ }
145
+ else {
146
+ // 非流式响应
147
+ finalText = await this.handleNonStreamResponse({
148
+ response,
149
+ deliver,
150
+ onReplyStart,
151
+ onError,
152
+ runtime,
153
+ displayMode,
154
+ });
155
+ }
156
+ // 空响应防御:API 返回了有效 HTTP 200 但没有生成任何内容
157
+ if (!finalText) {
158
+ this.logError(runtime, `Empty response from API (textLength=0). This usually means the model returned no content blocks. Check: 1) model name is correct, 2) systemPrompt is set, 3) the API provider supports the requested model.`);
159
+ finalText = "⚠️ AI 未返回有效内容,请稍后重试。";
160
+ }
161
+ // 最终 deliver
162
+ try {
163
+ await deliver({ text: finalText }, { kind: "final" });
164
+ }
165
+ catch (e) {
166
+ onError?.(e instanceof Error ? e : new Error(String(e)), { kind: "final" });
167
+ }
168
+ this.log(runtime, `Response complete: textLength=${finalText.length}`);
169
+ return finalText;
170
+ }
171
+ catch (err) {
172
+ if (err?.name === "AbortError") {
173
+ const error = new Error(`Request timed out after ${timeoutMs}ms`);
174
+ this.logError(runtime, error.message);
175
+ onError?.(error, { kind: "timeout" });
176
+ throw error;
177
+ }
178
+ throw err;
179
+ }
180
+ finally {
181
+ clearTimeout(timeoutId);
182
+ }
183
+ }
184
+ // ==========================================================================
185
+ // 消息构建
186
+ // ==========================================================================
187
+ /**
188
+ * 构建 Anthropic messages 数组
189
+ *
190
+ * 与 OpenAI 的差异:
191
+ * - 系统提示词不在 messages 中,而是顶层 system 字段(在 buildRequestBody 中处理)
192
+ * - 图片使用 { type: "image", source: { type: "base64", ... } } 格式
193
+ * - 不支持 role: "system",只有 "user" 和 "assistant"
194
+ */
195
+ buildMessages(options) {
196
+ const messages = [];
197
+ // 注入对话历史(在当前 user 消息之前)
198
+ if (options.historyMessages && options.historyMessages.length > 0) {
199
+ for (const msg of options.historyMessages) {
200
+ messages.push({
201
+ role: msg.role,
202
+ content: msg.content,
203
+ });
204
+ }
205
+ }
206
+ // 用户消息
207
+ const hasMedia = options.mediaPaths && options.mediaPaths.length > 0;
208
+ if (hasMedia) {
209
+ // 多模态消息:文本 + 图片
210
+ const contentParts = [];
211
+ // 添加引用内容
212
+ if (options.quoteContent) {
213
+ contentParts.push({
214
+ type: "text",
215
+ text: `[引用消息] ${options.quoteContent}`,
216
+ });
217
+ }
218
+ // 添加文本
219
+ if (options.text) {
220
+ contentParts.push({
221
+ type: "text",
222
+ text: options.text,
223
+ });
224
+ }
225
+ // 添加媒体文件
226
+ for (const media of options.mediaPaths) {
227
+ const isImage = media.contentType?.startsWith("image/");
228
+ if (isImage) {
229
+ try {
230
+ const buffer = fs.readFileSync(media.path);
231
+ const base64 = buffer.toString("base64");
232
+ const mimeType = media.contentType || "image/jpeg";
233
+ contentParts.push({
234
+ type: "image",
235
+ source: {
236
+ type: "base64",
237
+ media_type: mimeType,
238
+ data: base64,
239
+ },
240
+ });
241
+ }
242
+ catch {
243
+ contentParts.push({
244
+ type: "text",
245
+ text: `[图片文件: ${path.basename(media.path)}] (读取失败)`,
246
+ });
247
+ }
248
+ }
249
+ else {
250
+ // 非图片文件,以文本描述形式传入
251
+ const fileName = path.basename(media.path);
252
+ const fileType = media.contentType || "unknown";
253
+ contentParts.push({
254
+ type: "text",
255
+ text: `[文件: ${fileName}, 类型: ${fileType}]`,
256
+ });
257
+ }
258
+ }
259
+ // 确保至少有一个 content part
260
+ if (contentParts.length === 0) {
261
+ contentParts.push({ type: "text", text: "(空消息)" });
262
+ }
263
+ messages.push({ role: "user", content: contentParts });
264
+ }
265
+ else {
266
+ // 纯文本消息
267
+ let content = "";
268
+ if (options.quoteContent) {
269
+ content += `[引用消息] ${options.quoteContent}\n\n`;
270
+ }
271
+ content += options.text || "(空消息)";
272
+ messages.push({ role: "user", content });
273
+ }
274
+ return messages;
275
+ }
276
+ /**
277
+ * 构建 Anthropic 请求体
278
+ */
279
+ buildRequestBody(messages, endpoint, options) {
280
+ const requestBody = {
281
+ model: endpoint.model || "claude-sonnet-4-20250514",
282
+ messages,
283
+ max_tokens: options.maxTokens || 4096,
284
+ stream: endpoint.stream !== false,
285
+ };
286
+ // 系统提示词放在顶层 system 字段
287
+ if (endpoint.systemPrompt) {
288
+ requestBody.system = endpoint.systemPrompt;
289
+ }
290
+ return requestBody;
291
+ }
292
+ // ==========================================================================
293
+ // 流式响应处理
294
+ // ==========================================================================
295
+ /**
296
+ * 处理 Anthropic 流式响应
297
+ *
298
+ * Anthropic SSE 事件流程:
299
+ * message_start → content_block_start → content_block_delta(多次) →
300
+ * content_block_stop → ... → message_delta → message_stop
301
+ */
302
+ async handleStreamResponse(params) {
303
+ const { response, deliver, onReplyStart, onError, runtime, displayMode } = params;
304
+ const body = response.body;
305
+ if (!body) {
306
+ throw new Error("Response has no body for streaming");
307
+ }
308
+ const reader = body.getReader();
309
+ let accumulatedContent = "";
310
+ let accumulatedThinking = "";
311
+ let replyStarted = false;
312
+ let currentBlockType;
313
+ try {
314
+ for await (const { event, data } of this.readSSEStream(reader, runtime)) {
315
+ // 解析 JSON
316
+ let parsed;
317
+ try {
318
+ parsed = JSON.parse(data);
319
+ }
320
+ catch {
321
+ this.log(runtime, `Failed to parse Anthropic SSE JSON: ${data.slice(0, 200)}`);
322
+ continue;
323
+ }
324
+ // 使用 SSE event 字段或 JSON 中的 type 字段确定事件类型
325
+ const eventType = event || parsed.type;
326
+ switch (eventType) {
327
+ case "message_start":
328
+ // 记录 message id,初始化状态
329
+ this.log(runtime, `Anthropic message_start: id=${parsed.message?.id}`);
330
+ break;
331
+ case "content_block_start":
332
+ // 记录当前块类型(text / thinking / tool_use)
333
+ currentBlockType = parsed.content_block?.type;
334
+ this.log(runtime, `Anthropic content_block_start: type=${currentBlockType}, index=${parsed.index}`);
335
+ // 当 thinking 块开始时,提前触发 onReplyStart(让 thinking 动画立即显示)
336
+ // 这样 thinking 阶段用户能看到 thinking 动画,而不是完全无反馈
337
+ if (currentBlockType === "thinking" && !replyStarted) {
338
+ replyStarted = true;
339
+ try {
340
+ await onReplyStart?.();
341
+ }
342
+ catch (e) {
343
+ this.logError(runtime, `onReplyStart error: ${String(e)}`);
344
+ }
345
+ }
346
+ break;
347
+ case "content_block_delta": {
348
+ const deltaType = parsed.delta?.type;
349
+ if (deltaType === "text_delta" && parsed.delta?.text) {
350
+ // 文本增量
351
+ accumulatedContent += parsed.delta.text;
352
+ }
353
+ else if (deltaType === "thinking_delta" && parsed.delta?.thinking) {
354
+ // 思维链增量
355
+ accumulatedThinking += parsed.delta.thinking;
356
+ }
357
+ // 判断是否需要 deliver
358
+ const hasNewContent = deltaType === "text_delta" && parsed.delta?.text;
359
+ const hasNewThinking = deltaType === "thinking_delta" && parsed.delta?.thinking;
360
+ // deliver 策略:
361
+ // - hide 模式:仅有主内容 (content) 时 deliver,跳过纯推理帧
362
+ // - collapse 模式:thinking 阶段和 text 阶段都 deliver,但中间帧**不带 <think> 标签**
363
+ // thinking 阶段:直接发纯 thinking 文本(让用户看到模型在想什么)
364
+ // text 阶段:只发 content(不带 <think> 包裹),避免企微反复渲染巨大 think 块导致闪烁
365
+ // final 帧由 finishThinkingStream 发完整格式(含 <think> 标签)
366
+ // - show 模式:有推理或主内容时都 deliver
367
+ let shouldDeliver;
368
+ if (displayMode === "hide") {
369
+ shouldDeliver = !!hasNewContent;
370
+ }
371
+ else {
372
+ // collapse 和 show 模式:thinking 或 content 都 deliver
373
+ shouldDeliver = !!(hasNewContent || hasNewThinking);
374
+ }
375
+ if (shouldDeliver) {
376
+ // 首次收到有效内容时触发 onReplyStart(如果还未触发)
377
+ if (!replyStarted) {
378
+ replyStarted = true;
379
+ try {
380
+ await onReplyStart?.();
381
+ }
382
+ catch (e) {
383
+ this.logError(runtime, `onReplyStart error: ${String(e)}`);
384
+ }
385
+ }
386
+ // 中间帧 deliver:使用统一的 formatOutput,节流由 monitor 层统一控制
387
+ // 适配器层每个有效 delta 直接 deliver,monitor 层根据时间+增量双重条件决定是否发送到企微
388
+ const deliverText = this.formatOutput(accumulatedContent, accumulatedThinking, displayMode);
389
+ try {
390
+ await deliver({ text: deliverText }, { kind: "block" });
391
+ }
392
+ catch (e) {
393
+ onError?.(e instanceof Error ? e : new Error(String(e)), { kind: "block" });
394
+ }
395
+ }
396
+ break;
397
+ }
398
+ case "content_block_stop":
399
+ // 当前内容块结束
400
+ currentBlockType = undefined;
401
+ break;
402
+ case "message_delta":
403
+ // 提取 stop_reason 和 usage
404
+ if (parsed.delta?.stop_reason) {
405
+ this.log(runtime, `Anthropic stop_reason: ${parsed.delta.stop_reason}`);
406
+ }
407
+ if (parsed.usage?.output_tokens) {
408
+ this.log(runtime, `Anthropic output_tokens: ${parsed.usage.output_tokens}`);
409
+ }
410
+ break;
411
+ case "message_stop":
412
+ this.log(runtime, "Anthropic message_stop");
413
+ break;
414
+ case "ping":
415
+ // 忽略心跳
416
+ break;
417
+ case "error":
418
+ // 错误事件
419
+ const errorMsg = parsed.error?.message || "Unknown Anthropic error";
420
+ this.logError(runtime, `Anthropic error: ${errorMsg}`);
421
+ onError?.(new Error(errorMsg), { kind: "anthropic-error" });
422
+ break;
423
+ default:
424
+ this.log(runtime, `Unknown Anthropic event: ${eventType}`);
425
+ }
426
+ }
427
+ }
428
+ catch (err) {
429
+ this.logError(runtime, `SSE stream error: ${String(err)}`);
430
+ onError?.(err instanceof Error ? err : new Error(String(err)), { kind: "stream" });
431
+ }
432
+ return this.formatOutput(accumulatedContent, accumulatedThinking, displayMode);
433
+ }
434
+ // ==========================================================================
435
+ // 非流式响应处理
436
+ // ==========================================================================
437
+ /**
438
+ * 处理 Anthropic 非流式响应
439
+ *
440
+ * 遍历 content[] 数组,按 type 提取 text 和 thinking 内容
441
+ */
442
+ async handleNonStreamResponse(params) {
443
+ const { response, deliver, onReplyStart, onError, runtime, displayMode } = params;
444
+ let data;
445
+ try {
446
+ data = (await response.json());
447
+ }
448
+ catch (err) {
449
+ const error = new Error(`Failed to parse Anthropic response JSON: ${String(err)}`);
450
+ onError?.(error, { kind: "parse" });
451
+ throw error;
452
+ }
453
+ // 遍历 content[] 块数组,分离 text 和 thinking
454
+ let content = "";
455
+ let thinking = "";
456
+ for (const block of data.content || []) {
457
+ if (block.type === "thinking" && "thinking" in block) {
458
+ thinking += block.thinking;
459
+ }
460
+ else if (block.type === "text" && "text" in block) {
461
+ content += block.text;
462
+ }
463
+ }
464
+ const formattedText = this.formatOutput(content, thinking, displayMode);
465
+ if (formattedText) {
466
+ try {
467
+ await onReplyStart?.();
468
+ }
469
+ catch (e) {
470
+ this.logError(runtime, `onReplyStart error: ${String(e)}`);
471
+ }
472
+ try {
473
+ await deliver({ text: formattedText }, { kind: "block" });
474
+ }
475
+ catch (e) {
476
+ onError?.(e instanceof Error ? e : new Error(String(e)), { kind: "block" });
477
+ }
478
+ }
479
+ // 记录 token 使用情况
480
+ if (data.usage) {
481
+ this.log(runtime, `Token usage: input_tokens=${data.usage.input_tokens}, output_tokens=${data.usage.output_tokens}`);
482
+ }
483
+ return formattedText;
484
+ }
485
+ // ==========================================================================
486
+ // 输出格式化
487
+ // ==========================================================================
488
+ /**
489
+ * 格式化最终输出(根据 displayMode 配置)
490
+ *
491
+ * 复用 DeepSeek 的 show/hide/collapse 策略
492
+ */
493
+ formatOutput(content, thinking, displayMode) {
494
+ if (!thinking) {
495
+ return content;
496
+ }
497
+ switch (displayMode) {
498
+ case "show":
499
+ // 直接展示推理内容和回答
500
+ return `${thinking}\n\n${content}`;
501
+ case "hide":
502
+ // 仅展示回答,隐藏推理过程
503
+ return content;
504
+ case "collapse":
505
+ default:
506
+ // 折叠展示
507
+ return `<think>\n${thinking}\n</think>\n\n${content}`;
508
+ }
509
+ }
510
+ }
511
+
512
+ export { AnthropicAdapter };
@@ -0,0 +1,174 @@
1
+ /**
2
+ * 适配器基础抽象类
3
+ *
4
+ * 提供所有适配器共用的能力:
5
+ * - 超时控制
6
+ * - 请求头构建
7
+ * - SSE 流式解析工具方法
8
+ * - 日志工具
9
+ */
10
+ /**
11
+ * 适配器基础抽象类
12
+ */
13
+ class BaseAdapter {
14
+ // ==========================================================================
15
+ // 请求头构建
16
+ // ==========================================================================
17
+ /**
18
+ * 构建通用请求头
19
+ */
20
+ buildHeaders(endpoint) {
21
+ const headers = {
22
+ "Content-Type": "application/json",
23
+ ...(endpoint.headers || {}),
24
+ };
25
+ if (endpoint.apiKey) {
26
+ headers["Authorization"] = `Bearer ${endpoint.apiKey}`;
27
+ }
28
+ return headers;
29
+ }
30
+ // ==========================================================================
31
+ // 超时控制
32
+ // ==========================================================================
33
+ /**
34
+ * 创建带超时的 AbortController
35
+ *
36
+ * @param timeoutMs 超时时间(毫秒)
37
+ * @param externalSignal 外部中止信号(可选)
38
+ * @returns { controller, timeoutId }
39
+ */
40
+ createTimeoutController(timeoutMs, externalSignal) {
41
+ const controller = new AbortController();
42
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
43
+ // 合并外部 abortSignal
44
+ if (externalSignal) {
45
+ externalSignal.addEventListener("abort", () => controller.abort(), { once: true });
46
+ }
47
+ return { controller, timeoutId };
48
+ }
49
+ /**
50
+ * 包装 Promise 添加超时控制
51
+ */
52
+ async withTimeout(promise, timeoutMs, timeoutMessage = "Request timed out") {
53
+ let timeoutId;
54
+ const timeoutPromise = new Promise((_, reject) => {
55
+ timeoutId = setTimeout(() => {
56
+ reject(new Error(`${timeoutMessage} after ${timeoutMs}ms`));
57
+ }, timeoutMs);
58
+ });
59
+ try {
60
+ return await Promise.race([promise, timeoutPromise]);
61
+ }
62
+ finally {
63
+ clearTimeout(timeoutId);
64
+ }
65
+ }
66
+ // ==========================================================================
67
+ // SSE 解析工具
68
+ // ==========================================================================
69
+ /**
70
+ * 解析单行 SSE 数据
71
+ *
72
+ * @param line 原始 SSE 行
73
+ * @returns { event, data } 或 null(非 data 行或空行)
74
+ */
75
+ parseSSELine(line) {
76
+ const trimmed = line.trim();
77
+ // 跳过空行和注释
78
+ if (!trimmed || trimmed.startsWith(":")) {
79
+ return null;
80
+ }
81
+ // 解析 event: 行
82
+ if (trimmed.startsWith("event:")) {
83
+ return { event: trimmed.slice(6).trim() };
84
+ }
85
+ // 解析 data: 行
86
+ if (trimmed.startsWith("data:")) {
87
+ return { data: trimmed.slice(5).trim() };
88
+ }
89
+ return null;
90
+ }
91
+ /**
92
+ * 创建 SSE 流读取器
93
+ *
94
+ * 返回一个异步生成器,逐行产出 SSE 数据
95
+ */
96
+ async *readSSEStream(reader, runtime) {
97
+ const decoder = new TextDecoder("utf-8");
98
+ let buffer = "";
99
+ let currentEvent;
100
+ try {
101
+ while (true) {
102
+ const { done, value } = await reader.read();
103
+ if (done)
104
+ break;
105
+ buffer += decoder.decode(value, { stream: true });
106
+ // 逐行处理
107
+ const lines = buffer.split("\n");
108
+ buffer = lines.pop() || "";
109
+ for (const line of lines) {
110
+ const parsed = this.parseSSELine(line);
111
+ if (!parsed)
112
+ continue;
113
+ if (parsed.event !== undefined) {
114
+ currentEvent = parsed.event;
115
+ }
116
+ else if (parsed.data !== undefined) {
117
+ yield { event: currentEvent, data: parsed.data };
118
+ currentEvent = undefined; // 重置 event
119
+ }
120
+ }
121
+ }
122
+ // 处理残留 buffer
123
+ if (buffer.trim()) {
124
+ const parsed = this.parseSSELine(buffer);
125
+ if (parsed?.data !== undefined) {
126
+ yield { event: currentEvent, data: parsed.data };
127
+ }
128
+ }
129
+ }
130
+ catch (err) {
131
+ runtime?.error?.(`[wecom][adapter] SSE stream error: ${String(err)}`);
132
+ throw err;
133
+ }
134
+ }
135
+ // ==========================================================================
136
+ // HTTP 请求工具
137
+ // ==========================================================================
138
+ /**
139
+ * 发起 HTTP 请求并处理错误
140
+ */
141
+ async fetchWithErrorHandling(url, options, runtime) {
142
+ const response = await fetch(url, options);
143
+ if (!response.ok) {
144
+ const errorBody = await response.text().catch(() => "(unreadable)");
145
+ const error = new Error(`API error: HTTP ${response.status} ${response.statusText} — ${errorBody.slice(0, 500)}`);
146
+ runtime?.error?.(`[wecom][adapter] ${error.message}`);
147
+ throw error;
148
+ }
149
+ return response;
150
+ }
151
+ // ==========================================================================
152
+ // 日志工具
153
+ // ==========================================================================
154
+ /**
155
+ * 日志前缀
156
+ */
157
+ get logPrefix() {
158
+ return `[wecom][adapter:${this.name}]`;
159
+ }
160
+ /**
161
+ * 打印日志
162
+ */
163
+ log(runtime, message) {
164
+ runtime?.log?.(`${this.logPrefix} ${message}`);
165
+ }
166
+ /**
167
+ * 打印错误日志
168
+ */
169
+ logError(runtime, message) {
170
+ runtime?.error?.(`${this.logPrefix} ${message}`);
171
+ }
172
+ }
173
+
174
+ export { BaseAdapter };