@kirigaya/openclaw-onebot 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 (44) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +134 -0
  3. package/dist/channel.d.ts +100 -0
  4. package/dist/channel.js +173 -0
  5. package/dist/config.d.ts +6 -0
  6. package/dist/config.js +62 -0
  7. package/dist/connection.d.ts +94 -0
  8. package/dist/connection.js +426 -0
  9. package/dist/debug-log.d.ts +7 -0
  10. package/dist/debug-log.js +24 -0
  11. package/dist/gateway-proxy.d.ts +8 -0
  12. package/dist/gateway-proxy.js +36 -0
  13. package/dist/handlers/group-increase.d.ts +21 -0
  14. package/dist/handlers/group-increase.js +95 -0
  15. package/dist/handlers/process-inbound.d.ts +11 -0
  16. package/dist/handlers/process-inbound.js +224 -0
  17. package/dist/index.d.ts +11 -0
  18. package/dist/index.js +33 -0
  19. package/dist/load-script.d.ts +5 -0
  20. package/dist/load-script.js +22 -0
  21. package/dist/message.d.ts +6 -0
  22. package/dist/message.js +24 -0
  23. package/dist/reply-context.d.ts +12 -0
  24. package/dist/reply-context.js +33 -0
  25. package/dist/scheduler.d.ts +19 -0
  26. package/dist/scheduler.js +70 -0
  27. package/dist/sdk.d.ts +9 -0
  28. package/dist/sdk.js +36 -0
  29. package/dist/send.d.ts +23 -0
  30. package/dist/send.js +98 -0
  31. package/dist/service.d.ts +4 -0
  32. package/dist/service.js +71 -0
  33. package/dist/setup.d.ts +1 -0
  34. package/dist/setup.js +65 -0
  35. package/dist/tools.d.ts +18 -0
  36. package/dist/tools.js +188 -0
  37. package/dist/types.d.ts +28 -0
  38. package/dist/types.js +4 -0
  39. package/openclaw.plugin.json +72 -0
  40. package/package.json +74 -0
  41. package/skills/onebot-ops/SKILL.md +61 -0
  42. package/skills/onebot-ops/config.md +55 -0
  43. package/skills/onebot-ops/receive.md +85 -0
  44. package/skills/onebot-ops/send.md +39 -0
