@kirigaya/openclaw-onebot 1.0.2 → 1.0.3
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.
- package/README.md +79 -4
- package/dist/channel.js +2 -2
- package/dist/config.d.ts +6 -0
- package/dist/config.js +17 -0
- package/dist/connection.d.ts +10 -0
- package/dist/connection.js +92 -4
- package/dist/handlers/group-increase.d.ts +2 -1
- package/dist/handlers/group-increase.js +78 -12
- package/dist/handlers/process-inbound.d.ts +18 -0
- package/dist/handlers/process-inbound.js +245 -24
- package/dist/index.js +2 -0
- package/dist/load-script.d.ts +5 -1
- package/dist/load-script.js +7 -3
- package/dist/markdown-to-html.d.ts +6 -0
- package/dist/markdown-to-html.js +75 -0
- package/dist/markdown.d.ts +7 -0
- package/dist/markdown.js +43 -0
- package/dist/message.d.ts +6 -0
- package/dist/message.js +45 -5
- package/dist/og-image.d.ts +5 -0
- package/dist/og-image.js +45 -0
- package/dist/reply-context.d.ts +19 -0
- package/dist/reply-context.js +54 -0
- package/dist/send-debug-log.d.ts +27 -0
- package/dist/send-debug-log.js +28 -0
- package/dist/send.d.ts +16 -2
- package/dist/send.js +65 -8
- package/dist/setup.js +37 -6
- package/dist/tools.js +6 -2
- package/openclaw.plugin.json +34 -4
- package/package.json +9 -2
- package/skills/onebot-ops/config.md +71 -55
- package/skills/onebot-ops/receive.md +88 -12
package/README.md
CHANGED
|
@@ -1,9 +1,23 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
1
3
|
# openclaw-onebot
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
[OpenClaw](https://openclaw.ai) 的 **OneBot v11 协议**(QQ/Lagrange.Core、go-cqhttp 等)渠道插件。
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/@kirigaya/openclaw-onebot)
|
|
8
|
+
[](https://github.com/LSTM-Kirigaya/openclaw-onebot)
|
|
9
|
+
[](LICENSE)
|
|
10
|
+
[](https://nodejs.org)
|
|
11
|
+
[](https://www.typescriptlang.org/)
|
|
12
|
+
[](https://openclaw.ai)
|
|
13
|
+
|
|
14
|
+
</div>
|
|
4
15
|
|
|
5
|
-
|
|
6
|
-
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 教程
|
|
19
|
+
|
|
20
|
+
📖 **完整安装与配置教程**:[让 QQ 接入 openclaw!让你的助手掌管千人大群](https://kirigaya.cn/blog/article?seq=368)
|
|
7
21
|
|
|
8
22
|
## 功能
|
|
9
23
|
|
|
@@ -13,7 +27,6 @@
|
|
|
13
27
|
- ✅ TUI 配置向导:`openclaw onebot setup`
|
|
14
28
|
- ✅ 新成员入群欢迎
|
|
15
29
|
- ✅ 通过 `openclaw message send` CLI 发送(无 Agent 工具,降低 token 消耗)
|
|
16
|
-
- ✅ **内置定时任务**:配置 `cronJobs` 后,在指定时间直接执行脚本并推送到群聊,**无需 AI 介入**
|
|
17
30
|
|
|
18
31
|
## 安装
|
|
19
32
|
|
|
@@ -65,6 +78,68 @@ openclaw message send --channel onebot --target group:987654321 --media "file://
|
|
|
65
78
|
|
|
66
79
|
`--target` 格式:`user:QQ号` 或 `group:群号`。回复场景由 deliver 自动投递,Agent 输出 text/mediaUrl 即会送达。
|
|
67
80
|
|
|
81
|
+
## 新成员入群欢迎(自定义图片)
|
|
82
|
+
|
|
83
|
+
当有新成员加入群时,可根据其 ID 信息生成欢迎图片并发送。详见 [receive.md](skills/onebot-ops/receive.md#新成员入群欢迎)。
|
|
84
|
+
|
|
85
|
+
1. 在 `openclaw.json` 中配置:
|
|
86
|
+
|
|
87
|
+
```json
|
|
88
|
+
{
|
|
89
|
+
"channels": {
|
|
90
|
+
"onebot": {
|
|
91
|
+
"groupIncrease": {
|
|
92
|
+
"enabled": true,
|
|
93
|
+
"command": "npx tsx src/openclaw/trigger/welcome.ts",
|
|
94
|
+
"cwd": "C:/path/to/Tiphareth"
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
2. `command` 在 `cwd` 下用系统 shell 执行,环境变量传入 `GROUP_ID`、`GROUP_NAME`、`USER_ID`、`USER_NAME`、`AVATAR_URL`。命令可调用 `openclaw message send` 自行发送,或向 stdout 输出 JSON 行供 handler 发送。
|
|
102
|
+
|
|
103
|
+
3. 测试:`npm run test:group-increase-handler`(DRY_RUN 模式,仅生成图片)
|
|
104
|
+
|
|
105
|
+
## 回复白名单
|
|
106
|
+
|
|
107
|
+
默认为空回复所有人的消息。如果设置的话,那么机器人就只会回复设置的数组里的用户的消息。
|
|
108
|
+
|
|
109
|
+
```json
|
|
110
|
+
{
|
|
111
|
+
"channels": {
|
|
112
|
+
"onebot": {
|
|
113
|
+
"whitelistUserIds": [1193466151],
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## 新人入群触发器
|
|
120
|
+
|
|
121
|
+
如果有人入群之后,可以通过这个来实现触发器。
|
|
122
|
+
|
|
123
|
+
```json
|
|
124
|
+
{
|
|
125
|
+
"channels": {
|
|
126
|
+
"onebot": {
|
|
127
|
+
"groupIncrease": {
|
|
128
|
+
"enabled": true,
|
|
129
|
+
"command": "npx tsx welcome.ts",
|
|
130
|
+
"cwd": "/path/to/triggers"
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
实现的脚本必须支持这三个参数:
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
--userId ${userId} --username ${username} --groupId ${groupId}
|
|
141
|
+
```
|
|
142
|
+
|
|
68
143
|
## 测试连接
|
|
69
144
|
|
|
70
145
|
项目内提供测试脚本(需 `.env` 或环境变量):
|
package/dist/channel.js
CHANGED
|
@@ -136,7 +136,7 @@ export const OneBotChannelPlugin = {
|
|
|
136
136
|
}
|
|
137
137
|
const getConfig = () => getOneBotConfig(api, accountId);
|
|
138
138
|
try {
|
|
139
|
-
const result = await sendTextMessage(to, text, getConfig);
|
|
139
|
+
const result = await sendTextMessage(to, text, getConfig, cfg);
|
|
140
140
|
if (!result.ok) {
|
|
141
141
|
return { channel: "onebot", ok: false, messageId: "", error: new Error(result.error) };
|
|
142
142
|
}
|
|
@@ -159,7 +159,7 @@ export const OneBotChannelPlugin = {
|
|
|
159
159
|
}
|
|
160
160
|
const getConfig = () => getOneBotConfig(api, accountId);
|
|
161
161
|
try {
|
|
162
|
-
const result = await sendMediaMessage(to, mediaUrl, text, getConfig);
|
|
162
|
+
const result = await sendMediaMessage(to, mediaUrl, text, getConfig, cfg);
|
|
163
163
|
if (!result.ok) {
|
|
164
164
|
return { channel: "onebot", ok: false, messageId: "", error: new Error(result.error) };
|
|
165
165
|
}
|
package/dist/config.d.ts
CHANGED
|
@@ -3,4 +3,10 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import type { OneBotAccountConfig } from "./types.js";
|
|
5
5
|
export declare function getOneBotConfig(api: any, accountId?: string): OneBotAccountConfig | null;
|
|
6
|
+
/** 是否将机器人回复中的 Markdown 渲染为纯文本再发送,默认 true */
|
|
7
|
+
export declare function getRenderMarkdownToPlain(cfg: any): boolean;
|
|
8
|
+
/** 是否将连续多个换行压缩为单个换行,默认 true(AI 常输出 \n\n 导致双空行) */
|
|
9
|
+
export declare function getCollapseDoubleNewlines(cfg: any): boolean;
|
|
10
|
+
/** 白名单 QQ 号列表,为空则所有人可回复;非空则仅白名单内用户可触发 AI */
|
|
11
|
+
export declare function getWhitelistUserIds(cfg: any): number[];
|
|
6
12
|
export declare function listAccountIds(apiOrCfg: any): string[];
|
package/dist/config.js
CHANGED
|
@@ -50,6 +50,23 @@ export function getOneBotConfig(api, accountId) {
|
|
|
50
50
|
}
|
|
51
51
|
return null;
|
|
52
52
|
}
|
|
53
|
+
/** 是否将机器人回复中的 Markdown 渲染为纯文本再发送,默认 true */
|
|
54
|
+
export function getRenderMarkdownToPlain(cfg) {
|
|
55
|
+
const v = cfg?.channels?.onebot?.renderMarkdownToPlain;
|
|
56
|
+
return v === undefined ? true : Boolean(v);
|
|
57
|
+
}
|
|
58
|
+
/** 是否将连续多个换行压缩为单个换行,默认 true(AI 常输出 \n\n 导致双空行) */
|
|
59
|
+
export function getCollapseDoubleNewlines(cfg) {
|
|
60
|
+
const v = cfg?.channels?.onebot?.collapseDoubleNewlines;
|
|
61
|
+
return v === undefined ? true : Boolean(v);
|
|
62
|
+
}
|
|
63
|
+
/** 白名单 QQ 号列表,为空则所有人可回复;非空则仅白名单内用户可触发 AI */
|
|
64
|
+
export function getWhitelistUserIds(cfg) {
|
|
65
|
+
const v = cfg?.channels?.onebot?.whitelistUserIds;
|
|
66
|
+
if (!Array.isArray(v))
|
|
67
|
+
return [];
|
|
68
|
+
return v.filter((x) => typeof x === "number" || (typeof x === "string" && /^\d+$/.test(x))).map((x) => Number(x));
|
|
69
|
+
}
|
|
53
70
|
export function listAccountIds(apiOrCfg) {
|
|
54
71
|
const cfg = apiOrCfg?.config ?? apiOrCfg ?? globalThis.__onebotGatewayConfig;
|
|
55
72
|
const accounts = cfg?.channels?.onebot?.accounts;
|
package/dist/connection.d.ts
CHANGED
|
@@ -26,6 +26,16 @@ export declare function sendGroupImage(groupId: number, image: string, log?: {
|
|
|
26
26
|
info?: (s: string) => void;
|
|
27
27
|
warn?: (s: string) => void;
|
|
28
28
|
}, getConfig?: () => OneBotAccountConfig | null): Promise<number | undefined>;
|
|
29
|
+
/** 发送群合并转发消息。messages 为节点数组,每节点 { type: "node", data: { id } } 或 { type: "node", data: { user_id, nickname, content } } */
|
|
30
|
+
export declare function sendGroupForwardMsg(groupId: number, messages: Array<{
|
|
31
|
+
type: string;
|
|
32
|
+
data: Record<string, unknown>;
|
|
33
|
+
}>, getConfig?: () => OneBotAccountConfig | null): Promise<void>;
|
|
34
|
+
/** 发送私聊合并转发消息 */
|
|
35
|
+
export declare function sendPrivateForwardMsg(userId: number, messages: Array<{
|
|
36
|
+
type: string;
|
|
37
|
+
data: Record<string, unknown>;
|
|
38
|
+
}>, getConfig?: () => OneBotAccountConfig | null): Promise<void>;
|
|
29
39
|
export declare function sendPrivateImage(userId: number, image: string, log?: {
|
|
30
40
|
info?: (s: string) => void;
|
|
31
41
|
warn?: (s: string) => void;
|
package/dist/connection.js
CHANGED
|
@@ -11,6 +11,8 @@ import http from "http";
|
|
|
11
11
|
import { writeFileSync, mkdirSync, readdirSync, statSync, unlinkSync } from "fs";
|
|
12
12
|
import { join } from "path";
|
|
13
13
|
import { tmpdir } from "os";
|
|
14
|
+
import { logSend } from "./send-debug-log.js";
|
|
15
|
+
import { shouldBlockSendInForwardMode, getActiveReplyTarget, getActiveReplySessionId } from "./reply-context.js";
|
|
14
16
|
const IMAGE_TEMP_DIR = join(tmpdir(), "openclaw-onebot");
|
|
15
17
|
const DOWNLOAD_TIMEOUT_MS = 30000;
|
|
16
18
|
/** 使用 Node 内置 http(s) 下载 URL,避免 fetch 在某些环境下的兼容性问题 */
|
|
@@ -206,6 +208,18 @@ export async function ensureConnection(getConfig, timeoutMs = 30000) {
|
|
|
206
208
|
return waitForConnection(timeoutMs);
|
|
207
209
|
}
|
|
208
210
|
export async function sendPrivateMsg(userId, text, getConfig) {
|
|
211
|
+
if (shouldBlockSendInForwardMode("private", userId)) {
|
|
212
|
+
logSend("connection", "sendPrivateMsg", { targetId: userId, blocked: true, sessionId: getActiveReplyTarget(), replySessionId: getActiveReplySessionId() });
|
|
213
|
+
return undefined;
|
|
214
|
+
}
|
|
215
|
+
logSend("connection", "sendPrivateMsg", {
|
|
216
|
+
targetType: "user",
|
|
217
|
+
targetId: userId,
|
|
218
|
+
textPreview: text?.slice(0, 80),
|
|
219
|
+
textLen: text?.length,
|
|
220
|
+
sessionId: getActiveReplyTarget(),
|
|
221
|
+
replySessionId: getActiveReplySessionId(),
|
|
222
|
+
});
|
|
209
223
|
const socket = getConfig
|
|
210
224
|
? await ensureConnection(getConfig)
|
|
211
225
|
: await waitForConnection();
|
|
@@ -213,9 +227,23 @@ export async function sendPrivateMsg(userId, text, getConfig) {
|
|
|
213
227
|
if (res?.retcode !== 0) {
|
|
214
228
|
throw new Error(res?.msg ?? `OneBot send_private_msg failed (retcode=${res?.retcode})`);
|
|
215
229
|
}
|
|
216
|
-
|
|
230
|
+
const mid = res?.data?.message_id;
|
|
231
|
+
logSend("connection", "sendPrivateMsg", { targetId: userId, messageId: mid, sessionId: getActiveReplyTarget(), replySessionId: getActiveReplySessionId() });
|
|
232
|
+
return mid;
|
|
217
233
|
}
|
|
218
234
|
export async function sendGroupMsg(groupId, text, getConfig) {
|
|
235
|
+
if (shouldBlockSendInForwardMode("group", groupId)) {
|
|
236
|
+
logSend("connection", "sendGroupMsg", { targetId: groupId, blocked: true, sessionId: getActiveReplyTarget(), replySessionId: getActiveReplySessionId() });
|
|
237
|
+
return undefined;
|
|
238
|
+
}
|
|
239
|
+
logSend("connection", "sendGroupMsg", {
|
|
240
|
+
targetType: "group",
|
|
241
|
+
targetId: groupId,
|
|
242
|
+
textPreview: text?.slice(0, 80),
|
|
243
|
+
textLen: text?.length,
|
|
244
|
+
sessionId: getActiveReplyTarget(),
|
|
245
|
+
replySessionId: getActiveReplySessionId(),
|
|
246
|
+
});
|
|
219
247
|
const socket = getConfig
|
|
220
248
|
? await ensureConnection(getConfig)
|
|
221
249
|
: await waitForConnection();
|
|
@@ -223,9 +251,22 @@ export async function sendGroupMsg(groupId, text, getConfig) {
|
|
|
223
251
|
if (res?.retcode !== 0) {
|
|
224
252
|
throw new Error(res?.msg ?? `OneBot send_group_msg failed (retcode=${res?.retcode})`);
|
|
225
253
|
}
|
|
226
|
-
|
|
254
|
+
const mid = res?.data?.message_id;
|
|
255
|
+
logSend("connection", "sendGroupMsg", { targetId: groupId, messageId: mid, sessionId: getActiveReplyTarget(), replySessionId: getActiveReplySessionId() });
|
|
256
|
+
return mid;
|
|
227
257
|
}
|
|
228
258
|
export async function sendGroupImage(groupId, image, log = getLogger(), getConfig) {
|
|
259
|
+
if (shouldBlockSendInForwardMode("group", groupId)) {
|
|
260
|
+
logSend("connection", "sendGroupImage", { targetId: groupId, blocked: true, sessionId: getActiveReplyTarget(), replySessionId: getActiveReplySessionId() });
|
|
261
|
+
return undefined;
|
|
262
|
+
}
|
|
263
|
+
logSend("connection", "sendGroupImage", {
|
|
264
|
+
targetType: "group",
|
|
265
|
+
targetId: groupId,
|
|
266
|
+
imagePreview: image?.slice?.(0, 60),
|
|
267
|
+
sessionId: getActiveReplyTarget(),
|
|
268
|
+
replySessionId: getActiveReplySessionId(),
|
|
269
|
+
});
|
|
229
270
|
log.info?.(`[onebot] sendGroupImage entry: groupId=${groupId} image=${image?.slice?.(0, 80) ?? ""}`);
|
|
230
271
|
const socket = getConfig ? await ensureConnection(getConfig) : await waitForConnection();
|
|
231
272
|
log.info?.(`222[onebot] sendGroupImage entry: groupId=${groupId} image=${image?.slice?.(0, 80) ?? ""}`);
|
|
@@ -240,13 +281,58 @@ export async function sendGroupImage(groupId, image, log = getLogger(), getConfi
|
|
|
240
281
|
throw new Error(res?.msg ?? `OneBot send_group_msg (image) failed (retcode=${res?.retcode})`);
|
|
241
282
|
}
|
|
242
283
|
log.info?.(`[onebot] sendGroupImage done: retcode=${res?.retcode ?? "?"}`);
|
|
243
|
-
|
|
284
|
+
const mid = res?.data?.message_id;
|
|
285
|
+
logSend("connection", "sendGroupImage", { targetId: groupId, messageId: mid, sessionId: getActiveReplyTarget(), replySessionId: getActiveReplySessionId() });
|
|
286
|
+
return mid;
|
|
244
287
|
}
|
|
245
288
|
catch (error) {
|
|
246
289
|
log.warn?.(`[onebot] sendGroupImage error: ${error}`);
|
|
247
290
|
}
|
|
248
291
|
}
|
|
292
|
+
/** 发送群合并转发消息。messages 为节点数组,每节点 { type: "node", data: { id } } 或 { type: "node", data: { user_id, nickname, content } } */
|
|
293
|
+
export async function sendGroupForwardMsg(groupId, messages, getConfig) {
|
|
294
|
+
logSend("connection", "sendGroupForwardMsg", {
|
|
295
|
+
targetType: "group",
|
|
296
|
+
targetId: groupId,
|
|
297
|
+
nodeCount: messages.length,
|
|
298
|
+
isForward: true,
|
|
299
|
+
sessionId: getActiveReplyTarget(),
|
|
300
|
+
replySessionId: getActiveReplySessionId(),
|
|
301
|
+
});
|
|
302
|
+
const socket = getConfig ? await ensureConnection(getConfig) : await waitForConnection();
|
|
303
|
+
const res = await sendOneBotAction(socket, "send_group_forward_msg", { group_id: groupId, messages });
|
|
304
|
+
if (res?.retcode !== 0) {
|
|
305
|
+
throw new Error(res?.msg ?? `OneBot send_group_forward_msg failed (retcode=${res?.retcode})`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
/** 发送私聊合并转发消息 */
|
|
309
|
+
export async function sendPrivateForwardMsg(userId, messages, getConfig) {
|
|
310
|
+
logSend("connection", "sendPrivateForwardMsg", {
|
|
311
|
+
targetType: "user",
|
|
312
|
+
targetId: userId,
|
|
313
|
+
nodeCount: messages.length,
|
|
314
|
+
isForward: true,
|
|
315
|
+
sessionId: getActiveReplyTarget(),
|
|
316
|
+
replySessionId: getActiveReplySessionId(),
|
|
317
|
+
});
|
|
318
|
+
const socket = getConfig ? await ensureConnection(getConfig) : await waitForConnection();
|
|
319
|
+
const res = await sendOneBotAction(socket, "send_private_forward_msg", { user_id: userId, messages });
|
|
320
|
+
if (res?.retcode !== 0) {
|
|
321
|
+
throw new Error(res?.msg ?? `OneBot send_private_forward_msg failed (retcode=${res?.retcode})`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
249
324
|
export async function sendPrivateImage(userId, image, log = getLogger(), getConfig) {
|
|
325
|
+
if (shouldBlockSendInForwardMode("private", userId)) {
|
|
326
|
+
logSend("connection", "sendPrivateImage", { targetId: userId, blocked: true, sessionId: getActiveReplyTarget(), replySessionId: getActiveReplySessionId() });
|
|
327
|
+
return undefined;
|
|
328
|
+
}
|
|
329
|
+
logSend("connection", "sendPrivateImage", {
|
|
330
|
+
targetType: "user",
|
|
331
|
+
targetId: userId,
|
|
332
|
+
imagePreview: image?.slice?.(0, 60),
|
|
333
|
+
sessionId: getActiveReplyTarget(),
|
|
334
|
+
replySessionId: getActiveReplySessionId(),
|
|
335
|
+
});
|
|
250
336
|
log.info?.(`[onebot] sendPrivateImage entry: userId=${userId} image=${image?.slice?.(0, 80) ?? ""}`);
|
|
251
337
|
const socket = getConfig ? await ensureConnection(getConfig) : await waitForConnection();
|
|
252
338
|
const filePath = image.startsWith("[") ? null : await resolveImageToLocalPath(image);
|
|
@@ -258,7 +344,9 @@ export async function sendPrivateImage(userId, image, log = getLogger(), getConf
|
|
|
258
344
|
throw new Error(res?.msg ?? `OneBot send_private_msg (image) failed (retcode=${res?.retcode})`);
|
|
259
345
|
}
|
|
260
346
|
log.info?.(`[onebot] sendPrivateImage done: retcode=${res?.retcode ?? "?"}`);
|
|
261
|
-
|
|
347
|
+
const mid = res?.data?.message_id;
|
|
348
|
+
logSend("connection", "sendPrivateImage", { targetId: userId, messageId: mid, sessionId: getActiveReplyTarget(), replySessionId: getActiveReplySessionId() });
|
|
349
|
+
return mid;
|
|
262
350
|
}
|
|
263
351
|
export async function uploadGroupFile(groupId, file, name) {
|
|
264
352
|
if (!ws || ws.readyState !== WebSocket.OPEN)
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 支持:
|
|
5
5
|
* 1. 简单文本模板(message),占位符:{name}、{userId}、{groupName}、{groupId}、{avatarUrl}
|
|
6
|
-
* 2. 自定义
|
|
6
|
+
* 2. 自定义 command:在 cwd 下用系统 shell 执行命令,通过环境变量传入上下文
|
|
7
|
+
* 命令自行负责发送(如调用 openclaw message send),或向 stdout 输出 JSON 行供本 handler 发送
|
|
7
8
|
*/
|
|
8
9
|
import type { OneBotMessage } from "../types.js";
|
|
9
10
|
export interface GroupIncreaseContext {
|
|
@@ -3,11 +3,14 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 支持:
|
|
5
5
|
* 1. 简单文本模板(message),占位符:{name}、{userId}、{groupName}、{groupId}、{avatarUrl}
|
|
6
|
-
* 2. 自定义
|
|
6
|
+
* 2. 自定义 command:在 cwd 下用系统 shell 执行命令,通过环境变量传入上下文
|
|
7
|
+
* 命令自行负责发送(如调用 openclaw message send),或向 stdout 输出 JSON 行供本 handler 发送
|
|
7
8
|
*/
|
|
8
9
|
import { sendGroupMsg, sendGroupImage, getStrangerInfo, getGroupMemberInfo, getGroupInfo, getAvatarUrl, } from "../connection.js";
|
|
9
|
-
import {
|
|
10
|
+
import { getRenderMarkdownToPlain } from "../config.js";
|
|
11
|
+
import { markdownToPlain } from "../markdown.js";
|
|
10
12
|
import { resolve } from "path";
|
|
13
|
+
import { spawn } from "child_process";
|
|
11
14
|
async function resolveContext(groupId, userId) {
|
|
12
15
|
const [groupInfo, memberInfo] = await Promise.all([
|
|
13
16
|
getGroupInfo(groupId),
|
|
@@ -38,6 +41,49 @@ function applyTemplate(template, ctx) {
|
|
|
38
41
|
.replace(/\{groupId\}/g, String(ctx.groupId))
|
|
39
42
|
.replace(/\{avatarUrl\}/g, ctx.avatarUrl);
|
|
40
43
|
}
|
|
44
|
+
function escapeForShell(s) {
|
|
45
|
+
const str = String(s);
|
|
46
|
+
if (process.platform === "win32") {
|
|
47
|
+
return '"' + str.replace(/"/g, '""') + '"';
|
|
48
|
+
}
|
|
49
|
+
return '"' + str.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"';
|
|
50
|
+
}
|
|
51
|
+
function runCommand(command, cwd, args, env) {
|
|
52
|
+
const fullCmd = `${command} --userId ${args.userId} --username ${escapeForShell(args.username)} --groupId ${args.groupId}`;
|
|
53
|
+
return new Promise((resolvePromise) => {
|
|
54
|
+
const isWin = process.platform === "win32";
|
|
55
|
+
const shell = isWin ? "cmd.exe" : "sh";
|
|
56
|
+
const shellArg = isWin ? "/c" : "-c";
|
|
57
|
+
const child = spawn(shell, [shellArg, fullCmd], {
|
|
58
|
+
cwd,
|
|
59
|
+
env: { ...process.env, ...env },
|
|
60
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
61
|
+
});
|
|
62
|
+
let stdout = "";
|
|
63
|
+
let stderr = "";
|
|
64
|
+
child.stdout?.on("data", (d) => { stdout += d.toString(); });
|
|
65
|
+
child.stderr?.on("data", (d) => { stderr += d.toString(); });
|
|
66
|
+
child.on("close", (code) => {
|
|
67
|
+
resolvePromise({ stdout, stderr, code });
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
function parseCommandOutput(stdout) {
|
|
72
|
+
const line = stdout.trim().split("\n").pop();
|
|
73
|
+
if (!line)
|
|
74
|
+
return null;
|
|
75
|
+
try {
|
|
76
|
+
const data = JSON.parse(line);
|
|
77
|
+
return {
|
|
78
|
+
text: typeof data.text === "string" ? data.text : undefined,
|
|
79
|
+
imagePath: typeof data.imagePath === "string" ? data.imagePath : undefined,
|
|
80
|
+
imageUrl: typeof data.imageUrl === "string" ? data.imageUrl : undefined,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
41
87
|
export async function handleGroupIncrease(api, msg) {
|
|
42
88
|
const cfg = api.config;
|
|
43
89
|
const gi = cfg?.channels?.onebot?.groupIncrease;
|
|
@@ -54,24 +100,43 @@ export async function handleGroupIncrease(api, msg) {
|
|
|
54
100
|
return;
|
|
55
101
|
}
|
|
56
102
|
let result = {};
|
|
57
|
-
const
|
|
58
|
-
|
|
103
|
+
const command = gi?.command?.trim();
|
|
104
|
+
const cwd = gi?.cwd?.trim();
|
|
105
|
+
if (command && cwd) {
|
|
106
|
+
const env = {
|
|
107
|
+
GROUP_ID: String(ctx.groupId),
|
|
108
|
+
GROUP_NAME: ctx.groupName,
|
|
109
|
+
USER_ID: String(ctx.userId),
|
|
110
|
+
USER_NAME: ctx.userName,
|
|
111
|
+
AVATAR_URL: ctx.avatarUrl,
|
|
112
|
+
};
|
|
113
|
+
const args = {
|
|
114
|
+
userId: String(ctx.userId),
|
|
115
|
+
username: ctx.userName,
|
|
116
|
+
groupId: String(ctx.groupId),
|
|
117
|
+
};
|
|
59
118
|
try {
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
119
|
+
const { stdout, stderr, code } = await runCommand(command, resolve(cwd), args, env);
|
|
120
|
+
if (stderr)
|
|
121
|
+
api.logger?.warn?.(`[onebot] groupIncrease command stderr: ${stderr}`);
|
|
122
|
+
if (code !== 0)
|
|
123
|
+
api.logger?.warn?.(`[onebot] groupIncrease command exit code: ${code}`);
|
|
124
|
+
const parsed = parseCommandOutput(stdout);
|
|
125
|
+
if (parsed && (parsed.text || parsed.imagePath || parsed.imageUrl)) {
|
|
126
|
+
result = parsed;
|
|
64
127
|
}
|
|
65
128
|
}
|
|
66
129
|
catch (e) {
|
|
67
|
-
api.logger?.error?.(`[onebot] groupIncrease
|
|
130
|
+
api.logger?.error?.(`[onebot] groupIncrease command failed: ${e?.message}`);
|
|
68
131
|
}
|
|
69
132
|
}
|
|
70
133
|
const message = gi?.message;
|
|
71
|
-
if (message?.trim() && !result.text) {
|
|
134
|
+
if (message?.trim() && !result.text && !command) {
|
|
72
135
|
result.text = applyTemplate(message, ctx);
|
|
73
136
|
}
|
|
74
|
-
|
|
137
|
+
let text = (result.text ?? "").trim();
|
|
138
|
+
if (text && getRenderMarkdownToPlain(cfg))
|
|
139
|
+
text = markdownToPlain(text);
|
|
75
140
|
const imagePath = result.imagePath?.trim();
|
|
76
141
|
const imageUrl = result.imageUrl?.trim();
|
|
77
142
|
if (!text && !imagePath && !imageUrl)
|
|
@@ -80,9 +145,10 @@ export async function handleGroupIncrease(api, msg) {
|
|
|
80
145
|
if (text)
|
|
81
146
|
await sendGroupMsg(groupId, text);
|
|
82
147
|
if (imagePath) {
|
|
148
|
+
const baseDir = cwd || process.cwd();
|
|
83
149
|
const abs = imagePath.startsWith("file://") || imagePath.startsWith("http://") || imagePath.startsWith("https://")
|
|
84
150
|
? imagePath
|
|
85
|
-
: resolve(
|
|
151
|
+
: resolve(baseDir, imagePath);
|
|
86
152
|
await sendGroupImage(groupId, abs);
|
|
87
153
|
}
|
|
88
154
|
if (imageUrl && !imagePath)
|
|
@@ -8,4 +8,22 @@ export declare const sessionHistories: Map<string, {
|
|
|
8
8
|
timestamp: number;
|
|
9
9
|
messageId: string;
|
|
10
10
|
}[]>;
|
|
11
|
+
export declare function startForwardCleanupTimer(): void;
|
|
11
12
|
export declare function processInboundMessage(api: any, msg: OneBotMessage): Promise<void>;
|
|
13
|
+
/** 回复会话上下文,供 onReplySessionEnd 钩子使用 */
|
|
14
|
+
export interface ReplySessionContext {
|
|
15
|
+
/** 本次回复会话的唯一 ID,同一用户问题下的多次 deliver 共享此 ID */
|
|
16
|
+
replySessionId: string;
|
|
17
|
+
/** 会话标识,如 onebot:group:123 或 onebot:456 */
|
|
18
|
+
sessionId: string;
|
|
19
|
+
/** 回复目标,如 onebot:group:123 或 onebot:456 */
|
|
20
|
+
to: string;
|
|
21
|
+
/** 本次回复中已发送的所有块(按顺序) */
|
|
22
|
+
chunks: Array<{
|
|
23
|
+
index: number;
|
|
24
|
+
text?: string;
|
|
25
|
+
mediaUrl?: string;
|
|
26
|
+
}>;
|
|
27
|
+
/** 用户原始消息 */
|
|
28
|
+
userMessage: string;
|
|
29
|
+
}
|