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,215 @@
1
+ import crypto from "crypto";
2
+
3
+ interface AccessTokenResult {
4
+ token: string;
5
+ expiresAt: number;
6
+ }
7
+
8
+ interface SendMessageOptions {
9
+ toOpenid: string;
10
+ content: string;
11
+ messageId: string;
12
+ quoteRef?: string;
13
+ markdown?: boolean;
14
+ }
15
+
16
+ interface QQAccount {
17
+ appId: string;
18
+ clientSecret: string;
19
+ markdownSupport?: boolean;
20
+ }
21
+
22
+ const API_BASE = "https://api.sgroup.qq.com";
23
+ const TOKEN_URL = "https://bots.qq.com/app/getAppAccessToken";
24
+
25
+ let tokenCache: AccessTokenResult | null = null;
26
+
27
+ async function getAccessToken(
28
+ appId: string,
29
+ clientSecret: string,
30
+ ): Promise<string> {
31
+ if (tokenCache && Date.now() < tokenCache.expiresAt - 60000) {
32
+ return tokenCache.token;
33
+ }
34
+
35
+ const response = await fetch(TOKEN_URL, {
36
+ method: "POST",
37
+ headers: {
38
+ "Content-Type": "application/json",
39
+ },
40
+ body: JSON.stringify({ appId, clientSecret }),
41
+ });
42
+
43
+ if (!response.ok) {
44
+ const errorText = await response.text();
45
+ throw new Error(
46
+ `Failed to get access token: ${response.status} - ${errorText}`,
47
+ );
48
+ }
49
+
50
+ const data = (await response.json()) as {
51
+ access_token?: string;
52
+ expires_in?: number;
53
+ };
54
+
55
+ if (!data.access_token) {
56
+ throw new Error(`Failed to get access token: ${JSON.stringify(data)}`);
57
+ }
58
+
59
+ tokenCache = {
60
+ token: data.access_token,
61
+ expiresAt: Date.now() + (data.expires_in ?? 7200) * 1000,
62
+ };
63
+
64
+ return tokenCache.token;
65
+ }
66
+
67
+ function getNextMsgSeq(msgId: string): number {
68
+ const hash = crypto.createHash("md5").update(msgId).digest("hex");
69
+ return (parseInt(hash.substring(0, 8), 16) % 9007199254740990) + 1;
70
+ }
71
+
72
+ async function apiRequest(
73
+ accessToken: string,
74
+ method: string,
75
+ path: string,
76
+ body?: unknown,
77
+ ): Promise<unknown> {
78
+ const url = `${API_BASE}${path}`;
79
+
80
+ const response = await fetch(url, {
81
+ method,
82
+ headers: {
83
+ Authorization: `QQBot ${accessToken}`,
84
+ "Content-Type": "application/json",
85
+ },
86
+ body: body ? JSON.stringify(body) : undefined,
87
+ });
88
+
89
+ if (!response.ok) {
90
+ const errorText = await response.text();
91
+ throw new Error(`API Error [${path}]: ${response.status} - ${errorText}`);
92
+ }
93
+
94
+ return response.json();
95
+ }
96
+
97
+ export async function sendC2CMessage(
98
+ account: QQAccount,
99
+ options: SendMessageOptions,
100
+ ): Promise<void> {
101
+ const token = await getAccessToken(account.appId, account.clientSecret);
102
+ const msgSeq = getNextMsgSeq(options.messageId);
103
+ const useMarkdown = options.markdown ?? account.markdownSupport ?? false;
104
+
105
+ let body: Record<string, unknown>;
106
+
107
+ if (useMarkdown) {
108
+ body = {
109
+ markdown: { content: options.content },
110
+ msg_type: 2,
111
+ msg_seq: msgSeq,
112
+ msg_id: options.messageId,
113
+ };
114
+ } else {
115
+ body = {
116
+ content: options.content,
117
+ msg_type: 0,
118
+ msg_seq: msgSeq,
119
+ msg_id: options.messageId,
120
+ };
121
+ }
122
+
123
+ if (options.quoteRef && !useMarkdown) {
124
+ body.message_reference = { message_id: options.quoteRef };
125
+ }
126
+
127
+ await apiRequest(
128
+ token,
129
+ "POST",
130
+ `/v2/users/${options.toOpenid}/messages`,
131
+ body,
132
+ );
133
+ }
134
+
135
+ export async function sendC2CImageMessage(
136
+ account: QQAccount,
137
+ options: {
138
+ toOpenid: string;
139
+ imageUrl: string;
140
+ messageId: string;
141
+ },
142
+ ): Promise<void> {
143
+ const token = await getAccessToken(account.appId, account.clientSecret);
144
+ const msgSeq = getNextMsgSeq(options.messageId);
145
+
146
+ let imageData = options.imageUrl;
147
+
148
+ if (!imageData.startsWith("http://") && !imageData.startsWith("https://")) {
149
+ const fs = await import("fs");
150
+ const buffer = fs.readFileSync(imageData);
151
+ const base64 = buffer.toString("base64");
152
+ const ext = imageData.split(".").pop()?.toLowerCase() || "png";
153
+ const mimeTypes: Record<string, string> = {
154
+ jpg: "image/jpeg",
155
+ jpeg: "image/jpeg",
156
+ png: "image/png",
157
+ gif: "image/gif",
158
+ webp: "image/webp",
159
+ };
160
+ const mimeType = mimeTypes[ext] || "image/png";
161
+ imageData = `data:${mimeType};base64,${base64}`;
162
+ }
163
+
164
+ const body = {
165
+ content: imageData,
166
+ msg_type: 7,
167
+ msg_seq: msgSeq,
168
+ msg_id: options.messageId,
169
+ };
170
+
171
+ await apiRequest(
172
+ token,
173
+ "POST",
174
+ `/v2/users/${options.toOpenid}/messages`,
175
+ body,
176
+ );
177
+ }
178
+
179
+ export async function sendC2CFileMessage(
180
+ account: QQAccount,
181
+ options: {
182
+ toOpenid: string;
183
+ fileData?: string;
184
+ fileUrl?: string;
185
+ fileName: string;
186
+ messageId: string;
187
+ },
188
+ ): Promise<void> {
189
+ const token = await getAccessToken(account.appId, account.clientSecret);
190
+ const msgSeq = getNextMsgSeq(options.messageId);
191
+
192
+ const body: Record<string, unknown> = {
193
+ file_name: options.fileName,
194
+ msg_type: 6,
195
+ msg_seq: msgSeq,
196
+ msg_id: options.messageId,
197
+ };
198
+
199
+ if (options.fileData) {
200
+ body.file_data = options.fileData;
201
+ } else if (options.fileUrl) {
202
+ body.file_url = options.fileUrl;
203
+ }
204
+
205
+ await apiRequest(
206
+ token,
207
+ "POST",
208
+ `/v2/users/${options.toOpenid}/messages`,
209
+ body,
210
+ );
211
+ }
212
+
213
+ export function clearTokenCache(): void {
214
+ tokenCache = null;
215
+ }
package/src/types.ts ADDED
@@ -0,0 +1,52 @@
1
+ export interface QQConfig {
2
+ appId: string;
3
+ clientSecret: string;
4
+ markdownSupport?: boolean;
5
+ }
6
+
7
+ export interface OpencodeConfig {
8
+ port: number;
9
+ hostname: string;
10
+ config?: Record<string, unknown>;
11
+ }
12
+
13
+ export interface AppConfig {
14
+ workingDir: string;
15
+ }
16
+
17
+ export interface Config {
18
+ qq: QQConfig;
19
+ opencode: OpencodeConfig;
20
+ app: AppConfig;
21
+ }
22
+
23
+ export interface QQMessageEvent {
24
+ id: string;
25
+ content: string;
26
+ timestamp: number;
27
+ author: {
28
+ user_openid: string;
29
+ username?: string;
30
+ };
31
+ attachments?: Array<{
32
+ url?: string;
33
+ file_type?: number;
34
+ name?: string;
35
+ }>;
36
+ message_scene?: {
37
+ ext?: string;
38
+ };
39
+ }
40
+
41
+ export interface ParsedMessage {
42
+ content: string;
43
+ imageUrls: string[];
44
+ quoteRef?: string;
45
+ quoteId?: string;
46
+ }
47
+
48
+ export interface SessionInfo {
49
+ id: string;
50
+ title?: string;
51
+ createdAt: Date;
52
+ }