@@ -0,0 +1,426 @@
1
+ /**
2
+ * OneBot WebSocket 连接与 API 调用
3
+ *
4
+ * 图片消息:网络 URL 会先下载到本地再发送(兼容 Lagrange.Core retcode 1200),
5
+ * 并定期清理临时文件。
6
+ */
7
+ import WebSocket from "ws";
8
+ import { createServer } from "http";
9
+ import https from "https";
10
+ import http from "http";
11
+ import { writeFileSync, mkdirSync, readdirSync, statSync, unlinkSync } from "fs";
12
+ import { join } from "path";
13
+ import { tmpdir } from "os";
14
+ const IMAGE_TEMP_DIR = join(tmpdir(), "openclaw-onebot");
15
+ const DOWNLOAD_TIMEOUT_MS = 30000;
16
+ /** 使用 Node 内置 http(s) 下载 URL,避免 fetch 在某些环境下的兼容性问题 */
17
+ function downloadUrl(url) {
18
+ return new Promise((resolve, reject) => {
19
+ const lib = url.startsWith("https") ? https : http;
20
+ const req = lib.get(url, (res) => {
21
+ const redirect = res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location;
22
+ if (redirect) {
23
+ downloadUrl(redirect.startsWith("http") ? redirect : new URL(redirect, url).href).then(resolve).catch(reject);
24
+ return;
25
+ }
26
+ if (res.statusCode && res.statusCode >= 400) {
27
+ reject(new Error(`HTTP ${res.statusCode} ${res.statusMessage}`));
28
+ return;
29
+ }
30
+ const chunks = [];
31
+ res.on("data", (chunk) => chunks.push(chunk));
32
+ res.on("end", () => resolve(Buffer.concat(chunks)));
33
+ res.on("error", reject);
34
+ });
35
+ req.on("error", reject);
36
+ req.setTimeout(DOWNLOAD_TIMEOUT_MS, () => {
37
+ req.destroy();
38
+ reject(new Error("Download timeout"));
39
+ });
40
+ });
41
+ }
42
+ const IMAGE_TEMP_MAX_AGE_MS = 60 * 60 * 1000; // 1 小时
43
+ const IMAGE_TEMP_CLEANUP_INTERVAL_MS = 60 * 60 * 1000; // 每小时清理一次
44
+ let imageTempCleanupTimer = null;
45
+ /** 清理过期的临时图片文件 */
46
+ function cleanupImageTemp() {
47
+ try {
48
+ if (!readdirSync)
49
+ return;
50
+ const files = readdirSync(IMAGE_TEMP_DIR);
51
+ const now = Date.now();
52
+ for (const f of files) {
53
+ const p = join(IMAGE_TEMP_DIR, f);
54
+ try {
55
+ const st = statSync(p);
56
+ if (st.isFile() && now - st.mtimeMs > IMAGE_TEMP_MAX_AGE_MS) {
57
+ unlinkSync(p);
58
+ }
59
+ }
60
+ catch {
61
+ /* ignore */
62
+ }
63
+ }
64
+ }
65
+ catch {
66
+ /* dir not exist or readdir failed */
67
+ }
68
+ }
69
+ /** 将 mediaUrl 解析为可发送的 file 路径。网络 URL 下载到本地,base64 解码到本地,定期清理过期文件 */
70
+ async function resolveImageToLocalPath(image) {
71
+ const trimmed = image?.trim();
72
+ if (!trimmed)
73
+ throw new Error("Empty image");
74
+ if (/^https?:\/\//i.test(trimmed)) {
75
+ cleanupImageTemp();
76
+ const buf = await downloadUrl(trimmed);
77
+ const ext = (trimmed.match(/\.(png|jpg|jpeg|gif|webp|bmp)(?:\?|$)/i)?.[1] ?? "png").toLowerCase();
78
+ mkdirSync(IMAGE_TEMP_DIR, { recursive: true });
79
+ const tmpPath = join(IMAGE_TEMP_DIR, `img-${Date.now()}-${Math.random().toString(36).slice(2)}.${ext}`);
80
+ writeFileSync(tmpPath, buf);
81
+ return tmpPath.replace(/\\/g, "/");
82
+ }
83
+ if (trimmed.startsWith("base64://")) {
84
+ cleanupImageTemp();
85
+ const b64 = trimmed.slice(9);
86
+ const buf = Buffer.from(b64, "base64");
87
+ mkdirSync(IMAGE_TEMP_DIR, { recursive: true });
88
+ const tmpPath = join(IMAGE_TEMP_DIR, `img-${Date.now()}-${Math.random().toString(36).slice(2)}.png`);
89
+ writeFileSync(tmpPath, buf);
90
+ return tmpPath.replace(/\\/g, "/");
91
+ }
92
+ if (trimmed.startsWith("file://")) {
93
+ return trimmed.slice(7).replace(/\\/g, "/");
94
+ }
95
+ return trimmed.replace(/\\/g, "/");
96
+ }
97
+ /** 启动临时图片定期清理(每小时执行一次) */
98
+ export function startImageTempCleanup() {
99
+ stopImageTempCleanup();
100
+ imageTempCleanupTimer = setInterval(cleanupImageTemp, IMAGE_TEMP_CLEANUP_INTERVAL_MS);
101
+ }
102
+ /** 停止临时图片定期清理 */
103
+ export function stopImageTempCleanup() {
104
+ if (imageTempCleanupTimer) {
105
+ clearInterval(imageTempCleanupTimer);
106
+ imageTempCleanupTimer = null;
107
+ }
108
+ }
109
+ let ws = null;
110
+ let wsServer = null;
111
+ let httpServer = null;
112
+ const pendingEcho = new Map();
113
+ let echoCounter = 0;
114
+ let connectionReadyResolve = null;
115
+ const connectionReadyPromise = new Promise((r) => { connectionReadyResolve = r; });
116
+ function nextEcho() {
117
+ return `onebot-${Date.now()}-${++echoCounter}`;
118
+ }
119
+ export function handleEchoResponse(payload) {
120
+ if (payload?.echo && pendingEcho.has(payload.echo)) {
121
+ const h = pendingEcho.get(payload.echo);
122
+ h?.resolve(payload);
123
+ return true;
124
+ }
125
+ return false;
126
+ }
127
+ function getLogger() {
128
+ return globalThis.__onebotApi?.logger ?? {};
129
+ }
130
+ function sendOneBotAction(wsocket, action, params, log = getLogger()) {
131
+ const echo = nextEcho();
132
+ const payload = { action, params, echo };
133
+ return new Promise((resolve, reject) => {
134
+ const timeout = setTimeout(() => {
135
+ pendingEcho.delete(echo);
136
+ log.warn?.(`[onebot] sendOneBotAction ${action} timeout`);
137
+ reject(new Error(`OneBot action ${action} timeout`));
138
+ }, 15000);
139
+ pendingEcho.set(echo, {
140
+ resolve: (v) => {
141
+ clearTimeout(timeout);
142
+ pendingEcho.delete(echo);
143
+ if (v?.retcode !== 0)
144
+ log.warn?.(`[onebot] sendOneBotAction ${action} retcode=${v?.retcode} msg=${v?.msg ?? ""}`);
145
+ resolve(v);
146
+ },
147
+ });
148
+ wsocket.send(JSON.stringify(payload), (err) => {
149
+ if (err) {
150
+ pendingEcho.delete(echo);
151
+ clearTimeout(timeout);
152
+ reject(err);
153
+ }
154
+ });
155
+ });
156
+ }
157
+ export function getWs() {
158
+ return ws;
159
+ }
160
+ /** 为 WebSocket 设置 echo 响应处理(按需连接时需调用,以便 sendOneBotAction 能收到响应) */
161
+ function setupEchoHandler(socket) {
162
+ socket.on("message", (data) => {
163
+ try {
164
+ const payload = JSON.parse(data.toString());
165
+ handleEchoResponse(payload);
166
+ }
167
+ catch {
168
+ /* ignore */
169
+ }
170
+ });
171
+ }
172
+ /** 等待 WebSocket 连接就绪(service 启动后异步建立连接,发送前需先等待) */
173
+ export async function waitForConnection(timeoutMs = 30000) {
174
+ if (ws && ws.readyState === WebSocket.OPEN)
175
+ return ws;
176
+ const log = getLogger();
177
+ log.info?.("[onebot] waitForConnection: waiting for WebSocket...");
178
+ return Promise.race([
179
+ connectionReadyPromise.then(() => {
180
+ if (ws && ws.readyState === WebSocket.OPEN)
181
+ return ws;
182
+ throw new Error("OneBot WebSocket not connected");
183
+ }),
184
+ new Promise((_, reject) => setTimeout(() => reject(new Error(`OneBot WebSocket not connected after ${timeoutMs}ms. Ensure "openclaw gateway run" is running and OneBot (Lagrange.Core) is connected.`)), timeoutMs)),
185
+ ]);
186
+ }
187
+ /**
188
+ * 确保有可用的 WebSocket 连接。当 service 未启动时,
189
+ * forward-websocket 模式直接建立连接(message send 可独立运行);
190
+ * backward-websocket 模式需等待 gateway 的 service 建立连接。
191
+ */
192
+ export async function ensureConnection(getConfig, timeoutMs = 30000) {
193
+ if (ws && ws.readyState === WebSocket.OPEN)
194
+ return ws;
195
+ const config = getConfig();
196
+ if (!config)
197
+ throw new Error("OneBot not configured");
198
+ const log = getLogger();
199
+ if (config.type === "forward-websocket") {
200
+ log.info?.("[onebot] 连接 OneBot (forward-websocket)...");
201
+ const socket = await connectForward(config);
202
+ setupEchoHandler(socket);
203
+ setWs(socket);
204
+ return socket;
205
+ }
206
+ return waitForConnection(timeoutMs);
207
+ }
208
+ export async function sendPrivateMsg(userId, text, getConfig) {
209
+ const socket = getConfig
210
+ ? await ensureConnection(getConfig)
211
+ : await waitForConnection();
212
+ const res = await sendOneBotAction(socket, "send_private_msg", { user_id: userId, message: text });
213
+ if (res?.retcode !== 0) {
214
+ throw new Error(res?.msg ?? `OneBot send_private_msg failed (retcode=${res?.retcode})`);
215
+ }
216
+ return res?.data?.message_id;
217
+ }
218
+ export async function sendGroupMsg(groupId, text, getConfig) {
219
+ const socket = getConfig
220
+ ? await ensureConnection(getConfig)
221
+ : await waitForConnection();
222
+ const res = await sendOneBotAction(socket, "send_group_msg", { group_id: groupId, message: text });
223
+ if (res?.retcode !== 0) {
224
+ throw new Error(res?.msg ?? `OneBot send_group_msg failed (retcode=${res?.retcode})`);
225
+ }
226
+ return res?.data?.message_id;
227
+ }
228
+ export async function sendGroupImage(groupId, image, log = getLogger(), getConfig) {
229
+ log.info?.(`[onebot] sendGroupImage entry: groupId=${groupId} image=${image?.slice?.(0, 80) ?? ""}`);
230
+ const socket = getConfig ? await ensureConnection(getConfig) : await waitForConnection();
231
+ log.info?.(`222[onebot] sendGroupImage entry: groupId=${groupId} image=${image?.slice?.(0, 80) ?? ""}`);
232
+ try {
233
+ const filePath = image.startsWith("[") ? null : await resolveImageToLocalPath(image);
234
+ const seg = image.startsWith("[")
235
+ ? JSON.parse(image)
236
+ : [{ type: "image", data: { file: filePath } }];
237
+ log.info?.(`333[onebot] sendGroupImage entry: groupId=${groupId} image=${image?.slice?.(0, 80) ?? ""}`);
238
+ const res = await sendOneBotAction(socket, "send_group_msg", { group_id: groupId, message: seg }, log);
239
+ if (res?.retcode !== 0) {
240
+ throw new Error(res?.msg ?? `OneBot send_group_msg (image) failed (retcode=${res?.retcode})`);
241
+ }
242
+ log.info?.(`[onebot] sendGroupImage done: retcode=${res?.retcode ?? "?"}`);
243
+ return res?.data?.message_id;
244
+ }
245
+ catch (error) {
246
+ log.warn?.(`[onebot] sendGroupImage error: ${error}`);
247
+ }
248
+ }
249
+ export async function sendPrivateImage(userId, image, log = getLogger(), getConfig) {
250
+ log.info?.(`[onebot] sendPrivateImage entry: userId=${userId} image=${image?.slice?.(0, 80) ?? ""}`);
251
+ const socket = getConfig ? await ensureConnection(getConfig) : await waitForConnection();
252
+ const filePath = image.startsWith("[") ? null : await resolveImageToLocalPath(image);
253
+ const seg = image.startsWith("[")
254
+ ? JSON.parse(image)
255
+ : [{ type: "image", data: { file: filePath } }];
256
+ const res = await sendOneBotAction(socket, "send_private_msg", { user_id: userId, message: seg }, log);
257
+ if (res?.retcode !== 0) {
258
+ throw new Error(res?.msg ?? `OneBot send_private_msg (image) failed (retcode=${res?.retcode})`);
259
+ }
260
+ log.info?.(`[onebot] sendPrivateImage done: retcode=${res?.retcode ?? "?"}`);
261
+ return res?.data?.message_id;
262
+ }
263
+ export async function uploadGroupFile(groupId, file, name) {
264
+ if (!ws || ws.readyState !== WebSocket.OPEN)
265
+ throw new Error("OneBot WebSocket not connected");
266
+ await sendOneBotAction(ws, "upload_group_file", { group_id: groupId, file, name });
267
+ }
268
+ export async function uploadPrivateFile(userId, file, name) {
269
+ if (!ws || ws.readyState !== WebSocket.OPEN)
270
+ throw new Error("OneBot WebSocket not connected");
271
+ await sendOneBotAction(ws, "upload_private_file", { user_id: userId, file, name });
272
+ }
273
+ /** 撤回消息 */
274
+ export async function deleteMsg(messageId) {
275
+ if (!ws || ws.readyState !== WebSocket.OPEN)
276
+ throw new Error("OneBot WebSocket not connected");
277
+ await sendOneBotAction(ws, "delete_msg", { message_id: messageId });
278
+ }
279
+ /**
280
+ * 对消息进行表情回应(Lagrange/QQ NT 扩展 API)
281
+ * @param message_id 需要回应的消息 ID(用户发送的消息)
282
+ * @param emoji_id 表情 ID,1 通常为点赞
283
+ * @param is_set true 添加,false 取消
284
+ */
285
+ export async function setMsgEmojiLike(message_id, emoji_id, is_set = true) {
286
+ if (!ws || ws.readyState !== WebSocket.OPEN)
287
+ throw new Error("OneBot WebSocket not connected");
288
+ await sendOneBotAction(ws, "set_msg_emoji_like", { message_id, emoji_id, is_set });
289
+ }
290
+ /** 获取陌生人信息(含 nickname) */
291
+ export async function getStrangerInfo(userId) {
292
+ if (!ws || ws.readyState !== WebSocket.OPEN)
293
+ return null;
294
+ try {
295
+ const res = await sendOneBotAction(ws, "get_stranger_info", { user_id: userId, no_cache: false });
296
+ if (res?.retcode === 0 && res?.data)
297
+ return { nickname: String(res.data.nickname ?? "") };
298
+ return null;
299
+ }
300
+ catch {
301
+ return null;
302
+ }
303
+ }
304
+ /** 获取群成员信息(含 nickname、card) */
305
+ export async function getGroupMemberInfo(groupId, userId) {
306
+ if (!ws || ws.readyState !== WebSocket.OPEN)
307
+ return null;
308
+ try {
309
+ const res = await sendOneBotAction(ws, "get_group_member_info", { group_id: groupId, user_id: userId, no_cache: false });
310
+ if (res?.retcode === 0 && res?.data) {
311
+ return { nickname: String(res.data.nickname ?? ""), card: String(res.data.card ?? "") };
312
+ }
313
+ return null;
314
+ }
315
+ catch {
316
+ return null;
317
+ }
318
+ }
319
+ /** 获取群信息(含 group_name) */
320
+ export async function getGroupInfo(groupId) {
321
+ if (!ws || ws.readyState !== WebSocket.OPEN)
322
+ return null;
323
+ try {
324
+ const res = await sendOneBotAction(ws, "get_group_info", { group_id: groupId, no_cache: false });
325
+ if (res?.retcode === 0 && res?.data)
326
+ return { group_name: String(res.data.group_name ?? "") };
327
+ return null;
328
+ }
329
+ catch {
330
+ return null;
331
+ }
332
+ }
333
+ /** QQ 头像 URL,s=640 为常用尺寸 */
334
+ export function getAvatarUrl(userId, size = 640) {
335
+ return `https://q1.qlogo.cn/g?b=qq&nk=${userId}&s=${size}`;
336
+ }
337
+ /** 获取单条消息(需 OneBot 实现支持) */
338
+ export async function getMsg(messageId) {
339
+ if (!ws || ws.readyState !== WebSocket.OPEN)
340
+ return null;
341
+ try {
342
+ const res = await sendOneBotAction(ws, "get_msg", { message_id: messageId });
343
+ if (res?.retcode === 0 && res?.data)
344
+ return res.data;
345
+ return null;
346
+ }
347
+ catch {
348
+ return null;
349
+ }
350
+ }
351
+ /**
352
+ * 获取群聊历史消息(Lagrange.Core 扩展 API,go-cqhttp 等可能不支持)
353
+ * @param groupId 群号
354
+ * @param opts message_seq 起始序号;message_id 起始消息 ID;count 数量
355
+ */
356
+ export async function getGroupMsgHistory(groupId, opts = { count: 20 }) {
357
+ if (!ws || ws.readyState !== WebSocket.OPEN)
358
+ return [];
359
+ try {
360
+ const res = await sendOneBotAction(ws, "get_group_msg_history", {
361
+ group_id: groupId,
362
+ message_seq: opts.message_seq,
363
+ message_id: opts.message_id,
364
+ count: opts.count ?? 20,
365
+ });
366
+ if (res?.retcode === 0 && res?.data?.messages)
367
+ return res.data.messages;
368
+ return [];
369
+ }
370
+ catch {
371
+ return [];
372
+ }
373
+ }
374
+ export async function connectForward(config) {
375
+ const path = config.path ?? "/onebot/v11/ws";
376
+ const pathNorm = path.startsWith("/") ? path : `/${path}`;
377
+ const addr = `ws://${config.host}:${config.port}${pathNorm}`;
378
+ const headers = {};
379
+ if (config.accessToken) {
380
+ headers["Authorization"] = `Bearer ${config.accessToken}`;
381
+ }
382
+ const w = new WebSocket(addr, { headers });
383
+ await new Promise((resolve, reject) => {
384
+ w.on("open", () => resolve());
385
+ w.on("error", reject);
386
+ });
387
+ return w;
388
+ }
389
+ export async function createServerAndWait(config) {
390
+ const { WebSocketServer } = await import("ws");
391
+ const server = createServer();
392
+ httpServer = server;
393
+ const wss = new WebSocketServer({
394
+ server,
395
+ path: config.path ?? "/onebot/v11/ws",
396
+ });
397
+ const host = config.host || "0.0.0.0";
398
+ server.listen(config.port, host);
399
+ wsServer = wss;
400
+ return new Promise((resolve) => {
401
+ wss.on("connection", (socket) => {
402
+ resolve(socket);
403
+ });
404
+ });
405
+ }
406
+ export function setWs(socket) {
407
+ ws = socket;
408
+ if (socket && socket.readyState === WebSocket.OPEN && connectionReadyResolve) {
409
+ connectionReadyResolve();
410
+ connectionReadyResolve = null;
411
+ }
412
+ }
413
+ export function stopConnection() {
414
+ if (ws) {
415
+ ws.close();
416
+ ws = null;
417
+ }
418
+ if (wsServer) {
419
+ wsServer.close();
420
+ wsServer = null;
421
+ }
422
+ if (httpServer) {
423
+ httpServer.close();
424
+ httpServer = null;
425
+ }
426
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * 调试日志:写入文件,便于追踪 sendMedia / sendText 调用链
3
+ * 开发模式下生效(NODE_ENV !== 'production')
4
+ * 日志路径:process.cwd()/openclaw-onebot-debug.log
5
+ */
6
+ export declare function isDevLogEnabled(): boolean;
7
+ export declare function debugLog(layer: string, msg: string, data?: Record<string, unknown>): void;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * 调试日志:写入文件,便于追踪 sendMedia / sendText 调用链
3
+ * 开发模式下生效(NODE_ENV !== 'production')
4
+ * 日志路径:process.cwd()/openclaw-onebot-debug.log
5
+ */
6
+ import fs from "node:fs";
7
+ import path from "node:path";
8
+ function getLogPath() {
9
+ return path.join(process.cwd(), "openclaw-onebot-debug.log");
10
+ }
11
+ export function isDevLogEnabled() {
12
+ if (process.env.OPENCLAW_ONEBOT_DEBUG === "1")
13
+ return true;
14
+ return process.env.NODE_ENV !== "production";
15
+ }
16
+ export function debugLog(layer, msg, data) {
17
+ if (!isDevLogEnabled())
18
+ return;
19
+ try {
20
+ const line = `${new Date().toISOString()} [${layer}] ${msg}${data ? " " + JSON.stringify(data) : ""}\n`;
21
+ fs.appendFileSync(getLogPath(), line);
22
+ }
23
+ catch (_) { }
24
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * 当 openclaw message send 在独立进程运行(无 WebSocket)时,
3
+ * 通过 Gateway HTTP API /tools/invoke 代理发送
4
+ */
5
+ export declare function invokeGatewayTool(cfg: Record<string, unknown>, tool: string, args: Record<string, unknown>): Promise<{
6
+ ok: boolean;
7
+ error?: string;
8
+ }>;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * 当 openclaw message send 在独立进程运行(无 WebSocket)时,
3
+ * 通过 Gateway HTTP API /tools/invoke 代理发送
4
+ */
5
+ export async function invokeGatewayTool(cfg, tool, args) {
6
+ const gw = cfg?.gateway;
7
+ const port = gw?.port ?? 18789;
8
+ const bind = gw?.bind ?? "loopback";
9
+ const host = bind === "loopback" ? "127.0.0.1" : "0.0.0.0";
10
+ const auth = gw?.auth;
11
+ const token = auth?.token;
12
+ const url = `http://${host}:${port}/tools/invoke`;
13
+ const headers = { "Content-Type": "application/json" };
14
+ if (token)
15
+ headers["Authorization"] = `Bearer ${token}`;
16
+ try {
17
+ const res = await fetch(url, {
18
+ method: "POST",
19
+ headers,
20
+ body: JSON.stringify({ tool, action: "json", args }),
21
+ });
22
+ if (!res.ok) {
23
+ return { ok: false, error: `Gateway API ${res.status}: ${await res.text().catch(() => "")}` };
24
+ }
25
+ const data = (await res.json().catch(() => ({})));
26
+ if (data?.error)
27
+ return { ok: false, error: data.error };
28
+ return { ok: true };
29
+ }
30
+ catch (e) {
31
+ return {
32
+ ok: false,
33
+ error: e instanceof Error ? e.message : String(e),
34
+ };
35
+ }
36
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * 新成员入群欢迎
3
+ *
4
+ * 支持:
5
+ * 1. 简单文本模板(message),占位符:{name}、{userId}、{groupName}、{groupId}、{avatarUrl}
6
+ * 2. 自定义 handler 脚本,可生成图片并返回,接收完整上下文
7
+ */
8
+ import type { OneBotMessage } from "../types.js";
9
+ export interface GroupIncreaseContext {
10
+ groupId: number;
11
+ groupName: string;
12
+ userId: number;
13
+ userName: string;
14
+ avatarUrl: string;
15
+ }
16
+ export interface GroupIncreaseResult {
17
+ text?: string;
18
+ imagePath?: string;
19
+ imageUrl?: string;
20
+ }
21
+ export declare function handleGroupIncrease(api: any, msg: OneBotMessage): Promise<void>;
@@ -0,0 +1,95 @@
1
+ /**
2
+ * 新成员入群欢迎
3
+ *
4
+ * 支持:
5
+ * 1. 简单文本模板(message),占位符:{name}、{userId}、{groupName}、{groupId}、{avatarUrl}
6
+ * 2. 自定义 handler 脚本,可生成图片并返回,接收完整上下文
7
+ */
8
+ import { sendGroupMsg, sendGroupImage, getStrangerInfo, getGroupMemberInfo, getGroupInfo, getAvatarUrl, } from "../connection.js";
9
+ import { loadScript } from "../load-script.js";
10
+ import { resolve } from "path";
11
+ async function resolveContext(groupId, userId) {
12
+ const [groupInfo, memberInfo] = await Promise.all([
13
+ getGroupInfo(groupId),
14
+ getGroupMemberInfo(groupId, userId),
15
+ ]);
16
+ const groupName = groupInfo?.group_name ?? String(groupId);
17
+ let userName;
18
+ if (memberInfo) {
19
+ userName = (memberInfo.card || memberInfo.nickname || "").trim() || memberInfo.nickname || String(userId);
20
+ }
21
+ else {
22
+ const stranger = await getStrangerInfo(userId);
23
+ userName = stranger?.nickname?.trim() || String(userId);
24
+ }
25
+ return {
26
+ groupId,
27
+ groupName,
28
+ userId,
29
+ userName,
30
+ avatarUrl: getAvatarUrl(userId),
31
+ };
32
+ }
33
+ function applyTemplate(template, ctx) {
34
+ return template
35
+ .replace(/\{name\}/g, ctx.userName)
36
+ .replace(/\{userId\}/g, String(ctx.userId))
37
+ .replace(/\{groupName\}/g, ctx.groupName)
38
+ .replace(/\{groupId\}/g, String(ctx.groupId))
39
+ .replace(/\{avatarUrl\}/g, ctx.avatarUrl);
40
+ }
41
+ export async function handleGroupIncrease(api, msg) {
42
+ const cfg = api.config;
43
+ const gi = cfg?.channels?.onebot?.groupIncrease;
44
+ if (!gi?.enabled)
45
+ return;
46
+ const groupId = msg.group_id;
47
+ const userId = msg.user_id;
48
+ let ctx;
49
+ try {
50
+ ctx = await resolveContext(groupId, userId);
51
+ }
52
+ catch (e) {
53
+ api.logger?.error?.(`[onebot] groupIncrease resolveContext failed: ${e?.message}`);
54
+ return;
55
+ }
56
+ let result = {};
57
+ const handlerPath = gi?.handler;
58
+ if (handlerPath?.trim()) {
59
+ try {
60
+ const mod = await loadScript(handlerPath);
61
+ const fn = mod?.default ?? mod?.generate ?? mod;
62
+ if (typeof fn === "function") {
63
+ result = (await fn(ctx)) ?? {};
64
+ }
65
+ }
66
+ catch (e) {
67
+ api.logger?.error?.(`[onebot] groupIncrease handler failed: ${e?.message}`);
68
+ }
69
+ }
70
+ const message = gi?.message;
71
+ if (message?.trim() && !result.text) {
72
+ result.text = applyTemplate(message, ctx);
73
+ }
74
+ const text = (result.text ?? "").trim();
75
+ const imagePath = result.imagePath?.trim();
76
+ const imageUrl = result.imageUrl?.trim();
77
+ if (!text && !imagePath && !imageUrl)
78
+ return;
79
+ try {
80
+ if (text)
81
+ await sendGroupMsg(groupId, text);
82
+ if (imagePath) {
83
+ const abs = imagePath.startsWith("file://") || imagePath.startsWith("http://") || imagePath.startsWith("https://")
84
+ ? imagePath
85
+ : resolve(process.cwd(), imagePath);
86
+ await sendGroupImage(groupId, abs);
87
+ }
88
+ if (imageUrl && !imagePath)
89
+ await sendGroupImage(groupId, imageUrl);
90
+ api.logger?.info?.(`[onebot] sent group welcome to ${groupId} for user ${userId} (${ctx.userName})`);
91
+ }
92
+ catch (e) {
93
+ api.logger?.error?.(`[onebot] group welcome failed: ${e?.message}`);
94
+ }
95
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * 入站消息处理
3
+ */
4
+ import type { OneBotMessage } from "../types.js";
5
+ export declare const sessionHistories: Map<string, {
6
+ sender: string;
7
+ body: string;
8
+ timestamp: number;
9
+ messageId: string;
10
+ }[]>;
11
+ export declare function processInboundMessage(api: any, msg: OneBotMessage): Promise<void>;