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