wechat-ai 0.1.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.
@@ -0,0 +1,774 @@
1
+ // src/config.ts
2
+ import { readFile, writeFile, mkdir } from "fs/promises";
3
+ import { existsSync } from "fs";
4
+ import { homedir } from "os";
5
+ import { join } from "path";
6
+ var WAI_DIR = join(homedir(), ".wai");
7
+ var CONFIG_PATH = join(WAI_DIR, "config.json");
8
+ var DEFAULT_CONFIG = {
9
+ defaultProvider: "qwen",
10
+ providers: {
11
+ claude: {
12
+ type: "claude-agent",
13
+ allowedTools: ["Read", "Glob", "Grep", "Bash", "WebSearch", "WebFetch"]
14
+ },
15
+ qwen: {
16
+ type: "openai-compatible",
17
+ baseUrl: "https://dashscope.aliyuncs.com/compatible-mode/v1",
18
+ model: "qwen-plus",
19
+ apiKeyEnv: "DASHSCOPE_API_KEY"
20
+ },
21
+ deepseek: {
22
+ type: "openai-compatible",
23
+ baseUrl: "https://api.deepseek.com/v1",
24
+ model: "deepseek-chat",
25
+ apiKeyEnv: "DEEPSEEK_API_KEY"
26
+ },
27
+ gpt: {
28
+ type: "openai-compatible",
29
+ baseUrl: "https://api.openai.com/v1",
30
+ model: "gpt-4o",
31
+ apiKeyEnv: "OPENAI_API_KEY"
32
+ },
33
+ gemini: {
34
+ type: "openai-compatible",
35
+ baseUrl: "https://generativelanguage.googleapis.com/v1beta/openai",
36
+ model: "gemini-2.0-flash",
37
+ apiKeyEnv: "GEMINI_API_KEY"
38
+ },
39
+ minimax: {
40
+ type: "openai-compatible",
41
+ baseUrl: "https://api.minimax.chat/v1",
42
+ model: "MiniMax-Text-01",
43
+ apiKeyEnv: "MINIMAX_API_KEY"
44
+ },
45
+ zhipu: {
46
+ type: "openai-compatible",
47
+ baseUrl: "https://open.bigmodel.cn/api/paas/v4",
48
+ model: "glm-4-plus",
49
+ apiKeyEnv: "ZHIPU_API_KEY"
50
+ }
51
+ },
52
+ channels: {
53
+ weixin: {
54
+ type: "weixin",
55
+ enabled: true
56
+ }
57
+ },
58
+ systemPrompt: "You are a helpful AI assistant. Respond concisely.",
59
+ chunkSize: 4e3
60
+ };
61
+ async function ensureDir(dir) {
62
+ if (!existsSync(dir)) {
63
+ await mkdir(dir, { recursive: true });
64
+ }
65
+ }
66
+ async function loadConfig() {
67
+ await ensureDir(WAI_DIR);
68
+ if (!existsSync(CONFIG_PATH)) {
69
+ await writeFile(CONFIG_PATH, JSON.stringify(DEFAULT_CONFIG, null, 2));
70
+ return { ...DEFAULT_CONFIG };
71
+ }
72
+ const raw = await readFile(CONFIG_PATH, "utf-8");
73
+ return { ...DEFAULT_CONFIG, ...JSON.parse(raw) };
74
+ }
75
+ async function saveConfig(config) {
76
+ await ensureDir(WAI_DIR);
77
+ await writeFile(CONFIG_PATH, JSON.stringify(config, null, 2));
78
+ }
79
+ function getAccountsDir() {
80
+ return join(WAI_DIR, "accounts");
81
+ }
82
+
83
+ // src/logger.ts
84
+ var LEVELS = { debug: 0, info: 1, warn: 2, error: 3 };
85
+ var currentLevel = "info";
86
+ function setLogLevel(level) {
87
+ currentLevel = level;
88
+ }
89
+ function fmt(level, scope, msg) {
90
+ const ts = (/* @__PURE__ */ new Date()).toISOString().slice(11, 23);
91
+ const tag = level.toUpperCase().padEnd(5);
92
+ return `\x1B[90m${ts}\x1B[0m ${colorize(level, tag)} \x1B[36m[${scope}]\x1B[0m ${msg}`;
93
+ }
94
+ function colorize(level, text) {
95
+ switch (level) {
96
+ case "debug":
97
+ return `\x1B[90m${text}\x1B[0m`;
98
+ case "info":
99
+ return `\x1B[32m${text}\x1B[0m`;
100
+ case "warn":
101
+ return `\x1B[33m${text}\x1B[0m`;
102
+ case "error":
103
+ return `\x1B[31m${text}\x1B[0m`;
104
+ }
105
+ }
106
+ function createLogger(scope) {
107
+ return {
108
+ debug: (msg) => {
109
+ if (LEVELS[currentLevel] <= 0) console.log(fmt("debug", scope, msg));
110
+ },
111
+ info: (msg) => {
112
+ if (LEVELS[currentLevel] <= 1) console.log(fmt("info", scope, msg));
113
+ },
114
+ warn: (msg) => {
115
+ if (LEVELS[currentLevel] <= 2) console.warn(fmt("warn", scope, msg));
116
+ },
117
+ error: (msg) => {
118
+ if (LEVELS[currentLevel] <= 3) console.error(fmt("error", scope, msg));
119
+ }
120
+ };
121
+ }
122
+
123
+ // src/channels/weixin.ts
124
+ import { join as join2 } from "path";
125
+ import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
126
+ import { existsSync as existsSync2 } from "fs";
127
+ import { randomBytes, randomUUID } from "crypto";
128
+ var log = createLogger("weixin");
129
+ var DEFAULT_BASE_URL = "https://ilinkai.weixin.qq.com";
130
+ var CHANNEL_VERSION = "1.0.0";
131
+ var API_TIMEOUT_MS = 15e3;
132
+ var MessageType = { USER: 1, BOT: 2 };
133
+ var MessageState = { NEW: 0, GENERATING: 1, FINISH: 2 };
134
+ var MessageItemType = { TEXT: 1, IMAGE: 2, VOICE: 3, FILE: 4, VIDEO: 5 };
135
+ var WeixinChannel = class {
136
+ name = "weixin";
137
+ account = null;
138
+ syncBuf = "";
139
+ running = false;
140
+ abortController = null;
141
+ config;
142
+ // Cache typing_ticket per user
143
+ typingTickets = /* @__PURE__ */ new Map();
144
+ constructor(config) {
145
+ this.config = config;
146
+ }
147
+ // ── Auth ──
148
+ async login() {
149
+ const baseUrl = this.config.baseUrl || DEFAULT_BASE_URL;
150
+ log.info("\u83B7\u53D6\u4E8C\u7EF4\u7801\u4E2D...");
151
+ const qrRes = await this.api(baseUrl, "ilink/bot/get_bot_qrcode?bot_type=3", null, {
152
+ method: "GET",
153
+ timeout: 1e4
154
+ });
155
+ if (qrRes.ret !== 0) {
156
+ throw new Error(`\u83B7\u53D6\u4E8C\u7EF4\u7801\u5931\u8D25: ${qrRes.errmsg || qrRes.ret}`);
157
+ }
158
+ const qrUrl = qrRes.qrcode_img_content || qrRes.data?.qrcode_img_content;
159
+ const qrCode = qrRes.qrcode || qrRes.data?.qrcode;
160
+ if (!qrUrl || !qrCode) {
161
+ throw new Error(`\u4E8C\u7EF4\u7801\u54CD\u5E94\u7F3A\u5C11\u5B57\u6BB5: ${JSON.stringify(qrRes)}`);
162
+ }
163
+ log.info("\u8BF7\u7528\u5FAE\u4FE1\u626B\u63CF\u4E8C\u7EF4\u7801:");
164
+ console.log();
165
+ try {
166
+ const qrTerminal = await import("qrcode-terminal");
167
+ (qrTerminal.default || qrTerminal).generate(qrUrl, { small: true });
168
+ } catch {
169
+ console.log(` ${qrUrl}`);
170
+ }
171
+ console.log();
172
+ log.info("\u7B49\u5F85\u626B\u7801...");
173
+ let attempts = 0;
174
+ while (attempts < 60) {
175
+ const statusRes = await this.api(
176
+ baseUrl,
177
+ `ilink/bot/get_qrcode_status?qrcode=${encodeURIComponent(qrCode)}`,
178
+ null,
179
+ { method: "GET", timeout: 4e4 }
180
+ );
181
+ const status = statusRes.data?.status || statusRes.status;
182
+ if (status === "confirmed") {
183
+ const data = statusRes.data || statusRes;
184
+ const accountId = data.ilink_bot_id || data.bot_id;
185
+ const token = data.bot_token || data.token;
186
+ if (!accountId || !token) {
187
+ throw new Error("\u767B\u5F55\u6210\u529F\u4F46\u7F3A\u5C11\u51ED\u8BC1");
188
+ }
189
+ this.account = {
190
+ accountId,
191
+ token,
192
+ baseUrl: data.baseurl || baseUrl,
193
+ userId: data.ilink_user_id
194
+ };
195
+ await this.saveAccount();
196
+ log.info(`\u767B\u5F55\u6210\u529F\uFF01\u8D26\u53F7: ${accountId.slice(0, 8)}...`);
197
+ return;
198
+ }
199
+ if (status === "scaned") {
200
+ log.info("\u5DF2\u626B\u7801\uFF0C\u7B49\u5F85\u786E\u8BA4...");
201
+ }
202
+ if (status === "expired") {
203
+ log.warn("\u4E8C\u7EF4\u7801\u5DF2\u8FC7\u671F");
204
+ throw new Error("\u4E8C\u7EF4\u7801\u5DF2\u8FC7\u671F");
205
+ }
206
+ attempts++;
207
+ await sleep(500);
208
+ }
209
+ throw new Error("\u767B\u5F55\u8D85\u65F6");
210
+ }
211
+ // ── Message loop ──
212
+ async start(onMessage) {
213
+ if (!this.account) {
214
+ await this.loadAccount();
215
+ }
216
+ if (!this.account) {
217
+ log.info("\u672A\u627E\u5230\u8D26\u53F7\uFF0C\u5F00\u59CB\u767B\u5F55...");
218
+ await this.login();
219
+ }
220
+ await this.loadSyncBuf();
221
+ this.running = true;
222
+ log.info(`\u6D88\u606F\u76D1\u542C\u5DF2\u542F\u52A8 (${this.account.accountId.slice(0, 8)}...)`);
223
+ while (this.running) {
224
+ try {
225
+ this.abortController = new AbortController();
226
+ const res = await this.getUpdates();
227
+ if (res.ret === -14) {
228
+ log.warn("\u4F1A\u8BDD\u8FC7\u671F\uFF0C\u91CD\u65B0\u767B\u5F55...");
229
+ this.account = null;
230
+ await this.login();
231
+ continue;
232
+ }
233
+ if (res.ret && res.ret !== 0) {
234
+ log.warn(`\u62C9\u53D6\u6D88\u606F\u5931\u8D25: ${res.errmsg || JSON.stringify(res)}`);
235
+ await sleep(5e3);
236
+ continue;
237
+ }
238
+ if (res.get_updates_buf) {
239
+ this.syncBuf = res.get_updates_buf;
240
+ await this.saveSyncBuf();
241
+ }
242
+ if (res.msgs && res.msgs.length > 0) {
243
+ for (const msg of res.msgs) {
244
+ const text = this.extractText(msg);
245
+ if (!text || !msg.from_user_id) continue;
246
+ log.info(`\u6536\u5230\u6D88\u606F [${msg.from_user_id.slice(0, 8)}...]: ${text.slice(0, 50)}`);
247
+ onMessage({
248
+ id: String(msg.message_id || msg.seq || Date.now()),
249
+ channel: "weixin",
250
+ senderId: msg.from_user_id,
251
+ text,
252
+ replyToken: msg.context_token,
253
+ timestamp: msg.create_time_ms || Date.now()
254
+ });
255
+ }
256
+ }
257
+ } catch (err) {
258
+ if (!this.running) break;
259
+ const message = err instanceof Error ? err.message : String(err);
260
+ if (message.includes("aborted") || message.includes("AbortError")) continue;
261
+ log.error(`\u8F6E\u8BE2\u51FA\u9519: ${message}`);
262
+ await sleep(3e3);
263
+ }
264
+ }
265
+ }
266
+ // ── Send typing indicator ──
267
+ async sendTyping(userId, contextToken) {
268
+ if (!this.account) return;
269
+ try {
270
+ let ticket = this.typingTickets.get(userId);
271
+ if (!ticket) {
272
+ const configRes = await this.api(this.account.baseUrl, "ilink/bot/getconfig", {
273
+ ilink_user_id: userId,
274
+ context_token: contextToken,
275
+ base_info: { channel_version: CHANNEL_VERSION }
276
+ }, { timeout: 1e4 });
277
+ ticket = configRes.typing_ticket;
278
+ if (ticket) {
279
+ this.typingTickets.set(userId, ticket);
280
+ }
281
+ }
282
+ if (!ticket) return;
283
+ await this.api(this.account.baseUrl, "ilink/bot/sendtyping", {
284
+ ilink_user_id: userId,
285
+ typing_ticket: ticket,
286
+ status: 1,
287
+ base_info: { channel_version: CHANNEL_VERSION }
288
+ }, { timeout: 1e4 });
289
+ log.debug(`\u5DF2\u53D1\u9001\u8F93\u5165\u72B6\u6001\u7ED9 ${userId.slice(0, 8)}...`);
290
+ } catch {
291
+ }
292
+ }
293
+ // ── Send message ──
294
+ async send(msg) {
295
+ if (!this.account) throw new Error("\u672A\u767B\u5F55");
296
+ const chunks = this.chunkText(msg.text, 4e3);
297
+ for (const chunk of chunks) {
298
+ const body = {
299
+ msg: {
300
+ from_user_id: "",
301
+ to_user_id: msg.targetId,
302
+ client_id: generateClientId(),
303
+ message_type: MessageType.BOT,
304
+ message_state: MessageState.FINISH,
305
+ context_token: msg.replyToken || void 0,
306
+ item_list: [{ type: MessageItemType.TEXT, text_item: { text: chunk } }]
307
+ },
308
+ base_info: { channel_version: CHANNEL_VERSION }
309
+ };
310
+ const res = await this.api(
311
+ this.account.baseUrl,
312
+ "ilink/bot/sendmessage",
313
+ body,
314
+ { timeout: API_TIMEOUT_MS }
315
+ );
316
+ if (res.ret && res.ret !== 0) {
317
+ log.error(`\u53D1\u9001\u5931\u8D25: ${res.errmsg || JSON.stringify(res)}`);
318
+ }
319
+ }
320
+ }
321
+ async stop() {
322
+ this.running = false;
323
+ this.abortController?.abort();
324
+ log.info("\u5DF2\u505C\u6B62");
325
+ }
326
+ // ── Internal ──
327
+ async getUpdates() {
328
+ if (!this.account) throw new Error("\u672A\u767B\u5F55");
329
+ return this.api(this.account.baseUrl, "ilink/bot/getupdates", {
330
+ get_updates_buf: this.syncBuf,
331
+ base_info: { channel_version: CHANNEL_VERSION }
332
+ }, { timeout: 5e4 });
333
+ }
334
+ extractText(msg) {
335
+ if (!msg.item_list?.length) return null;
336
+ for (const item of msg.item_list) {
337
+ if (item.type === 1 && item.text_item?.text) {
338
+ return item.text_item.text;
339
+ }
340
+ }
341
+ return null;
342
+ }
343
+ chunkText(text, maxLen) {
344
+ if (text.length <= maxLen) return [text];
345
+ const chunks = [];
346
+ let remaining = text;
347
+ while (remaining.length > 0) {
348
+ let breakAt = remaining.lastIndexOf("\n", maxLen);
349
+ if (breakAt <= 0) breakAt = maxLen;
350
+ chunks.push(remaining.slice(0, breakAt));
351
+ remaining = remaining.slice(breakAt);
352
+ }
353
+ return chunks;
354
+ }
355
+ async api(baseUrl, path, body, opts = {}) {
356
+ const url = `${baseUrl.replace(/\/$/, "")}/${path}`;
357
+ const method = opts.method || "POST";
358
+ const bodyStr = body ? JSON.stringify(body) : void 0;
359
+ const headers = {
360
+ "Content-Type": "application/json"
361
+ };
362
+ if (this.account?.token) {
363
+ headers["AuthorizationType"] = "ilink_bot_token";
364
+ headers["Authorization"] = `Bearer ${this.account.token}`;
365
+ headers["X-WECHAT-UIN"] = randomUin();
366
+ if (bodyStr) {
367
+ headers["Content-Length"] = String(Buffer.byteLength(bodyStr, "utf-8"));
368
+ }
369
+ }
370
+ const controller = new AbortController();
371
+ const timer = setTimeout(() => controller.abort(), opts.timeout || API_TIMEOUT_MS);
372
+ try {
373
+ const res = await fetch(url, {
374
+ method,
375
+ headers,
376
+ body: bodyStr,
377
+ signal: controller.signal
378
+ });
379
+ return await res.json();
380
+ } finally {
381
+ clearTimeout(timer);
382
+ }
383
+ }
384
+ // ── Persistence ──
385
+ accountFile() {
386
+ return join2(getAccountsDir(), "weixin.json");
387
+ }
388
+ syncFile() {
389
+ return join2(getAccountsDir(), "weixin-sync.json");
390
+ }
391
+ async saveAccount() {
392
+ await ensureDir(getAccountsDir());
393
+ await writeFile2(this.accountFile(), JSON.stringify(this.account, null, 2));
394
+ }
395
+ async loadAccount() {
396
+ const path = this.accountFile();
397
+ if (!existsSync2(path)) return;
398
+ try {
399
+ const raw = await readFile2(path, "utf-8");
400
+ this.account = JSON.parse(raw);
401
+ log.info(`\u5DF2\u52A0\u8F7D\u8D26\u53F7: ${this.account.accountId.slice(0, 8)}...`);
402
+ } catch {
403
+ log.warn("\u52A0\u8F7D\u8D26\u53F7\u5931\u8D25");
404
+ }
405
+ }
406
+ async saveSyncBuf() {
407
+ await ensureDir(getAccountsDir());
408
+ await writeFile2(this.syncFile(), JSON.stringify({ get_updates_buf: this.syncBuf }));
409
+ }
410
+ async loadSyncBuf() {
411
+ const path = this.syncFile();
412
+ if (!existsSync2(path)) return;
413
+ try {
414
+ const raw = await readFile2(path, "utf-8");
415
+ const data = JSON.parse(raw);
416
+ this.syncBuf = data.get_updates_buf || "";
417
+ } catch {
418
+ }
419
+ }
420
+ };
421
+ function randomUin() {
422
+ const uint32 = randomBytes(4).readUInt32BE(0);
423
+ return Buffer.from(String(uint32), "utf-8").toString("base64");
424
+ }
425
+ function generateClientId() {
426
+ return `wai-${randomUUID().replace(/-/g, "").slice(0, 16)}`;
427
+ }
428
+ function sleep(ms) {
429
+ return new Promise((r) => setTimeout(r, ms));
430
+ }
431
+
432
+ // src/providers/claude-agent.ts
433
+ var log2 = createLogger("claude");
434
+ var DEFAULT_TOOLS = ["Read", "Glob", "Grep", "Bash", "WebSearch", "WebFetch"];
435
+ var ClaudeAgentProvider = class {
436
+ name = "claude-agent";
437
+ config;
438
+ sessions = /* @__PURE__ */ new Map();
439
+ // userId -> sessionId
440
+ constructor(config) {
441
+ this.config = config;
442
+ }
443
+ async query(prompt, sessionId, options) {
444
+ const { query } = await import("@anthropic-ai/claude-agent-sdk");
445
+ const allowedTools = options?.allowedTools || this.config.allowedTools || DEFAULT_TOOLS;
446
+ const existingSession = this.sessions.get(sessionId);
447
+ const sdkOptions = {
448
+ allowedTools,
449
+ permissionMode: "acceptEdits"
450
+ };
451
+ if (options?.maxTokens) {
452
+ sdkOptions.maxTokens = options.maxTokens;
453
+ }
454
+ if (options?.cwd) {
455
+ sdkOptions.cwd = options.cwd;
456
+ }
457
+ if (existingSession) {
458
+ sdkOptions.resume = existingSession;
459
+ }
460
+ if (options?.systemPrompt) {
461
+ sdkOptions.systemPrompt = options.systemPrompt;
462
+ }
463
+ log2.info(`Querying Claude (session: ${sessionId.slice(0, 8)}...)`);
464
+ let result = "";
465
+ let newSessionId;
466
+ try {
467
+ for await (const message of query({
468
+ prompt,
469
+ options: sdkOptions
470
+ })) {
471
+ if (isInitMessage(message)) {
472
+ newSessionId = message.session_id;
473
+ }
474
+ if (isResultMessage(message)) {
475
+ result = message.result;
476
+ }
477
+ if (isAssistantMessage(message)) {
478
+ const textContent = extractText(message);
479
+ if (textContent) {
480
+ result = textContent;
481
+ }
482
+ }
483
+ }
484
+ } catch (err) {
485
+ const errMsg = err instanceof Error ? err.message : String(err);
486
+ log2.error(`Claude query failed: ${errMsg}`);
487
+ throw err;
488
+ }
489
+ if (newSessionId) {
490
+ this.sessions.set(sessionId, newSessionId);
491
+ }
492
+ if (!result) {
493
+ result = "(No response from Claude)";
494
+ }
495
+ log2.info(`Response: ${result.length} chars`);
496
+ return result;
497
+ }
498
+ };
499
+ function isInitMessage(msg) {
500
+ return msg?.type === "system" && msg?.subtype === "init" && typeof msg?.session_id === "string";
501
+ }
502
+ function isResultMessage(msg) {
503
+ return typeof msg?.result === "string";
504
+ }
505
+ function isAssistantMessage(msg) {
506
+ return msg?.type === "assistant" && msg?.message?.content;
507
+ }
508
+ function extractText(msg) {
509
+ if (!msg?.message?.content) return null;
510
+ const parts = [];
511
+ for (const block of msg.message.content) {
512
+ if (block.type === "text" && typeof block.text === "string") {
513
+ parts.push(block.text);
514
+ }
515
+ }
516
+ return parts.length > 0 ? parts.join("") : null;
517
+ }
518
+
519
+ // src/providers/openai-compatible.ts
520
+ var log3 = createLogger("openai-compat");
521
+ var OpenAICompatibleProvider = class {
522
+ name;
523
+ config;
524
+ histories = /* @__PURE__ */ new Map();
525
+ constructor(name, config) {
526
+ this.name = name;
527
+ this.config = config;
528
+ }
529
+ async query(prompt, sessionId, options) {
530
+ const baseUrl = this.config.baseUrl;
531
+ const apiKey = this.config.apiKey || process.env[this.config.apiKeyEnv || ""];
532
+ const model = options?.model || this.config.model;
533
+ if (!baseUrl) throw new Error(`${this.name}: baseUrl is required`);
534
+ if (!apiKey) throw new Error(`${this.name}: apiKey is required`);
535
+ if (!model) throw new Error(`${this.name}: model is required`);
536
+ let history = this.histories.get(sessionId);
537
+ if (!history) {
538
+ history = [];
539
+ this.histories.set(sessionId, history);
540
+ }
541
+ const messages = [];
542
+ const systemPrompt = options?.systemPrompt || this.config.systemPrompt;
543
+ if (systemPrompt) {
544
+ messages.push({ role: "system", content: systemPrompt });
545
+ }
546
+ const maxHistory = this.config.maxHistory || 20;
547
+ const recentHistory = history.slice(-maxHistory);
548
+ messages.push(...recentHistory);
549
+ messages.push({ role: "user", content: prompt });
550
+ log3.info(`Querying ${this.name} (model: ${model}, session: ${sessionId.slice(0, 8)}...)`);
551
+ const url = `${baseUrl.replace(/\/$/, "")}/chat/completions`;
552
+ const res = await fetch(url, {
553
+ method: "POST",
554
+ headers: {
555
+ "Content-Type": "application/json",
556
+ "Authorization": `Bearer ${apiKey}`
557
+ },
558
+ body: JSON.stringify({
559
+ model,
560
+ messages,
561
+ max_tokens: options?.maxTokens || this.config.maxTokens || 4096,
562
+ temperature: this.config.temperature ?? 0.7
563
+ })
564
+ });
565
+ if (!res.ok) {
566
+ const errBody = await res.text();
567
+ log3.error(`${this.name} API error ${res.status}: ${errBody.slice(0, 200)}`);
568
+ throw new Error(`${this.name} API error: ${res.status}`);
569
+ }
570
+ const data = await res.json();
571
+ const reply = data.choices[0]?.message.content || "(No response)";
572
+ if (data.usage) {
573
+ log3.info(`Tokens: ${data.usage.prompt_tokens} in / ${data.usage.completion_tokens} out`);
574
+ }
575
+ history.push({ role: "user", content: prompt });
576
+ history.push({ role: "assistant", content: reply });
577
+ log3.info(`Response: ${reply.length} chars`);
578
+ return reply;
579
+ }
580
+ };
581
+
582
+ // src/gateway.ts
583
+ var log4 = createLogger("\u7F51\u5173");
584
+ var Gateway = class {
585
+ channels = /* @__PURE__ */ new Map();
586
+ providers = /* @__PURE__ */ new Map();
587
+ config;
588
+ processing = /* @__PURE__ */ new Set();
589
+ constructor(config) {
590
+ this.config = config;
591
+ }
592
+ init() {
593
+ for (const [name, chConfig] of Object.entries(this.config.channels)) {
594
+ if (chConfig.enabled === false) continue;
595
+ switch (chConfig.type) {
596
+ case "weixin":
597
+ this.channels.set(name, new WeixinChannel(chConfig));
598
+ break;
599
+ default:
600
+ log4.warn(`\u672A\u77E5\u6E20\u9053\u7C7B\u578B: ${chConfig.type}`);
601
+ }
602
+ }
603
+ for (const [name, provConfig] of Object.entries(this.config.providers)) {
604
+ switch (provConfig.type) {
605
+ case "claude-agent":
606
+ this.providers.set(name, new ClaudeAgentProvider(provConfig));
607
+ break;
608
+ case "openai-compatible":
609
+ this.providers.set(name, new OpenAICompatibleProvider(name, provConfig));
610
+ break;
611
+ default:
612
+ log4.warn(`\u672A\u77E5\u6A21\u578B\u7C7B\u578B: ${provConfig.type}`);
613
+ }
614
+ }
615
+ log4.info(`\u5DF2\u521D\u59CB\u5316 ${this.channels.size} \u4E2A\u6E20\u9053, ${this.providers.size} \u4E2A\u6A21\u578B`);
616
+ }
617
+ async login(channelName) {
618
+ const channel = this.channels.get(channelName);
619
+ if (!channel) {
620
+ throw new Error(`\u6E20\u9053 "${channelName}" \u4E0D\u5B58\u5728`);
621
+ }
622
+ await channel.login();
623
+ }
624
+ async start() {
625
+ if (this.providers.size === 0) {
626
+ throw new Error("\u672A\u914D\u7F6E\u4EFB\u4F55\u6A21\u578B");
627
+ }
628
+ const startPromises = [...this.channels.entries()].map(([name, channel]) => {
629
+ log4.info(`\u542F\u52A8\u6E20\u9053: ${name}`);
630
+ return channel.start((msg) => this.handleMessage(msg)).catch((err) => {
631
+ log4.error(`\u6E20\u9053 ${name} \u5F02\u5E38: ${err instanceof Error ? err.message : err}`);
632
+ });
633
+ });
634
+ await Promise.all(startPromises);
635
+ }
636
+ async stop() {
637
+ log4.info("\u6B63\u5728\u5173\u95ED...");
638
+ const stops = [...this.channels.values()].map((ch) => ch.stop());
639
+ await Promise.allSettled(stops);
640
+ log4.info("\u5DF2\u5173\u95ED");
641
+ }
642
+ async handleMessage(msg) {
643
+ const lockKey = `${msg.channel}:${msg.senderId}`;
644
+ if (this.processing.has(lockKey)) {
645
+ log4.warn(`\u8DF3\u8FC7\u6D88\u606F (\u4E0A\u6761\u4ECD\u5728\u5904\u7406)`);
646
+ return;
647
+ }
648
+ this.processing.add(lockKey);
649
+ try {
650
+ if (msg.text.startsWith("/")) {
651
+ await this.handleCommand(msg);
652
+ return;
653
+ }
654
+ const providerName = this.config.userRoutes?.[msg.senderId] || this.config.defaultProvider;
655
+ const provider = this.providers.get(providerName);
656
+ if (!provider) {
657
+ log4.error(`\u6A21\u578B "${providerName}" \u672A\u627E\u5230`);
658
+ return;
659
+ }
660
+ log4.info(`\u8C03\u7528 ${providerName} \u5904\u7406\u4E2D...`);
661
+ const channel = this.channels.get(msg.channel);
662
+ if (channel && "sendTyping" in channel) {
663
+ channel.sendTyping(msg.senderId, msg.replyToken);
664
+ }
665
+ const options = {};
666
+ if (this.config.systemPrompt) {
667
+ options.systemPrompt = this.config.systemPrompt;
668
+ }
669
+ const sessionKey = `${msg.channel}:${msg.senderId}`;
670
+ const response = await provider.query(msg.text, sessionKey, options);
671
+ if (!channel) return;
672
+ await channel.send({
673
+ targetId: msg.senderId,
674
+ text: response,
675
+ replyToken: msg.replyToken
676
+ });
677
+ log4.info(`\u5DF2\u56DE\u590D (${response.length} \u5B57\u7B26)`);
678
+ } catch (err) {
679
+ const errMsg = err instanceof Error ? err.message : String(err);
680
+ log4.error(`\u5904\u7406\u6D88\u606F\u5931\u8D25: ${errMsg}`);
681
+ try {
682
+ const channel = this.channels.get(msg.channel);
683
+ if (channel) {
684
+ await channel.send({
685
+ targetId: msg.senderId,
686
+ text: `[\u51FA\u9519\u4E86] \u5904\u7406\u6D88\u606F\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5\u3002`,
687
+ replyToken: msg.replyToken
688
+ });
689
+ }
690
+ } catch {
691
+ }
692
+ } finally {
693
+ this.processing.delete(lockKey);
694
+ }
695
+ }
696
+ async handleCommand(msg) {
697
+ const channel = this.channels.get(msg.channel);
698
+ if (!channel) return;
699
+ const parts = msg.text.trim().split(/\s+/);
700
+ const cmd = parts[0].toLowerCase();
701
+ const arg = parts[1];
702
+ switch (cmd) {
703
+ case "/model": {
704
+ if (!arg) {
705
+ const current = this.config.userRoutes?.[msg.senderId] || this.config.defaultProvider;
706
+ const available = [...this.providers.keys()].join(", ");
707
+ await channel.send({
708
+ targetId: msg.senderId,
709
+ text: `\u5F53\u524D\u6A21\u578B: ${current}
710
+ \u53EF\u7528\u6A21\u578B: ${available}
711
+ \u7528\u6CD5: /model <\u540D\u79F0>`,
712
+ replyToken: msg.replyToken
713
+ });
714
+ } else if (this.providers.has(arg)) {
715
+ if (!this.config.userRoutes) this.config.userRoutes = {};
716
+ this.config.userRoutes[msg.senderId] = arg;
717
+ await channel.send({
718
+ targetId: msg.senderId,
719
+ text: `\u5DF2\u5207\u6362\u5230: ${arg}`,
720
+ replyToken: msg.replyToken
721
+ });
722
+ } else {
723
+ await channel.send({
724
+ targetId: msg.senderId,
725
+ text: `\u672A\u77E5\u6A21\u578B: ${arg}
726
+ \u53EF\u7528: ${[...this.providers.keys()].join(", ")}`,
727
+ replyToken: msg.replyToken
728
+ });
729
+ }
730
+ break;
731
+ }
732
+ case "/help": {
733
+ await channel.send({
734
+ targetId: msg.senderId,
735
+ text: [
736
+ "wx-ai \u6307\u4EE4:",
737
+ "/model [\u540D\u79F0] - \u5207\u6362AI\u6A21\u578B",
738
+ "/help - \u663E\u793A\u5E2E\u52A9",
739
+ "/ping - \u68C0\u67E5\u72B6\u6001"
740
+ ].join("\n"),
741
+ replyToken: msg.replyToken
742
+ });
743
+ break;
744
+ }
745
+ case "/ping": {
746
+ await channel.send({
747
+ targetId: msg.senderId,
748
+ text: `pong (${Date.now() - msg.timestamp}ms)`,
749
+ replyToken: msg.replyToken
750
+ });
751
+ break;
752
+ }
753
+ default: {
754
+ await channel.send({
755
+ targetId: msg.senderId,
756
+ text: `\u672A\u77E5\u6307\u4EE4: ${cmd}\uFF0C\u8BD5\u8BD5 /help`,
757
+ replyToken: msg.replyToken
758
+ });
759
+ }
760
+ }
761
+ }
762
+ };
763
+
764
+ export {
765
+ loadConfig,
766
+ saveConfig,
767
+ setLogLevel,
768
+ createLogger,
769
+ WeixinChannel,
770
+ ClaudeAgentProvider,
771
+ OpenAICompatibleProvider,
772
+ Gateway
773
+ };
774
+ //# sourceMappingURL=chunk-PRU3A74K.js.map