@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 CHANGED
@@ -1,9 +1,23 @@
1
+ <div align="center">
2
+
1
3
  # openclaw-onebot
2
4
 
3
- **OneBot v11 协议**(QQ/Lagrange.Core、go-cqhttp 等)接入 [OpenClaw](https://openclaw.ai) Gateway 的渠道插件。
5
+ [OpenClaw](https://openclaw.ai) 的 **OneBot v11 协议**(QQ/Lagrange.Core、go-cqhttp 等)渠道插件。
6
+
7
+ [![npm version](https://img.shields.io/npm/v/@kirigaya/openclaw-onebot?style=flat-square)](https://www.npmjs.com/package/@kirigaya/openclaw-onebot)
8
+ [![GitHub stars](https://img.shields.io/github/stars/LSTM-Kirigaya/openclaw-onebot?style=flat-square)](https://github.com/LSTM-Kirigaya/openclaw-onebot)
9
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg?style=flat-square)](LICENSE)
10
+ [![Node.js](https://img.shields.io/badge/node-%3E%3D22-brightgreen?style=flat-square)](https://nodejs.org)
11
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.4-blue?style=flat-square)](https://www.typescriptlang.org/)
12
+ [![OpenClaw](https://img.shields.io/badge/OpenClaw-Plugin-9cf?style=flat-square)](https://openclaw.ai)
13
+
14
+ </div>
4
15
 
5
- [![npm version](https://img.shields.io/npm/v/openclaw-onebot.svg)](https://www.npmjs.com/package/openclaw-onebot)
6
- [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
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;
@@ -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;
@@ -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
- return res?.data?.message_id;
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
- return res?.data?.message_id;
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
- return res?.data?.message_id;
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
- return res?.data?.message_id;
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. 自定义 handler 脚本,可生成图片并返回,接收完整上下文
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. 自定义 handler 脚本,可生成图片并返回,接收完整上下文
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 { loadScript } from "../load-script.js";
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 handlerPath = gi?.handler;
58
- if (handlerPath?.trim()) {
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 mod = await loadScript(handlerPath);
61
- const fn = mod?.default ?? mod?.generate ?? mod;
62
- if (typeof fn === "function") {
63
- result = (await fn(ctx)) ?? {};
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 handler failed: ${e?.message}`);
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
- const text = (result.text ?? "").trim();
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(process.cwd(), imagePath);
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
+ }