@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,260 @@
1
+ 'use strict';
2
+
3
+ var baseAdapter = require('./base-adapter.js');
4
+
5
+ /**
6
+ * Webhook 适配器
7
+ *
8
+ * 适用于:企业自建的非标准 HTTP API
9
+ *
10
+ * 核心机制:通过配置定义请求和响应的字段映射模板,无需编写代码
11
+ *
12
+ * 支持的模板变量:
13
+ * - {{text}} - 用户消息文本
14
+ * - {{user}} - 用户 ID
15
+ * - {{chatId}} - 聊天 ID
16
+ * - {{chatType}} - 聊天类型(single/group)
17
+ */
18
+ /**
19
+ * Webhook 适配器
20
+ */
21
+ class WebhookAdapter extends baseAdapter.BaseAdapter {
22
+ constructor() {
23
+ super(...arguments);
24
+ this.name = "webhook";
25
+ this.displayName = "Custom Webhook";
26
+ }
27
+ /**
28
+ * 获取 Webhook 配置选项
29
+ */
30
+ getOptions(endpoint) {
31
+ return endpoint.providerOptions || {};
32
+ }
33
+ /**
34
+ * 转发请求到 Webhook API
35
+ */
36
+ async forward(request, endpoint, callbacks) {
37
+ const { text, abortSignal, runtime, context } = request;
38
+ const { deliver, onReplyStart, onError } = callbacks;
39
+ const options = this.getOptions(endpoint);
40
+ this.log(runtime, `Forwarding to Webhook: url=${endpoint.url}`);
41
+ // 构建模板变量
42
+ const variables = {
43
+ text,
44
+ user: context?.userId || "",
45
+ chatId: context?.chatId || "",
46
+ chatType: context?.chatType || "",
47
+ };
48
+ // 构建请求体
49
+ const requestBody = this.buildRequestBody(options.requestTemplate, variables);
50
+ // 构建请求头
51
+ const headers = this.buildHeaders(endpoint);
52
+ // 超时控制
53
+ const timeoutMs = endpoint.timeoutMs || 300000;
54
+ const { controller, timeoutId } = this.createTimeoutController(timeoutMs, abortSignal);
55
+ const method = options.method || "POST";
56
+ try {
57
+ const response = await this.fetchWithErrorHandling(endpoint.url, {
58
+ method,
59
+ headers,
60
+ body: method !== "GET" ? JSON.stringify(requestBody) : undefined,
61
+ signal: controller.signal,
62
+ }, runtime);
63
+ let finalText;
64
+ // 检查是否为流式响应
65
+ const contentType = response.headers.get("content-type") || "";
66
+ const isStreaming = contentType.includes("text/event-stream") && options.streamEventField;
67
+ if (isStreaming) {
68
+ // 流式响应
69
+ finalText = await this.handleStreamResponse({
70
+ response,
71
+ options,
72
+ deliver,
73
+ onReplyStart,
74
+ onError,
75
+ runtime,
76
+ });
77
+ }
78
+ else {
79
+ // 非流式响应
80
+ finalText = await this.handleNonStreamResponse({
81
+ response,
82
+ options,
83
+ deliver,
84
+ onReplyStart,
85
+ onError,
86
+ runtime,
87
+ });
88
+ }
89
+ // 最终 deliver
90
+ if (finalText) {
91
+ try {
92
+ await deliver({ text: finalText }, { kind: "final" });
93
+ }
94
+ catch (e) {
95
+ onError?.(e instanceof Error ? e : new Error(String(e)), { kind: "final" });
96
+ }
97
+ }
98
+ this.log(runtime, `Response complete: textLength=${finalText.length}`);
99
+ return finalText;
100
+ }
101
+ catch (err) {
102
+ if (err?.name === "AbortError") {
103
+ const error = new Error(`Request timed out after ${timeoutMs}ms`);
104
+ this.logError(runtime, error.message);
105
+ onError?.(error, { kind: "timeout" });
106
+ throw error;
107
+ }
108
+ throw err;
109
+ }
110
+ finally {
111
+ clearTimeout(timeoutId);
112
+ }
113
+ }
114
+ /**
115
+ * 构建请求体(应用模板变量替换)
116
+ */
117
+ buildRequestBody(template, variables) {
118
+ if (!template) {
119
+ // 默认请求体
120
+ return { message: variables.text };
121
+ }
122
+ return this.applyTemplate(template, variables);
123
+ }
124
+ /**
125
+ * 递归应用模板变量替换
126
+ */
127
+ applyTemplate(obj, variables) {
128
+ if (typeof obj === "string") {
129
+ // 字符串替换
130
+ return obj.replace(/\{\{(\w+)\}\}/g, (_, key) => variables[key] || "");
131
+ }
132
+ if (Array.isArray(obj)) {
133
+ return obj.map((item) => this.applyTemplate(item, variables));
134
+ }
135
+ if (obj && typeof obj === "object") {
136
+ const result = {};
137
+ for (const [key, value] of Object.entries(obj)) {
138
+ result[key] = this.applyTemplate(value, variables);
139
+ }
140
+ return result;
141
+ }
142
+ return obj;
143
+ }
144
+ /**
145
+ * 从对象中按路径获取值
146
+ *
147
+ * @param obj 源对象
148
+ * @param path 字段路径(如 "data.reply.text")
149
+ */
150
+ getValueByPath(obj, path) {
151
+ const keys = path.split(".");
152
+ let current = obj;
153
+ for (const key of keys) {
154
+ if (current == null || typeof current !== "object") {
155
+ return undefined;
156
+ }
157
+ current = current[key];
158
+ }
159
+ return current;
160
+ }
161
+ /**
162
+ * 处理流式响应
163
+ */
164
+ async handleStreamResponse(params) {
165
+ const { response, options, deliver, onReplyStart, onError, runtime } = params;
166
+ const body = response.body;
167
+ if (!body) {
168
+ throw new Error("Response has no body for streaming");
169
+ }
170
+ const reader = body.getReader();
171
+ let accumulatedText = "";
172
+ let replyStarted = false;
173
+ const streamEndMarker = options.streamEndMarker || "[DONE]";
174
+ const streamEventField = options.streamEventField || "content";
175
+ try {
176
+ for await (const { data } of this.readSSEStream(reader, runtime)) {
177
+ // 检查结束标记
178
+ if (data === streamEndMarker) {
179
+ this.log(runtime, `SSE stream end marker: ${streamEndMarker}`);
180
+ break;
181
+ }
182
+ // 解析 JSON
183
+ let parsed;
184
+ try {
185
+ parsed = JSON.parse(data);
186
+ }
187
+ catch {
188
+ // 非 JSON,尝试直接作为文本
189
+ if (data) {
190
+ accumulatedText += data;
191
+ continue;
192
+ }
193
+ this.log(runtime, `Failed to parse SSE JSON: ${data.slice(0, 200)}`);
194
+ continue;
195
+ }
196
+ // 提取文本内容
197
+ const content = this.getValueByPath(parsed, streamEventField);
198
+ if (content && typeof content === "string") {
199
+ if (!replyStarted) {
200
+ replyStarted = true;
201
+ try {
202
+ await onReplyStart?.();
203
+ }
204
+ catch (e) {
205
+ this.logError(runtime, `onReplyStart error: ${String(e)}`);
206
+ }
207
+ }
208
+ accumulatedText += content;
209
+ try {
210
+ await deliver({ text: accumulatedText }, { kind: "block" });
211
+ }
212
+ catch (e) {
213
+ onError?.(e instanceof Error ? e : new Error(String(e)), { kind: "block" });
214
+ }
215
+ }
216
+ }
217
+ }
218
+ catch (err) {
219
+ this.logError(runtime, `SSE stream error: ${String(err)}`);
220
+ onError?.(err instanceof Error ? err : new Error(String(err)), { kind: "stream" });
221
+ }
222
+ return accumulatedText;
223
+ }
224
+ /**
225
+ * 处理非流式响应
226
+ */
227
+ async handleNonStreamResponse(params) {
228
+ const { response, options, deliver, onReplyStart, onError, runtime } = params;
229
+ let data;
230
+ try {
231
+ data = await response.json();
232
+ }
233
+ catch (err) {
234
+ const error = new Error(`Failed to parse Webhook response JSON: ${String(err)}`);
235
+ onError?.(error, { kind: "parse" });
236
+ throw error;
237
+ }
238
+ // 提取响应文本
239
+ const responseTextField = options.responseTextField || "result";
240
+ const content = this.getValueByPath(data, responseTextField);
241
+ const text = typeof content === "string" ? content : String(content || "");
242
+ if (text) {
243
+ try {
244
+ await onReplyStart?.();
245
+ }
246
+ catch (e) {
247
+ this.logError(runtime, `onReplyStart error: ${String(e)}`);
248
+ }
249
+ try {
250
+ await deliver({ text }, { kind: "block" });
251
+ }
252
+ catch (e) {
253
+ onError?.(e instanceof Error ? e : new Error(String(e)), { kind: "block" });
254
+ }
255
+ }
256
+ return text;
257
+ }
258
+ }
259
+
260
+ exports.WebhookAdapter = WebhookAdapter;
@@ -0,0 +1,87 @@
1
+ 'use strict';
2
+
3
+ var index = require('./adapters/index.js');
4
+ var conversationManager = require('./conversation-manager.js');
5
+
6
+ /**
7
+ * 智能体转发器(适配器调度层)
8
+ *
9
+ * 核心模块,负责:
10
+ * 1. 根据端点配置解析对应的适配器
11
+ * 2. 读取对话历史,注入到适配器请求中
12
+ * 3. 调用适配器执行转发
13
+ * 4. AI 回复后,将用户消息和 AI 回复保存到对话历史
14
+ * 5. 通过 deliver 回调驱动 monitor 的流式回复逻辑
15
+ *
16
+ * v1.1.0 重构:从 OpenAI 专用转发器改为可插拔适配器调度层
17
+ * v1.1.1 新增:多轮对话上下文记忆
18
+ */
19
+ // ============================================================================
20
+ // 公开 API
21
+ // ============================================================================
22
+ /**
23
+ * 将企微消息转发到外部智能体
24
+ *
25
+ * 完整流程:
26
+ * 1. 根据 endpoint.provider 解析适配器
27
+ * 2. 读取对话历史(如果 maxHistoryRounds > 0)
28
+ * 3. 构建适配器请求(含对话历史)
29
+ * 4. 调用 adapter.forward() 执行转发
30
+ * 5. 通过 deliver 回调驱动 monitor 的流式回复
31
+ * 6. AI 回复完成后,保存本轮对话到历史
32
+ */
33
+ async function forwardToAgent(options) {
34
+ const { text, mediaPaths, quoteContent, agent, deliver, onReplyStart, onError, abortSignal, runtime, context } = options;
35
+ // 解析适配器
36
+ const adapter = index.resolveAdapter(agent);
37
+ runtime?.log?.(`[wecom][agent] Forwarding to agent: endpoint=${agent.name}, provider=${adapter.name}, url=${agent.url}`);
38
+ // 对话历史管理
39
+ const maxRounds = agent.maxHistoryRounds ?? 20; // 默认 20 轮
40
+ const chatId = context?.chatId;
41
+ let historyMessages;
42
+ // 构建会话隔离 key:accountId + agentName + chatId
43
+ // 防止多账号/多 endpoint 共用同一 chatId 时历史消息串联
44
+ const accountId = context?.accountId || "default";
45
+ const conversationKey = chatId ? `${accountId}:${agent.name}:${chatId}` : undefined;
46
+ if (maxRounds > 0 && conversationKey) {
47
+ const conversationManager$1 = conversationManager.getConversationManager({ maxRounds });
48
+ historyMessages = conversationManager$1.getHistory(conversationKey);
49
+ if (historyMessages.length > 0) {
50
+ runtime?.log?.(`[wecom][agent] Injecting ${historyMessages.length} history messages for chat=${chatId}, key=${conversationKey}`);
51
+ }
52
+ // 先记录用户消息(在转发之前)
53
+ conversationManager$1.addUserMessage(conversationKey, text);
54
+ }
55
+ // 构建适配器请求
56
+ const request = {
57
+ text,
58
+ mediaPaths,
59
+ quoteContent,
60
+ abortSignal,
61
+ runtime,
62
+ context,
63
+ historyMessages: historyMessages && historyMessages.length > 0 ? historyMessages : undefined,
64
+ };
65
+ // 构建回调集
66
+ const callbacks = {
67
+ deliver,
68
+ onReplyStart,
69
+ onError,
70
+ };
71
+ // 调用适配器执行转发
72
+ const replyText = await adapter.forward(request, agent, callbacks);
73
+ // 保存 AI 回复到对话历史
74
+ if (maxRounds > 0 && conversationKey && replyText) {
75
+ const conversationManager$1 = conversationManager.getConversationManager();
76
+ // 去除 <think>...</think> 标签后保存(避免思维链污染历史)
77
+ const cleanReply = replyText.replace(/<think>[\s\S]*?<\/think>/g, "").trim();
78
+ if (cleanReply) {
79
+ conversationManager$1.addAssistantMessage(conversationKey, cleanReply);
80
+ runtime?.log?.(`[wecom][agent] Saved assistant reply to history for chat=${chatId}, key=${conversationKey}, historySize=${conversationManager$1.getHistory(conversationKey).length}`);
81
+ }
82
+ }
83
+ }
84
+
85
+ exports.adapterRegistry = index.adapterRegistry;
86
+ exports.resolveAdapter = index.resolveAdapter;
87
+ exports.forwardToAgent = forwardToAgent;
@@ -0,0 +1,162 @@
1
+ 'use strict';
2
+
3
+ var tls = require('tls');
4
+ var fs = require('fs');
5
+ var path = require('path');
6
+ var module$1 = require('module');
7
+
8
+ var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
9
+ function _interopNamespaceDefault(e) {
10
+ var n = Object.create(null);
11
+ if (e) {
12
+ Object.keys(e).forEach(function (k) {
13
+ if (k !== 'default') {
14
+ var d = Object.getOwnPropertyDescriptor(e, k);
15
+ Object.defineProperty(n, k, d.get ? d : {
16
+ enumerable: true,
17
+ get: function () { return e[k]; }
18
+ });
19
+ }
20
+ });
21
+ }
22
+ n.default = e;
23
+ return Object.freeze(n);
24
+ }
25
+
26
+ var tls__namespace = /*#__PURE__*/_interopNamespaceDefault(tls);
27
+ var fs__namespace = /*#__PURE__*/_interopNamespaceDefault(fs);
28
+ var path__namespace = /*#__PURE__*/_interopNamespaceDefault(path);
29
+
30
+ /**
31
+ * 自签名 CA 证书运行时注入模块
32
+ *
33
+ * 背景:
34
+ * NODE_EXTRA_CA_CERTS 环境变量只在 Node.js 进程启动时读取一次,
35
+ * 但用户配置 caCert 可能在运行时加载,此时进程已在运行,
36
+ * 所以不能依赖 NODE_EXTRA_CA_CERTS。
37
+ *
38
+ * 原理:
39
+ * Node.js 所有 TLS 连接最终都会调用 tls.createSecureContext(),
40
+ * 我们在这一层做拦截(monkey-patch),将自签名 CA 证书追加到
41
+ * 每次创建的 TLS 上下文中,从而覆盖全部网络出口。
42
+ *
43
+ * 安全性说明:
44
+ * - 仅追加 CA 证书,不禁用证书验证
45
+ * - 不覆盖系统默认 CA 列表,只在默认列表基础上追加
46
+ * - patch 是幂等的,多次调用只生效一次
47
+ * - 调用 removeCaCertPatch() 可还原到原始状态
48
+ *
49
+ * 副作用:
50
+ * patch 是进程全局的,会影响同一进程中所有 TLS 连接。
51
+ */
52
+ /**
53
+ * 获取可写的 tls 模块引用。
54
+ */
55
+ function getMutableTls() {
56
+ if (!Object.isSealed(tls__namespace)) {
57
+ return tls__namespace;
58
+ }
59
+ try {
60
+ const require$1 = module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('src/ca-cert.js', document.baseURI).href)));
61
+ return require$1("tls");
62
+ }
63
+ catch (e) {
64
+ throw new Error(`[ca-cert] 无法获取可写的 tls 模块引用。` +
65
+ `ESM namespace 是 sealed 的,且 createRequire 失败: ${e.message}`);
66
+ }
67
+ }
68
+ const _mutableTls = getMutableTls();
69
+ // 保存原始的 createSecureContext 引用,用于还原
70
+ const _originalCreateSecureContext = _mutableTls.createSecureContext;
71
+ // 当前注入的 CA 证书内容(用于幂等性判断和日志)
72
+ let _injectedCaCertContent = null;
73
+ // 是否已经安装了 patch
74
+ let _patched = false;
75
+ // ============================================================================
76
+ // 证书路径工具函数
77
+ // ============================================================================
78
+ /**
79
+ * 校验证书文件是否存在且为有效 PEM 格式
80
+ *
81
+ * @param certPath - 证书文件的绝对路径
82
+ * @returns 校验结果
83
+ */
84
+ function validateCaCertFile(certPath) {
85
+ const fullPath = path__namespace.resolve(certPath);
86
+ // 检查文件是否存在
87
+ if (!fs__namespace.existsSync(fullPath)) {
88
+ return { ok: false, error: `证书文件不存在: ${fullPath}`, fullPath };
89
+ }
90
+ // 读取文件内容
91
+ let content;
92
+ try {
93
+ content = fs__namespace.readFileSync(fullPath, "utf-8").trim();
94
+ }
95
+ catch (err) {
96
+ return { ok: false, error: `读取证书文件失败: ${err.message}`, fullPath };
97
+ }
98
+ // PEM 格式校验
99
+ if (!content.includes("-----BEGIN CERTIFICATE-----")) {
100
+ return { ok: false, error: `证书格式无效(非 PEM 格式),请确认文件内容正确: ${fullPath}`, fullPath };
101
+ }
102
+ return { ok: true, content, fullPath };
103
+ }
104
+ // ============================================================================
105
+ // CA 证书注入
106
+ // ============================================================================
107
+ /**
108
+ * 将自签名 CA 证书注入到 Node.js 全局 TLS 层
109
+ *
110
+ * @param certPath - 证书文件的绝对路径(PEM 格式)
111
+ * @param logger - 可选的日志函数
112
+ * @returns true 表示注入成功,false 表示跳过或失败
113
+ */
114
+ function injectCaCert(certPath, logger) {
115
+ // 1. 校验证书文件
116
+ const result = validateCaCertFile(certPath);
117
+ if (!result.ok) {
118
+ logger?.(`[ca-cert] ${result.error}`);
119
+ return false;
120
+ }
121
+ const { content: caCertContent, fullPath: caCertPath } = result;
122
+ // 2. 幂等性检查:如果已经注入了相同的证书,跳过
123
+ if (_patched && _injectedCaCertContent === caCertContent) {
124
+ logger?.(`[ca-cert] CA 证书已注入,跳过重复操作: ${caCertPath}`);
125
+ return true;
126
+ }
127
+ // 3. 如果之前已安装过不同的证书,先还原再重新安装
128
+ if (_patched) {
129
+ removeCaCertPatch(logger);
130
+ }
131
+ // 4. 安装 monkey-patch
132
+ _mutableTls.createSecureContext = function patchedCreateSecureContext(options) {
133
+ const context = _originalCreateSecureContext.call(this, options);
134
+ try {
135
+ context.context.addCACert(caCertContent);
136
+ }
137
+ catch (e) {
138
+ logger?.(`[ca-cert] 警告: addCACert 失败: ${e.message}`);
139
+ }
140
+ return context;
141
+ };
142
+ _patched = true;
143
+ _injectedCaCertContent = caCertContent;
144
+ logger?.(`[ca-cert] CA 证书注入成功: ${caCertPath}`);
145
+ return true;
146
+ }
147
+ /**
148
+ * 移除 CA 证书的 monkey-patch,还原到 Node.js 原始行为
149
+ */
150
+ function removeCaCertPatch(logger) {
151
+ if (!_patched) {
152
+ return;
153
+ }
154
+ _mutableTls.createSecureContext = _originalCreateSecureContext;
155
+ _patched = false;
156
+ _injectedCaCertContent = null;
157
+ logger?.("[ca-cert] CA 证书 patch 已移除,TLS 已还原为默认行为");
158
+ }
159
+
160
+ exports.injectCaCert = injectCaCert;
161
+ exports.removeCaCertPatch = removeCaCertPatch;
162
+ exports.validateCaCertFile = validateCaCertFile;
@@ -0,0 +1,169 @@
1
+ 'use strict';
2
+
3
+ var fs = require('node:fs');
4
+ var path = require('node:path');
5
+ var index = require('./adapters/index.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
+ * 插件配置加载模块
29
+ *
30
+ * 从 JSON 配置文件加载插件配置,校验必填字段,提供合理默认值
31
+ */
32
+ // ============================================================================
33
+ // 默认值
34
+ // ============================================================================
35
+ const DEFAULT_PORT = 3000;
36
+ const DEFAULT_DATA_DIR = "./data";
37
+ const DEFAULT_AGENT_TIMEOUT_MS = 300000; // 5 分钟
38
+ // ============================================================================
39
+ // 配置加载
40
+ // ============================================================================
41
+ /**
42
+ * 从 JSON 文件加载插件配置
43
+ *
44
+ * @param configPath - 配置文件路径
45
+ * @returns 解析后的配置
46
+ */
47
+ function loadConfig(configPath) {
48
+ const resolvedPath = path__namespace.resolve(configPath);
49
+ if (!fs__namespace.existsSync(resolvedPath)) {
50
+ throw new Error(`配置文件不存在: ${resolvedPath}`);
51
+ }
52
+ let raw;
53
+ try {
54
+ raw = fs__namespace.readFileSync(resolvedPath, "utf-8");
55
+ }
56
+ catch (err) {
57
+ throw new Error(`读取配置文件失败: ${err.message}`);
58
+ }
59
+ let config;
60
+ try {
61
+ config = JSON.parse(raw);
62
+ }
63
+ catch (err) {
64
+ throw new Error(`解析配置文件 JSON 失败: ${err.message}`);
65
+ }
66
+ // 校验必填字段
67
+ validateConfig(config);
68
+ // 应用默认值
69
+ config.port = config.port ?? DEFAULT_PORT;
70
+ config.dataDir = config.dataDir ?? DEFAULT_DATA_DIR;
71
+ // 确保数据目录存在
72
+ const dataDir = path__namespace.resolve(config.dataDir);
73
+ if (!fs__namespace.existsSync(dataDir)) {
74
+ fs__namespace.mkdirSync(dataDir, { recursive: true });
75
+ }
76
+ return config;
77
+ }
78
+ /**
79
+ * 校验配置必填字段
80
+ */
81
+ function validateConfig(config) {
82
+ if (!config.agents || !Array.isArray(config.agents) || config.agents.length === 0) {
83
+ throw new Error("配置错误: agents 列表不能为空,至少需要配置一个智能体端点");
84
+ }
85
+ // 获取已注册的适配器列表
86
+ const registeredProviders = index.adapterRegistry.list().map((a) => a.name);
87
+ for (let i = 0; i < config.agents.length; i++) {
88
+ const agent = config.agents[i];
89
+ if (!agent.name) {
90
+ throw new Error(`配置错误: agents[${i}].name 不能为空`);
91
+ }
92
+ if (!agent.url) {
93
+ throw new Error(`配置错误: agents[${i}].url 不能为空`);
94
+ }
95
+ // 校验 provider 字段(如果配置了)
96
+ if (agent.provider && !registeredProviders.includes(agent.provider)) {
97
+ throw new Error(`配置错误: agents[${i}].provider "${agent.provider}" 不是有效的适配器类型。` +
98
+ `可用的适配器: ${registeredProviders.join(", ")}`);
99
+ }
100
+ }
101
+ if (!config.accounts || !Array.isArray(config.accounts) || config.accounts.length === 0) {
102
+ throw new Error("配置错误: accounts 列表不能为空,至少需要配置一个企微账号");
103
+ }
104
+ for (let i = 0; i < config.accounts.length; i++) {
105
+ const account = config.accounts[i];
106
+ if (!account.websocketUrl) {
107
+ throw new Error(`配置错误: accounts[${i}].websocketUrl 不能为空`);
108
+ }
109
+ if (!account.botId) {
110
+ throw new Error(`配置错误: accounts[${i}].botId 不能为空`);
111
+ }
112
+ if (!account.secret) {
113
+ throw new Error(`配置错误: accounts[${i}].secret 不能为空`);
114
+ }
115
+ }
116
+ }
117
+ /**
118
+ * 解析账号配置,填充默认值,关联智能体端点
119
+ */
120
+ function resolveAccount(account, config) {
121
+ const accountId = account.accountId || account.botId;
122
+ // 查找智能体端点
123
+ let agent;
124
+ if (account.agentName) {
125
+ const found = config.agents.find((a) => a.name === account.agentName);
126
+ if (!found) {
127
+ throw new Error(`账号 ${accountId} 配置的智能体 "${account.agentName}" 未在 agents 列表中找到`);
128
+ }
129
+ agent = found;
130
+ }
131
+ else {
132
+ // 默认使用第一个智能体端点
133
+ agent = config.agents[0];
134
+ }
135
+ // 应用默认值
136
+ return {
137
+ accountId,
138
+ name: account.name || accountId,
139
+ enabled: account.enabled !== false,
140
+ websocketUrl: account.websocketUrl,
141
+ botId: account.botId,
142
+ secret: account.secret,
143
+ caCert: account.caCert,
144
+ sendThinkingMessage: account.sendThinkingMessage !== false,
145
+ dmPolicy: account.dmPolicy || "open",
146
+ allowFrom: account.allowFrom || [],
147
+ groupPolicy: account.groupPolicy || "open",
148
+ groupAllowFrom: account.groupAllowFrom || [],
149
+ groups: account.groups || {},
150
+ mediaLocalRoots: account.mediaLocalRoots || [],
151
+ agent: {
152
+ ...agent,
153
+ stream: agent.stream !== false,
154
+ timeoutMs: agent.timeoutMs ?? DEFAULT_AGENT_TIMEOUT_MS,
155
+ },
156
+ };
157
+ }
158
+ /**
159
+ * 解析所有启用的账号配置
160
+ */
161
+ function resolveAllAccounts(config) {
162
+ return config.accounts
163
+ .filter((a) => a.enabled !== false)
164
+ .map((a) => resolveAccount(a, config));
165
+ }
166
+
167
+ exports.loadConfig = loadConfig;
168
+ exports.resolveAccount = resolveAccount;
169
+ exports.resolveAllAccounts = resolveAllAccounts;