opencode-feishu-bot 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.
Files changed (41) hide show
  1. package/README.md +314 -0
  2. package/bin/cli.js +3 -0
  3. package/dist/auth/whitelist.d.ts +25 -0
  4. package/dist/auth/whitelist.d.ts.map +1 -0
  5. package/dist/cli.d.ts +27 -0
  6. package/dist/cli.d.ts.map +1 -0
  7. package/dist/commands/handler.d.ts +51 -0
  8. package/dist/commands/handler.d.ts.map +1 -0
  9. package/dist/commands/parser.d.ts +24 -0
  10. package/dist/commands/parser.d.ts.map +1 -0
  11. package/dist/config.d.ts +44 -0
  12. package/dist/config.d.ts.map +1 -0
  13. package/dist/database/index.d.ts +96 -0
  14. package/dist/database/index.d.ts.map +1 -0
  15. package/dist/database/schema.sql +49 -0
  16. package/dist/events/handler.d.ts +13 -0
  17. package/dist/events/handler.d.ts.map +1 -0
  18. package/dist/feishu/client.d.ts +218 -0
  19. package/dist/feishu/client.d.ts.map +1 -0
  20. package/dist/feishu/formatter.d.ts +110 -0
  21. package/dist/feishu/formatter.d.ts.map +1 -0
  22. package/dist/feishu/menu.d.ts +44 -0
  23. package/dist/feishu/menu.d.ts.map +1 -0
  24. package/dist/feishu/question-card.d.ts +24 -0
  25. package/dist/feishu/question-card.d.ts.map +1 -0
  26. package/dist/feishu/streamer.d.ts +53 -0
  27. package/dist/feishu/streamer.d.ts.map +1 -0
  28. package/dist/feishu/welcome.d.ts +9 -0
  29. package/dist/feishu/welcome.d.ts.map +1 -0
  30. package/dist/index.d.ts +2 -0
  31. package/dist/index.d.ts.map +1 -0
  32. package/dist/index.js +4034 -0
  33. package/dist/opencode/client.d.ts +86 -0
  34. package/dist/opencode/client.d.ts.map +1 -0
  35. package/dist/session/manager.d.ts +75 -0
  36. package/dist/session/manager.d.ts.map +1 -0
  37. package/dist/utils/logger.d.ts +12 -0
  38. package/dist/utils/logger.d.ts.map +1 -0
  39. package/dist/utils/reconnect.d.ts +39 -0
  40. package/dist/utils/reconnect.d.ts.map +1 -0
  41. package/package.json +74 -0
