qqbot-opencode 1.0.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 (66) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +197 -0
  3. package/bin/qqbot.js +16 -0
  4. package/dist/app.d.ts +2 -0
  5. package/dist/app.d.ts.map +1 -0
  6. package/dist/app.js +154 -0
  7. package/dist/app.js.map +1 -0
  8. package/dist/bundle.cjs +850 -0
  9. package/dist/bundle.js +826 -0
  10. package/dist/config.d.ts +8 -0
  11. package/dist/config.d.ts.map +1 -0
  12. package/dist/config.js +179 -0
  13. package/dist/config.js.map +1 -0
  14. package/dist/handlers/index.d.ts +3 -0
  15. package/dist/handlers/index.d.ts.map +1 -0
  16. package/dist/handlers/index.js +3 -0
  17. package/dist/handlers/index.js.map +1 -0
  18. package/dist/handlers/message.d.ts +8 -0
  19. package/dist/handlers/message.d.ts.map +1 -0
  20. package/dist/handlers/message.js +57 -0
  21. package/dist/handlers/message.js.map +1 -0
  22. package/dist/handlers/session.d.ts +13 -0
  23. package/dist/handlers/session.d.ts.map +1 -0
  24. package/dist/handlers/session.js +104 -0
  25. package/dist/handlers/session.js.map +1 -0
  26. package/dist/opencode/client.d.ts +23 -0
  27. package/dist/opencode/client.d.ts.map +1 -0
  28. package/dist/opencode/client.js +141 -0
  29. package/dist/opencode/client.js.map +1 -0
  30. package/dist/opencode/index.d.ts +2 -0
  31. package/dist/opencode/index.d.ts.map +1 -0
  32. package/dist/opencode/index.js +2 -0
  33. package/dist/opencode/index.js.map +1 -0
  34. package/dist/qq/connection.d.ts +23 -0
  35. package/dist/qq/connection.d.ts.map +1 -0
  36. package/dist/qq/connection.js +188 -0
  37. package/dist/qq/connection.js.map +1 -0
  38. package/dist/qq/index.d.ts +5 -0
  39. package/dist/qq/index.d.ts.map +1 -0
  40. package/dist/qq/index.js +4 -0
  41. package/dist/qq/index.js.map +1 -0
  42. package/dist/qq/parser.d.ts +4 -0
  43. package/dist/qq/parser.d.ts.map +1 -0
  44. package/dist/qq/parser.js +99 -0
  45. package/dist/qq/parser.js.map +1 -0
  46. package/dist/qq/sender.d.ts +28 -0
  47. package/dist/qq/sender.d.ts.map +1 -0
  48. package/dist/qq/sender.js +123 -0
  49. package/dist/qq/sender.js.map +1 -0
  50. package/dist/types.d.ts +47 -0
  51. package/dist/types.d.ts.map +1 -0
  52. package/dist/types.js +2 -0
  53. package/dist/types.js.map +1 -0
  54. package/package.json +58 -0
  55. package/src/app.ts +204 -0
  56. package/src/config.ts +200 -0
  57. package/src/handlers/index.ts +2 -0
  58. package/src/handlers/message.ts +86 -0
  59. package/src/handlers/session.ts +130 -0
  60. package/src/opencode/client.ts +204 -0
  61. package/src/opencode/index.ts +1 -0
  62. package/src/qq/connection.ts +252 -0
  63. package/src/qq/index.ts +9 -0
  64. package/src/qq/parser.ts +126 -0
  65. package/src/qq/sender.ts +215 -0
  66. package/src/types.ts +52 -0
