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