package/dist/index.js ADDED
@@ -0,0 +1,4034 @@
1
+ var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
2
+
3
+ // package.json
4
+ var require_package = __commonJS((exports, module) => {
5
+ module.exports = {
6
+ name: "opencode-feishu-bot",
7
+ version: "0.1.0",
8
+ description: "飞书机器人,用于与 OpenCode AI 编程助手交互",
9
+ type: "module",
10
+ main: "./dist/index.js",
11
+ types: "./dist/index.d.ts",
12
+ exports: {
13
+ ".": {
14
+ types: "./dist/index.d.ts",
15
+ import: "./dist/index.js",
16
+ default: "./dist/index.js"
17
+ }
18
+ },
19
+ bin: {
20
+ "opencode-feishu-bot": "./bin/cli.js"
21
+ },
22
+ files: [
23
+ "dist",
24
+ "bin"
25
+ ],
26
+ scripts: {
27
+ start: "bun run src/index.ts",
28
+ dev: "bun --watch src/index.ts",
29
+ build: "bun run build:clean && bun run build:js && bun run build:types && bun run build:assets",
30
+ "build:clean": "rm -rf dist",
31
+ "build:js": "bun build ./src/index.ts --outdir ./dist --target node --format esm --packages external",
32
+ "build:types": "bunx tsc -p tsconfig.build.json",
33
+ "build:assets": "mkdir -p dist/database && cp src/database/schema.sql dist/database/",
34
+ test: "bun test",
35
+ "test:watch": "bun test --watch",
36
+ prepublishOnly: "bun run build",
37
+ prepack: "bun run build"
38
+ },
39
+ keywords: [
40
+ "feishu",
41
+ "lark",
42
+ "飞书",
43
+ "opencode",
44
+ "ai",
45
+ "chatbot",
46
+ "bot"
47
+ ],
48
+ author: "",
49
+ license: "MIT",
50
+ repository: {
51
+ type: "git",
52
+ url: "git+https://github.com/kylin1020/opencode-feishu-bot.git"
53
+ },
54
+ bugs: {
55
+ url: "https://github.com/kylin1020/opencode-feishu-bot/issues"
56
+ },
57
+ homepage: "https://github.com/kylin1020/opencode-feishu-bot#readme",
58
+ engines: {
59
+ bun: ">=1.2.0"
60
+ },
61
+ devDependencies: {
62
+ "@types/bun": "latest",
63
+ typescript: "^5"
64
+ },
65
+ peerDependencies: {
66
+ typescript: "^5"
67
+ },
68
+ peerDependenciesMeta: {
69
+ typescript: {
70
+ optional: true
71
+ }
72
+ },
73
+ dependencies: {
74
+ "@larksuiteoapi/node-sdk": "^1.58.0",
75
+ "@opencode-ai/sdk": "^1.1.47",
76
+ zod: "^4.3.6"
77
+ }
78
+ };
79
+ });
80
+
81
+ // src/config.ts
82
+ import { z } from "zod";
83
+ var envSchema = z.object({
84
+ FEISHU_APP_ID: z.string().min(1, "必须提供 FEISHU_APP_ID"),
85
+ FEISHU_APP_SECRET: z.string().min(1, "必须提供 FEISHU_APP_SECRET"),
86
+ ADMIN_USER_IDS: z.string().default(""),
87
+ DATABASE_PATH: z.string().default("./data/bot.db"),
88
+ LOG_LEVEL: z.enum(["debug", "info", "warn", "error"]).default("info"),
89
+ ALLOW_ALL_USERS: z.preprocess((v) => v === undefined ? true : v !== "false", z.boolean()).default(true),
90
+ PROJECTS: z.string().default(""),
91
+ MENU_SERVER_URL: z.string().optional(),
92
+ AVAILABLE_MODELS: z.string().default(""),
93
+ DEFAULT_MODEL: z.string().optional()
94
+ });
95
+ function loadConfig(overrides) {
96
+ const result = envSchema.safeParse(process.env);
97
+ if (!result.success) {
98
+ const errors = result.error.issues.map((e) => `${String(e.path.join("."))}: ${e.message}`).join(`
99
+ `);
100
+ throw new Error(`配置验证失败:
101
+ ${errors}`);
102
+ }
103
+ const config = result.data;
104
+ if (overrides?.model) {
105
+ config.DEFAULT_MODEL = overrides.model;
106
+ }
107
+ if (overrides?.logLevel) {
108
+ config.LOG_LEVEL = overrides.logLevel;
109
+ }
110
+ return config;
111
+ }
112
+ function getAdminUserIds(config) {
113
+ return config.ADMIN_USER_IDS.split(",").map((id) => id.trim()).filter((id) => id.length > 0);
114
+ }
115
+ function getDefaultProjectPath(override) {
116
+ return override || process.cwd();
117
+ }
118
+ function getDefaultModel(config) {
119
+ return config.DEFAULT_MODEL;
120
+ }
121
+ function getProjects(config) {
122
+ if (!config.PROJECTS.trim()) {
123
+ return [];
124
+ }
125
+ return config.PROJECTS.split(",").map((item) => {
126
+ const [path, name] = item.split(":").map((s) => s.trim());
127
+ if (!path)
128
+ return null;
129
+ return { path, name: name || path };
130
+ }).filter((item) => item !== null);
131
+ }
132
+ function getAvailableModels(config) {
133
+ if (!config.AVAILABLE_MODELS.trim()) {
134
+ return [];
135
+ }
136
+ const models = [];
137
+ for (const item of config.AVAILABLE_MODELS.split(",")) {
138
+ const trimmed = item.trim();
139
+ if (!trimmed)
140
+ continue;
141
+ const lastColonIndex = trimmed.lastIndexOf(":");
142
+ if (lastColonIndex === -1) {
143
+ models.push({ id: trimmed });
144
+ } else {
145
+ const id = trimmed.slice(0, lastColonIndex).trim();
146
+ const name = trimmed.slice(lastColonIndex + 1).trim();
147
+ if (id) {
148
+ models.push(name ? { id, name } : { id });
149
+ }
150
+ }
151
+ }
152
+ return models;
153
+ }
154
+ function filterModels(allModels, configuredModels) {
155
+ if (configuredModels.length === 0) {
156
+ return allModels;
157
+ }
158
+ const result = [];
159
+ for (const configured of configuredModels) {
160
+ const found = allModels.find((m) => m.id === configured.id);
161
+ if (found) {
162
+ result.push(configured.name ? { ...found, name: configured.name } : found);
163
+ }
164
+ }
165
+ return result;
166
+ }
167
+
168
+ // src/cli.ts
169
+ var ARG_DEFS = {
170
+ model: {
171
+ short: "m",
172
+ long: "model",
173
+ description: "设置默认模型 (格式: provider/model)",
174
+ hasValue: true,
175
+ envVar: "DEFAULT_MODEL"
176
+ },
177
+ project: {
178
+ short: "p",
179
+ long: "project",
180
+ description: "设置默认项目目录",
181
+ hasValue: true
182
+ },
183
+ logLevel: {
184
+ short: "l",
185
+ long: "log-level",
186
+ description: "日志级别 (debug|info|warn|error)",
187
+ hasValue: true,
188
+ envVar: "LOG_LEVEL"
189
+ },
190
+ help: {
191
+ short: "h",
192
+ long: "help",
193
+ description: "显示帮助信息",
194
+ hasValue: false
195
+ },
196
+ version: {
197
+ short: "v",
198
+ long: "version",
199
+ description: "显示版本号",
200
+ hasValue: false
201
+ },
202
+ listModels: {
203
+ long: "list-models",
204
+ description: "列出可用模型并退出",
205
+ hasValue: false
206
+ }
207
+ };
208
+ function parseArgs(args = process.argv.slice(2)) {
209
+ const options = {};
210
+ let i = 0;
211
+ while (i < args.length) {
212
+ const arg = args[i];
213
+ if (!arg || !arg.startsWith("-") && !arg.startsWith("--")) {
214
+ i++;
215
+ continue;
216
+ }
217
+ let key;
218
+ let value;
219
+ if (arg.startsWith("--")) {
220
+ const longArg = arg.slice(2);
221
+ const eqIndex = longArg.indexOf("=");
222
+ if (eqIndex >= 0) {
223
+ const longName = longArg.slice(0, eqIndex);
224
+ value = longArg.slice(eqIndex + 1);
225
+ key = findKeyByLong(longName);
226
+ } else {
227
+ key = findKeyByLong(longArg);
228
+ const def = key ? ARG_DEFS[key] : undefined;
229
+ const nextArg = args[i + 1];
230
+ if (def?.hasValue && nextArg && !nextArg.startsWith("-")) {
231
+ value = args[++i];
232
+ }
233
+ }
234
+ } else if (arg.startsWith("-")) {
235
+ const shortArg = arg.slice(1);
236
+ key = findKeyByShort(shortArg);
237
+ const def = key ? ARG_DEFS[key] : undefined;
238
+ const nextArg = args[i + 1];
239
+ if (def?.hasValue && nextArg && !nextArg.startsWith("-")) {
240
+ value = args[++i];
241
+ }
242
+ }
243
+ if (key) {
244
+ const def = ARG_DEFS[key];
245
+ if (def.hasValue) {
246
+ options[key] = value;
247
+ } else {
248
+ options[key] = true;
249
+ }
250
+ }
251
+ i++;
252
+ }
253
+ return options;
254
+ }
255
+ function findKeyByLong(long) {
256
+ for (const [key, def] of Object.entries(ARG_DEFS)) {
257
+ if (def.long === long) {
258
+ return key;
259
+ }
260
+ }
261
+ return;
262
+ }
263
+ function findKeyByShort(short) {
264
+ for (const [key, def] of Object.entries(ARG_DEFS)) {
265
+ if (def.short === short) {
266
+ return key;
267
+ }
268
+ }
269
+ return;
270
+ }
271
+ function formatHelp() {
272
+ const lines = [
273
+ "OpenCode 飞书机器人",
274
+ "",
275
+ "用法: opencode-feishu-bot [选项]",
276
+ "",
277
+ "选项:"
278
+ ];
279
+ for (const [, def] of Object.entries(ARG_DEFS)) {
280
+ const shortPart = def.short ? `-${def.short}, ` : " ";
281
+ const longPart = `--${def.long}`;
282
+ const valuePart = def.hasValue ? " <value>" : "";
283
+ lines.push(` ${shortPart}${longPart}${valuePart}`);
284
+ lines.push(` ${def.description}`);
285
+ }
286
+ lines.push("");
287
+ lines.push("环境变量:");
288
+ lines.push(" FEISHU_APP_ID 飞书应用 ID (必需)");
289
+ lines.push(" FEISHU_APP_SECRET 飞书应用密钥 (必需)");
290
+ lines.push(" DEFAULT_MODEL 默认模型 (可被 --model 覆盖)");
291
+ lines.push(" AVAILABLE_MODELS 可用模型列表 (逗号分隔)");
292
+ lines.push(" PROJECTS 预配置项目列表");
293
+ lines.push(" DATABASE_PATH 数据库路径 (默认: ./data/bot.db)");
294
+ lines.push(" LOG_LEVEL 日志级别 (默认: info)");
295
+ lines.push("");
296
+ lines.push("示例:");
297
+ lines.push(" # 使用指定模型启动");
298
+ lines.push(" opencode-feishu-bot --model anthropic/claude-sonnet-4-20250514");
299
+ lines.push("");
300
+ lines.push(" # 指定项目目录启动");
301
+ lines.push(" opencode-feishu-bot -p /path/to/project");
302
+ lines.push("");
303
+ lines.push(" # 列出可用模型");
304
+ lines.push(" opencode-feishu-bot --list-models");
305
+ return lines.join(`
306
+ `);
307
+ }
308
+ function getVersion() {
309
+ try {
310
+ const pkg = require_package();
311
+ return pkg.version || "0.0.0";
312
+ } catch {
313
+ return "0.0.0";
314
+ }
315
+ }
316
+ function isValidLogLevel(level) {
317
+ return ["debug", "info", "warn", "error"].includes(level);
318
+ }
319
+
320
+ // src/utils/logger.ts
321
+ var LOG_LEVELS = {
322
+ debug: 0,
323
+ info: 1,
324
+ warn: 2,
325
+ error: 3
326
+ };
327
+ var currentLevel = "info";
328
+ function setLogLevel(level) {
329
+ currentLevel = level;
330
+ }
331
+ function shouldLog(level) {
332
+ return LOG_LEVELS[level] >= LOG_LEVELS[currentLevel];
333
+ }
334
+ function formatMessage(level, message, data) {
335
+ const timestamp = new Date().toISOString();
336
+ const prefix = `[${timestamp}] [${level.toUpperCase()}]`;
337
+ if (data !== undefined) {
338
+ return `${prefix} ${message} ${JSON.stringify(data)}`;
339
+ }
340
+ return `${prefix} ${message}`;
341
+ }
342
+ var logger = {
343
+ debug(message, data) {
344
+ if (shouldLog("debug")) {
345
+ console.debug(formatMessage("debug", message, data));
346
+ }
347
+ },
348
+ info(message, data) {
349
+ if (shouldLog("info")) {
350
+ console.info(formatMessage("info", message, data));
351
+ }
352
+ },
353
+ warn(message, data) {
354
+ if (shouldLog("warn")) {
355
+ console.warn(formatMessage("warn", message, data));
356
+ }
357
+ },
358
+ error(message, data) {
359
+ if (shouldLog("error")) {
360
+ console.error(formatMessage("error", message, data));
361
+ }
362
+ }
363
+ };
364
+
365
+ // src/database/index.ts
366
+ import { Database } from "bun:sqlite";
367
+ import { readFileSync } from "node:fs";
368
+ import { join, dirname } from "node:path";
369
+ import { fileURLToPath } from "node:url";
370
+ import { mkdirSync, existsSync } from "node:fs";
371
+ var __dirname2 = dirname(fileURLToPath(import.meta.url));
372
+
373
+ class BotDatabase {
374
+ db;
375
+ dedupWindowMs;
376
+ constructor(dbPath, dedupWindowMs = 5 * 60 * 1000) {
377
+ if (dbPath !== ":memory:") {
378
+ const dir = dirname(dbPath);
379
+ if (!existsSync(dir)) {
380
+ mkdirSync(dir, { recursive: true });
381
+ }
382
+ }
383
+ this.db = new Database(dbPath);
384
+ this.dedupWindowMs = dedupWindowMs;
385
+ this.initialize();
386
+ }
387
+ initialize() {
388
+ const schemaPath = join(__dirname2, "schema.sql");
389
+ const schema = readFileSync(schemaPath, "utf-8");
390
+ this.db.exec(schema);
391
+ }
392
+ getSession(chatId) {
393
+ const stmt = this.db.prepare("SELECT * FROM user_sessions WHERE chat_id = ?");
394
+ return stmt.get(chatId) ?? null;
395
+ }
396
+ upsertSession(chatId, sessionId, projectPath) {
397
+ const stmt = this.db.prepare(`
398
+ INSERT INTO user_sessions (chat_id, session_id, project_path, updated_at)
399
+ VALUES (?, ?, ?, CURRENT_TIMESTAMP)
400
+ ON CONFLICT(chat_id) DO UPDATE SET
401
+ session_id = excluded.session_id,
402
+ project_path = excluded.project_path,
403
+ updated_at = CURRENT_TIMESTAMP
404
+ `);
405
+ stmt.run(chatId, sessionId, projectPath);
406
+ }
407
+ deleteSession(chatId) {
408
+ const stmt = this.db.prepare("DELETE FROM user_sessions WHERE chat_id = ?");
409
+ const result = stmt.run(chatId);
410
+ return result.changes > 0;
411
+ }
412
+ isUserWhitelisted(userId) {
413
+ const stmt = this.db.prepare("SELECT COUNT(*) as count FROM user_whitelist WHERE user_id = ?");
414
+ const result = stmt.get(userId);
415
+ return (result?.count ?? 0) > 0;
416
+ }
417
+ addToWhitelist(userId, addedBy) {
418
+ try {
419
+ const stmt = this.db.prepare(`
420
+ INSERT INTO user_whitelist (user_id, added_by)
421
+ VALUES (?, ?)
422
+ ON CONFLICT(user_id) DO NOTHING
423
+ `);
424
+ const result = stmt.run(userId, addedBy);
425
+ return result.changes > 0;
426
+ } catch {
427
+ return false;
428
+ }
429
+ }
430
+ removeFromWhitelist(userId) {
431
+ const stmt = this.db.prepare("DELETE FROM user_whitelist WHERE user_id = ?");
432
+ const result = stmt.run(userId);
433
+ return result.changes > 0;
434
+ }
435
+ getWhitelistedUsers() {
436
+ const stmt = this.db.prepare("SELECT * FROM user_whitelist");
437
+ return stmt.all();
438
+ }
439
+ getProjectPath(chatId) {
440
+ const stmt = this.db.prepare("SELECT * FROM project_mappings WHERE chat_id = ?");
441
+ const result = stmt.get(chatId);
442
+ return result?.project_path ?? null;
443
+ }
444
+ setProjectPath(chatId, projectPath) {
445
+ const stmt = this.db.prepare(`
446
+ INSERT INTO project_mappings (chat_id, project_path, updated_at)
447
+ VALUES (?, ?, CURRENT_TIMESTAMP)
448
+ ON CONFLICT(chat_id) DO UPDATE SET
449
+ project_path = excluded.project_path,
450
+ updated_at = CURRENT_TIMESTAMP
451
+ `);
452
+ stmt.run(chatId, projectPath);
453
+ }
454
+ isEventProcessed(eventId) {
455
+ const stmt = this.db.prepare("SELECT COUNT(*) as count FROM event_dedup WHERE event_id = ?");
456
+ const result = stmt.get(eventId);
457
+ return (result?.count ?? 0) > 0;
458
+ }
459
+ markEventProcessed(eventId) {
460
+ if (this.isEventProcessed(eventId)) {
461
+ return false;
462
+ }
463
+ const stmt = this.db.prepare("INSERT INTO event_dedup (event_id) VALUES (?)");
464
+ stmt.run(eventId);
465
+ return true;
466
+ }
467
+ cleanupOldEvents() {
468
+ const cutoff = new Date(Date.now() - this.dedupWindowMs).toISOString();
469
+ const stmt = this.db.prepare("DELETE FROM event_dedup WHERE processed_at < ?");
470
+ const result = stmt.run(cutoff);
471
+ return result.changes;
472
+ }
473
+ saveMessageMapping(userMessageId, chatId) {
474
+ const stmt = this.db.prepare(`
475
+ INSERT OR REPLACE INTO message_mappings (user_message_id, chat_id)
476
+ VALUES (?, ?)
477
+ `);
478
+ stmt.run(userMessageId, chatId);
479
+ }
480
+ updateBotMessageId(userMessageId, botMessageId) {
481
+ const stmt = this.db.prepare(`
482
+ UPDATE message_mappings SET bot_message_id = ? WHERE user_message_id = ?
483
+ `);
484
+ stmt.run(botMessageId, userMessageId);
485
+ }
486
+ getMessageMapping(userMessageId) {
487
+ const stmt = this.db.prepare("SELECT * FROM message_mappings WHERE user_message_id = ?");
488
+ return stmt.get(userMessageId) ?? null;
489
+ }
490
+ deleteMessageMapping(userMessageId) {
491
+ const stmt = this.db.prepare("DELETE FROM message_mappings WHERE user_message_id = ?");
492
+ const result = stmt.run(userMessageId);
493
+ return result.changes > 0;
494
+ }
495
+ getMessageMappingsAfter(userMessageId, chatId) {
496
+ const targetMapping = this.getMessageMapping(userMessageId);
497
+ if (!targetMapping) {
498
+ return [];
499
+ }
500
+ const stmt = this.db.prepare(`
501
+ SELECT * FROM message_mappings
502
+ WHERE chat_id = ? AND created_at >= ?
503
+ ORDER BY created_at ASC
504
+ `);
505
+ return stmt.all(chatId, targetMapping.created_at);
506
+ }
507
+ deleteMessageMappings(userMessageIds) {
508
+ if (userMessageIds.length === 0)
509
+ return 0;
510
+ const placeholders = userMessageIds.map(() => "?").join(",");
511
+ const stmt = this.db.prepare(`DELETE FROM message_mappings WHERE user_message_id IN (${placeholders})`);
512
+ const result = stmt.run(...userMessageIds);
513
+ return result.changes;
514
+ }
515
+ deleteMessageMappingsByChatId(chatId) {
516
+ const stmt = this.db.prepare("DELETE FROM message_mappings WHERE chat_id = ?");
517
+ const result = stmt.run(chatId);
518
+ return result.changes;
519
+ }
520
+ cleanupOldMessageMappings(maxAgeMs = 24 * 60 * 60 * 1000) {
521
+ const cutoff = new Date(Date.now() - maxAgeMs).toISOString();
522
+ const stmt = this.db.prepare("DELETE FROM message_mappings WHERE created_at < ?");
523
+ const result = stmt.run(cutoff);
524
+ return result.changes;
525
+ }
526
+ createSessionChat(chatId, sessionId, ownerId, projectPath) {
527
+ const stmt = this.db.prepare(`
528
+ INSERT INTO session_chats (chat_id, session_id, owner_id, project_path)
529
+ VALUES (?, ?, ?, ?)
530
+ `);
531
+ stmt.run(chatId, sessionId, ownerId, projectPath);
532
+ }
533
+ getSessionChat(chatId) {
534
+ const stmt = this.db.prepare("SELECT * FROM session_chats WHERE chat_id = ?");
535
+ return stmt.get(chatId) ?? null;
536
+ }
537
+ getSessionChatsByOwner(ownerId) {
538
+ const stmt = this.db.prepare("SELECT * FROM session_chats WHERE owner_id = ? ORDER BY created_at DESC");
539
+ return stmt.all(ownerId);
540
+ }
541
+ updateSessionChatTitle(chatId, title) {
542
+ const stmt = this.db.prepare(`
543
+ UPDATE session_chats
544
+ SET title = ?, title_set = TRUE, updated_at = CURRENT_TIMESTAMP
545
+ WHERE chat_id = ?
546
+ `);
547
+ stmt.run(title, chatId);
548
+ }
549
+ deleteSessionChat(chatId) {
550
+ const stmt = this.db.prepare("DELETE FROM session_chats WHERE chat_id = ?");
551
+ const result = stmt.run(chatId);
552
+ return result.changes > 0;
553
+ }
554
+ isSessionChat(chatId) {
555
+ const stmt = this.db.prepare("SELECT COUNT(*) as count FROM session_chats WHERE chat_id = ?");
556
+ const result = stmt.get(chatId);
557
+ return (result?.count ?? 0) > 0;
558
+ }
559
+ close() {
560
+ this.db.close();
561
+ }
562
+ }
563
+ var defaultDb = null;
564
+ function initializeDatabase(dbPath) {
565
+ if (defaultDb) {
566
+ defaultDb.close();
567
+ }
568
+ defaultDb = new BotDatabase(dbPath);
569
+ return defaultDb;
570
+ }
571
+
572
+ // src/feishu/client.ts
573
+ import * as Lark from "@larksuiteoapi/node-sdk";
574
+ class FeishuClient {
575
+ client;
576
+ wsClient;
577
+ appId;
578
+ appSecret;
579
+ messageHandler = null;
580
+ botAddedHandler = null;
581
+ messageRecalledHandler = null;
582
+ botRemovedHandler = null;
583
+ botMenuHandler = null;
584
+ userLeftChatHandler = null;
585
+ chatDisbandedHandler = null;
586
+ cardActionHandler = null;
587
+ isConnected = false;
588
+ constructor(config) {
589
+ this.appId = config.appId;
590
+ this.appSecret = config.appSecret;
591
+ const baseConfig = {
592
+ appId: config.appId,
593
+ appSecret: config.appSecret,
594
+ domain: config.domain ?? Lark.Domain.Feishu
595
+ };
596
+ this.client = new Lark.Client(baseConfig);
597
+ this.wsClient = new Lark.WSClient({
598
+ appId: config.appId,
599
+ appSecret: config.appSecret,
600
+ loggerLevel: Lark.LoggerLevel.error
601
+ });
602
+ }
603
+ onMessage(handler) {
604
+ this.messageHandler = handler;
605
+ }
606
+ onBotAdded(handler) {
607
+ this.botAddedHandler = handler;
608
+ }
609
+ onMessageRecalled(handler) {
610
+ this.messageRecalledHandler = handler;
611
+ }
612
+ onBotRemoved(handler) {
613
+ this.botRemovedHandler = handler;
614
+ }
615
+ onBotMenu(handler) {
616
+ this.botMenuHandler = handler;
617
+ }
618
+ onUserLeftChat(handler) {
619
+ this.userLeftChatHandler = handler;
620
+ }
621
+ onChatDisbanded(handler) {
622
+ this.chatDisbandedHandler = handler;
623
+ }
624
+ onCardAction(handler) {
625
+ this.cardActionHandler = handler;
626
+ }
627
+ async start() {
628
+ if (this.isConnected) {
629
+ logger.warn("飞书客户端已连接");
630
+ return;
631
+ }
632
+ const eventDispatcher = new Lark.EventDispatcher({}).register({
633
+ "im.message.receive_v1": async (data) => {
634
+ try {
635
+ await this.handleMessageEvent(data);
636
+ } catch (error) {
637
+ logger.error("处理消息事件时出错", error);
638
+ }
639
+ },
640
+ "im.chat.member.bot.added_v1": async (data) => {
641
+ try {
642
+ await this.handleBotAddedEvent(data);
643
+ } catch (error) {
644
+ logger.error("处理机器人进群事件时出错", error);
645
+ }
646
+ },
647
+ "im.message.recalled_v1": async (data) => {
648
+ try {
649
+ await this.handleMessageRecalledEvent(data);
650
+ } catch (error) {
651
+ logger.error("处理消息撤回事件时出错", error);
652
+ }
653
+ },
654
+ "im.chat.member.bot.deleted_v1": async (data) => {
655
+ try {
656
+ await this.handleBotRemovedEvent(data);
657
+ } catch (error) {
658
+ logger.error("处理机器人移出群事件时出错", error);
659
+ }
660
+ },
661
+ "application.bot.menu_v6": async (data) => {
662
+ try {
663
+ await this.handleBotMenuEvent(data);
664
+ } catch (error) {
665
+ logger.error("处理菜单点击事件时出错", error);
666
+ }
667
+ },
668
+ "im.chat.member.user.deleted_v1": async (data) => {
669
+ try {
670
+ await this.handleUserLeftChatEvent(data);
671
+ } catch (error) {
672
+ logger.error("处理用户退群事件时出错", error);
673
+ }
674
+ },
675
+ "im.chat.disbanded_v1": async (data) => {
676
+ try {
677
+ await this.handleChatDisbandedEvent(data);
678
+ } catch (error) {
679
+ logger.error("处理群解散事件时出错", error);
680
+ }
681
+ },
682
+ "card.action.trigger": async (data) => {
683
+ try {
684
+ await this.handleCardActionEvent(data);
685
+ } catch (error) {
686
+ logger.error("处理卡片交互事件时出错", error);
687
+ }
688
+ }
689
+ });
690
+ await this.wsClient.start({ eventDispatcher });
691
+ this.isConnected = true;
692
+ logger.info("飞书 WebSocket 已连接");
693
+ }
694
+ async handleMessageEvent(rawData) {
695
+ const data = this.extractEventData(rawData);
696
+ if (!data) {
697
+ logger.warn("无效的消息事件结构");
698
+ return;
699
+ }
700
+ const { header, event } = data;
701
+ if (!event?.message || !event?.sender) {
702
+ logger.warn("无效的消息事件:缺少 message 或 sender");
703
+ return;
704
+ }
705
+ const chatType = event.message.chat_type;
706
+ const mentions = event.message.mentions?.map((m) => ({
707
+ key: m.key ?? "",
708
+ id: m.id?.open_id ?? "",
709
+ name: m.name ?? ""
710
+ }));
711
+ const messageEvent = {
712
+ eventId: header?.event_id ?? "",
713
+ messageId: event.message.message_id ?? "",
714
+ chatId: event.message.chat_id ?? "",
715
+ chatType,
716
+ senderId: event.sender.sender_id?.open_id ?? "",
717
+ senderType: event.sender.sender_type ?? "",
718
+ messageType: event.message.message_type ?? "",
719
+ content: event.message.content ?? "",
720
+ createTime: event.message.create_time ?? "",
721
+ mentions
722
+ };
723
+ if (this.messageHandler) {
724
+ await this.messageHandler(messageEvent);
725
+ }
726
+ }
727
+ async handleBotAddedEvent(rawData) {
728
+ if (!rawData || typeof rawData !== "object") {
729
+ logger.warn("无效的机器人进群事件结构");
730
+ return;
731
+ }
732
+ const data = rawData;
733
+ const eventId = data.event_id ?? "";
734
+ const chatId = data.chat_id ?? "";
735
+ const operatorId = data.operator_id?.open_id ?? "";
736
+ const chatName = data.name ?? data.i18n_names?.zh_cn;
737
+ if (!chatId) {
738
+ logger.warn("机器人进群事件缺少 chat_id");
739
+ return;
740
+ }
741
+ const event = { eventId, chatId, operatorId, chatName };
742
+ if (this.botAddedHandler) {
743
+ await this.botAddedHandler(event);
744
+ }
745
+ }
746
+ async handleMessageRecalledEvent(rawData) {
747
+ if (!rawData || typeof rawData !== "object") {
748
+ logger.warn("无效的消息撤回事件结构");
749
+ return;
750
+ }
751
+ const data = rawData;
752
+ const eventId = data.event_id ?? "";
753
+ const messageId = data.message_id ?? "";
754
+ const chatId = data.chat_id ?? "";
755
+ const recallTime = data.recall_time ?? "";
756
+ const recallType = data.recall_type ?? "message_owner";
757
+ if (!messageId) {
758
+ logger.warn("消息撤回事件缺少 message_id");
759
+ return;
760
+ }
761
+ const event = { eventId, messageId, chatId, recallTime, recallType };
762
+ if (this.messageRecalledHandler) {
763
+ await this.messageRecalledHandler(event);
764
+ }
765
+ }
766
+ async handleBotRemovedEvent(rawData) {
767
+ if (!rawData || typeof rawData !== "object") {
768
+ logger.warn("无效的机器人移出群事件结构");
769
+ return;
770
+ }
771
+ const data = rawData;
772
+ const eventId = data.event_id ?? "";
773
+ const chatId = data.chat_id ?? "";
774
+ const operatorId = data.operator_id?.open_id ?? "";
775
+ if (!chatId) {
776
+ logger.warn("机器人移出群事件缺少 chat_id");
777
+ return;
778
+ }
779
+ const event = { eventId, chatId, operatorId };
780
+ if (this.botRemovedHandler) {
781
+ await this.botRemovedHandler(event);
782
+ }
783
+ }
784
+ async handleBotMenuEvent(rawData) {
785
+ if (!rawData || typeof rawData !== "object") {
786
+ logger.warn("无效的菜单点击事件结构");
787
+ return;
788
+ }
789
+ const data = rawData;
790
+ const eventId = data.event_id ?? "";
791
+ const eventKey = data.event_key ?? "";
792
+ const timestamp = data.timestamp ?? "";
793
+ const operator = data.operator;
794
+ const operatorId = operator?.operator_id?.open_id ?? "";
795
+ const operatorName = operator?.operator_name;
796
+ if (!eventKey) {
797
+ logger.warn("菜单点击事件缺少 event_key");
798
+ return;
799
+ }
800
+ const event = {
801
+ eventId,
802
+ eventKey,
803
+ operatorId,
804
+ operatorName,
805
+ timestamp
806
+ };
807
+ if (this.botMenuHandler) {
808
+ await this.botMenuHandler(event);
809
+ }
810
+ }
811
+ async handleUserLeftChatEvent(rawData) {
812
+ if (!rawData || typeof rawData !== "object") {
813
+ logger.warn("无效的用户退群事件结构");
814
+ return;
815
+ }
816
+ const data = rawData;
817
+ const eventId = data.event_id ?? "";
818
+ const chatId = data.chat_id ?? "";
819
+ const operatorId = data.operator_id?.open_id ?? "";
820
+ const chatName = data.name ?? data.i18n_names?.zh_cn;
821
+ const rawUsers = data.users;
822
+ const users = rawUsers?.map((u) => ({
823
+ userId: u.user_id?.open_id ?? "",
824
+ name: u.name
825
+ })) ?? [];
826
+ if (!chatId) {
827
+ logger.warn("用户退群事件缺少 chat_id");
828
+ return;
829
+ }
830
+ const event = { eventId, chatId, operatorId, users, chatName };
831
+ if (this.userLeftChatHandler) {
832
+ await this.userLeftChatHandler(event);
833
+ }
834
+ }
835
+ async handleChatDisbandedEvent(rawData) {
836
+ if (!rawData || typeof rawData !== "object") {
837
+ logger.warn("无效的群解散事件结构");
838
+ return;
839
+ }
840
+ const data = rawData;
841
+ const eventId = data.event_id ?? "";
842
+ const chatId = data.chat_id ?? "";
843
+ const operatorId = data.operator_id?.open_id ?? "";
844
+ const chatName = data.name ?? data.i18n_names?.zh_cn;
845
+ if (!chatId) {
846
+ logger.warn("群解散事件缺少 chat_id");
847
+ return;
848
+ }
849
+ const event = { eventId, chatId, operatorId, chatName };
850
+ if (this.chatDisbandedHandler) {
851
+ await this.chatDisbandedHandler(event);
852
+ }
853
+ }
854
+ async handleCardActionEvent(rawData) {
855
+ if (!rawData || typeof rawData !== "object") {
856
+ logger.warn("无效的卡片交互事件结构");
857
+ return;
858
+ }
859
+ const data = rawData;
860
+ const eventId = data.event_id ?? "";
861
+ const operatorId = data.operator_id?.open_id ?? "";
862
+ const openChatId = data.open_chat_id ?? undefined;
863
+ const openMessageId = data.open_message_id ?? undefined;
864
+ const actionData = data.action;
865
+ if (!actionData) {
866
+ logger.warn("卡片交互事件缺少 action");
867
+ return;
868
+ }
869
+ const event = {
870
+ eventId,
871
+ operatorId,
872
+ chatId: openChatId,
873
+ messageId: openMessageId,
874
+ action: {
875
+ tag: actionData.tag ?? "",
876
+ value: actionData.value ?? {},
877
+ option: actionData.option
878
+ }
879
+ };
880
+ if (this.cardActionHandler) {
881
+ await this.cardActionHandler(event);
882
+ }
883
+ }
884
+ extractEventData(rawData) {
885
+ if (!rawData || typeof rawData !== "object") {
886
+ return null;
887
+ }
888
+ const data = rawData;
889
+ let header = data.header;
890
+ let event = data.event;
891
+ if (!event && data.message) {
892
+ const senderData = data.sender;
893
+ const messageData = data.message;
894
+ event = {
895
+ sender: senderData,
896
+ message: messageData
897
+ };
898
+ header = { event_id: data.event_id };
899
+ }
900
+ return { header, event };
901
+ }
902
+ async sendMessage(chatId, content, msgType = "text") {
903
+ try {
904
+ const response = await this.client.im.v1.message.create({
905
+ params: { receive_id_type: "chat_id" },
906
+ data: {
907
+ receive_id: chatId,
908
+ msg_type: msgType,
909
+ content
910
+ }
911
+ });
912
+ if (response.code !== 0) {
913
+ logger.error("发送消息失败", { code: response.code, msg: response.msg });
914
+ return null;
915
+ }
916
+ return response.data?.message_id ?? null;
917
+ } catch (error) {
918
+ logger.error("发送消息时出错", error);
919
+ return null;
920
+ }
921
+ }
922
+ async sendMessageToUser(userId, content, msgType = "text") {
923
+ try {
924
+ const response = await this.client.im.v1.message.create({
925
+ params: { receive_id_type: "open_id" },
926
+ data: {
927
+ receive_id: userId,
928
+ msg_type: msgType,
929
+ content
930
+ }
931
+ });
932
+ if (response.code !== 0) {
933
+ logger.error("发送用户消息失败", { code: response.code, msg: response.msg });
934
+ return null;
935
+ }
936
+ return response.data?.message_id ?? null;
937
+ } catch (error) {
938
+ logger.error("发送用户消息时出错", error);
939
+ return null;
940
+ }
941
+ }
942
+ async sendCardToUser(userId, card) {
943
+ const content = JSON.stringify(card);
944
+ return this.sendMessageToUser(userId, content, "interactive");
945
+ }
946
+ async sendTextMessage(chatId, text) {
947
+ const content = JSON.stringify({ text });
948
+ return this.sendMessage(chatId, content, "text");
949
+ }
950
+ async sendCard(chatId, card) {
951
+ const content = JSON.stringify(card);
952
+ return this.sendMessage(chatId, content, "interactive");
953
+ }
954
+ async updateCard(messageId, card) {
955
+ try {
956
+ const response = await this.client.im.v1.message.patch({
957
+ path: { message_id: messageId },
958
+ data: {
959
+ content: JSON.stringify(card)
960
+ }
961
+ });
962
+ if (response.code !== 0) {
963
+ const isRateLimited = response.code === 230020;
964
+ if (!isRateLimited) {
965
+ logger.error("更新卡片失败", { code: response.code, msg: response.msg });
966
+ }
967
+ return { success: false, rateLimited: isRateLimited };
968
+ }
969
+ return { success: true };
970
+ } catch (error) {
971
+ const axiosError = error;
972
+ const isRateLimited = axiosError?.response?.data?.code === 230020;
973
+ if (!isRateLimited) {
974
+ logger.error("更新卡片时出错", error);
975
+ }
976
+ return { success: false, rateLimited: isRateLimited };
977
+ }
978
+ }
979
+ async deleteMessage(messageId) {
980
+ try {
981
+ const response = await this.client.im.v1.message.delete({
982
+ path: { message_id: messageId }
983
+ });
984
+ if (response.code !== 0) {
985
+ logger.error("撤回消息失败", { code: response.code, msg: response.msg });
986
+ return false;
987
+ }
988
+ return true;
989
+ } catch (error) {
990
+ logger.error("撤回消息时出错", error);
991
+ return false;
992
+ }
993
+ }
994
+ async createChat(name, userIds) {
995
+ try {
996
+ const response = await this.client.im.v1.chat.create({
997
+ params: {
998
+ user_id_type: "open_id",
999
+ set_bot_manager: true
1000
+ },
1001
+ data: {
1002
+ name,
1003
+ user_id_list: userIds,
1004
+ chat_type: "private",
1005
+ join_message_visibility: "only_owner",
1006
+ leave_message_visibility: "only_owner",
1007
+ membership_approval: "no_approval_required"
1008
+ }
1009
+ });
1010
+ if (response.code !== 0) {
1011
+ logger.error("创建群失败", { code: response.code, msg: response.msg });
1012
+ return null;
1013
+ }
1014
+ const chatId = response.data?.chat_id;
1015
+ if (!chatId) {
1016
+ logger.error("创建群成功但未返回 chat_id");
1017
+ return null;
1018
+ }
1019
+ return { chatId };
1020
+ } catch (error) {
1021
+ logger.error("创建群时出错", error);
1022
+ return null;
1023
+ }
1024
+ }
1025
+ async updateChatName(chatId, name) {
1026
+ try {
1027
+ const response = await this.client.im.v1.chat.update({
1028
+ path: { chat_id: chatId },
1029
+ data: { name }
1030
+ });
1031
+ if (response.code !== 0) {
1032
+ logger.error("更新群名失败", { code: response.code, msg: response.msg });
1033
+ return false;
1034
+ }
1035
+ return true;
1036
+ } catch (error) {
1037
+ logger.error("更新群名时出错", error);
1038
+ return false;
1039
+ }
1040
+ }
1041
+ async deleteChat(chatId) {
1042
+ try {
1043
+ const response = await this.client.im.v1.chat.delete({
1044
+ path: { chat_id: chatId }
1045
+ });
1046
+ if (response.code !== 0) {
1047
+ logger.error("解散群失败", { code: response.code, msg: response.msg });
1048
+ return false;
1049
+ }
1050
+ return true;
1051
+ } catch (error) {
1052
+ logger.error("解散群时出错", error);
1053
+ return false;
1054
+ }
1055
+ }
1056
+ async getChatInfo(chatId) {
1057
+ try {
1058
+ const response = await this.client.im.v1.chat.get({
1059
+ path: { chat_id: chatId },
1060
+ params: { user_id_type: "open_id" }
1061
+ });
1062
+ if (response.code !== 0) {
1063
+ logger.error("获取群信息失败", { code: response.code, msg: response.msg });
1064
+ return null;
1065
+ }
1066
+ return {
1067
+ name: response.data?.name,
1068
+ userCount: response.data?.user_count ? parseInt(response.data.user_count, 10) : undefined
1069
+ };
1070
+ } catch (error) {
1071
+ logger.error("获取群信息时出错", error);
1072
+ return null;
1073
+ }
1074
+ }
1075
+ async pinMessage(messageId) {
1076
+ try {
1077
+ const response = await this.client.im.v1.pin.create({
1078
+ data: { message_id: messageId }
1079
+ });
1080
+ if (response.code !== 0) {
1081
+ logger.error("置顶消息失败", { code: response.code, msg: response.msg, messageId });
1082
+ return false;
1083
+ }
1084
+ logger.info("置顶消息成功", { messageId });
1085
+ return true;
1086
+ } catch (error) {
1087
+ logger.error("置顶消息时出错", { messageId, error });
1088
+ return false;
1089
+ }
1090
+ }
1091
+ async unpinMessage(messageId) {
1092
+ try {
1093
+ const response = await this.client.im.v1.pin.delete({
1094
+ path: { message_id: messageId }
1095
+ });
1096
+ if (response.code !== 0) {
1097
+ logger.error("取消置顶失败", { code: response.code, msg: response.msg, messageId });
1098
+ return false;
1099
+ }
1100
+ return true;
1101
+ } catch (error) {
1102
+ logger.error("取消置顶时出错", { messageId, error });
1103
+ return false;
1104
+ }
1105
+ }
1106
+ async createChatMenu(chatId, menuTree) {
1107
+ try {
1108
+ const response = await this.client.im.v1.chatMenuTree.create({
1109
+ path: { chat_id: chatId },
1110
+ data: { menu_tree: menuTree }
1111
+ });
1112
+ if (response.code !== 0) {
1113
+ logger.error("创建群聊菜单失败", { code: response.code, msg: response.msg, chatId });
1114
+ return false;
1115
+ }
1116
+ logger.info("创建群聊菜单成功", { chatId });
1117
+ return true;
1118
+ } catch (error) {
1119
+ logger.error("创建群聊菜单时出错", { chatId, error });
1120
+ return false;
1121
+ }
1122
+ }
1123
+ async deleteChatMenu(chatId, menuIds) {
1124
+ try {
1125
+ const response = await this.client.im.v1.chatMenuTree.delete({
1126
+ path: { chat_id: chatId },
1127
+ data: {
1128
+ chat_menu_top_level_ids: menuIds
1129
+ }
1130
+ });
1131
+ if (response.code !== 0) {
1132
+ logger.error("删除群聊菜单失败", { code: response.code, msg: response.msg, chatId });
1133
+ return false;
1134
+ }
1135
+ return true;
1136
+ } catch (error) {
1137
+ logger.error("删除群聊菜单时出错", { chatId, error });
1138
+ return false;
1139
+ }
1140
+ }
1141
+ async getChatMenu(chatId) {
1142
+ try {
1143
+ const response = await this.client.im.v1.chatMenuTree.get({
1144
+ path: { chat_id: chatId }
1145
+ });
1146
+ if (response.code !== 0) {
1147
+ logger.error("获取群聊菜单失败", { code: response.code, msg: response.msg, chatId });
1148
+ return null;
1149
+ }
1150
+ return response.data?.menu_tree ?? null;
1151
+ } catch (error) {
1152
+ logger.error("获取群聊菜单时出错", { chatId, error });
1153
+ return null;
1154
+ }
1155
+ }
1156
+ async getMessageImage(messageId, imageKey) {
1157
+ const maxRetries = 3;
1158
+ for (let attempt = 1;attempt <= maxRetries; attempt++) {
1159
+ try {
1160
+ const tokenResponse = await fetch("https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal", {
1161
+ method: "POST",
1162
+ headers: { "Content-Type": "application/json" },
1163
+ body: JSON.stringify({
1164
+ app_id: this.appId,
1165
+ app_secret: this.appSecret
1166
+ })
1167
+ });
1168
+ const tokenData = await tokenResponse.json();
1169
+ if (!tokenData.tenant_access_token) {
1170
+ logger.error("获取 access token 失败", { code: tokenData.code, msg: tokenData.msg });
1171
+ return null;
1172
+ }
1173
+ const url = `https://open.feishu.cn/open-apis/im/v1/messages/${messageId}/resources/${imageKey}?type=image`;
1174
+ const response = await fetch(url, {
1175
+ method: "GET",
1176
+ headers: { Authorization: `Bearer ${tokenData.tenant_access_token}` }
1177
+ });
1178
+ if (!response.ok) {
1179
+ logger.error("图片请求失败", { status: response.status, messageId, imageKey });
1180
+ return null;
1181
+ }
1182
+ const arrayBuffer = await response.arrayBuffer();
1183
+ const data = Buffer.from(arrayBuffer);
1184
+ const contentType = response.headers.get("content-type") || "image/png";
1185
+ return { data, mimeType: contentType };
1186
+ } catch (error) {
1187
+ if (attempt === maxRetries) {
1188
+ logger.error("获取消息图片失败", { messageId, imageKey, error });
1189
+ return null;
1190
+ }
1191
+ await new Promise((resolve) => setTimeout(resolve, 1000 * attempt));
1192
+ }
1193
+ }
1194
+ return null;
1195
+ }
1196
+ getApiClient() {
1197
+ return this.client;
1198
+ }
1199
+ isReady() {
1200
+ return this.isConnected;
1201
+ }
1202
+ async stop() {
1203
+ this.isConnected = false;
1204
+ logger.info("飞书客户端已停止");
1205
+ }
1206
+ }
1207
+ function parseTextContent(content) {
1208
+ try {
1209
+ const parsed = JSON.parse(content);
1210
+ return parsed.text ?? "";
1211
+ } catch {
1212
+ return "";
1213
+ }
1214
+ }
1215
+ function parseImageContent(content) {
1216
+ try {
1217
+ const parsed = JSON.parse(content);
1218
+ return parsed.image_key ?? null;
1219
+ } catch {
1220
+ return null;
1221
+ }
1222
+ }
1223
+ function cleanMentionsFromText(text, mentions) {
1224
+ if (!mentions || mentions.length === 0)
1225
+ return text;
1226
+ let cleaned = text;
1227
+ for (const mention of mentions) {
1228
+ if (mention.key) {
1229
+ cleaned = cleaned.replace(new RegExp(`@${mention.key}\\s*`, "g"), "");
1230
+ }
1231
+ }
1232
+ return cleaned.trim();
1233
+ }
1234
+ function createFeishuClient(config) {
1235
+ return new FeishuClient(config);
1236
+ }
1237
+
1238
+ // src/opencode/client.ts
1239
+ import { createOpencode } from "@opencode-ai/sdk";
1240
+ class OpencodeWrapper {
1241
+ client = null;
1242
+ serverCloseFn = null;
1243
+ serverUrl = null;
1244
+ directory;
1245
+ constructor(config) {
1246
+ this.directory = config.directory;
1247
+ }
1248
+ async start() {
1249
+ if (this.client) {
1250
+ logger.warn("OpenCode 已启动");
1251
+ return this.serverUrl;
1252
+ }
1253
+ logger.info("正在启动 OpenCode 服务器...");
1254
+ const { client, server } = await createOpencode({
1255
+ port: 0
1256
+ });
1257
+ this.client = client;
1258
+ this.serverUrl = server.url;
1259
+ this.serverCloseFn = server.close;
1260
+ logger.info("OpenCode 服务器已启动", { url: this.serverUrl });
1261
+ return this.serverUrl;
1262
+ }
1263
+ stop() {
1264
+ if (this.serverCloseFn) {
1265
+ logger.info("正在停止 OpenCode 服务器...");
1266
+ this.serverCloseFn();
1267
+ this.serverCloseFn = null;
1268
+ this.client = null;
1269
+ this.serverUrl = null;
1270
+ logger.info("OpenCode 服务器已停止");
1271
+ }
1272
+ }
1273
+ getServerUrl() {
1274
+ return this.serverUrl;
1275
+ }
1276
+ isStarted() {
1277
+ return this.client !== null;
1278
+ }
1279
+ ensureClient() {
1280
+ if (!this.client) {
1281
+ throw new Error("OpenCode 未启动,请先调用 start()");
1282
+ }
1283
+ return this.client;
1284
+ }
1285
+ async createSession() {
1286
+ const client = this.ensureClient();
1287
+ const response = await client.session.create({
1288
+ query: { directory: this.directory }
1289
+ });
1290
+ if (!response.data) {
1291
+ throw new Error("创建会话失败:未返回数据");
1292
+ }
1293
+ return response.data.id;
1294
+ }
1295
+ async sendPrompt(sessionId, prompt, images) {
1296
+ const client = this.ensureClient();
1297
+ const parts = [
1298
+ { type: "text", text: prompt }
1299
+ ];
1300
+ if (images && images.length > 0) {
1301
+ for (const image of images) {
1302
+ const base64Data = image.data.toString("base64");
1303
+ const dataUrl = `data:${image.mimeType};base64,${base64Data}`;
1304
+ parts.push({
1305
+ type: "file",
1306
+ mime: image.mimeType,
1307
+ url: dataUrl,
1308
+ filename: image.filename
1309
+ });
1310
+ }
1311
+ }
1312
+ await client.session.promptAsync({
1313
+ path: { id: sessionId },
1314
+ query: { directory: this.directory },
1315
+ body: { parts }
1316
+ });
1317
+ }
1318
+ async abortSession(sessionId) {
1319
+ try {
1320
+ const client = this.ensureClient();
1321
+ await client.session.abort({
1322
+ path: { id: sessionId },
1323
+ query: { directory: this.directory }
1324
+ });
1325
+ return true;
1326
+ } catch (error) {
1327
+ logger.error("中止会话失败", error);
1328
+ return false;
1329
+ }
1330
+ }
1331
+ async subscribeToEvents(sessionId, callback) {
1332
+ const client = this.ensureClient();
1333
+ const abortController = new AbortController;
1334
+ const eventResult = await client.event.subscribe({
1335
+ query: { directory: this.directory }
1336
+ });
1337
+ const processEvents = async () => {
1338
+ try {
1339
+ for await (const eventData of eventResult.stream) {
1340
+ if (abortController.signal.aborted)
1341
+ break;
1342
+ const event = eventData;
1343
+ if (!event || !event.type)
1344
+ continue;
1345
+ const properties = "properties" in event ? event.properties : {};
1346
+ const eventInfo = properties;
1347
+ const info = eventInfo.info;
1348
+ const part = eventInfo.part;
1349
+ const eventSessionId = eventInfo.sessionID ?? part?.sessionID ?? info?.id ?? info?.sessionID;
1350
+ if (eventSessionId === sessionId || !eventSessionId) {
1351
+ callback({
1352
+ type: event.type,
1353
+ properties
1354
+ });
1355
+ }
1356
+ }
1357
+ } catch (error) {
1358
+ if (!abortController.signal.aborted) {
1359
+ logger.error("事件流错误", { sessionId, error });
1360
+ }
1361
+ }
1362
+ };
1363
+ processEvents();
1364
+ return () => {
1365
+ abortController.abort();
1366
+ };
1367
+ }
1368
+ async getSessionStatus(sessionId) {
1369
+ try {
1370
+ const client = this.ensureClient();
1371
+ const response = await client.session.status({
1372
+ query: { directory: this.directory }
1373
+ });
1374
+ const data = response.data;
1375
+ if (data && data[sessionId]) {
1376
+ return data[sessionId] === "busy" ? "busy" : "idle";
1377
+ }
1378
+ return "unknown";
1379
+ } catch (error) {
1380
+ logger.error("获取会话状态失败", error);
1381
+ return "unknown";
1382
+ }
1383
+ }
1384
+ getClient() {
1385
+ return this.client;
1386
+ }
1387
+ async executeCommand(sessionId, command, args = "") {
1388
+ try {
1389
+ const client = this.ensureClient();
1390
+ await client.session.command({
1391
+ path: { id: sessionId },
1392
+ query: { directory: this.directory },
1393
+ body: {
1394
+ command,
1395
+ arguments: args
1396
+ }
1397
+ });
1398
+ return true;
1399
+ } catch (error) {
1400
+ logger.error("执行命令失败", { command, error });
1401
+ return false;
1402
+ }
1403
+ }
1404
+ async listCommands() {
1405
+ try {
1406
+ const client = this.ensureClient();
1407
+ const response = await client.command.list({
1408
+ query: { directory: this.directory }
1409
+ });
1410
+ return response.data ?? [];
1411
+ } catch (error) {
1412
+ logger.error("获取命令列表失败", error);
1413
+ return [];
1414
+ }
1415
+ }
1416
+ async listModels() {
1417
+ try {
1418
+ const client = this.ensureClient();
1419
+ const response = await client.config.providers({
1420
+ query: { directory: this.directory }
1421
+ });
1422
+ const models = [];
1423
+ const data = response.data;
1424
+ if (data?.providers) {
1425
+ for (const provider of data.providers) {
1426
+ if (provider.models) {
1427
+ for (const [modelKey, model] of Object.entries(provider.models)) {
1428
+ models.push({
1429
+ id: `${provider.id}/${model.id}`,
1430
+ name: model.name,
1431
+ providerId: provider.id
1432
+ });
1433
+ }
1434
+ }
1435
+ }
1436
+ }
1437
+ return models;
1438
+ } catch (error) {
1439
+ logger.error("获取模型列表失败", error);
1440
+ return [];
1441
+ }
1442
+ }
1443
+ async getSession(sessionId) {
1444
+ try {
1445
+ const client = this.ensureClient();
1446
+ const response = await client.session.get({
1447
+ path: { id: sessionId },
1448
+ query: { directory: this.directory }
1449
+ });
1450
+ return response.data;
1451
+ } catch (error) {
1452
+ logger.error("获取会话信息失败", error);
1453
+ return null;
1454
+ }
1455
+ }
1456
+ async getSessionMessages(sessionId) {
1457
+ try {
1458
+ const client = this.ensureClient();
1459
+ const response = await client.session.messages({
1460
+ path: { id: sessionId },
1461
+ query: { directory: this.directory }
1462
+ });
1463
+ return response.data ?? [];
1464
+ } catch (error) {
1465
+ logger.error("获取会话消息失败", error);
1466
+ return [];
1467
+ }
1468
+ }
1469
+ async replyQuestion(requestId, answers) {
1470
+ if (!this.serverUrl) {
1471
+ logger.error("回复问题失败:服务器未启动");
1472
+ return false;
1473
+ }
1474
+ try {
1475
+ const url = new URL(`/question/${encodeURIComponent(requestId)}/reply`, this.serverUrl);
1476
+ if (this.directory) {
1477
+ url.searchParams.set("directory", this.directory);
1478
+ }
1479
+ const response = await fetch(url.toString(), {
1480
+ method: "POST",
1481
+ headers: { "Content-Type": "application/json" },
1482
+ body: JSON.stringify({ answers })
1483
+ });
1484
+ return response.ok;
1485
+ } catch (error) {
1486
+ logger.error("回复问题失败", error);
1487
+ return false;
1488
+ }
1489
+ }
1490
+ async rejectQuestion(requestId) {
1491
+ if (!this.serverUrl) {
1492
+ logger.error("拒绝问题失败:服务器未启动");
1493
+ return false;
1494
+ }
1495
+ try {
1496
+ const url = new URL(`/question/${encodeURIComponent(requestId)}/reject`, this.serverUrl);
1497
+ if (this.directory) {
1498
+ url.searchParams.set("directory", this.directory);
1499
+ }
1500
+ const response = await fetch(url.toString(), {
1501
+ method: "POST"
1502
+ });
1503
+ return response.ok;
1504
+ } catch (error) {
1505
+ logger.error("拒绝问题失败", error);
1506
+ return false;
1507
+ }
1508
+ }
1509
+ }
1510
+ function createOpencodeWrapper(config) {
1511
+ return new OpencodeWrapper(config);
1512
+ }
1513
+ function extractTextFromPart(part) {
1514
+ if (!part || typeof part !== "object")
1515
+ return null;
1516
+ const p = part;
1517
+ if (p.synthetic || p.ignored)
1518
+ return null;
1519
+ if (p.type === "text" && typeof p.text === "string") {
1520
+ return p.text;
1521
+ }
1522
+ if (p.type === "reasoning" && typeof p.text === "string") {
1523
+ return `[思考中] ${p.text}`;
1524
+ }
1525
+ return null;
1526
+ }
1527
+ function extractToolCallFromPart(part) {
1528
+ if (!part || typeof part !== "object")
1529
+ return null;
1530
+ const p = part;
1531
+ if (p.type === "tool" && typeof p.tool === "string") {
1532
+ const stateObj = p.state;
1533
+ return {
1534
+ name: p.tool,
1535
+ state: stateObj?.status ?? "pending",
1536
+ title: stateObj?.title,
1537
+ input: stateObj?.input,
1538
+ output: stateObj?.output,
1539
+ error: stateObj?.error
1540
+ };
1541
+ }
1542
+ return null;
1543
+ }
1544
+
1545
+ // src/feishu/formatter.ts
1546
+ import { resolve, isAbsolute } from "node:path";
1547
+ var MAX_CARD_CONTENT_LENGTH = 28000;
1548
+ var TRUNCATION_SUFFIX = `
1549
+
1550
+ ... (内容已截断)`;
1551
+ function createCard(content, title, template) {
1552
+ const truncatedContent = truncateContent(content);
1553
+ const card = {
1554
+ config: { wide_screen_mode: true },
1555
+ elements: [{
1556
+ tag: "markdown",
1557
+ content: truncatedContent
1558
+ }]
1559
+ };
1560
+ if (title) {
1561
+ card.header = {
1562
+ title: { tag: "plain_text", content: title },
1563
+ template: template ?? "blue"
1564
+ };
1565
+ }
1566
+ return card;
1567
+ }
1568
+ function isPathKey(key) {
1569
+ const lowerKey = key.toLowerCase();
1570
+ return lowerKey.includes("path") || lowerKey.includes("file") || lowerKey === "workdir";
1571
+ }
1572
+ function ensureAbsolutePath(value) {
1573
+ if (isAbsolute(value)) {
1574
+ return value;
1575
+ }
1576
+ return resolve(process.cwd(), value);
1577
+ }
1578
+ function formatToolInput(input) {
1579
+ const lines = [];
1580
+ for (const [key, value] of Object.entries(input)) {
1581
+ if (value === undefined || value === null)
1582
+ continue;
1583
+ let displayValue;
1584
+ if (typeof value === "string") {
1585
+ let processedValue = value;
1586
+ if (isPathKey(key) && value.trim()) {
1587
+ processedValue = ensureAbsolutePath(value);
1588
+ }
1589
+ displayValue = processedValue.length > 100 ? processedValue.slice(0, 100) + "..." : processedValue;
1590
+ } else {
1591
+ displayValue = JSON.stringify(value);
1592
+ if (displayValue.length > 100) {
1593
+ displayValue = displayValue.slice(0, 100) + "...";
1594
+ }
1595
+ }
1596
+ lines.push(`**${key}**: \`${displayValue}\``);
1597
+ }
1598
+ return lines.join(`
1599
+ `);
1600
+ }
1601
+ function escapeCodeBlockContent(text) {
1602
+ return text.replace(/```/g, "` ` `");
1603
+ }
1604
+ function getStatusEmoji(status) {
1605
+ switch (status.toLowerCase()) {
1606
+ case "running":
1607
+ case "pending":
1608
+ return "⏳";
1609
+ case "completed":
1610
+ case "complete":
1611
+ case "success":
1612
+ return "✅";
1613
+ case "error":
1614
+ case "failed":
1615
+ return "❌";
1616
+ default:
1617
+ return "\uD83D\uDD27";
1618
+ }
1619
+ }
1620
+ function truncateContent(content) {
1621
+ if (content.length <= MAX_CARD_CONTENT_LENGTH) {
1622
+ return content;
1623
+ }
1624
+ const availableLength = MAX_CARD_CONTENT_LENGTH - TRUNCATION_SUFFIX.length;
1625
+ return content.slice(0, availableLength) + TRUNCATION_SUFFIX;
1626
+ }
1627
+ function groupConsecutiveParts(parts) {
1628
+ const groups = [];
1629
+ for (const part of parts) {
1630
+ const lastGroup = groups[groups.length - 1];
1631
+ if (lastGroup && lastGroup.type === part.type) {
1632
+ lastGroup.parts.push(part);
1633
+ } else {
1634
+ groups.push({
1635
+ type: part.type,
1636
+ parts: [part]
1637
+ });
1638
+ }
1639
+ }
1640
+ return groups;
1641
+ }
1642
+ function estimateElementSize(element) {
1643
+ return JSON.stringify(element).length;
1644
+ }
1645
+ var MAX_CARD_SIZE = 25000;
1646
+ var MAX_REASONING_LENGTH = 3000;
1647
+ var MAX_TOOL_OUTPUT_LENGTH = 5000;
1648
+ function buildStreamingCardsV2(parts, isComplete, title) {
1649
+ const groups = groupConsecutiveParts(parts);
1650
+ const cards = [];
1651
+ let currentElements = [];
1652
+ let currentSize = 0;
1653
+ let cardIndex = 0;
1654
+ let reasoningIndex = 0;
1655
+ const createCard2 = (elements, isFinal) => {
1656
+ const template = isFinal && isComplete ? "green" : "wathet";
1657
+ const headerTitle = title ?? (isFinal && isComplete ? "响应完成" : "处理中...");
1658
+ const cardTitle = cardIndex > 0 ? `${headerTitle} (续${cardIndex})` : headerTitle;
1659
+ return {
1660
+ schema: "2.0",
1661
+ header: {
1662
+ title: { tag: "plain_text", content: cardTitle },
1663
+ template
1664
+ },
1665
+ body: { elements }
1666
+ };
1667
+ };
1668
+ const flushCard = () => {
1669
+ if (currentElements.length > 0) {
1670
+ cards.push(createCard2(currentElements, false));
1671
+ cardIndex++;
1672
+ currentElements = [];
1673
+ currentSize = 0;
1674
+ }
1675
+ };
1676
+ const addElement = (element) => {
1677
+ const elementSize = estimateElementSize(element);
1678
+ if (currentSize + elementSize > MAX_CARD_SIZE && currentElements.length > 0) {
1679
+ flushCard();
1680
+ }
1681
+ currentElements.push(element);
1682
+ currentSize += elementSize;
1683
+ };
1684
+ for (const group of groups) {
1685
+ switch (group.type) {
1686
+ case "reasoning": {
1687
+ reasoningIndex++;
1688
+ const reasoningTexts = group.parts.map((p) => p.text).filter((t) => !!t);
1689
+ if (reasoningTexts.length === 0)
1690
+ break;
1691
+ let combinedText = reasoningTexts.join(`
1692
+
1693
+ `);
1694
+ if (combinedText.length > MAX_REASONING_LENGTH) {
1695
+ combinedText = combinedText.slice(0, MAX_REASONING_LENGTH) + `
1696
+ ... (思考内容已截断)`;
1697
+ }
1698
+ const panelTitle = groups.filter((g) => g.type === "reasoning").length > 1 ? `\uD83D\uDCAD 思考过程 ${reasoningIndex}` : "\uD83D\uDCAD 思考过程";
1699
+ addElement({
1700
+ tag: "collapsible_panel",
1701
+ expanded: false,
1702
+ header: {
1703
+ title: { tag: "plain_text", content: panelTitle }
1704
+ },
1705
+ elements: [{
1706
+ tag: "markdown",
1707
+ content: "```\n" + escapeCodeBlockContent(combinedText) + "\n```"
1708
+ }]
1709
+ });
1710
+ break;
1711
+ }
1712
+ case "tool-call": {
1713
+ for (const tool of group.parts) {
1714
+ if (!tool.name)
1715
+ continue;
1716
+ const emoji = getStatusEmoji(tool.state ?? "pending");
1717
+ const toolElements = [];
1718
+ if (tool.input && Object.keys(tool.input).length > 0) {
1719
+ const inputLines = formatToolInput(tool.input);
1720
+ if (inputLines) {
1721
+ toolElements.push({
1722
+ tag: "markdown",
1723
+ content: inputLines
1724
+ });
1725
+ }
1726
+ }
1727
+ if (tool.error) {
1728
+ toolElements.push({
1729
+ tag: "markdown",
1730
+ content: `**错误:**
1731
+ \`\`\`
1732
+ ${escapeCodeBlockContent(tool.error)}
1733
+ \`\`\``
1734
+ });
1735
+ } else if (tool.output) {
1736
+ let outputText = tool.output;
1737
+ if (outputText.length > MAX_TOOL_OUTPUT_LENGTH) {
1738
+ outputText = outputText.slice(0, MAX_TOOL_OUTPUT_LENGTH) + `
1739
+ ... (输出已截断)`;
1740
+ }
1741
+ toolElements.push({
1742
+ tag: "markdown",
1743
+ content: "```\n" + escapeCodeBlockContent(outputText) + "\n```"
1744
+ });
1745
+ }
1746
+ const panelTitle = tool.title || tool.name;
1747
+ addElement({
1748
+ tag: "collapsible_panel",
1749
+ expanded: false,
1750
+ header: {
1751
+ title: { tag: "plain_text", content: `${emoji} ${panelTitle}` }
1752
+ },
1753
+ elements: toolElements.length > 0 ? toolElements : [{
1754
+ tag: "markdown",
1755
+ content: "*执行中...*"
1756
+ }]
1757
+ });
1758
+ }
1759
+ break;
1760
+ }
1761
+ case "text": {
1762
+ const textContents = group.parts.map((p) => p.text).filter((t) => !!t);
1763
+ if (textContents.length === 0)
1764
+ break;
1765
+ if (currentElements.length > 0) {
1766
+ addElement({ tag: "hr" });
1767
+ }
1768
+ const combinedText = textContents.join(`
1769
+
1770
+ `);
1771
+ addElement({
1772
+ tag: "markdown",
1773
+ content: truncateContent(combinedText)
1774
+ });
1775
+ break;
1776
+ }
1777
+ }
1778
+ }
1779
+ if (currentElements.length === 0) {
1780
+ currentElements.push({
1781
+ tag: "markdown",
1782
+ content: isComplete ? "(无内容)" : "..."
1783
+ });
1784
+ }
1785
+ cards.push(createCard2(currentElements, true));
1786
+ return { cards, hasMore: false };
1787
+ }
1788
+ function buildStreamingCard(content, isComplete, title) {
1789
+ const template = isComplete ? "green" : "wathet";
1790
+ const headerTitle = title ?? (isComplete ? "响应完成" : "处理中...");
1791
+ return createCard(content, headerTitle, template);
1792
+ }
1793
+
1794
+ // src/feishu/streamer.ts
1795
+ var DEFAULT_THROTTLE_MS = 500;
1796
+ var MIN_THROTTLE_MS = 500;
1797
+ var RATE_LIMIT_RETRY_DELAY_MS = 600;
1798
+ var MAX_RETRIES = 2;
1799
+ function sleep(ms) {
1800
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
1801
+ }
1802
+
1803
+ class CardStreamer {
1804
+ client;
1805
+ chatId;
1806
+ messageIds = [];
1807
+ buffer = "";
1808
+ orderedParts = [];
1809
+ lastUpdateTimePerMessage = new Map;
1810
+ throttleMs;
1811
+ pendingUpdate = null;
1812
+ isComplete = false;
1813
+ title;
1814
+ useV2 = false;
1815
+ isUpdating = false;
1816
+ hasPendingData = false;
1817
+ constructor(client, chatId, config) {
1818
+ this.client = client;
1819
+ this.chatId = chatId;
1820
+ this.throttleMs = Math.max(config?.throttleMs ?? DEFAULT_THROTTLE_MS, MIN_THROTTLE_MS);
1821
+ }
1822
+ setTitle(title) {
1823
+ this.title = title;
1824
+ }
1825
+ async start() {
1826
+ if (this.messageIds.length > 0)
1827
+ return;
1828
+ const cards = this.useV2 ? buildStreamingCardsV2([], false, this.title ?? "思考中...").cards : [buildStreamingCard("", false, this.title ?? "思考中...")];
1829
+ const card = cards[0];
1830
+ if (!card)
1831
+ return;
1832
+ try {
1833
+ const messageId = await this.client.sendCard(this.chatId, card);
1834
+ if (messageId) {
1835
+ this.messageIds.push(messageId);
1836
+ this.lastUpdateTimePerMessage.set(messageId, Date.now());
1837
+ } else {
1838
+ logger.error("创建初始卡片消息失败");
1839
+ }
1840
+ } catch (error) {
1841
+ logger.error("发送初始卡片时出错", error);
1842
+ }
1843
+ }
1844
+ async append(content) {
1845
+ this.buffer += content;
1846
+ await this.scheduleUpdate();
1847
+ }
1848
+ async setContent(content) {
1849
+ this.buffer = content;
1850
+ this.useV2 = false;
1851
+ await this.scheduleUpdate();
1852
+ }
1853
+ async setOrderedParts(parts) {
1854
+ this.orderedParts = parts;
1855
+ this.useV2 = true;
1856
+ await this.scheduleUpdate();
1857
+ }
1858
+ async setParts(parts) {
1859
+ this.orderedParts = parts.map((p) => ({
1860
+ type: p.type,
1861
+ text: p.text,
1862
+ name: p.name,
1863
+ state: p.state,
1864
+ title: p.title,
1865
+ input: p.input,
1866
+ output: p.output,
1867
+ error: p.error
1868
+ }));
1869
+ this.useV2 = true;
1870
+ await this.scheduleUpdate();
1871
+ }
1872
+ async scheduleUpdate() {
1873
+ this.hasPendingData = true;
1874
+ if (this.isUpdating) {
1875
+ return;
1876
+ }
1877
+ if (this.pendingUpdate) {
1878
+ return;
1879
+ }
1880
+ this.pendingUpdate = setTimeout(async () => {
1881
+ this.pendingUpdate = null;
1882
+ await this.flush();
1883
+ }, this.throttleMs);
1884
+ }
1885
+ async flush() {
1886
+ if (!this.buffer && this.orderedParts.length === 0)
1887
+ return;
1888
+ if (this.isUpdating) {
1889
+ this.hasPendingData = true;
1890
+ return;
1891
+ }
1892
+ this.isUpdating = true;
1893
+ this.hasPendingData = false;
1894
+ try {
1895
+ if (this.useV2) {
1896
+ const result = buildStreamingCardsV2(this.orderedParts, this.isComplete, this.title);
1897
+ await this.syncCards(result.cards);
1898
+ } else {
1899
+ const card = buildStreamingCard(this.buffer, this.isComplete, this.title);
1900
+ await this.syncCards([card]);
1901
+ }
1902
+ } catch (error) {
1903
+ logger.error("刷新卡片时出错", error);
1904
+ } finally {
1905
+ this.isUpdating = false;
1906
+ if (this.hasPendingData && !this.isComplete) {
1907
+ this.pendingUpdate = setTimeout(async () => {
1908
+ this.pendingUpdate = null;
1909
+ await this.flush();
1910
+ }, this.throttleMs);
1911
+ }
1912
+ }
1913
+ }
1914
+ async updateCardWithRateLimit(messageId, card) {
1915
+ const lastUpdate = this.lastUpdateTimePerMessage.get(messageId) ?? 0;
1916
+ const timeSinceLastUpdate = Date.now() - lastUpdate;
1917
+ if (timeSinceLastUpdate < this.throttleMs) {
1918
+ const waitTime = this.throttleMs - timeSinceLastUpdate;
1919
+ await sleep(waitTime);
1920
+ }
1921
+ for (let retry = 0;retry <= MAX_RETRIES; retry++) {
1922
+ const result = await this.client.updateCard(messageId, card);
1923
+ if (result.success) {
1924
+ this.lastUpdateTimePerMessage.set(messageId, Date.now());
1925
+ return true;
1926
+ }
1927
+ if (result.rateLimited && retry < MAX_RETRIES) {
1928
+ logger.debug("卡片更新触发频率限制,等待重试", { messageId, retry });
1929
+ await sleep(RATE_LIMIT_RETRY_DELAY_MS);
1930
+ continue;
1931
+ }
1932
+ if (!result.rateLimited) {
1933
+ logger.warn("更新卡片失败", { messageId });
1934
+ }
1935
+ return false;
1936
+ }
1937
+ return false;
1938
+ }
1939
+ async syncCards(cards) {
1940
+ for (let i = 0;i < cards.length; i++) {
1941
+ const card = cards[i];
1942
+ if (!card)
1943
+ continue;
1944
+ const existingMessageId = this.messageIds[i];
1945
+ if (existingMessageId) {
1946
+ await this.updateCardWithRateLimit(existingMessageId, card);
1947
+ } else {
1948
+ const messageId = await this.client.sendCard(this.chatId, card);
1949
+ if (messageId) {
1950
+ this.messageIds.push(messageId);
1951
+ this.lastUpdateTimePerMessage.set(messageId, Date.now());
1952
+ } else {
1953
+ logger.error("创建续卡片失败", { index: i });
1954
+ }
1955
+ }
1956
+ }
1957
+ while (this.messageIds.length > cards.length) {
1958
+ const extraMessageId = this.messageIds.pop();
1959
+ if (extraMessageId) {
1960
+ this.lastUpdateTimePerMessage.delete(extraMessageId);
1961
+ await this.client.deleteMessage(extraMessageId);
1962
+ }
1963
+ }
1964
+ }
1965
+ async complete() {
1966
+ this.isComplete = true;
1967
+ if (this.pendingUpdate) {
1968
+ clearTimeout(this.pendingUpdate);
1969
+ this.pendingUpdate = null;
1970
+ }
1971
+ while (this.isUpdating) {
1972
+ await sleep(50);
1973
+ }
1974
+ await this.flush();
1975
+ }
1976
+ async sendError(errorMessage) {
1977
+ this.isComplete = true;
1978
+ if (this.pendingUpdate) {
1979
+ clearTimeout(this.pendingUpdate);
1980
+ this.pendingUpdate = null;
1981
+ }
1982
+ while (this.isUpdating) {
1983
+ await sleep(50);
1984
+ }
1985
+ const card = {
1986
+ config: { wide_screen_mode: true },
1987
+ header: {
1988
+ title: { tag: "plain_text", content: "错误" },
1989
+ template: "red"
1990
+ },
1991
+ elements: [{
1992
+ tag: "markdown",
1993
+ content: `**发生错误:**
1994
+ \`\`\`
1995
+ ${errorMessage}
1996
+ \`\`\``
1997
+ }]
1998
+ };
1999
+ try {
2000
+ if (this.messageIds.length === 0) {
2001
+ const messageId = await this.client.sendCard(this.chatId, card);
2002
+ if (messageId) {
2003
+ this.messageIds.push(messageId);
2004
+ }
2005
+ } else {
2006
+ const firstMessageId = this.messageIds[0];
2007
+ if (firstMessageId) {
2008
+ await this.updateCardWithRateLimit(firstMessageId, card);
2009
+ }
2010
+ for (let i = 1;i < this.messageIds.length; i++) {
2011
+ const msgId = this.messageIds[i];
2012
+ if (msgId) {
2013
+ await this.client.deleteMessage(msgId);
2014
+ }
2015
+ }
2016
+ this.messageIds = this.messageIds.slice(0, 1);
2017
+ }
2018
+ } catch (error) {
2019
+ logger.error("发送错误卡片时出错", error);
2020
+ }
2021
+ }
2022
+ getMessageId() {
2023
+ return this.messageIds[0] ?? null;
2024
+ }
2025
+ getMessageIds() {
2026
+ return [...this.messageIds];
2027
+ }
2028
+ getContent() {
2029
+ return this.buffer;
2030
+ }
2031
+ reset() {
2032
+ this.messageIds = [];
2033
+ this.buffer = "";
2034
+ this.orderedParts = [];
2035
+ this.lastUpdateTimePerMessage.clear();
2036
+ this.isComplete = false;
2037
+ this.title = undefined;
2038
+ this.useV2 = false;
2039
+ this.isUpdating = false;
2040
+ this.hasPendingData = false;
2041
+ if (this.pendingUpdate) {
2042
+ clearTimeout(this.pendingUpdate);
2043
+ this.pendingUpdate = null;
2044
+ }
2045
+ }
2046
+ }
2047
+ function createCardStreamer(client, chatId, config) {
2048
+ return new CardStreamer(client, chatId, config);
2049
+ }
2050
+
2051
+ // src/feishu/question-card.ts
2052
+ function createHeader(title, color = "blue") {
2053
+ return {
2054
+ template: color,
2055
+ title: { tag: "plain_text", content: title }
2056
+ };
2057
+ }
2058
+ function createMarkdown(content) {
2059
+ return {
2060
+ tag: "markdown",
2061
+ content
2062
+ };
2063
+ }
2064
+ function createDivider() {
2065
+ return { tag: "hr" };
2066
+ }
2067
+ function createQuestionCard(request) {
2068
+ const elements = [];
2069
+ const firstQuestion = request.questions[0];
2070
+ const headerTitle = firstQuestion?.header || "\uD83E\uDD14 请选择";
2071
+ request.questions.forEach((q, questionIndex) => {
2072
+ if (questionIndex > 0) {
2073
+ elements.push(createDivider());
2074
+ }
2075
+ elements.push(createMarkdown(`**${q.question}**`));
2076
+ if (q.options && q.options.length > 0) {
2077
+ const descriptions = q.options.filter((opt) => opt.description).map((opt) => `• **${opt.label}**: ${opt.description}`).join(`
2078
+ `);
2079
+ if (descriptions) {
2080
+ elements.push(createMarkdown(descriptions));
2081
+ }
2082
+ const useDropdown = q.multiple === true || q.options.length > 3;
2083
+ if (useDropdown) {
2084
+ const options = q.options.map((opt) => ({
2085
+ text: { tag: "plain_text", content: opt.label },
2086
+ value: opt.label
2087
+ }));
2088
+ elements.push({
2089
+ tag: "action",
2090
+ actions: [
2091
+ {
2092
+ tag: "select_static",
2093
+ placeholder: { tag: "plain_text", content: q.multiple ? "选择答案(可多选)" : "选择答案" },
2094
+ value: {
2095
+ action: "question_answer",
2096
+ requestId: request.id,
2097
+ questionIndex
2098
+ },
2099
+ options
2100
+ }
2101
+ ]
2102
+ });
2103
+ } else {
2104
+ const buttons = q.options.map((opt) => ({
2105
+ tag: "button",
2106
+ text: { tag: "plain_text", content: opt.label },
2107
+ type: "default",
2108
+ value: {
2109
+ action: "question_answer",
2110
+ requestId: request.id,
2111
+ questionIndex,
2112
+ answerLabel: opt.label
2113
+ }
2114
+ }));
2115
+ elements.push({
2116
+ tag: "action",
2117
+ actions: buttons
2118
+ });
2119
+ }
2120
+ }
2121
+ });
2122
+ elements.push(createDivider());
2123
+ elements.push(createMarkdown("\uD83D\uDCAC 或直接发送消息输入自定义答案"));
2124
+ return {
2125
+ config: { wide_screen_mode: true },
2126
+ header: createHeader(headerTitle, "orange"),
2127
+ elements
2128
+ };
2129
+ }
2130
+ function createAnsweredCard(question, answer) {
2131
+ return {
2132
+ config: { wide_screen_mode: true },
2133
+ header: createHeader("✅ 已回答", "green"),
2134
+ elements: [
2135
+ createMarkdown(`**问题**: ${question}`),
2136
+ createMarkdown(`**答案**: ${answer}`)
2137
+ ]
2138
+ };
2139
+ }
2140
+ function createQuestionErrorCard(message) {
2141
+ return {
2142
+ config: { wide_screen_mode: true },
2143
+ header: createHeader("❌ 操作失败", "red"),
2144
+ elements: [
2145
+ createMarkdown(message)
2146
+ ]
2147
+ };
2148
+ }
2149
+
2150
+ // src/session/manager.ts
2151
+ class SessionManager {
2152
+ db;
2153
+ feishuClient;
2154
+ opencodeClient;
2155
+ config;
2156
+ activeSessions = new Map;
2157
+ messageQueue = new Map;
2158
+ pendingQuestions = new Map;
2159
+ constructor(db, feishuClient, opencodeClient, config) {
2160
+ this.db = db;
2161
+ this.feishuClient = feishuClient;
2162
+ this.opencodeClient = opencodeClient;
2163
+ this.config = config;
2164
+ }
2165
+ async handleMessage(event) {
2166
+ const { chatId, senderId, eventId, messageId, content, messageType, mentions, chatType } = event;
2167
+ if (this.db.isEventProcessed(eventId)) {
2168
+ return;
2169
+ }
2170
+ this.db.markEventProcessed(eventId);
2171
+ if (!this.isUserAuthorized(senderId)) {
2172
+ await this.sendUnauthorizedMessage(chatId);
2173
+ return;
2174
+ }
2175
+ const supportedTypes = ["text", "image", "post", "file"];
2176
+ if (!supportedTypes.includes(messageType)) {
2177
+ logger.warn("不支持的消息类型", { messageType, chatId });
2178
+ await this.feishuClient.sendTextMessage(chatId, `暂不支持该消息类型 (${messageType}),目前支持:文本、图片、富文本`);
2179
+ return;
2180
+ }
2181
+ let text = "";
2182
+ let images = [];
2183
+ if (messageType === "text") {
2184
+ text = parseTextContent(content);
2185
+ text = cleanMentionsFromText(text, mentions);
2186
+ if (!text.trim()) {
2187
+ return;
2188
+ }
2189
+ } else if (messageType === "image") {
2190
+ const imageKey = parseImageContent(content);
2191
+ if (!imageKey) {
2192
+ await this.feishuClient.sendTextMessage(chatId, "无法解析图片内容");
2193
+ return;
2194
+ }
2195
+ const imageData = await this.feishuClient.getMessageImage(messageId, imageKey);
2196
+ if (!imageData) {
2197
+ await this.feishuClient.sendTextMessage(chatId, "获取图片失败,请重试");
2198
+ return;
2199
+ }
2200
+ images = [{
2201
+ data: imageData.data,
2202
+ mimeType: imageData.mimeType,
2203
+ filename: `${imageKey}.${imageData.mimeType.split("/")[1] || "png"}`
2204
+ }];
2205
+ text = "请分析这张图片";
2206
+ } else if (messageType === "post") {
2207
+ const result = await this.parsePostContent(content, messageId);
2208
+ text = result.text;
2209
+ images = result.images;
2210
+ if (!text.trim() && images.length === 0) {
2211
+ return;
2212
+ }
2213
+ if (!text.trim() && images.length > 0) {
2214
+ text = "请分析这些图片";
2215
+ }
2216
+ } else if (messageType === "file") {
2217
+ await this.feishuClient.sendTextMessage(chatId, "暂不支持文件消息,请直接发送图片或文本");
2218
+ return;
2219
+ }
2220
+ const sessionChat = this.db.getSessionChat(chatId);
2221
+ const isSessionChat = sessionChat !== null;
2222
+ if (chatType === "group" && !isSessionChat) {
2223
+ return;
2224
+ }
2225
+ const pendingQuestion = this.pendingQuestions.get(chatId);
2226
+ if (pendingQuestion && messageType === "text") {
2227
+ await this.handleQuestionTextAnswer(chatId, text, pendingQuestion);
2228
+ return;
2229
+ }
2230
+ try {
2231
+ this.db.saveMessageMapping(messageId, chatId);
2232
+ } catch (e) {
2233
+ logger.error("保存消息映射失败", e);
2234
+ }
2235
+ await this.queueMessage(chatId, async () => {
2236
+ await this.processMessage(chatId, senderId, messageId, text, images, isSessionChat ? sessionChat : undefined);
2237
+ });
2238
+ }
2239
+ async queueMessage(chatId, handler) {
2240
+ const existingPromise = this.messageQueue.get(chatId) ?? Promise.resolve();
2241
+ const newPromise = existingPromise.then(async () => {
2242
+ try {
2243
+ await handler();
2244
+ } catch (error) {
2245
+ logger.error("处理队列消息时出错", error);
2246
+ }
2247
+ });
2248
+ this.messageQueue.set(chatId, newPromise);
2249
+ await newPromise;
2250
+ }
2251
+ async processMessage(chatId, senderId, userMessageId, text, images, sessionChat) {
2252
+ const streamer = createCardStreamer(this.feishuClient, chatId);
2253
+ streamer.setTitle("思考中...");
2254
+ await streamer.start();
2255
+ let sessionId;
2256
+ if (sessionChat) {
2257
+ sessionId = sessionChat.session_id;
2258
+ } else {
2259
+ sessionId = await this.getOrCreateSession(chatId);
2260
+ }
2261
+ const activeSession = {
2262
+ sessionId,
2263
+ chatId,
2264
+ userMessageId,
2265
+ streamer,
2266
+ unsubscribe: null,
2267
+ parts: [],
2268
+ firstTextSkipped: false
2269
+ };
2270
+ this.activeSessions.set(chatId, activeSession);
2271
+ try {
2272
+ const unsubscribe = await this.opencodeClient.subscribeToEvents(sessionId, (event) => this.handleOpencodeEvent(chatId, event));
2273
+ activeSession.unsubscribe = unsubscribe;
2274
+ await this.opencodeClient.sendPrompt(sessionId, text, images.length > 0 ? images : undefined);
2275
+ } catch (error) {
2276
+ logger.error("发送提示时出错", { chatId, sessionId, error });
2277
+ await streamer.sendError(error instanceof Error ? error.message : "未知错误");
2278
+ this.cleanupSession(chatId);
2279
+ }
2280
+ }
2281
+ async setSessionChatTitle(chatId, title, sessionId) {
2282
+ const maxLength = 60;
2283
+ const shortId = sessionId.replace(/^ses_/, "").slice(0, 8);
2284
+ const prefix = `o-${shortId}-`;
2285
+ const availableLength = maxLength - prefix.length;
2286
+ const truncatedTitle = title.length > availableLength ? title.slice(0, availableLength - 3) + "..." : title;
2287
+ const chatName = `${prefix}${truncatedTitle}`;
2288
+ const success = await this.feishuClient.updateChatName(chatId, chatName);
2289
+ if (success) {
2290
+ this.db.updateSessionChatTitle(chatId, title);
2291
+ }
2292
+ }
2293
+ async handleOpencodeEvent(chatId, event) {
2294
+ const session = this.activeSessions.get(chatId);
2295
+ if (!session)
2296
+ return;
2297
+ const { streamer, parts } = session;
2298
+ const properties = event.properties;
2299
+ switch (event.type) {
2300
+ case "message.part.updated": {
2301
+ const part = properties.part;
2302
+ if (!part)
2303
+ break;
2304
+ const partId = part.id;
2305
+ const formattedPart = this.formatPart(part);
2306
+ if (!formattedPart)
2307
+ break;
2308
+ if (formattedPart.type === "text" && !session.firstTextSkipped) {
2309
+ session.firstTextSkipped = true;
2310
+ break;
2311
+ }
2312
+ const existingIndex = parts.findIndex((p) => p.id === partId);
2313
+ if (existingIndex >= 0) {
2314
+ parts[existingIndex] = { ...formattedPart, id: partId };
2315
+ } else {
2316
+ parts.push({ ...formattedPart, id: partId });
2317
+ }
2318
+ await streamer.setParts(parts);
2319
+ const botMessageId = streamer.getMessageId();
2320
+ if (botMessageId && session.userMessageId) {
2321
+ this.db.updateBotMessageId(session.userMessageId, botMessageId);
2322
+ }
2323
+ break;
2324
+ }
2325
+ case "session.idle": {
2326
+ const info = properties.info;
2327
+ const sessionInfo = info;
2328
+ if (sessionInfo?.sessionID === session.sessionId) {
2329
+ await streamer.complete();
2330
+ this.cleanupSession(chatId);
2331
+ }
2332
+ break;
2333
+ }
2334
+ case "session.error": {
2335
+ const errorInfo = properties;
2336
+ const errorMessage = errorInfo.error?.message ?? "发生未知错误";
2337
+ logger.error("会话错误", { chatId, error: errorMessage });
2338
+ await streamer.sendError(errorMessage);
2339
+ this.cleanupSession(chatId);
2340
+ break;
2341
+ }
2342
+ case "message.updated": {
2343
+ const info = properties.info;
2344
+ if (info?.error) {
2345
+ const error = info.error;
2346
+ const errorMessage = error.data?.message ?? "消息处理错误";
2347
+ logger.error("消息错误", { chatId, error: errorMessage });
2348
+ await streamer.sendError(errorMessage);
2349
+ this.cleanupSession(chatId);
2350
+ }
2351
+ break;
2352
+ }
2353
+ case "session.updated": {
2354
+ const info = properties.info;
2355
+ if (info?.id === session.sessionId && info?.title) {
2356
+ const sessionChat = this.db.getSessionChat(chatId);
2357
+ if (sessionChat) {
2358
+ await this.setSessionChatTitle(chatId, info.title, session.sessionId);
2359
+ }
2360
+ }
2361
+ break;
2362
+ }
2363
+ case "question.asked": {
2364
+ const questionRequest = properties;
2365
+ if (questionRequest?.id && questionRequest?.questions?.length > 0) {
2366
+ await this.handleQuestionAsked(chatId, questionRequest);
2367
+ }
2368
+ break;
2369
+ }
2370
+ }
2371
+ }
2372
+ formatPart(part) {
2373
+ const textResult = extractTextFromPart(part);
2374
+ if (textResult !== null) {
2375
+ return {
2376
+ type: part.type,
2377
+ text: textResult.startsWith("[思考中]") ? textResult.replace("[思考中] ", "") : textResult
2378
+ };
2379
+ }
2380
+ const toolResult = extractToolCallFromPart(part);
2381
+ if (toolResult !== null) {
2382
+ return {
2383
+ type: "tool-call",
2384
+ name: toolResult.name,
2385
+ state: toolResult.state,
2386
+ title: toolResult.title,
2387
+ input: toolResult.input,
2388
+ output: toolResult.output,
2389
+ error: toolResult.error
2390
+ };
2391
+ }
2392
+ return null;
2393
+ }
2394
+ async handleQuestionAsked(chatId, request) {
2395
+ const card = createQuestionCard(request);
2396
+ const messageId = await this.feishuClient.sendCard(chatId, card);
2397
+ if (messageId) {
2398
+ this.pendingQuestions.set(chatId, {
2399
+ requestId: request.id,
2400
+ messageId,
2401
+ questions: request.questions,
2402
+ chatId
2403
+ });
2404
+ logger.info("收到问题请求", { chatId, requestId: request.id, questionCount: request.questions.length });
2405
+ }
2406
+ }
2407
+ async handleQuestionTextAnswer(chatId, text, pending) {
2408
+ const answers = pending.questions.map(() => [text]);
2409
+ const success = await this.opencodeClient.replyQuestion(pending.requestId, answers);
2410
+ if (success) {
2411
+ const firstQuestion = pending.questions[0]?.question || "问题";
2412
+ const answeredCard = createAnsweredCard(firstQuestion, text);
2413
+ await this.feishuClient.updateCard(pending.messageId, answeredCard);
2414
+ logger.info("文字回答问题", { chatId, requestId: pending.requestId, answer: text });
2415
+ } else {
2416
+ await this.feishuClient.sendTextMessage(chatId, "提交答案失败,请重试");
2417
+ }
2418
+ this.pendingQuestions.delete(chatId);
2419
+ }
2420
+ async handleQuestionAnswer(chatId, requestId, questionIndex, answerLabel, messageId) {
2421
+ const pending = this.pendingQuestions.get(chatId);
2422
+ if (!pending || pending.requestId !== requestId) {
2423
+ return false;
2424
+ }
2425
+ const answers = pending.questions.map((_, idx) => idx === questionIndex ? [answerLabel] : []);
2426
+ const success = await this.opencodeClient.replyQuestion(requestId, answers);
2427
+ if (success && messageId) {
2428
+ const firstQuestion = pending.questions[0]?.question || "问题";
2429
+ const answeredCard = createAnsweredCard(firstQuestion, answerLabel);
2430
+ await this.feishuClient.updateCard(messageId, answeredCard);
2431
+ }
2432
+ this.pendingQuestions.delete(chatId);
2433
+ logger.info("卡片回答问题", { chatId, requestId, questionIndex, answerLabel });
2434
+ return success;
2435
+ }
2436
+ getPendingQuestion(chatId) {
2437
+ return this.pendingQuestions.get(chatId);
2438
+ }
2439
+ async parsePostContent(content, messageId) {
2440
+ let text = "";
2441
+ const images = [];
2442
+ try {
2443
+ const parsed = JSON.parse(content);
2444
+ let postContent = parsed.content;
2445
+ if (!postContent && Array.isArray(parsed)) {
2446
+ postContent = parsed;
2447
+ }
2448
+ if (!postContent && parsed.zh_cn?.content) {
2449
+ postContent = parsed.zh_cn.content;
2450
+ }
2451
+ if (!postContent && parsed.en_us?.content) {
2452
+ postContent = parsed.en_us.content;
2453
+ }
2454
+ if (!postContent) {
2455
+ logger.warn("富文本消息格式异常", { content: content.substring(0, 200) });
2456
+ return { text, images };
2457
+ }
2458
+ for (const paragraph of postContent) {
2459
+ if (!Array.isArray(paragraph))
2460
+ continue;
2461
+ for (const element of paragraph) {
2462
+ if (element.tag === "text") {
2463
+ text += element.text || "";
2464
+ } else if (element.tag === "img") {
2465
+ const imageKey = element.image_key;
2466
+ if (imageKey) {
2467
+ const imageData = await this.feishuClient.getMessageImage(messageId, imageKey);
2468
+ if (imageData) {
2469
+ images.push({
2470
+ data: imageData.data,
2471
+ mimeType: imageData.mimeType,
2472
+ filename: `${imageKey}.${imageData.mimeType.split("/")[1] || "png"}`
2473
+ });
2474
+ }
2475
+ }
2476
+ }
2477
+ }
2478
+ text += `
2479
+ `;
2480
+ }
2481
+ text = text.trim();
2482
+ } catch (e) {
2483
+ logger.error("解析富文本消息失败", { error: e });
2484
+ }
2485
+ return { text, images };
2486
+ }
2487
+ async getOrCreateSession(chatId) {
2488
+ const existingSession = this.db.getSession(chatId);
2489
+ if (existingSession) {
2490
+ return existingSession.session_id;
2491
+ }
2492
+ const projectPath = this.db.getProjectPath(chatId) ?? this.config.defaultProjectPath;
2493
+ const sessionId = await this.opencodeClient.createSession();
2494
+ this.db.upsertSession(chatId, sessionId, projectPath);
2495
+ return sessionId;
2496
+ }
2497
+ cleanupSession(chatId) {
2498
+ const session = this.activeSessions.get(chatId);
2499
+ if (session) {
2500
+ if (session.unsubscribe) {
2501
+ session.unsubscribe();
2502
+ }
2503
+ this.activeSessions.delete(chatId);
2504
+ }
2505
+ }
2506
+ isUserAuthorized(userId) {
2507
+ if (this.config.allowAllUsers) {
2508
+ return true;
2509
+ }
2510
+ if (this.config.adminUserIds.includes(userId)) {
2511
+ return true;
2512
+ }
2513
+ return this.db.isUserWhitelisted(userId);
2514
+ }
2515
+ async sendUnauthorizedMessage(chatId) {
2516
+ await this.feishuClient.sendTextMessage(chatId, "你没有权限使用此机器人。请联系管理员申请访问权限。");
2517
+ }
2518
+ async createNewSession(chatId) {
2519
+ this.cleanupSession(chatId);
2520
+ this.db.deleteSession(chatId);
2521
+ const projectPath = this.db.getProjectPath(chatId) ?? this.config.defaultProjectPath;
2522
+ const sessionId = await this.opencodeClient.createSession();
2523
+ this.db.upsertSession(chatId, sessionId, projectPath);
2524
+ return sessionId;
2525
+ }
2526
+ async switchProject(chatId, projectPath) {
2527
+ this.db.setProjectPath(chatId, projectPath);
2528
+ this.db.deleteSession(chatId);
2529
+ this.cleanupSession(chatId);
2530
+ }
2531
+ async abortCurrentSession(chatId) {
2532
+ const session = this.activeSessions.get(chatId);
2533
+ if (!session) {
2534
+ return false;
2535
+ }
2536
+ const result = await this.opencodeClient.abortSession(session.sessionId);
2537
+ if (result) {
2538
+ this.cleanupSession(chatId);
2539
+ }
2540
+ return result;
2541
+ }
2542
+ isAdmin(userId) {
2543
+ return this.config.adminUserIds.includes(userId);
2544
+ }
2545
+ getActiveSessionCount() {
2546
+ return this.activeSessions.size;
2547
+ }
2548
+ getDefaultProjectPath() {
2549
+ return this.config.defaultProjectPath;
2550
+ }
2551
+ async handleMessageRecall(userMessageId) {
2552
+ const mapping = this.db.getMessageMapping(userMessageId);
2553
+ if (!mapping) {
2554
+ return { aborted: false, botMessagesDeleted: 0 };
2555
+ }
2556
+ const { chat_id: chatId } = mapping;
2557
+ const messagesToRecall = this.db.getMessageMappingsAfter(userMessageId, chatId);
2558
+ let aborted = false;
2559
+ const activeSession = this.activeSessions.get(chatId);
2560
+ if (activeSession) {
2561
+ aborted = await this.opencodeClient.abortSession(activeSession.sessionId);
2562
+ this.cleanupSession(chatId);
2563
+ }
2564
+ let botMessagesDeleted = 0;
2565
+ for (const msg of messagesToRecall) {
2566
+ if (msg.bot_message_id) {
2567
+ const deleted = await this.feishuClient.deleteMessage(msg.bot_message_id);
2568
+ if (deleted)
2569
+ botMessagesDeleted++;
2570
+ }
2571
+ }
2572
+ const userMessageIds = messagesToRecall.map((m) => m.user_message_id);
2573
+ this.db.deleteMessageMappings(userMessageIds);
2574
+ logger.info("处理消息撤回", {
2575
+ userMessageId,
2576
+ aborted,
2577
+ botMessagesDeleted,
2578
+ totalMessagesRecalled: messagesToRecall.length
2579
+ });
2580
+ return { aborted, botMessagesDeleted };
2581
+ }
2582
+ cleanupChatData(chatId) {
2583
+ this.cleanupSession(chatId);
2584
+ this.db.deleteSession(chatId);
2585
+ this.db.deleteMessageMappingsByChatId(chatId);
2586
+ logger.info("清理聊天数据", { chatId });
2587
+ }
2588
+ async getOrCreateUserSession(userId) {
2589
+ const chatId = `user:${userId}`;
2590
+ const existingSession = this.db.getSession(chatId);
2591
+ if (existingSession) {
2592
+ return existingSession.session_id;
2593
+ }
2594
+ const projectPath = this.db.getProjectPath(chatId) ?? this.config.defaultProjectPath;
2595
+ const sessionId = await this.opencodeClient.createSession();
2596
+ this.db.upsertSession(chatId, sessionId, projectPath);
2597
+ return sessionId;
2598
+ }
2599
+ async createNewUserSession(userId) {
2600
+ const chatId = `user:${userId}`;
2601
+ this.cleanupSession(chatId);
2602
+ this.db.deleteSession(chatId);
2603
+ const projectPath = this.db.getProjectPath(chatId) ?? this.config.defaultProjectPath;
2604
+ const sessionId = await this.opencodeClient.createSession();
2605
+ this.db.upsertSession(chatId, sessionId, projectPath);
2606
+ return sessionId;
2607
+ }
2608
+ async getUserSessionInfo(userId) {
2609
+ const chatId = `user:${userId}`;
2610
+ const session = this.db.getSession(chatId);
2611
+ if (!session) {
2612
+ return null;
2613
+ }
2614
+ const isActive = this.activeSessions.has(chatId);
2615
+ return {
2616
+ sessionId: session.session_id,
2617
+ projectPath: session.project_path,
2618
+ isActive
2619
+ };
2620
+ }
2621
+ async switchUserProject(userId, projectPath) {
2622
+ const chatId = `user:${userId}`;
2623
+ this.db.setProjectPath(chatId, projectPath);
2624
+ this.db.deleteSession(chatId);
2625
+ this.cleanupSession(chatId);
2626
+ }
2627
+ async createSessionChat(userId, projectPath) {
2628
+ const sessionId = await this.opencodeClient.createSession();
2629
+ const shortId = sessionId.replace(/^ses_/, "").slice(0, 8);
2630
+ const chatName = `o-${shortId}`;
2631
+ const result = await this.feishuClient.createChat(chatName, [userId]);
2632
+ if (!result) {
2633
+ return null;
2634
+ }
2635
+ try {
2636
+ this.db.createSessionChat(result.chatId, sessionId, userId, projectPath);
2637
+ logger.info("创建会话群", { chatId: result.chatId, sessionId, userId });
2638
+ } catch (e) {
2639
+ logger.error("保存会话群到数据库失败", e);
2640
+ return null;
2641
+ }
2642
+ return { chatId: result.chatId, sessionId };
2643
+ }
2644
+ async cleanupSessionChat(chatId) {
2645
+ const sessionChat = this.db.getSessionChat(chatId);
2646
+ if (!sessionChat) {
2647
+ return;
2648
+ }
2649
+ this.cleanupSession(chatId);
2650
+ this.db.deleteSessionChat(chatId);
2651
+ this.db.deleteMessageMappingsByChatId(chatId);
2652
+ logger.info("清理会话群", { chatId, sessionId: sessionChat.session_id });
2653
+ }
2654
+ async handleUserLeftSessionChat(chatId) {
2655
+ const sessionChat = this.db.getSessionChat(chatId);
2656
+ if (!sessionChat)
2657
+ return;
2658
+ this.cleanupSession(chatId);
2659
+ this.db.deleteSessionChat(chatId);
2660
+ this.db.deleteMessageMappingsByChatId(chatId);
2661
+ this.pendingQuestions.delete(chatId);
2662
+ const deleted = await this.feishuClient.deleteChat(chatId);
2663
+ if (!deleted) {
2664
+ logger.debug("群聊可能已自动删除", { chatId });
2665
+ }
2666
+ logger.info("用户退出,清理会话群", { chatId, sessionId: sessionChat.session_id });
2667
+ }
2668
+ getSessionChat(chatId) {
2669
+ return this.db.getSessionChat(chatId);
2670
+ }
2671
+ getUserSessionChats(userId) {
2672
+ return this.db.getSessionChatsByOwner(userId);
2673
+ }
2674
+ cleanup() {
2675
+ for (const chatId of this.activeSessions.keys()) {
2676
+ this.cleanupSession(chatId);
2677
+ }
2678
+ this.messageQueue.clear();
2679
+ }
2680
+ }
2681
+ function createSessionManager(db, feishuClient, opencodeClient, config) {
2682
+ return new SessionManager(db, feishuClient, opencodeClient, config);
2683
+ }
2684
+
2685
+ // src/feishu/menu.ts
2686
+ function createHeader2(title, color = "blue") {
2687
+ return {
2688
+ template: color,
2689
+ title: { tag: "plain_text", content: title }
2690
+ };
2691
+ }
2692
+ function createDivider2() {
2693
+ return { tag: "hr" };
2694
+ }
2695
+ function createMarkdown2(content) {
2696
+ return {
2697
+ tag: "markdown",
2698
+ content
2699
+ };
2700
+ }
2701
+ function createProjectSelectCard(projects, description, currentProject) {
2702
+ const elements = [];
2703
+ if (projects.length === 0) {
2704
+ elements.push(createMarkdown2("*暂无配置项目,请在 .env 中配置 PROJECTS*"));
2705
+ return {
2706
+ config: { wide_screen_mode: true },
2707
+ header: createHeader2("\uD83C\uDD95 新建会话"),
2708
+ elements
2709
+ };
2710
+ }
2711
+ if (description) {
2712
+ elements.push(createMarkdown2(description));
2713
+ elements.push(createDivider2());
2714
+ }
2715
+ const projectList = projects.map((p, i) => `**${i + 1}.** \`${p.name}\` - ${p.path}`).join(`
2716
+ `);
2717
+ elements.push(createMarkdown2(`**可用项目:**
2718
+ ` + projectList));
2719
+ elements.push(createDivider2());
2720
+ elements.push(createMarkdown2("发送指令创建会话:\n`/new <编号>` 例如:`/new 1`"));
2721
+ if (currentProject) {
2722
+ elements.push(createDivider2());
2723
+ elements.push(createMarkdown2(`当前项目:\`${currentProject}\``));
2724
+ }
2725
+ return {
2726
+ config: { wide_screen_mode: true },
2727
+ header: createHeader2("\uD83C\uDD95 新建会话"),
2728
+ elements
2729
+ };
2730
+ }
2731
+ function createModelSelectCard(models, currentModel) {
2732
+ const elements = [];
2733
+ if (models.length === 0) {
2734
+ elements.push(createMarkdown2("*暂无可用模型*"));
2735
+ return {
2736
+ config: { wide_screen_mode: true },
2737
+ header: createHeader2("\uD83D\uDD04 切换模型"),
2738
+ elements
2739
+ };
2740
+ }
2741
+ if (currentModel) {
2742
+ elements.push(createMarkdown2(`当前模型:\`${currentModel}\``));
2743
+ }
2744
+ const options = models.map((m) => ({
2745
+ text: { tag: "plain_text", content: m.name },
2746
+ value: m.id
2747
+ }));
2748
+ elements.push({
2749
+ tag: "action",
2750
+ actions: [
2751
+ {
2752
+ tag: "select_static",
2753
+ placeholder: { tag: "plain_text", content: "选择模型" },
2754
+ value: { action: "switch_model" },
2755
+ options
2756
+ }
2757
+ ]
2758
+ });
2759
+ return {
2760
+ config: { wide_screen_mode: true },
2761
+ header: createHeader2("\uD83D\uDD04 切换模型"),
2762
+ elements
2763
+ };
2764
+ }
2765
+ function createStatusCard(status) {
2766
+ const lines = [
2767
+ `**会话 ID**: \`${status.sessionId}\``,
2768
+ `**项目路径**: \`${status.projectPath}\``,
2769
+ status.model ? `**当前模型**: \`${status.model}\`` : null,
2770
+ status.messageCount !== undefined ? `**消息数量**: ${status.messageCount}` : null,
2771
+ `**状态**: ${status.isActive ? "\uD83D\uDFE2 活跃" : "⚪ 空闲"}`
2772
+ ].filter(Boolean);
2773
+ return {
2774
+ config: { wide_screen_mode: true },
2775
+ header: createHeader2("\uD83D\uDCCA 会话状态", "green"),
2776
+ elements: [createMarkdown2(lines.join(`
2777
+ `))]
2778
+ };
2779
+ }
2780
+ function createSuccessCard(title, message) {
2781
+ return {
2782
+ config: { wide_screen_mode: true },
2783
+ header: createHeader2(title, "green"),
2784
+ elements: [createMarkdown2(message)]
2785
+ };
2786
+ }
2787
+ function createSessionChatCreatedCard(chatId, sessionId, projectPath) {
2788
+ const chatName = `o-${sessionId}`;
2789
+ const chatLink = `https://applink.feishu.cn/client/chat/open?openChatId=${chatId}`;
2790
+ return {
2791
+ config: { wide_screen_mode: true },
2792
+ header: createHeader2("✅ 会话群已创建", "green"),
2793
+ elements: [
2794
+ createMarkdown2(`**群名称**: ${chatName}
2795
+ **项目**: \`${projectPath}\``),
2796
+ {
2797
+ tag: "action",
2798
+ actions: [
2799
+ {
2800
+ tag: "button",
2801
+ text: { tag: "plain_text", content: "进入会话群" },
2802
+ type: "primary",
2803
+ url: chatLink
2804
+ }
2805
+ ]
2806
+ }
2807
+ ]
2808
+ };
2809
+ }
2810
+ function createErrorCard(title, message) {
2811
+ return {
2812
+ config: { wide_screen_mode: true },
2813
+ header: createHeader2(title, "red"),
2814
+ elements: [createMarkdown2(message)]
2815
+ };
2816
+ }
2817
+ function createSessionChatWelcomeCard(info) {
2818
+ const shortSessionId = info.sessionId.replace(/^ses_/, "").slice(0, 8);
2819
+ const elements = [];
2820
+ elements.push(createMarkdown2(`**当前状态**
2821
+ ` + `- \uD83D\uDCC1 工作目录:\`${info.projectPath}\`
2822
+ ` + `- \uD83D\uDD11 会话 ID:\`${shortSessionId}\``));
2823
+ elements.push(createDivider2());
2824
+ const actions = [];
2825
+ if (info.projects.length > 0) {
2826
+ const projectOptions = info.projects.map((p) => ({
2827
+ text: { tag: "plain_text", content: `${p.name}` },
2828
+ value: p.path
2829
+ }));
2830
+ actions.push({
2831
+ tag: "select_static",
2832
+ placeholder: { tag: "plain_text", content: "\uD83D\uDCC2 切换项目" },
2833
+ value: { action: "switch_project_in_chat", chatId: info.chatId },
2834
+ options: projectOptions
2835
+ });
2836
+ }
2837
+ if (info.models.length > 0) {
2838
+ const modelOptions = info.models.map((m) => ({
2839
+ text: { tag: "plain_text", content: m.name },
2840
+ value: m.id
2841
+ }));
2842
+ actions.push({
2843
+ tag: "select_static",
2844
+ placeholder: { tag: "plain_text", content: info.currentModel ? `\uD83E\uDD16 ${info.currentModel}` : "\uD83E\uDD16 切换模型" },
2845
+ value: { action: "switch_model", chatId: info.chatId },
2846
+ options: modelOptions
2847
+ });
2848
+ }
2849
+ if (actions.length > 0) {
2850
+ elements.push({
2851
+ tag: "action",
2852
+ actions
2853
+ });
2854
+ }
2855
+ if (info.projects.length === 0) {
2856
+ elements.push(createMarkdown2("\uD83D\uDCA1 切换目录:`/switch_project <路径>`"));
2857
+ }
2858
+ elements.push(createDivider2());
2859
+ elements.push({
2860
+ tag: "note",
2861
+ elements: [{ tag: "plain_text", content: "⚡ 发送任何消息即可开始对话" }]
2862
+ });
2863
+ return {
2864
+ config: { wide_screen_mode: true },
2865
+ header: createHeader2("\uD83C\uDF89 会话群已就绪", "green"),
2866
+ elements
2867
+ };
2868
+ }
2869
+ function createProjectSwitchedCard(projectName, projectPath, sessionId) {
2870
+ const shortSessionId = sessionId.replace(/^ses_/, "").slice(0, 8);
2871
+ return {
2872
+ config: { wide_screen_mode: true },
2873
+ header: createHeader2("✅ 项目已切换", "green"),
2874
+ elements: [
2875
+ createMarkdown2(`**${projectName}**
2876
+ ` + `- \uD83D\uDCC1 路径:\`${projectPath}\`
2877
+ ` + `- \uD83D\uDD11 新会话:\`${shortSessionId}\``)
2878
+ ]
2879
+ };
2880
+ }
2881
+
2882
+ // src/commands/parser.ts
2883
+ var COMMAND_PREFIX = "/";
2884
+ var COMMANDS = {
2885
+ new: {
2886
+ name: "new",
2887
+ description: "创建新会话(从预配置项目中选择)",
2888
+ usage: "/new <项目编号>",
2889
+ adminOnly: false
2890
+ },
2891
+ model: {
2892
+ name: "model",
2893
+ description: "切换 AI 模型",
2894
+ usage: "/model <编号或模型ID>",
2895
+ adminOnly: false
2896
+ },
2897
+ compact: {
2898
+ name: "compact",
2899
+ description: "压缩当前会话上下文",
2900
+ usage: "/compact",
2901
+ adminOnly: false
2902
+ },
2903
+ clear: {
2904
+ name: "clear",
2905
+ description: "清除历史,创建新会话",
2906
+ usage: "/clear",
2907
+ adminOnly: false
2908
+ },
2909
+ switch_project: {
2910
+ name: "switch_project",
2911
+ description: "切换到不同的项目目录",
2912
+ usage: "/switch_project <路径>",
2913
+ adminOnly: false
2914
+ },
2915
+ new_session: {
2916
+ name: "new_session",
2917
+ description: "创建新的 OpenCode 会话",
2918
+ usage: "/new_session",
2919
+ adminOnly: false
2920
+ },
2921
+ help: {
2922
+ name: "help",
2923
+ description: "显示可用命令",
2924
+ usage: "/help",
2925
+ adminOnly: false
2926
+ },
2927
+ abort: {
2928
+ name: "abort",
2929
+ description: "中止当前运行的任务",
2930
+ usage: "/abort",
2931
+ adminOnly: false
2932
+ },
2933
+ status: {
2934
+ name: "status",
2935
+ description: "显示当前会话状态",
2936
+ usage: "/status",
2937
+ adminOnly: false
2938
+ },
2939
+ whitelist_add: {
2940
+ name: "whitelist_add",
2941
+ description: "将用户添加到白名单",
2942
+ usage: "/whitelist_add <用户ID>",
2943
+ adminOnly: true
2944
+ },
2945
+ whitelist_remove: {
2946
+ name: "whitelist_remove",
2947
+ description: "从白名单移除用户",
2948
+ usage: "/whitelist_remove <用户ID>",
2949
+ adminOnly: true
2950
+ },
2951
+ whitelist_list: {
2952
+ name: "whitelist_list",
2953
+ description: "列出所有白名单用户",
2954
+ usage: "/whitelist_list",
2955
+ adminOnly: true
2956
+ }
2957
+ };
2958
+ function isCommand(text) {
2959
+ return text.trim().startsWith(COMMAND_PREFIX);
2960
+ }
2961
+ function parseCommand(text) {
2962
+ const trimmed = text.trim();
2963
+ if (!trimmed.startsWith(COMMAND_PREFIX)) {
2964
+ return null;
2965
+ }
2966
+ const withoutPrefix = trimmed.slice(COMMAND_PREFIX.length);
2967
+ const parts = withoutPrefix.split(/\s+/);
2968
+ const command = parts[0]?.toLowerCase() ?? "";
2969
+ const args = parts.slice(1);
2970
+ const rawArgs = withoutPrefix.slice(command.length).trim();
2971
+ if (!command) {
2972
+ return null;
2973
+ }
2974
+ return {
2975
+ command,
2976
+ args,
2977
+ rawArgs
2978
+ };
2979
+ }
2980
+ function getCommand(name) {
2981
+ return COMMANDS[name] ?? null;
2982
+ }
2983
+ function getAvailableCommands(isAdmin) {
2984
+ return Object.values(COMMANDS).filter((cmd) => !cmd.adminOnly || isAdmin);
2985
+ }
2986
+ function formatHelpMessage(isAdmin) {
2987
+ const commands = getAvailableCommands(isAdmin);
2988
+ let message = `**可用命令:**
2989
+
2990
+ `;
2991
+ for (const cmd of commands) {
2992
+ message += `\`${cmd.usage}\`
2993
+ ${cmd.description}
2994
+
2995
+ `;
2996
+ }
2997
+ return message;
2998
+ }
2999
+ function formatCommandError(message) {
3000
+ return `**命令错误:** ${message}`;
3001
+ }
3002
+ function formatCommandSuccess(message) {
3003
+ return `**成功:** ${message}`;
3004
+ }
3005
+
3006
+ // src/commands/handler.ts
3007
+ class CommandHandler {
3008
+ db;
3009
+ feishuClient;
3010
+ sessionManager;
3011
+ opencodeClient;
3012
+ projects;
3013
+ availableModels;
3014
+ cachedModels = null;
3015
+ constructor(db, feishuClient, sessionManager, opencodeClient, config) {
3016
+ this.db = db;
3017
+ this.feishuClient = feishuClient;
3018
+ this.sessionManager = sessionManager;
3019
+ this.opencodeClient = opencodeClient;
3020
+ this.projects = config.projects;
3021
+ this.availableModels = config.availableModels;
3022
+ }
3023
+ async getModels() {
3024
+ if (!this.cachedModels) {
3025
+ this.cachedModels = await this.opencodeClient.listModels();
3026
+ }
3027
+ const allModels = this.cachedModels.map((m) => ({ id: m.id, name: m.name }));
3028
+ return filterModels(allModels, this.availableModels);
3029
+ }
3030
+ async handleIfCommand(text, context) {
3031
+ if (!isCommand(text)) {
3032
+ return { success: true, message: "", handled: false };
3033
+ }
3034
+ const parsed = parseCommand(text);
3035
+ if (!parsed) {
3036
+ return {
3037
+ success: false,
3038
+ message: formatCommandError("无效的命令格式"),
3039
+ handled: true
3040
+ };
3041
+ }
3042
+ const command = getCommand(parsed.command);
3043
+ if (!command) {
3044
+ return {
3045
+ success: false,
3046
+ message: formatCommandError(`未知命令:${parsed.command}`),
3047
+ handled: true
3048
+ };
3049
+ }
3050
+ if (command.adminOnly && !context.isAdmin) {
3051
+ return {
3052
+ success: false,
3053
+ message: formatCommandError("此命令需要管理员权限"),
3054
+ handled: true
3055
+ };
3056
+ }
3057
+ const result = await this.executeCommand(parsed.command, parsed.args, parsed.rawArgs, context);
3058
+ if (result.message) {
3059
+ await this.feishuClient.sendTextMessage(context.chatId, result.message);
3060
+ }
3061
+ return result;
3062
+ }
3063
+ async executeCommand(command, args, rawArgs, context) {
3064
+ try {
3065
+ switch (command) {
3066
+ case "help":
3067
+ return this.handleHelp(context);
3068
+ case "switch_project":
3069
+ return await this.handleSwitchProject(rawArgs, context);
3070
+ case "new_session":
3071
+ return await this.handleNewSession(context);
3072
+ case "abort":
3073
+ return await this.handleAbort(context);
3074
+ case "status":
3075
+ return await this.handleStatus(context);
3076
+ case "new":
3077
+ return await this.handleNew(args, context);
3078
+ case "model":
3079
+ return await this.handleModel(args, rawArgs, context);
3080
+ case "compact":
3081
+ return await this.handleCompact(context);
3082
+ case "clear":
3083
+ return await this.handleClear(context);
3084
+ case "whitelist_add":
3085
+ return await this.handleWhitelistAdd(args, context);
3086
+ case "whitelist_remove":
3087
+ return await this.handleWhitelistRemove(args, context);
3088
+ case "whitelist_list":
3089
+ return await this.handleWhitelistList(context);
3090
+ default:
3091
+ return {
3092
+ success: false,
3093
+ message: formatCommandError(`命令未实现:${command}`),
3094
+ handled: true
3095
+ };
3096
+ }
3097
+ } catch (error) {
3098
+ logger.error("命令执行错误", { command, error });
3099
+ return {
3100
+ success: false,
3101
+ message: formatCommandError(error instanceof Error ? error.message : "未知错误"),
3102
+ handled: true
3103
+ };
3104
+ }
3105
+ }
3106
+ handleHelp(context) {
3107
+ const message = formatHelpMessage(context.isAdmin);
3108
+ return { success: true, message, handled: true };
3109
+ }
3110
+ async handleSwitchProject(path, context) {
3111
+ if (!path.trim()) {
3112
+ return {
3113
+ success: false,
3114
+ message: formatCommandError("请提供项目路径。用法:/switch_project <路径>"),
3115
+ handled: true
3116
+ };
3117
+ }
3118
+ await this.sessionManager.switchProject(context.chatId, path.trim());
3119
+ return {
3120
+ success: true,
3121
+ message: formatCommandSuccess(`已切换到项目:${path.trim()}
3122
+ 下次发消息时将创建新会话。`),
3123
+ handled: true
3124
+ };
3125
+ }
3126
+ async handleNewSession(context) {
3127
+ const sessionId = await this.sessionManager.createNewSession(context.chatId);
3128
+ return {
3129
+ success: true,
3130
+ message: formatCommandSuccess(`新会话已创建:${sessionId.slice(0, 8)}...`),
3131
+ handled: true
3132
+ };
3133
+ }
3134
+ async handleAbort(context) {
3135
+ const aborted = await this.sessionManager.abortCurrentSession(context.chatId);
3136
+ if (aborted) {
3137
+ return {
3138
+ success: true,
3139
+ message: formatCommandSuccess("当前任务已中止"),
3140
+ handled: true
3141
+ };
3142
+ } else {
3143
+ return {
3144
+ success: false,
3145
+ message: formatCommandError("没有正在运行的任务"),
3146
+ handled: true
3147
+ };
3148
+ }
3149
+ }
3150
+ async handleStatus(context) {
3151
+ const session = this.db.getSession(context.chatId);
3152
+ const projectPath = this.db.getProjectPath(context.chatId);
3153
+ const activeCount = this.sessionManager.getActiveSessionCount();
3154
+ let message = `**会话状态:**
3155
+
3156
+ `;
3157
+ if (session) {
3158
+ message += `会话 ID:\`${session.session_id.slice(0, 8)}...\`
3159
+ `;
3160
+ message += `项目:\`${session.project_path}\`
3161
+ `;
3162
+ message += `创建时间:${session.created_at}
3163
+ `;
3164
+ } else {
3165
+ message += `无活动会话
3166
+ `;
3167
+ }
3168
+ if (projectPath) {
3169
+ message += `
3170
+ 配置的项目:\`${projectPath}\`
3171
+ `;
3172
+ }
3173
+ message += `
3174
+ 活动会话数(所有用户):${activeCount}`;
3175
+ return { success: true, message, handled: true };
3176
+ }
3177
+ async handleWhitelistAdd(args, context) {
3178
+ const userId = args[0];
3179
+ if (!userId) {
3180
+ return {
3181
+ success: false,
3182
+ message: formatCommandError("请提供用户 ID。用法:/whitelist_add <用户ID>"),
3183
+ handled: true
3184
+ };
3185
+ }
3186
+ const added = this.db.addToWhitelist(userId, context.userId);
3187
+ if (added) {
3188
+ return {
3189
+ success: true,
3190
+ message: formatCommandSuccess(`用户 ${userId} 已添加到白名单`),
3191
+ handled: true
3192
+ };
3193
+ } else {
3194
+ return {
3195
+ success: false,
3196
+ message: formatCommandError("用户已在白名单中"),
3197
+ handled: true
3198
+ };
3199
+ }
3200
+ }
3201
+ async handleWhitelistRemove(args, context) {
3202
+ const userId = args[0];
3203
+ if (!userId) {
3204
+ return {
3205
+ success: false,
3206
+ message: formatCommandError("请提供用户 ID。用法:/whitelist_remove <用户ID>"),
3207
+ handled: true
3208
+ };
3209
+ }
3210
+ const removed = this.db.removeFromWhitelist(userId);
3211
+ if (removed) {
3212
+ return {
3213
+ success: true,
3214
+ message: formatCommandSuccess(`用户 ${userId} 已从白名单移除`),
3215
+ handled: true
3216
+ };
3217
+ } else {
3218
+ return {
3219
+ success: false,
3220
+ message: formatCommandError("用户不在白名单中"),
3221
+ handled: true
3222
+ };
3223
+ }
3224
+ }
3225
+ async handleWhitelistList(context) {
3226
+ const users = this.db.getWhitelistedUsers();
3227
+ if (users.length === 0) {
3228
+ return {
3229
+ success: true,
3230
+ message: "**白名单:** 无用户",
3231
+ handled: true
3232
+ };
3233
+ }
3234
+ let message = `**白名单用户:**
3235
+
3236
+ `;
3237
+ for (const user of users) {
3238
+ message += `- \`${user.user_id}\`(由 ${user.added_by} 于 ${user.added_at} 添加)
3239
+ `;
3240
+ }
3241
+ return { success: true, message, handled: true };
3242
+ }
3243
+ async handleNew(args, context) {
3244
+ if (this.projects.length === 0) {
3245
+ const sessionChat2 = this.sessionManager.getSessionChat(context.chatId);
3246
+ if (sessionChat2) {
3247
+ const sessionId = await this.sessionManager.createNewSession(context.chatId);
3248
+ return {
3249
+ success: true,
3250
+ message: formatCommandSuccess(`已创建新会话
3251
+ 会话 ID:${sessionId.slice(0, 8)}...`),
3252
+ handled: true
3253
+ };
3254
+ }
3255
+ const loadingMsgId2 = await this.feishuClient.sendTextMessage(context.chatId, "正在创建会话群,请稍候...");
3256
+ const defaultPath = this.sessionManager.getDefaultProjectPath();
3257
+ const result2 = await this.sessionManager.createSessionChat(context.userId, defaultPath);
3258
+ if (loadingMsgId2) {
3259
+ await this.feishuClient.deleteMessage(loadingMsgId2);
3260
+ }
3261
+ if (!result2) {
3262
+ return {
3263
+ success: false,
3264
+ message: formatCommandError("创建会话群失败,请检查机器人权限"),
3265
+ handled: true
3266
+ };
3267
+ }
3268
+ const welcomeCard2 = createSessionChatWelcomeCard({
3269
+ sessionId: result2.sessionId,
3270
+ projectPath: defaultPath,
3271
+ projects: this.projects,
3272
+ chatId: result2.chatId,
3273
+ models: await this.getModels()
3274
+ });
3275
+ await this.feishuClient.sendCard(result2.chatId, welcomeCard2);
3276
+ const card2 = createSessionChatCreatedCard(result2.chatId, result2.sessionId, defaultPath);
3277
+ await this.feishuClient.sendCard(context.chatId, card2);
3278
+ return {
3279
+ success: true,
3280
+ message: "",
3281
+ handled: true
3282
+ };
3283
+ }
3284
+ const indexStr = args[0];
3285
+ if (!indexStr) {
3286
+ const projectList = this.projects.map((p, i) => `${i + 1}. ${p.name} - ${p.path}`).join(`
3287
+ `);
3288
+ return {
3289
+ success: false,
3290
+ message: formatCommandError(`请提供项目编号。
3291
+
3292
+ 可用项目:
3293
+ ${projectList}
3294
+
3295
+ 用法:/new <编号>`),
3296
+ handled: true
3297
+ };
3298
+ }
3299
+ const index = parseInt(indexStr, 10);
3300
+ if (isNaN(index) || index < 1 || index > this.projects.length) {
3301
+ return {
3302
+ success: false,
3303
+ message: formatCommandError(`无效的项目编号。请输入 1-${this.projects.length} 之间的数字`),
3304
+ handled: true
3305
+ };
3306
+ }
3307
+ const project = this.projects[index - 1];
3308
+ if (!project) {
3309
+ return {
3310
+ success: false,
3311
+ message: formatCommandError(`项目不存在`),
3312
+ handled: true
3313
+ };
3314
+ }
3315
+ const sessionChat = this.sessionManager.getSessionChat(context.chatId);
3316
+ if (sessionChat) {
3317
+ await this.sessionManager.switchProject(context.chatId, project.path);
3318
+ const sessionId = await this.sessionManager.createNewSession(context.chatId);
3319
+ return {
3320
+ success: true,
3321
+ message: formatCommandSuccess(`已切换到项目 "${project.name}"
3322
+ 会话 ID:${sessionId.slice(0, 8)}...`),
3323
+ handled: true
3324
+ };
3325
+ }
3326
+ const loadingMsgId = await this.feishuClient.sendTextMessage(context.chatId, "正在创建会话群,请稍候...");
3327
+ const result = await this.sessionManager.createSessionChat(context.userId, project.path);
3328
+ if (loadingMsgId) {
3329
+ await this.feishuClient.deleteMessage(loadingMsgId);
3330
+ }
3331
+ if (!result) {
3332
+ return {
3333
+ success: false,
3334
+ message: formatCommandError("创建会话群失败,请检查机器人权限"),
3335
+ handled: true
3336
+ };
3337
+ }
3338
+ const welcomeCard = createSessionChatWelcomeCard({
3339
+ sessionId: result.sessionId,
3340
+ projectPath: project.path,
3341
+ projects: this.projects,
3342
+ chatId: result.chatId,
3343
+ models: await this.getModels()
3344
+ });
3345
+ await this.feishuClient.sendCard(result.chatId, welcomeCard);
3346
+ const card = createSessionChatCreatedCard(result.chatId, result.sessionId, project.path);
3347
+ await this.feishuClient.sendCard(context.chatId, card);
3348
+ return {
3349
+ success: true,
3350
+ message: "",
3351
+ handled: true
3352
+ };
3353
+ }
3354
+ async handleModel(args, rawArgs, context) {
3355
+ const models = await this.getModels();
3356
+ if (models.length === 0) {
3357
+ return {
3358
+ success: false,
3359
+ message: formatCommandError("没有可用的模型"),
3360
+ handled: true
3361
+ };
3362
+ }
3363
+ const arg = rawArgs.trim();
3364
+ if (!arg) {
3365
+ const modelList = models.map((m, i) => `${i + 1}. ${m.name} - ${m.id}`).join(`
3366
+ `);
3367
+ return {
3368
+ success: false,
3369
+ message: formatCommandError(`请提供模型编号或 ID。
3370
+
3371
+ 可用模型:
3372
+ ${modelList}
3373
+
3374
+ 用法:/model <编号或ID>`),
3375
+ handled: true
3376
+ };
3377
+ }
3378
+ let selectedModel;
3379
+ const index = parseInt(arg, 10);
3380
+ if (!isNaN(index) && index >= 1 && index <= models.length) {
3381
+ selectedModel = models[index - 1];
3382
+ } else {
3383
+ selectedModel = models.find((m) => m.id === arg || m.id.endsWith(`/${arg}`));
3384
+ }
3385
+ if (!selectedModel) {
3386
+ return {
3387
+ success: false,
3388
+ message: formatCommandError(`未找到模型:${arg}`),
3389
+ handled: true
3390
+ };
3391
+ }
3392
+ const session = this.db.getSession(context.chatId);
3393
+ if (!session) {
3394
+ return {
3395
+ success: false,
3396
+ message: formatCommandError("请先发送消息创建会话"),
3397
+ handled: true
3398
+ };
3399
+ }
3400
+ const success = await this.opencodeClient.executeCommand(session.session_id, "model", selectedModel.id);
3401
+ if (success) {
3402
+ return {
3403
+ success: true,
3404
+ message: formatCommandSuccess(`已切换到模型:${selectedModel.name}`),
3405
+ handled: true
3406
+ };
3407
+ } else {
3408
+ return {
3409
+ success: false,
3410
+ message: formatCommandError("切换模型失败"),
3411
+ handled: true
3412
+ };
3413
+ }
3414
+ }
3415
+ async handleCompact(context) {
3416
+ const session = this.db.getSession(context.chatId);
3417
+ if (!session) {
3418
+ return {
3419
+ success: false,
3420
+ message: formatCommandError("没有活动会话"),
3421
+ handled: true
3422
+ };
3423
+ }
3424
+ const success = await this.opencodeClient.executeCommand(session.session_id, "compact");
3425
+ if (success) {
3426
+ return {
3427
+ success: true,
3428
+ message: formatCommandSuccess("上下文已压缩"),
3429
+ handled: true
3430
+ };
3431
+ } else {
3432
+ return {
3433
+ success: false,
3434
+ message: formatCommandError("压缩上下文失败"),
3435
+ handled: true
3436
+ };
3437
+ }
3438
+ }
3439
+ async handleClear(context) {
3440
+ const sessionId = await this.sessionManager.createNewSession(context.chatId);
3441
+ return {
3442
+ success: true,
3443
+ message: formatCommandSuccess(`历史已清除,新会话:${sessionId.slice(0, 8)}...`),
3444
+ handled: true
3445
+ };
3446
+ }
3447
+ }
3448
+ function createCommandHandler(db, feishuClient, sessionManager, opencodeClient, config) {
3449
+ return new CommandHandler(db, feishuClient, sessionManager, opencodeClient, config);
3450
+ }
3451
+
3452
+ // src/utils/reconnect.ts
3453
+ var DEFAULT_CONFIG = {
3454
+ maxRetries: 10,
3455
+ initialDelayMs: 1000,
3456
+ maxDelayMs: 30000,
3457
+ backoffMultiplier: 2
3458
+ };
3459
+
3460
+ class ReconnectionManager {
3461
+ config;
3462
+ retryCount = 0;
3463
+ isConnected = false;
3464
+ shouldReconnect = true;
3465
+ connectFn;
3466
+ disconnectFn;
3467
+ reconnectTimeout = null;
3468
+ constructor(connectFn, disconnectFn, config) {
3469
+ this.connectFn = connectFn;
3470
+ this.disconnectFn = disconnectFn;
3471
+ this.config = { ...DEFAULT_CONFIG, ...config };
3472
+ }
3473
+ async connect() {
3474
+ this.shouldReconnect = true;
3475
+ await this.attemptConnection();
3476
+ }
3477
+ async attemptConnection() {
3478
+ try {
3479
+ await this.connectFn();
3480
+ this.isConnected = true;
3481
+ this.retryCount = 0;
3482
+ logger.info("连接已建立");
3483
+ } catch (error) {
3484
+ this.isConnected = false;
3485
+ logger.error("连接失败", error);
3486
+ await this.scheduleReconnect();
3487
+ }
3488
+ }
3489
+ async scheduleReconnect() {
3490
+ if (!this.shouldReconnect) {
3491
+ logger.info("重连已禁用,不进行重试");
3492
+ return;
3493
+ }
3494
+ if (this.retryCount >= this.config.maxRetries) {
3495
+ logger.error("已达最大重试次数,放弃重连", { retryCount: this.retryCount });
3496
+ return;
3497
+ }
3498
+ const delay = this.calculateDelay();
3499
+ this.retryCount++;
3500
+ logger.info("计划重连", {
3501
+ retryCount: this.retryCount,
3502
+ delayMs: delay,
3503
+ maxRetries: this.config.maxRetries
3504
+ });
3505
+ this.reconnectTimeout = setTimeout(async () => {
3506
+ this.reconnectTimeout = null;
3507
+ await this.attemptConnection();
3508
+ }, delay);
3509
+ }
3510
+ calculateDelay() {
3511
+ const delay = this.config.initialDelayMs * Math.pow(this.config.backoffMultiplier, this.retryCount);
3512
+ return Math.min(delay, this.config.maxDelayMs);
3513
+ }
3514
+ async disconnect() {
3515
+ this.shouldReconnect = false;
3516
+ if (this.reconnectTimeout) {
3517
+ clearTimeout(this.reconnectTimeout);
3518
+ this.reconnectTimeout = null;
3519
+ }
3520
+ if (this.isConnected && this.disconnectFn) {
3521
+ try {
3522
+ await this.disconnectFn();
3523
+ } catch (error) {
3524
+ logger.error("断开连接时出错", error);
3525
+ }
3526
+ }
3527
+ this.isConnected = false;
3528
+ this.retryCount = 0;
3529
+ logger.info("已断开连接");
3530
+ }
3531
+ onDisconnect() {
3532
+ if (this.isConnected) {
3533
+ this.isConnected = false;
3534
+ logger.warn("连接丢失,计划重连");
3535
+ this.scheduleReconnect();
3536
+ }
3537
+ }
3538
+ getStatus() {
3539
+ return {
3540
+ isConnected: this.isConnected,
3541
+ retryCount: this.retryCount
3542
+ };
3543
+ }
3544
+ }
3545
+ function createReconnectionManager(connectFn, disconnectFn, config) {
3546
+ return new ReconnectionManager(connectFn, disconnectFn, config);
3547
+ }
3548
+
3549
+ class GlobalErrorHandler {
3550
+ static instance = null;
3551
+ handlers = [];
3552
+ static getInstance() {
3553
+ if (!GlobalErrorHandler.instance) {
3554
+ GlobalErrorHandler.instance = new GlobalErrorHandler;
3555
+ }
3556
+ return GlobalErrorHandler.instance;
3557
+ }
3558
+ register(handler) {
3559
+ this.handlers.push(handler);
3560
+ return () => {
3561
+ const index = this.handlers.indexOf(handler);
3562
+ if (index >= 0) {
3563
+ this.handlers.splice(index, 1);
3564
+ }
3565
+ };
3566
+ }
3567
+ handle(error) {
3568
+ logger.error("全局错误", error);
3569
+ for (const handler of this.handlers) {
3570
+ try {
3571
+ handler(error);
3572
+ } catch (handlerError) {
3573
+ logger.error("错误处理器出错", handlerError);
3574
+ }
3575
+ }
3576
+ }
3577
+ setupProcessHandlers() {
3578
+ process.on("uncaughtException", (error) => {
3579
+ logger.error("未捕获的异常", error);
3580
+ this.handle(error);
3581
+ });
3582
+ process.on("unhandledRejection", (reason) => {
3583
+ const error = reason instanceof Error ? reason : new Error(String(reason));
3584
+ logger.error("未处理的 Promise 拒绝", error);
3585
+ this.handle(error);
3586
+ });
3587
+ }
3588
+ }
3589
+ function setupGlobalErrorHandling() {
3590
+ const handler = GlobalErrorHandler.getInstance();
3591
+ handler.setupProcessHandlers();
3592
+ return handler;
3593
+ }
3594
+
3595
+ // src/feishu/welcome.ts
3596
+ function createWelcomeCard(info, chatName) {
3597
+ const greeting = chatName ? `已加入「${chatName}」` : "你好!";
3598
+ const elements = [
3599
+ {
3600
+ tag: "markdown",
3601
+ content: `我是 OpenCode AI 编程助手,可以帮助你完成编程任务。
3602
+
3603
+ **当前状态**
3604
+ - 工作目录:\`${info.projectPath}\`
3605
+ - 活跃会话:${info.activeSessionCount} 个
3606
+ ${info.version ? `- 版本:${info.version}` : ""}`
3607
+ },
3608
+ { tag: "hr" },
3609
+ {
3610
+ tag: "markdown",
3611
+ content: `**常用命令**
3612
+ \`/help\` - 显示帮助信息
3613
+ \`/status\` - 查看当前状态
3614
+ \`/new_session\` - 创建新会话
3615
+ \`/switch_project <路径>\` - 切换项目目录
3616
+ \`/abort\` - 中止当前任务`
3617
+ },
3618
+ { tag: "hr" },
3619
+ {
3620
+ tag: "note",
3621
+ elements: [{ tag: "plain_text", content: "直接发送消息即可开始对话" }]
3622
+ }
3623
+ ];
3624
+ return {
3625
+ config: { wide_screen_mode: true },
3626
+ header: {
3627
+ title: { tag: "plain_text", content: greeting },
3628
+ template: "blue"
3629
+ },
3630
+ elements
3631
+ };
3632
+ }
3633
+
3634
+ // src/events/handler.ts
3635
+ function setupEventHandlers(config) {
3636
+ const { feishuClient, sessionManager, opencodeClient, projects, availableModels } = config;
3637
+ const getFilteredModels = async () => {
3638
+ const allModels = await opencodeClient.listModels();
3639
+ const mapped = allModels.map((m) => ({ id: m.id, name: m.name }));
3640
+ return filterModels(mapped, availableModels);
3641
+ };
3642
+ feishuClient.onBotAdded(async (event) => {
3643
+ logger.info("机器人被添加到群聊", { chatId: event.chatId, chatName: event.chatName });
3644
+ const botInfo = {
3645
+ projectPath: sessionManager.getDefaultProjectPath(),
3646
+ activeSessionCount: sessionManager.getActiveSessionCount()
3647
+ };
3648
+ const card = createWelcomeCard(botInfo, event.chatName);
3649
+ await feishuClient.sendCard(event.chatId, card);
3650
+ });
3651
+ feishuClient.onMessageRecalled(async (event) => {
3652
+ logger.info("消息被撤回", { messageId: event.messageId, chatId: event.chatId });
3653
+ const result = await sessionManager.handleMessageRecall(event.messageId);
3654
+ if (result.aborted || result.botMessagesDeleted > 0) {
3655
+ logger.info("撤回处理完成", { ...result, messageId: event.messageId });
3656
+ }
3657
+ });
3658
+ feishuClient.onBotRemoved(async (event) => {
3659
+ logger.info("机器人被移出群聊", { chatId: event.chatId });
3660
+ const sessionChat = sessionManager.getSessionChat(event.chatId);
3661
+ if (sessionChat) {
3662
+ await sessionManager.cleanupSessionChat(event.chatId);
3663
+ } else {
3664
+ sessionManager.cleanupChatData(event.chatId);
3665
+ }
3666
+ });
3667
+ feishuClient.onUserLeftChat(async (event) => {
3668
+ logger.info("用户退出群聊", { chatId: event.chatId, users: event.users });
3669
+ await sessionManager.handleUserLeftSessionChat(event.chatId);
3670
+ });
3671
+ feishuClient.onChatDisbanded(async (event) => {
3672
+ logger.info("群聊被解散", { chatId: event.chatId });
3673
+ await sessionManager.cleanupSessionChat(event.chatId);
3674
+ });
3675
+ feishuClient.onBotMenu(async (event) => {
3676
+ logger.info("菜单点击", { eventKey: event.eventKey, operatorId: event.operatorId });
3677
+ const userId = event.operatorId;
3678
+ if (!userId) {
3679
+ logger.warn("菜单事件缺少操作者 ID");
3680
+ return;
3681
+ }
3682
+ try {
3683
+ switch (event.eventKey) {
3684
+ case "new_session": {
3685
+ if (projects.length === 0) {
3686
+ const defaultPath = sessionManager.getDefaultProjectPath();
3687
+ const loadingCard = createSuccessCard("正在创建...", "正在创建会话群,请稍候...");
3688
+ const loadingMsgId = await feishuClient.sendCardToUser(userId, loadingCard);
3689
+ const result = await sessionManager.createSessionChat(userId, defaultPath);
3690
+ if (result) {
3691
+ const models = await getFilteredModels();
3692
+ const welcomeCard = createSessionChatWelcomeCard({
3693
+ sessionId: result.sessionId,
3694
+ projectPath: defaultPath,
3695
+ projects,
3696
+ chatId: result.chatId,
3697
+ models
3698
+ });
3699
+ await feishuClient.sendCard(result.chatId, welcomeCard);
3700
+ const successCard = createSessionChatCreatedCard(result.chatId, result.sessionId, defaultPath);
3701
+ if (loadingMsgId) {
3702
+ await feishuClient.updateCard(loadingMsgId, successCard);
3703
+ } else {
3704
+ await feishuClient.sendCardToUser(userId, successCard);
3705
+ }
3706
+ } else {
3707
+ const errorCard = createErrorCard("创建失败", "创建会话群时发生错误,请稍后重试");
3708
+ if (loadingMsgId) {
3709
+ await feishuClient.updateCard(loadingMsgId, errorCard);
3710
+ } else {
3711
+ await feishuClient.sendCardToUser(userId, errorCard);
3712
+ }
3713
+ }
3714
+ break;
3715
+ }
3716
+ if (event.chatId) {
3717
+ const sessionChat = sessionManager.getSessionChat(event.chatId);
3718
+ if (sessionChat) {
3719
+ const card = createProjectSelectCard(projects, "切换项目后将创建新会话");
3720
+ await feishuClient.sendMessage(event.chatId, JSON.stringify(card), "interactive");
3721
+ } else {
3722
+ const card = createProjectSelectCard(projects);
3723
+ await feishuClient.sendCardToUser(userId, card);
3724
+ }
3725
+ } else {
3726
+ const card = createProjectSelectCard(projects);
3727
+ await feishuClient.sendCardToUser(userId, card);
3728
+ }
3729
+ break;
3730
+ }
3731
+ case "switch_model": {
3732
+ const models = await getFilteredModels();
3733
+ const card = createModelSelectCard(models);
3734
+ if (event.chatId) {
3735
+ await feishuClient.sendCard(event.chatId, card);
3736
+ } else {
3737
+ await feishuClient.sendCardToUser(userId, card);
3738
+ }
3739
+ break;
3740
+ }
3741
+ case "compact": {
3742
+ const sessionId = await sessionManager.getOrCreateUserSession(userId);
3743
+ if (sessionId) {
3744
+ const success = await opencodeClient.executeCommand(sessionId, "compact");
3745
+ const card = success ? createSuccessCard("压缩上下文", "上下文已成功压缩") : createErrorCard("压缩失败", "执行 compact 命令时发生错误");
3746
+ await feishuClient.sendCardToUser(userId, card);
3747
+ }
3748
+ break;
3749
+ }
3750
+ case "clear_history": {
3751
+ await sessionManager.createNewUserSession(userId);
3752
+ const card = createSuccessCard("清除历史", "会话历史已清除,已创建新会话");
3753
+ await feishuClient.sendCardToUser(userId, card);
3754
+ break;
3755
+ }
3756
+ case "show_status": {
3757
+ const session = await sessionManager.getUserSessionInfo(userId);
3758
+ if (session) {
3759
+ const status = {
3760
+ sessionId: session.sessionId,
3761
+ projectPath: session.projectPath,
3762
+ isActive: session.isActive
3763
+ };
3764
+ const card = createStatusCard(status);
3765
+ await feishuClient.sendCardToUser(userId, card);
3766
+ } else {
3767
+ const card = createErrorCard("无会话", "当前没有活跃的会话");
3768
+ await feishuClient.sendCardToUser(userId, card);
3769
+ }
3770
+ break;
3771
+ }
3772
+ case "show_cost": {
3773
+ const card = createErrorCard("功能开发中", "费用统计功能正在开发中");
3774
+ await feishuClient.sendCardToUser(userId, card);
3775
+ break;
3776
+ }
3777
+ default:
3778
+ logger.warn("未知的菜单事件", { eventKey: event.eventKey });
3779
+ }
3780
+ } catch (error) {
3781
+ logger.error("处理菜单事件时出错", error);
3782
+ const card = createErrorCard("操作失败", `处理菜单操作时发生错误: ${error instanceof Error ? error.message : "未知错误"}`);
3783
+ await feishuClient.sendCardToUser(userId, card);
3784
+ }
3785
+ });
3786
+ feishuClient.onCardAction(async (event) => {
3787
+ const actionType = event.action.value?.action;
3788
+ logger.info("卡片交互", { action: actionType, operatorId: event.operatorId });
3789
+ if (!event.operatorId) {
3790
+ logger.warn("卡片交互事件缺少操作者 ID");
3791
+ return;
3792
+ }
3793
+ try {
3794
+ switch (actionType) {
3795
+ case "switch_model": {
3796
+ const modelId = event.action.option;
3797
+ if (!modelId) {
3798
+ logger.warn("切换模型未选择模型");
3799
+ return;
3800
+ }
3801
+ const chatId = event.chatId;
3802
+ if (!chatId) {
3803
+ logger.warn("切换模型缺少 chatId");
3804
+ return;
3805
+ }
3806
+ const sessionChat = sessionManager.getSessionChat(chatId);
3807
+ let sessionId = null;
3808
+ if (sessionChat) {
3809
+ sessionId = sessionChat.session_id;
3810
+ } else {
3811
+ sessionId = await sessionManager.getOrCreateUserSession(event.operatorId);
3812
+ }
3813
+ if (!sessionId) {
3814
+ logger.error("获取会话失败");
3815
+ return;
3816
+ }
3817
+ const success = await opencodeClient.executeCommand(sessionId, "model", modelId);
3818
+ if (event.messageId) {
3819
+ const card = success ? createSuccessCard("切换成功", `已切换到模型:\`${modelId}\``) : createErrorCard("切换失败", "切换模型时发生错误");
3820
+ await feishuClient.updateCard(event.messageId, card);
3821
+ }
3822
+ break;
3823
+ }
3824
+ case "question_answer": {
3825
+ const chatId = event.chatId;
3826
+ if (!chatId) {
3827
+ logger.warn("问题回答缺少 chatId");
3828
+ return;
3829
+ }
3830
+ const { requestId, questionIndex, answerLabel } = event.action.value;
3831
+ const selectedOption = event.action.option || answerLabel;
3832
+ if (!requestId || questionIndex === undefined || !selectedOption) {
3833
+ logger.warn("问题回答缺少必要参数", { requestId, questionIndex, selectedOption });
3834
+ return;
3835
+ }
3836
+ const success = await sessionManager.handleQuestionAnswer(chatId, requestId, questionIndex, selectedOption, event.messageId);
3837
+ if (!success && event.messageId) {
3838
+ const errorCard = createQuestionErrorCard("问题已过期或不存在");
3839
+ await feishuClient.updateCard(event.messageId, errorCard);
3840
+ }
3841
+ break;
3842
+ }
3843
+ case "switch_project_in_chat": {
3844
+ const chatId = event.chatId || event.action.value?.chatId;
3845
+ const projectPath = event.action.option;
3846
+ if (!chatId || !projectPath) {
3847
+ logger.warn("切换项目缺少必要参数", { chatId, projectPath });
3848
+ return;
3849
+ }
3850
+ const sessionChat = sessionManager.getSessionChat(chatId);
3851
+ if (!sessionChat) {
3852
+ logger.warn("切换项目:非会话群", { chatId });
3853
+ return;
3854
+ }
3855
+ await sessionManager.switchProject(chatId, projectPath);
3856
+ const newSessionId = await sessionManager.createNewSession(chatId);
3857
+ const project = projects.find((p) => p.path === projectPath);
3858
+ const projectName = project?.name || projectPath;
3859
+ const card = createProjectSwitchedCard(projectName, projectPath, newSessionId);
3860
+ await feishuClient.sendCard(chatId, card);
3861
+ break;
3862
+ }
3863
+ case "new_session_in_chat": {
3864
+ const chatId = event.chatId || event.action.value?.chatId;
3865
+ if (!chatId) {
3866
+ logger.warn("新建会话缺少 chatId");
3867
+ return;
3868
+ }
3869
+ const sessionChat = sessionManager.getSessionChat(chatId);
3870
+ if (!sessionChat) {
3871
+ logger.warn("新建会话:非会话群", { chatId });
3872
+ return;
3873
+ }
3874
+ const newSessionId = await sessionManager.createNewSession(chatId);
3875
+ const shortId = newSessionId.replace(/^ses_/, "").slice(0, 8);
3876
+ const card = createSuccessCard("✅ 新会话已创建", `会话 ID:\`${shortId}\`
3877
+
3878
+ 发送消息开始新对话`);
3879
+ await feishuClient.sendCard(chatId, card);
3880
+ break;
3881
+ }
3882
+ case "show_status_in_chat": {
3883
+ const chatId = event.chatId || event.action.value?.chatId;
3884
+ if (!chatId) {
3885
+ logger.warn("显示状态缺少 chatId");
3886
+ return;
3887
+ }
3888
+ const sessionChat = sessionManager.getSessionChat(chatId);
3889
+ if (!sessionChat) {
3890
+ const card2 = createErrorCard("无会话", "当前没有活跃的会话");
3891
+ await feishuClient.sendCard(chatId, card2);
3892
+ return;
3893
+ }
3894
+ const status = {
3895
+ sessionId: sessionChat.session_id,
3896
+ projectPath: sessionChat.project_path,
3897
+ isActive: true
3898
+ };
3899
+ const card = createStatusCard(status);
3900
+ await feishuClient.sendCard(chatId, card);
3901
+ break;
3902
+ }
3903
+ default:
3904
+ logger.debug("未处理的卡片交互", { action: actionType });
3905
+ }
3906
+ } catch (error) {
3907
+ logger.error("处理卡片交互时出错", error);
3908
+ }
3909
+ });
3910
+ }
3911
+
3912
+ // src/index.ts
3913
+ async function listAvailableModels() {
3914
+ const tempClient = createOpencodeWrapper({});
3915
+ try {
3916
+ await tempClient.start();
3917
+ const models = await tempClient.listModels();
3918
+ console.log(`
3919
+ 可用模型列表:
3920
+ `);
3921
+ models.forEach((model, index) => {
3922
+ console.log(` ${index + 1}. ${model.name}`);
3923
+ console.log(` ID: ${model.id}`);
3924
+ console.log("");
3925
+ });
3926
+ console.log(`共 ${models.length} 个模型
3927
+ `);
3928
+ } finally {
3929
+ tempClient.stop();
3930
+ }
3931
+ }
3932
+ async function main() {
3933
+ const cliOptions = parseArgs();
3934
+ if (cliOptions.help) {
3935
+ console.log(formatHelp());
3936
+ process.exit(0);
3937
+ }
3938
+ if (cliOptions.version) {
3939
+ console.log(`v${getVersion()}`);
3940
+ process.exit(0);
3941
+ }
3942
+ if (cliOptions.listModels) {
3943
+ await listAvailableModels();
3944
+ process.exit(0);
3945
+ }
3946
+ setupGlobalErrorHandling();
3947
+ const logLevel = cliOptions.logLevel && isValidLogLevel(cliOptions.logLevel) ? cliOptions.logLevel : undefined;
3948
+ const config = loadConfig({
3949
+ model: cliOptions.model,
3950
+ logLevel
3951
+ });
3952
+ setLogLevel(config.LOG_LEVEL);
3953
+ logger.info("正在启动飞书 OpenCode 机器人...");
3954
+ const db = initializeDatabase(config.DATABASE_PATH);
3955
+ logger.info("数据库已初始化", { path: config.DATABASE_PATH });
3956
+ const feishuClient = createFeishuClient({
3957
+ appId: config.FEISHU_APP_ID,
3958
+ appSecret: config.FEISHU_APP_SECRET
3959
+ });
3960
+ const defaultProjectPath = getDefaultProjectPath(cliOptions.project);
3961
+ const defaultModel = getDefaultModel(config);
3962
+ const opencodeClient = createOpencodeWrapper({
3963
+ directory: defaultProjectPath
3964
+ });
3965
+ const opencodeUrl = await opencodeClient.start();
3966
+ const adminUserIds = getAdminUserIds(config);
3967
+ const sessionManager = createSessionManager(db, feishuClient, opencodeClient, {
3968
+ defaultProjectPath,
3969
+ adminUserIds,
3970
+ allowAllUsers: config.ALLOW_ALL_USERS
3971
+ });
3972
+ const projects = getProjects(config);
3973
+ const availableModels = getAvailableModels(config);
3974
+ const commandHandler = createCommandHandler(db, feishuClient, sessionManager, opencodeClient, { projects, availableModels });
3975
+ setupEventHandlers({
3976
+ feishuClient,
3977
+ sessionManager,
3978
+ opencodeClient,
3979
+ projects,
3980
+ availableModels
3981
+ });
3982
+ feishuClient.onMessage(async (event) => {
3983
+ logger.debug("收到消息", {
3984
+ chatId: event.chatId,
3985
+ senderId: event.senderId,
3986
+ messageType: event.messageType
3987
+ });
3988
+ const text = parseTextContent(event.content);
3989
+ if (isCommand(text)) {
3990
+ const result = await commandHandler.handleIfCommand(text, {
3991
+ chatId: event.chatId,
3992
+ userId: event.senderId,
3993
+ isAdmin: sessionManager.isAdmin(event.senderId)
3994
+ });
3995
+ if (result.handled) {
3996
+ return;
3997
+ }
3998
+ }
3999
+ await sessionManager.handleMessage(event);
4000
+ });
4001
+ const reconnectionManager = createReconnectionManager(async () => {
4002
+ await feishuClient.start();
4003
+ }, async () => {
4004
+ await feishuClient.stop();
4005
+ }, {
4006
+ maxRetries: 10,
4007
+ initialDelayMs: 1000,
4008
+ maxDelayMs: 30000
4009
+ });
4010
+ await reconnectionManager.connect();
4011
+ logger.info("飞书 OpenCode 机器人启动成功");
4012
+ logger.info("配置信息", {
4013
+ appId: config.FEISHU_APP_ID.substring(0, 8) + "...",
4014
+ opencodeUrl,
4015
+ defaultProject: defaultProjectPath,
4016
+ defaultModel: defaultModel || "(未设置)",
4017
+ adminCount: adminUserIds.length
4018
+ });
4019
+ const shutdown = async (signal) => {
4020
+ logger.info(`收到 ${signal} 信号,正在关闭...`);
4021
+ sessionManager.cleanup();
4022
+ await reconnectionManager.disconnect();
4023
+ opencodeClient.stop();
4024
+ db.close();
4025
+ logger.info("关闭完成");
4026
+ process.exit(0);
4027
+ };
4028
+ process.on("SIGINT", () => shutdown("SIGINT"));
4029
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
4030
+ }
4031
+ main().catch((error) => {
4032
+ logger.error("致命错误", error);
4033
+ process.exit(1);
4034
+ });