package/dist/bundle.js ADDED
@@ -0,0 +1,826 @@
1
+ // src/config.ts
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import yaml from "js-yaml";
5
+ function loadConfig(configPath) {
6
+ const absolutePath = path.resolve(configPath);
7
+ if (!fs.existsSync(absolutePath)) {
8
+ throw new Error(`Config file not found: ${absolutePath}`);
9
+ }
10
+ const rawContent = fs.readFileSync(absolutePath, "utf-8");
11
+ const config = yaml.load(rawContent);
12
+ validateConfig(config);
13
+ return config;
14
+ }
15
+ function validateConfig(config) {
16
+ if (!config.qq?.appId) {
17
+ throw new Error("Missing required field: qq.appId");
18
+ }
19
+ if (!config.qq?.clientSecret) {
20
+ throw new Error("Missing required field: qq.clientSecret");
21
+ }
22
+ if (!config.opencode?.port) {
23
+ config.opencode.port = 4096;
24
+ }
25
+ if (!config.opencode?.hostname) {
26
+ config.opencode.hostname = "127.0.0.1";
27
+ }
28
+ if (!config.app?.workingDir) {
29
+ config.app.workingDir = process.cwd();
30
+ }
31
+ }
32
+ function expandEnvVariables(obj) {
33
+ if (typeof obj === "string") {
34
+ const envVarPattern = /\$\{([^}]+)\}/g;
35
+ return obj.replace(envVarPattern, (_, envName) => {
36
+ return process.env[envName] || "";
37
+ });
38
+ }
39
+ if (Array.isArray(obj)) {
40
+ return obj.map(expandEnvVariables);
41
+ }
42
+ if (obj && typeof obj === "object") {
43
+ const result = {};
44
+ for (const [key, value] of Object.entries(obj)) {
45
+ result[key] = expandEnvVariables(value);
46
+ }
47
+ return result;
48
+ }
49
+ return obj;
50
+ }
51
+
52
+ // src/qq/connection.ts
53
+ import WebSocket from "ws";
54
+ var INTENTS = {
55
+ GUILDS: 1 << 0,
56
+ GUILD_MEMBERS: 1 << 1,
57
+ PUBLIC_GUILD_MESSAGES: 1 << 30,
58
+ DIRECT_MESSAGE: 1 << 12,
59
+ GROUP_AND_C2C: 1 << 25
60
+ };
61
+ var FULL_INTENTS = INTENTS.PUBLIC_GUILD_MESSAGES | INTENTS.DIRECT_MESSAGE | INTENTS.GROUP_AND_C2C;
62
+ var API_BASE = "https://api.sgroup.qq.com";
63
+ var TOKEN_URL = "https://bots.qq.com/app/getAppAccessToken";
64
+ var accessToken = null;
65
+ var sessionId = null;
66
+ var lastSeq = null;
67
+ var reconnectAttempts = 0;
68
+ var MAX_RECONNECT_ATTEMPTS = 10;
69
+ var RECONNECT_DELAY = 5e3;
70
+ async function getAccessToken(appId, clientSecret) {
71
+ const response = await fetch(TOKEN_URL, {
72
+ method: "POST",
73
+ headers: {
74
+ "Content-Type": "application/json"
75
+ },
76
+ body: JSON.stringify({ appId, clientSecret })
77
+ });
78
+ if (!response.ok) {
79
+ const errorText = await response.text();
80
+ throw new Error(`Failed to get access token: ${response.status} - ${errorText}`);
81
+ }
82
+ const data = await response.json();
83
+ if (!data.access_token) {
84
+ throw new Error(`Failed to get access token: ${JSON.stringify(data)}`);
85
+ }
86
+ return data.access_token;
87
+ }
88
+ async function getGatewayUrl(token) {
89
+ const response = await fetch(`${API_BASE}/gateway`, {
90
+ method: "GET",
91
+ headers: {
92
+ Authorization: `QQBot ${token}`
93
+ }
94
+ });
95
+ if (!response.ok) {
96
+ throw new Error(`Failed to get gateway URL: ${response.status}`);
97
+ }
98
+ const data = await response.json();
99
+ return data.url;
100
+ }
101
+ async function startQQConnection(options) {
102
+ const { qq, onMessage, onReady, onError, onDisconnect } = options;
103
+ accessToken = await getAccessToken(qq.appId, qq.clientSecret);
104
+ return new Promise((resolve, reject) => {
105
+ let ws = null;
106
+ let heartbeatInterval = null;
107
+ let isReconnecting = false;
108
+ async function connect() {
109
+ const gatewayUrl = await getGatewayUrl(accessToken);
110
+ console.log(`[QQ] Connecting to gateway: ${gatewayUrl}`);
111
+ ws = new WebSocket(gatewayUrl);
112
+ ws.on("open", () => {
113
+ console.log("[QQ] WebSocket connected");
114
+ reconnectAttempts = 0;
115
+ });
116
+ ws.on("message", async (data) => {
117
+ try {
118
+ const rawData = data.toString();
119
+ const payload = JSON.parse(rawData);
120
+ const { op, d, s, t } = payload;
121
+ if (s) {
122
+ lastSeq = s;
123
+ }
124
+ if (op === 10) {
125
+ console.log("[QQ] Hello received");
126
+ if (sessionId && lastSeq !== null) {
127
+ console.log("[QQ] Attempting to resume session");
128
+ ws?.send(JSON.stringify({
129
+ op: 6,
130
+ d: {
131
+ token: `QQBot ${accessToken}`,
132
+ session_id: sessionId,
133
+ seq: lastSeq
134
+ }
135
+ }));
136
+ } else {
137
+ console.log("[QQ] Sending identify with intents:", FULL_INTENTS);
138
+ ws?.send(JSON.stringify({
139
+ op: 2,
140
+ d: {
141
+ token: `QQBot ${accessToken}`,
142
+ intents: FULL_INTENTS,
143
+ shard: [0, 1]
144
+ }
145
+ }));
146
+ }
147
+ const interval = d.heartbeat_interval;
148
+ if (heartbeatInterval) clearInterval(heartbeatInterval);
149
+ heartbeatInterval = setInterval(() => {
150
+ if (ws?.readyState === WebSocket.OPEN) {
151
+ ws.send(JSON.stringify({ op: 1, d: lastSeq }));
152
+ }
153
+ }, interval);
154
+ } else if (op === 0) {
155
+ if (t === "READY") {
156
+ const readyData = d;
157
+ sessionId = readyData.session_id;
158
+ console.log("[QQ] Ready, session:", sessionId);
159
+ onReady?.();
160
+ } else if (t === "C2C_MESSAGE_CREATE") {
161
+ const event = d;
162
+ if (event.author?.user_openid) {
163
+ onMessage(event);
164
+ }
165
+ }
166
+ } else if (op === 11) {
167
+ } else if (op === 7) {
168
+ console.log("[QQ] Server requested reconnect");
169
+ cleanup();
170
+ scheduleReconnect();
171
+ } else if (op === 9) {
172
+ const canResume = d;
173
+ console.log("[QQ] Invalid session, can resume:", canResume);
174
+ if (!canResume) {
175
+ sessionId = null;
176
+ lastSeq = null;
177
+ }
178
+ cleanup();
179
+ scheduleReconnect();
180
+ }
181
+ } catch (err) {
182
+ console.error("[QQ] Message parse error:", err);
183
+ }
184
+ });
185
+ ws.on("close", (code, reason) => {
186
+ console.log(`[QQ] WebSocket closed: ${code} ${reason.toString()}`);
187
+ cleanup();
188
+ if (!isReconnecting) {
189
+ onDisconnect?.();
190
+ scheduleReconnect();
191
+ }
192
+ });
193
+ ws.on("error", (err) => {
194
+ console.error("[QQ] WebSocket error:", err.message);
195
+ onError?.(new Error(err.message));
196
+ });
197
+ }
198
+ function cleanup() {
199
+ if (heartbeatInterval) {
200
+ clearInterval(heartbeatInterval);
201
+ heartbeatInterval = null;
202
+ }
203
+ }
204
+ function scheduleReconnect() {
205
+ if (isReconnecting) return;
206
+ if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
207
+ console.error("[QQ] Max reconnect attempts reached");
208
+ reject(new Error("Max reconnect attempts reached"));
209
+ return;
210
+ }
211
+ isReconnecting = true;
212
+ reconnectAttempts++;
213
+ const delay = RECONNECT_DELAY * reconnectAttempts;
214
+ console.log(`[QQ] Reconnecting in ${delay}ms (attempt ${reconnectAttempts})`);
215
+ setTimeout(async () => {
216
+ isReconnecting = false;
217
+ try {
218
+ accessToken = await getAccessToken(qq.appId, qq.clientSecret);
219
+ await connect();
220
+ } catch (err) {
221
+ console.error("[QQ] Reconnect failed:", err);
222
+ scheduleReconnect();
223
+ }
224
+ }, delay);
225
+ }
226
+ connect().catch(reject);
227
+ });
228
+ }
229
+
230
+ // src/qq/sender.ts
231
+ import crypto from "crypto";
232
+ var API_BASE2 = "https://api.sgroup.qq.com";
233
+ var TOKEN_URL2 = "https://bots.qq.com/app/getAppAccessToken";
234
+ var tokenCache = null;
235
+ async function getAccessToken2(appId, clientSecret) {
236
+ if (tokenCache && Date.now() < tokenCache.expiresAt - 6e4) {
237
+ return tokenCache.token;
238
+ }
239
+ const response = await fetch(TOKEN_URL2, {
240
+ method: "POST",
241
+ headers: {
242
+ "Content-Type": "application/json"
243
+ },
244
+ body: JSON.stringify({ appId, clientSecret })
245
+ });
246
+ if (!response.ok) {
247
+ const errorText = await response.text();
248
+ throw new Error(`Failed to get access token: ${response.status} - ${errorText}`);
249
+ }
250
+ const data = await response.json();
251
+ if (!data.access_token) {
252
+ throw new Error(`Failed to get access token: ${JSON.stringify(data)}`);
253
+ }
254
+ tokenCache = {
255
+ token: data.access_token,
256
+ expiresAt: Date.now() + (data.expires_in ?? 7200) * 1e3
257
+ };
258
+ return tokenCache.token;
259
+ }
260
+ function getNextMsgSeq(msgId) {
261
+ const hash = crypto.createHash("md5").update(msgId).digest("hex");
262
+ return parseInt(hash.substring(0, 8), 16) % 9007199254740990 + 1;
263
+ }
264
+ async function apiRequest(accessToken2, method, path2, body) {
265
+ const url = `${API_BASE2}${path2}`;
266
+ const response = await fetch(url, {
267
+ method,
268
+ headers: {
269
+ Authorization: `QQBot ${accessToken2}`,
270
+ "Content-Type": "application/json"
271
+ },
272
+ body: body ? JSON.stringify(body) : void 0
273
+ });
274
+ if (!response.ok) {
275
+ const errorText = await response.text();
276
+ throw new Error(`API Error [${path2}]: ${response.status} - ${errorText}`);
277
+ }
278
+ return response.json();
279
+ }
280
+ async function sendC2CMessage(account, options) {
281
+ const token = await getAccessToken2(account.appId, account.clientSecret);
282
+ const msgSeq = getNextMsgSeq(options.messageId);
283
+ const useMarkdown = options.markdown ?? account.markdownSupport ?? false;
284
+ let body;
285
+ if (useMarkdown) {
286
+ body = {
287
+ markdown: { content: options.content },
288
+ msg_type: 2,
289
+ msg_seq: msgSeq,
290
+ msg_id: options.messageId
291
+ };
292
+ } else {
293
+ body = {
294
+ content: options.content,
295
+ msg_type: 0,
296
+ msg_seq: msgSeq,
297
+ msg_id: options.messageId
298
+ };
299
+ }
300
+ if (options.quoteRef && !useMarkdown) {
301
+ body.message_reference = { message_id: options.quoteRef };
302
+ }
303
+ await apiRequest(token, "POST", `/v2/users/${options.toOpenid}/messages`, body);
304
+ }
305
+ function clearTokenCache() {
306
+ tokenCache = null;
307
+ }
308
+
309
+ // src/qq/parser.ts
310
+ var FACE_TAG_REGEX = /<face name="([^"]+)"[^/]*\/>/gi;
311
+ function parseMessage(event) {
312
+ let content = event.content;
313
+ content = parseFaceTags(content);
314
+ content = content.trim();
315
+ const imageUrls = extractImageUrls(content);
316
+ content = removeImageUrls(content);
317
+ const { quoteRef, quoteId } = parseQuoteRef(event);
318
+ return {
319
+ content,
320
+ imageUrls,
321
+ quoteRef,
322
+ quoteId
323
+ };
324
+ }
325
+ function parseFaceTags(content) {
326
+ let result = content;
327
+ let match;
328
+ FACE_TAG_REGEX.lastIndex = 0;
329
+ while ((match = FACE_TAG_REGEX.exec(content)) !== null) {
330
+ const faceName = match[1];
331
+ result = result.replace(match[0], `[\u8868\u60C5:${faceName}]`);
332
+ }
333
+ return result;
334
+ }
335
+ function extractImageUrls(content) {
336
+ const urls = [];
337
+ let match;
338
+ const regex = /https?:\/\/[^\s"'<>]+\.(?:png|jpg|jpeg|gif|webp)(?:\?[^\s"'<>]*)?/gi;
339
+ while ((match = regex.exec(content)) !== null) {
340
+ urls.push(match[0]);
341
+ }
342
+ return urls;
343
+ }
344
+ function removeImageUrls(content) {
345
+ return content.replace(/https?:\/\/[^\s"'<>]+\.(?:png|jpg|jpeg|gif|webp)(?:\?[^\s"'<>]*)?/gi, "").trim();
346
+ }
347
+ function parseQuoteRef(event) {
348
+ const ext = event.message_scene?.ext;
349
+ if (!ext) {
350
+ return {};
351
+ }
352
+ try {
353
+ const scene = JSON.parse(ext);
354
+ if (scene?.refMsgIdx) {
355
+ return {
356
+ quoteRef: String(scene.refMsgIdx),
357
+ quoteId: scene.refMsgIdx
358
+ };
359
+ }
360
+ } catch {
361
+ }
362
+ return {};
363
+ }
364
+ function chunkText(text, limit) {
365
+ if (!text || text.length === 0) {
366
+ return [];
367
+ }
368
+ if (text.length <= limit) {
369
+ return [text];
370
+ }
371
+ const chunks = [];
372
+ const lines = text.split("\n");
373
+ let currentChunk = "";
374
+ for (const line of lines) {
375
+ if (currentChunk.length + line.length + 1 <= limit) {
376
+ currentChunk += (currentChunk ? "\n" : "") + line;
377
+ } else {
378
+ if (currentChunk) {
379
+ chunks.push(currentChunk);
380
+ }
381
+ if (line.length <= limit) {
382
+ currentChunk = line;
383
+ } else {
384
+ const subChunks = splitLongLine(line, limit);
385
+ chunks.push(...subChunks.slice(0, -1));
386
+ currentChunk = subChunks[subChunks.length - 1] || "";
387
+ }
388
+ }
389
+ }
390
+ if (currentChunk) {
391
+ chunks.push(currentChunk);
392
+ }
393
+ return chunks;
394
+ }
395
+ function splitLongLine(line, limit) {
396
+ const chunks = [];
397
+ for (let i = 0; i < line.length; i += limit) {
398
+ chunks.push(line.slice(i, i + limit));
399
+ }
400
+ return chunks;
401
+ }
402
+
403
+ // src/opencode/client.ts
404
+ import { createOpencode } from "@opencode-ai/sdk";
405
+ var instance = null;
406
+ async function initOpencodeClient(config) {
407
+ const opencodeConfig = config.config || {};
408
+ const { client, server } = await createOpencode({
409
+ hostname: config.hostname,
410
+ port: config.port,
411
+ config: opencodeConfig
412
+ });
413
+ instance = {
414
+ client,
415
+ server,
416
+ currentSessionId: null
417
+ };
418
+ const sessionsResponse = await client.session.list();
419
+ const sessions = sessionsResponse.data;
420
+ if (sessions && sessions.length > 0) {
421
+ instance.currentSessionId = sessions[0].id;
422
+ console.log(`[Opencode] Resumed session: ${sessions[0].id}`);
423
+ }
424
+ return instance;
425
+ }
426
+ async function createSession() {
427
+ if (!instance) {
428
+ throw new Error("Opencode client not initialized");
429
+ }
430
+ const sessionResponse = await instance.client.session.create({
431
+ body: {}
432
+ });
433
+ const session = sessionResponse.data;
434
+ if (!session) {
435
+ throw new Error("Failed to create session");
436
+ }
437
+ instance.currentSessionId = session.id;
438
+ console.log(`[Opencode] Created new session: ${session.id}`);
439
+ return {
440
+ id: session.id,
441
+ title: session.title
442
+ };
443
+ }
444
+ async function switchSession(sessionId2) {
445
+ if (!instance) {
446
+ throw new Error("Opencode client not initialized");
447
+ }
448
+ const sessionResponse = await instance.client.session.get({
449
+ path: { id: sessionId2 }
450
+ });
451
+ const session = sessionResponse.data;
452
+ if (!session) {
453
+ throw new Error(`Session not found: ${sessionId2}`);
454
+ }
455
+ instance.currentSessionId = sessionId2;
456
+ console.log(`[Opencode] Switched to session: ${sessionId2}`);
457
+ return {
458
+ id: session.id,
459
+ title: session.title
460
+ };
461
+ }
462
+ async function listSessions() {
463
+ if (!instance) {
464
+ throw new Error("Opencode client not initialized");
465
+ }
466
+ const sessionsResponse = await instance.client.session.list();
467
+ const sessions = sessionsResponse.data;
468
+ if (!sessions) {
469
+ return [];
470
+ }
471
+ return sessions.map((s) => ({
472
+ id: s.id,
473
+ title: s.title
474
+ }));
475
+ }
476
+ async function getCurrentSession() {
477
+ if (!instance || !instance.currentSessionId) {
478
+ return null;
479
+ }
480
+ const sessionResponse = await instance.client.session.get({
481
+ path: { id: instance.currentSessionId }
482
+ });
483
+ const session = sessionResponse.data;
484
+ if (!session) {
485
+ return null;
486
+ }
487
+ return {
488
+ id: session.id,
489
+ title: session.title
490
+ };
491
+ }
492
+ async function sendPrompt(message, imageUrls = []) {
493
+ if (!instance || !instance.currentSessionId) {
494
+ throw new Error("Opencode client not initialized or no active session");
495
+ }
496
+ console.log(`[Opencode] Sending prompt to session ${instance.currentSessionId}: "${message.slice(0, 50)}..."`);
497
+ const parts = [
498
+ { type: "text", text: message }
499
+ ];
500
+ for (const imageUrl of imageUrls) {
501
+ if (imageUrl.startsWith("http://") || imageUrl.startsWith("https://")) {
502
+ parts.push({ type: "image_url", url: imageUrl });
503
+ }
504
+ }
505
+ try {
506
+ const resultResponse = await instance.client.session.prompt({
507
+ path: { id: instance.currentSessionId },
508
+ body: {
509
+ parts
510
+ }
511
+ });
512
+ console.log(`[Opencode] Prompt response:`, JSON.stringify(resultResponse, null, 2).slice(0, 500));
513
+ const result = resultResponse.data;
514
+ let text = "";
515
+ if (result?.parts && Array.isArray(result.parts)) {
516
+ for (const part of result.parts) {
517
+ console.log(`[Opencode] Part type: ${part.type}`, part);
518
+ if (part.type === "text" && part.text) {
519
+ text += part.text;
520
+ }
521
+ }
522
+ } else {
523
+ console.log(`[Opencode] No parts in response or unexpected format`);
524
+ }
525
+ if (!text) {
526
+ console.log(`[Opencode] Empty response text, checking info...`);
527
+ if (result?.info) {
528
+ console.log(`[Opencode] Response info:`, JSON.stringify(result.info).slice(0, 500));
529
+ }
530
+ }
531
+ return { text };
532
+ } catch (err) {
533
+ console.error(`[Opencode] Prompt error:`, err);
534
+ throw err;
535
+ }
536
+ }
537
+ async function closeOpencodeClient() {
538
+ if (instance) {
539
+ instance.server.close();
540
+ instance = null;
541
+ console.log("[Opencode] Client closed");
542
+ }
543
+ }
544
+
545
+ // src/handlers/session.ts
546
+ async function handleSessionNew() {
547
+ try {
548
+ const session = await createSession();
549
+ return {
550
+ text: `\u5DF2\u521B\u5EFA\u65B0\u4F1A\u8BDD: ${session.id}
551
+ \u6807\u9898: ${session.title || "\u65E0"}`,
552
+ success: true
553
+ };
554
+ } catch (err) {
555
+ return {
556
+ text: `\u521B\u5EFA\u4F1A\u8BDD\u5931\u8D25: ${err instanceof Error ? err.message : String(err)}`,
557
+ success: false
558
+ };
559
+ }
560
+ }
561
+ async function handleSessionSwitch(sessionId2) {
562
+ if (!sessionId2 || sessionId2.trim() === "") {
563
+ return {
564
+ text: "\u8BF7\u63D0\u4F9B\u4F1A\u8BDD ID\uFF0C\u4F8B\u5982: /session-switch <id>",
565
+ success: false
566
+ };
567
+ }
568
+ try {
569
+ const session = await switchSession(sessionId2.trim());
570
+ return {
571
+ text: `\u5DF2\u5207\u6362\u5230\u4F1A\u8BDD: ${session.id}
572
+ \u6807\u9898: ${session.title || "\u65E0"}`,
573
+ success: true
574
+ };
575
+ } catch (err) {
576
+ return {
577
+ text: `\u5207\u6362\u4F1A\u8BDD\u5931\u8D25: ${err instanceof Error ? err.message : String(err)}`,
578
+ success: false
579
+ };
580
+ }
581
+ }
582
+ async function handleSessionList() {
583
+ try {
584
+ const sessions = await listSessions();
585
+ if (sessions.length === 0) {
586
+ return {
587
+ text: "\u6682\u65E0\u4F1A\u8BDD",
588
+ success: true
589
+ };
590
+ }
591
+ const currentSession = await getCurrentSession();
592
+ const currentId = currentSession?.id;
593
+ const lines = sessions.map((s, i) => {
594
+ const marker = s.id === currentId ? " [\u5F53\u524D]" : "";
595
+ const title = s.title ? ` - ${s.title}` : "";
596
+ return `${i + 1}. ${s.id}${title}${marker}`;
597
+ });
598
+ return {
599
+ text: `\u4F1A\u8BDD\u5217\u8868:
600
+ ${lines.join("\n")}`,
601
+ success: true
602
+ };
603
+ } catch (err) {
604
+ return {
605
+ text: `\u83B7\u53D6\u4F1A\u8BDD\u5217\u8868\u5931\u8D25: ${err instanceof Error ? err.message : String(err)}`,
606
+ success: false
607
+ };
608
+ }
609
+ }
610
+ async function handleSessionCurrent() {
611
+ try {
612
+ const session = await getCurrentSession();
613
+ if (!session) {
614
+ return {
615
+ text: "\u5F53\u524D\u6CA1\u6709\u6D3B\u8DC3\u7684\u4F1A\u8BDD",
616
+ success: true
617
+ };
618
+ }
619
+ return {
620
+ text: `\u5F53\u524D\u4F1A\u8BDD:
621
+ ID: ${session.id}
622
+ \u6807\u9898: ${session.title || "\u65E0"}`,
623
+ success: true
624
+ };
625
+ } catch (err) {
626
+ return {
627
+ text: `\u83B7\u53D6\u5F53\u524D\u4F1A\u8BDD\u5931\u8D25: ${err instanceof Error ? err.message : String(err)}`,
628
+ success: false
629
+ };
630
+ }
631
+ }
632
+ function parseSessionCommand(content) {
633
+ const trimmed = content.trim();
634
+ if (trimmed === "/session-new") {
635
+ return { command: "new", args: "" };
636
+ }
637
+ if (trimmed.startsWith("/session-switch ")) {
638
+ const args = trimmed.slice("/session-switch ".length).trim();
639
+ return { command: "switch", args };
640
+ }
641
+ if (trimmed === "/session-list") {
642
+ return { command: "list", args: "" };
643
+ }
644
+ if (trimmed === "/session-current") {
645
+ return { command: "current", args: "" };
646
+ }
647
+ return null;
648
+ }
649
+
650
+ // src/handlers/message.ts
651
+ async function handleMessage(content, parsedMessage) {
652
+ const sessionCmd = parseSessionCommand(content);
653
+ if (sessionCmd) {
654
+ return handleSessionCommand(sessionCmd.command, sessionCmd.args);
655
+ }
656
+ return handleAIMessage(content, parsedMessage);
657
+ }
658
+ async function handleSessionCommand(command, args) {
659
+ let result;
660
+ switch (command) {
661
+ case "new":
662
+ result = await handleSessionNew();
663
+ break;
664
+ case "switch":
665
+ result = await handleSessionSwitch(args);
666
+ break;
667
+ case "list":
668
+ result = await handleSessionList();
669
+ break;
670
+ case "current":
671
+ result = await handleSessionCurrent();
672
+ break;
673
+ default:
674
+ result = {
675
+ text: "\u672A\u77E5\u7684 session \u547D\u4EE4",
676
+ success: false
677
+ };
678
+ }
679
+ return {
680
+ ...result,
681
+ isSessionCommand: true
682
+ };
683
+ }
684
+ async function handleAIMessage(content, parsedMessage) {
685
+ try {
686
+ const result = await sendPrompt(content, parsedMessage.imageUrls);
687
+ if (!result.text || result.text.trim() === "") {
688
+ return {
689
+ text: "AI \u8FD4\u56DE\u4E86\u7A7A\u54CD\u5E94",
690
+ success: true
691
+ };
692
+ }
693
+ return {
694
+ text: result.text,
695
+ success: true
696
+ };
697
+ } catch (err) {
698
+ return {
699
+ text: `\u5904\u7406\u6D88\u606F\u5931\u8D25: ${err instanceof Error ? err.message : String(err)}`,
700
+ success: false
701
+ };
702
+ }
703
+ }
704
+
705
+ // src/app.ts
706
+ var TEXT_CHUNK_LIMIT = 500;
707
+ function parseArgs() {
708
+ const args = process.argv.slice(2);
709
+ const result = {};
710
+ for (let i = 0; i < args.length; i++) {
711
+ if (args[i] === "--config" && i + 1 < args.length) {
712
+ result.config = args[i + 1];
713
+ i++;
714
+ } else if (args[i] === "--help" || args[i] === "-h") {
715
+ printHelp();
716
+ process.exit(0);
717
+ }
718
+ }
719
+ return result;
720
+ }
721
+ function printHelp() {
722
+ console.log(`
723
+ QQ Bot with OpenCode SDK
724
+
725
+ Usage:
726
+ npx tsx src/app.ts --config <config.yaml>
727
+
728
+ Options:
729
+ --config <path> Path to config.yaml file (required)
730
+ --help, -h Show this help message
731
+
732
+ Example:
733
+ npx tsx src/app.ts --config ./config.yaml
734
+ `);
735
+ }
736
+ async function main() {
737
+ const args = parseArgs();
738
+ if (!args.config) {
739
+ console.error("Error: --config option is required");
740
+ printHelp();
741
+ process.exit(1);
742
+ }
743
+ console.log("[App] Loading configuration...");
744
+ let config;
745
+ try {
746
+ config = loadConfig(args.config);
747
+ } catch (err) {
748
+ console.error(`[App] Failed to load config: ${err instanceof Error ? err.message : String(err)}`);
749
+ process.exit(1);
750
+ }
751
+ const expandedConfig = expandEnvVariables(config);
752
+ console.log("[App] Initializing OpenCode client...");
753
+ try {
754
+ await initOpencodeClient(expandedConfig.opencode);
755
+ console.log("[App] OpenCode client initialized");
756
+ } catch (err) {
757
+ console.error(`[App] Failed to initialize OpenCode: ${err instanceof Error ? err.message : String(err)}`);
758
+ process.exit(1);
759
+ }
760
+ try {
761
+ await createSession();
762
+ } catch (err) {
763
+ console.error(`[App] Failed to create initial session: ${err instanceof Error ? err.message : String(err)}`);
764
+ }
765
+ console.log("[App] Starting QQ connection...");
766
+ try {
767
+ await startQQConnection({
768
+ qq: expandedConfig.qq,
769
+ onMessage: async (event) => {
770
+ console.log(`[App] Received message from ${event.author.user_openid}: ${event.content.slice(0, 50)}...`);
771
+ try {
772
+ const parsed = parseMessage(event);
773
+ const result = await handleMessage(parsed.content, parsed);
774
+ const chunks = chunkText(result.text, TEXT_CHUNK_LIMIT);
775
+ for (const chunk of chunks) {
776
+ await sendC2CMessage(expandedConfig.qq, {
777
+ toOpenid: event.author.user_openid,
778
+ content: chunk,
779
+ messageId: event.id
780
+ });
781
+ }
782
+ console.log(`[App] Sent response to ${event.author.user_openid}`);
783
+ } catch (err) {
784
+ console.error(`[App] Error handling message: ${err instanceof Error ? err.message : String(err)}`);
785
+ const errorText = `\u5904\u7406\u6D88\u606F\u65F6\u51FA\u9519: ${err instanceof Error ? err.message : String(err)}`;
786
+ try {
787
+ await sendC2CMessage(expandedConfig.qq, {
788
+ toOpenid: event.author.user_openid,
789
+ content: errorText,
790
+ messageId: event.id
791
+ });
792
+ } catch (sendErr) {
793
+ console.error(`[App] Failed to send error message: ${sendErr}`);
794
+ }
795
+ }
796
+ },
797
+ onReady: () => {
798
+ console.log("[App] QQ Bot is ready!");
799
+ },
800
+ onError: (err) => {
801
+ console.error(`[App] QQ connection error: ${err.message}`);
802
+ },
803
+ onDisconnect: () => {
804
+ console.log("[App] QQ disconnected, will attempt to reconnect...");
805
+ }
806
+ });
807
+ } catch (err) {
808
+ console.error(`[App] Failed to start QQ connection: ${err instanceof Error ? err.message : String(err)}`);
809
+ await closeOpencodeClient();
810
+ process.exit(1);
811
+ }
812
+ console.log("[App] Bot is running. Press Ctrl+C to stop.");
813
+ const shutdown = async () => {
814
+ console.log("\n[App] Shutting down...");
815
+ clearTokenCache();
816
+ await closeOpencodeClient();
817
+ console.log("[App] Goodbye!");
818
+ process.exit(0);
819
+ };
820
+ process.on("SIGINT", shutdown);
821
+ process.on("SIGTERM", shutdown);
822
+ }
823
+ main().catch((err) => {
824
+ console.error(`[App] Fatal error: ${err}`);
825
+ process.exit(1);
826
+ });