@kirigaya/openclaw-onebot 1.0.1 → 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.
@@ -4,8 +4,27 @@
4
4
  * (Agent 可能传入裸数字或 user:xxx,导致误发私聊)
5
5
  */
6
6
  export declare function setActiveReplyTarget(to: string): void;
7
+ export declare function setActiveReplySelfId(selfId: number | null): void;
8
+ export declare function getActiveReplySelfId(): number | null;
9
+ export declare function setForwardSuppressDelivery(suppress: boolean): void;
10
+ export declare function getForwardSuppressDelivery(): boolean;
7
11
  export declare function clearActiveReplyTarget(): void;
8
12
  export declare function getActiveReplyTarget(): string | null;
13
+ /**
14
+ * 回复会话 ID:同一用户问题下,AI 的多次 deliver 调用共享此 ID,便于统一处理
15
+ * 在 processInboundMessage 开始 dispatch 时设置,结束时清除
16
+ */
17
+ export declare function setActiveReplySessionId(id: string | null): void;
18
+ export declare function getActiveReplySessionId(): string | null;
19
+ /** 判断 to 是否与当前回复目标相同(用于 forward 模式下拦截 outbound 自动发送) */
20
+ export declare function isTargetActiveReply(to: string): boolean;
21
+ /**
22
+ * forward 模式下:发往用户(非 selfId)的普通消息应被拦截,只保留发给自己(构建转发)和最终的合并转发
23
+ * @param type "private" | "group"
24
+ * @param targetId 目标 ID(userId 或 groupId)
25
+ * @returns true 表示应拦截,不发送
26
+ */
27
+ export declare function shouldBlockSendInForwardMode(type: "private" | "group", targetId: number): boolean;
9
28
  /**
10
29
  * 若当前有活跃群聊回复目标,且传入的 to 可能被误判为私聊(裸数字或 user:xxx 与群号相同),则返回修正后的 target
11
30
  */
@@ -4,15 +4,69 @@
4
4
  * (Agent 可能传入裸数字或 user:xxx,导致误发私聊)
5
5
  */
6
6
  let activeReplyTarget = null;
7
+ let activeReplySessionId = null;
8
+ /** forward 模式下需拦截 channel outbound 的自动发送,避免 N 条重复发出 */
9
+ let forwardSuppressDelivery = false;
10
+ /** 机器人自身 QQ,forward 时发给自己需放行 */
11
+ let activeReplySelfId = null;
7
12
  export function setActiveReplyTarget(to) {
8
13
  activeReplyTarget = to;
9
14
  }
15
+ export function setActiveReplySelfId(selfId) {
16
+ activeReplySelfId = selfId;
17
+ }
18
+ export function getActiveReplySelfId() {
19
+ return activeReplySelfId;
20
+ }
21
+ export function setForwardSuppressDelivery(suppress) {
22
+ forwardSuppressDelivery = suppress;
23
+ }
24
+ export function getForwardSuppressDelivery() {
25
+ return forwardSuppressDelivery;
26
+ }
10
27
  export function clearActiveReplyTarget() {
11
28
  activeReplyTarget = null;
12
29
  }
13
30
  export function getActiveReplyTarget() {
14
31
  return activeReplyTarget;
15
32
  }
33
+ /**
34
+ * 回复会话 ID:同一用户问题下,AI 的多次 deliver 调用共享此 ID,便于统一处理
35
+ * 在 processInboundMessage 开始 dispatch 时设置,结束时清除
36
+ */
37
+ export function setActiveReplySessionId(id) {
38
+ activeReplySessionId = id;
39
+ }
40
+ export function getActiveReplySessionId() {
41
+ return activeReplySessionId;
42
+ }
43
+ /** 规范化 target 便于比较:group:123 / onebot:group:123 -> group:123 */
44
+ function normalizeTargetForCompare(t) {
45
+ const s = (t || "").replace(/^(onebot|qq|lagrange):/i, "").trim().toLowerCase();
46
+ return s || "";
47
+ }
48
+ /** 判断 to 是否与当前回复目标相同(用于 forward 模式下拦截 outbound 自动发送) */
49
+ export function isTargetActiveReply(to) {
50
+ const target = activeReplyTarget;
51
+ if (!target)
52
+ return false;
53
+ return normalizeTargetForCompare(to) === normalizeTargetForCompare(target);
54
+ }
55
+ /**
56
+ * forward 模式下:发往用户(非 selfId)的普通消息应被拦截,只保留发给自己(构建转发)和最终的合并转发
57
+ * @param type "private" | "group"
58
+ * @param targetId 目标 ID(userId 或 groupId)
59
+ * @returns true 表示应拦截,不发送
60
+ */
61
+ export function shouldBlockSendInForwardMode(type, targetId) {
62
+ if (!forwardSuppressDelivery)
63
+ return false;
64
+ const selfId = activeReplySelfId;
65
+ if (type === "private") {
66
+ return targetId !== selfId;
67
+ }
68
+ return true;
69
+ }
16
70
  /**
17
71
  * 若当前有活跃群聊回复目标,且传入的 to 可能被误判为私聊(裸数字或 user:xxx 与群号相同),则返回修正后的 target
18
72
  */
@@ -0,0 +1,27 @@
1
+ /**
2
+ * 发送调试日志:记录所有文字/媒体发送相关的关键信息,便于排查
3
+ * 日志路径:项目根目录的绝对路径 send-debug.log
4
+ * 启用:设置环境变量 OPENCLAW_ONEBOT_SEND_DEBUG=1
5
+ */
6
+ /** 绝对路径:openclaw-onebot/send-debug.log,运行前设置 OPENCLAW_ONEBOT_SEND_DEBUG=1 启用 */
7
+ export declare const SEND_DEBUG_LOG_PATH: string;
8
+ export declare function logSend(layer: "send" | "connection", fn: string, data: {
9
+ targetType?: "group" | "user" | "to";
10
+ targetId?: number | string;
11
+ to?: string;
12
+ textPreview?: string;
13
+ textLen?: number;
14
+ suppressed?: boolean;
15
+ forwardSuppress?: boolean;
16
+ activeReplyTarget?: string | null;
17
+ /** 会话 ID,如 onebot:group:123 */
18
+ sessionId?: string | null;
19
+ /** 回复会话 ID,同一问题下多次 deliver 共享 */
20
+ replySessionId?: string | null;
21
+ messageId?: number | string;
22
+ isForward?: boolean;
23
+ nodeCount?: number;
24
+ imagePreview?: string;
25
+ mediaUrlPreview?: string;
26
+ blocked?: boolean;
27
+ }): void;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * 发送调试日志:记录所有文字/媒体发送相关的关键信息,便于排查
3
+ * 日志路径:项目根目录的绝对路径 send-debug.log
4
+ * 启用:设置环境变量 OPENCLAW_ONEBOT_SEND_DEBUG=1
5
+ */
6
+ import fs from "node:fs";
7
+ import path from "node:path";
8
+ import { fileURLToPath } from "node:url";
9
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
+ /** 绝对路径:openclaw-onebot/send-debug.log,运行前设置 OPENCLAW_ONEBOT_SEND_DEBUG=1 启用 */
11
+ export const SEND_DEBUG_LOG_PATH = path.resolve(__dirname, "..", "send-debug.log");
12
+ function isEnabled() {
13
+ return process.env.OPENCLAW_ONEBOT_SEND_DEBUG === "1";
14
+ }
15
+ let sessionLogged = false;
16
+ export function logSend(layer, fn, data) {
17
+ if (!isEnabled())
18
+ return;
19
+ try {
20
+ if (!sessionLogged) {
21
+ fs.appendFileSync(SEND_DEBUG_LOG_PATH, `${new Date().toISOString()} [session] 日志路径(绝对): ${SEND_DEBUG_LOG_PATH}\n`);
22
+ sessionLogged = true;
23
+ }
24
+ const line = `${new Date().toISOString()} [${layer}] ${fn} ${JSON.stringify(data)}\n`;
25
+ fs.appendFileSync(SEND_DEBUG_LOG_PATH, line);
26
+ }
27
+ catch (_) { }
28
+ }
package/dist/send.d.ts CHANGED
@@ -12,12 +12,26 @@ type OneBotConfigGetter = () => OneBotAccountConfig | null;
12
12
  /**
13
13
  * 发送文本消息到 OneBot 目标(私聊或群聊)
14
14
  * @param getConfig 可选,用于按需连接(forward-websocket 下 message send 可独立运行)
15
+ * @param cfg 可选,用于读取 renderMarkdownToPlain 配置
15
16
  */
16
- export declare function sendTextMessage(to: string, text: string, getConfig?: OneBotConfigGetter): Promise<OneBotSendResult>;
17
+ export declare function sendTextMessage(to: string, text: string, getConfig?: OneBotConfigGetter, cfg?: {
18
+ channels?: {
19
+ onebot?: {
20
+ renderMarkdownToPlain?: boolean;
21
+ };
22
+ };
23
+ }): Promise<OneBotSendResult>;
17
24
  /**
18
25
  * 发送媒体消息(图片等)到 OneBot 目标
19
26
  * mediaUrl 支持 file:// 路径、http(s):// URL、base64://
20
27
  * @param getConfig 可选,用于按需连接
28
+ * @param cfg 可选,用于读取 renderMarkdownToPlain 配置
21
29
  */
22
- export declare function sendMediaMessage(to: string, mediaUrl: string, text?: string, getConfig?: OneBotConfigGetter): Promise<OneBotSendResult>;
30
+ export declare function sendMediaMessage(to: string, mediaUrl: string, text?: string, getConfig?: OneBotConfigGetter, cfg?: {
31
+ channels?: {
32
+ onebot?: {
33
+ renderMarkdownToPlain?: boolean;
34
+ };
35
+ };
36
+ }): Promise<OneBotSendResult>;
23
37
  export {};
package/dist/send.js CHANGED
@@ -3,7 +3,10 @@
3
3
  * 对应 Lagrange.onebot context.ts 的 sendPrivateMsg / sendGroupMsg / 图片消息
4
4
  */
5
5
  import { sendPrivateMsg, sendGroupMsg, sendPrivateImage, sendGroupImage, } from "./connection.js";
6
- import { resolveTargetForReply } from "./reply-context.js";
6
+ import { resolveTargetForReply, getForwardSuppressDelivery, isTargetActiveReply, getActiveReplyTarget, getActiveReplySessionId } from "./reply-context.js";
7
+ import { logSend } from "./send-debug-log.js";
8
+ import { getRenderMarkdownToPlain, getCollapseDoubleNewlines } from "./config.js";
9
+ import { markdownToPlain, collapseDoubleNewlines } from "./markdown.js";
7
10
  function parseTarget(to) {
8
11
  const t = to.replace(/^(onebot|qq|lagrange):/i, "").trim();
9
12
  if (!t)
@@ -26,8 +29,25 @@ function parseTarget(to) {
26
29
  /**
27
30
  * 发送文本消息到 OneBot 目标(私聊或群聊)
28
31
  * @param getConfig 可选,用于按需连接(forward-websocket 下 message send 可独立运行)
32
+ * @param cfg 可选,用于读取 renderMarkdownToPlain 配置
29
33
  */
30
- export async function sendTextMessage(to, text, getConfig) {
34
+ export async function sendTextMessage(to, text, getConfig, cfg) {
35
+ const forwardSuppress = getForwardSuppressDelivery();
36
+ const activeTarget = getActiveReplyTarget();
37
+ const suppressed = forwardSuppress && isTargetActiveReply(to);
38
+ logSend("send", "sendTextMessage", {
39
+ to,
40
+ textPreview: text?.slice(0, 80),
41
+ textLen: text?.length,
42
+ suppressed,
43
+ forwardSuppress,
44
+ activeReplyTarget: activeTarget,
45
+ sessionId: activeTarget,
46
+ replySessionId: getActiveReplySessionId(),
47
+ });
48
+ if (suppressed) {
49
+ return { ok: true, messageId: "" };
50
+ }
31
51
  const resolvedTo = resolveTargetForReply(to);
32
52
  const target = parseTarget(resolvedTo);
33
53
  if (!target) {
@@ -36,14 +56,24 @@ export async function sendTextMessage(to, text, getConfig) {
36
56
  if (!text?.trim()) {
37
57
  return { ok: false, error: "No text provided" };
38
58
  }
59
+ let finalText = getRenderMarkdownToPlain(cfg) ? markdownToPlain(text) : text.trim();
60
+ if (getCollapseDoubleNewlines(cfg))
61
+ finalText = collapseDoubleNewlines(finalText);
39
62
  try {
40
63
  let messageId;
41
64
  if (target.type === "group") {
42
- messageId = await sendGroupMsg(target.id, text, getConfig);
65
+ messageId = await sendGroupMsg(target.id, finalText, getConfig);
43
66
  }
44
67
  else {
45
- messageId = await sendPrivateMsg(target.id, text, getConfig);
68
+ messageId = await sendPrivateMsg(target.id, finalText, getConfig);
46
69
  }
70
+ logSend("send", "sendTextMessage", {
71
+ targetType: target.type,
72
+ targetId: target.id,
73
+ messageId,
74
+ sessionId: activeTarget,
75
+ replySessionId: getActiveReplySessionId(),
76
+ });
47
77
  return { ok: true, messageId: messageId != null ? String(messageId) : "" };
48
78
  }
49
79
  catch (err) {
@@ -57,8 +87,25 @@ export async function sendTextMessage(to, text, getConfig) {
57
87
  * 发送媒体消息(图片等)到 OneBot 目标
58
88
  * mediaUrl 支持 file:// 路径、http(s):// URL、base64://
59
89
  * @param getConfig 可选,用于按需连接
90
+ * @param cfg 可选,用于读取 renderMarkdownToPlain 配置
60
91
  */
61
- export async function sendMediaMessage(to, mediaUrl, text, getConfig) {
92
+ export async function sendMediaMessage(to, mediaUrl, text, getConfig, cfg) {
93
+ const forwardSuppress = getForwardSuppressDelivery();
94
+ const activeTarget = getActiveReplyTarget();
95
+ const suppressed = forwardSuppress && isTargetActiveReply(to);
96
+ logSend("send", "sendMediaMessage", {
97
+ to,
98
+ textPreview: text?.slice(0, 40),
99
+ mediaUrlPreview: mediaUrl?.slice(0, 60),
100
+ suppressed,
101
+ forwardSuppress,
102
+ activeReplyTarget: activeTarget,
103
+ sessionId: activeTarget,
104
+ replySessionId: getActiveReplySessionId(),
105
+ });
106
+ if (suppressed) {
107
+ return { ok: true, messageId: "" };
108
+ }
62
109
  const resolvedTo = resolveTargetForReply(to);
63
110
  const target = parseTarget(resolvedTo);
64
111
  if (!target) {
@@ -67,14 +114,17 @@ export async function sendMediaMessage(to, mediaUrl, text, getConfig) {
67
114
  if (!mediaUrl?.trim()) {
68
115
  return { ok: false, error: "No mediaUrl provided" };
69
116
  }
117
+ let finalText = text?.trim() ? (getRenderMarkdownToPlain(cfg) ? markdownToPlain(text) : text.trim()) : "";
118
+ if (finalText && getCollapseDoubleNewlines(cfg))
119
+ finalText = collapseDoubleNewlines(finalText);
70
120
  try {
71
121
  let messageId;
72
- if (text?.trim()) {
122
+ if (finalText) {
73
123
  if (target.type === "group") {
74
- messageId = await sendGroupMsg(target.id, text, getConfig);
124
+ messageId = await sendGroupMsg(target.id, finalText, getConfig);
75
125
  }
76
126
  else {
77
- messageId = await sendPrivateMsg(target.id, text, getConfig);
127
+ messageId = await sendPrivateMsg(target.id, finalText, getConfig);
78
128
  }
79
129
  }
80
130
  if (target.type === "group") {
@@ -87,6 +137,13 @@ export async function sendMediaMessage(to, mediaUrl, text, getConfig) {
87
137
  if (id != null)
88
138
  messageId = id;
89
139
  }
140
+ logSend("send", "sendMediaMessage", {
141
+ targetType: target.type,
142
+ targetId: target.id,
143
+ messageId,
144
+ sessionId: activeTarget,
145
+ replySessionId: getActiveReplySessionId(),
146
+ });
90
147
  return { ok: true, messageId: messageId != null ? String(messageId) : "" };
91
148
  }
92
149
  catch (err) {
package/dist/setup.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * OneBot TUI 配置向导
3
3
  * openclaw onebot setup
4
4
  */
5
- import { cancel as clackCancel, isCancel, note as clackNote, outro as clackOutro, select as clackSelect, text as clackText, } from "@clack/prompts";
5
+ import { cancel as clackCancel, confirm as clackConfirm, isCancel, note as clackNote, outro as clackOutro, select as clackSelect, text as clackText, } from "@clack/prompts";
6
6
  import { existsSync, readFileSync, writeFileSync } from "fs";
7
7
  import { homedir } from "os";
8
8
  import { join } from "path";
@@ -22,25 +22,37 @@ export async function runOneBotSetup() {
22
22
  { value: "forward-websocket", label: "forward-websocket(正向,主动连接 OneBot)" },
23
23
  { value: "backward-websocket", label: "backward-websocket(反向,OneBot 连接本机)" },
24
24
  ],
25
- initialValue: process.env.LAGRANGE_WS_TYPE === "backward-websocket" ? "backward-websocket" : "forward-websocket",
25
+ initialValue: process.env.ONEBOT_WS_TYPE === "backward-websocket" ? "backward-websocket" : "forward-websocket",
26
26
  }));
27
27
  const host = guardCancel(await clackText({
28
28
  message: "主机地址",
29
- initialValue: process.env.LAGRANGE_WS_HOST || "127.0.0.1",
29
+ initialValue: process.env.ONEBOT_WS_HOST || "127.0.0.1",
30
30
  }));
31
31
  const portStr = guardCancel(await clackText({
32
32
  message: "端口",
33
- initialValue: process.env.LAGRANGE_WS_PORT || "3001",
33
+ initialValue: process.env.ONEBOT_WS_PORT || "3001",
34
34
  }));
35
35
  const accessToken = guardCancel(await clackText({
36
36
  message: "Access Token(可选,留空回车跳过)",
37
- initialValue: process.env.LAGRANGE_WS_ACCESS_TOKEN || "",
37
+ initialValue: process.env.ONEBOT_WS_ACCESS_TOKEN || "",
38
+ }));
39
+ const renderMarkdownToPlain = guardCancel(await clackConfirm({
40
+ message: "是否将机器人回复中的 Markdown 渲染为纯文本再发送?(去除 **、# 等标记,推荐开启)",
41
+ initialValue: true,
42
+ }));
43
+ const longMessageMode = guardCancel(await clackSelect({
44
+ message: "长消息处理模式(单次回复超过阈值时):",
45
+ options: [
46
+ { value: "normal", label: "正常发送(分段发送)" },
47
+ { value: "og_image", label: "生成图片发送(需安装 node-html-to-image)" },
48
+ { value: "forward", label: "合并转发发送(发给自己后打包转发)" },
49
+ ],
50
+ initialValue: "normal",
51
+ }));
52
+ const longMessageThreshold = guardCancel(await clackText({
53
+ message: "长消息阈值(字符数,超过则启用上述模式)",
54
+ initialValue: "300",
38
55
  }));
39
- const port = parseInt(String(portStr).trim(), 10);
40
- if (!Number.isFinite(port)) {
41
- console.error("端口必须为数字");
42
- process.exit(1);
43
- }
44
56
  let existing = {};
45
57
  if (existsSync(CONFIG_PATH)) {
46
58
  try {
@@ -48,7 +60,22 @@ export async function runOneBotSetup() {
48
60
  }
49
61
  catch { }
50
62
  }
63
+ const prevOnebot = (existing.channels || {}).onebot;
64
+ const whitelistInitial = Array.isArray(prevOnebot?.whitelistUserIds)
65
+ ? prevOnebot.whitelistUserIds.join(", ")
66
+ : "";
67
+ const whitelistInput = guardCancel(await clackText({
68
+ message: "白名单 QQ 号(逗号分隔,留空则所有人可回复)",
69
+ initialValue: whitelistInitial,
70
+ }));
71
+ const port = parseInt(String(portStr).trim(), 10);
72
+ if (!Number.isFinite(port)) {
73
+ console.error("端口必须为数字");
74
+ process.exit(1);
75
+ }
51
76
  const channels = existing.channels || {};
77
+ const thresholdNum = parseInt(String(longMessageThreshold).trim(), 10);
78
+ const whitelistIds = (String(whitelistInput).trim().split(/[,\s]+/).map((s) => s.trim()).filter(Boolean).map((s) => (/^\d+$/.test(s) ? Number(s) : null)).filter((n) => n != null));
52
79
  channels.onebot = {
53
80
  ...(channels.onebot || {}),
54
81
  type,
@@ -57,6 +84,10 @@ export async function runOneBotSetup() {
57
84
  ...(accessToken?.trim() ? { accessToken: String(accessToken).trim() } : {}),
58
85
  enabled: true,
59
86
  requireMention: true,
87
+ renderMarkdownToPlain,
88
+ longMessageMode,
89
+ longMessageThreshold: Number.isFinite(thresholdNum) ? thresholdNum : 300,
90
+ ...(whitelistIds.length > 0 ? { whitelistUserIds: whitelistIds } : {}),
60
91
  };
61
92
  const next = { ...existing, channels };
62
93
  writeFileSync(CONFIG_PATH, JSON.stringify(next, null, 2), "utf-8");
package/dist/tools.js CHANGED
@@ -5,6 +5,8 @@
5
5
  import WebSocket from "ws";
6
6
  import { loadScript } from "./load-script.js";
7
7
  import { getWs, sendPrivateMsg, sendGroupMsg, sendGroupImage, sendPrivateImage, uploadGroupFile, uploadPrivateFile, getGroupMsgHistory, getGroupInfo, getStrangerInfo, getGroupMemberInfo, getAvatarUrl, } from "./connection.js";
8
+ import { getRenderMarkdownToPlain } from "./config.js";
9
+ import { markdownToPlain } from "./markdown.js";
8
10
  export const onebotClient = {
9
11
  sendGroupMsg,
10
12
  sendGroupImage,
@@ -35,14 +37,16 @@ export function registerTools(api) {
35
37
  if (!w || w.readyState !== WebSocket.OPEN) {
36
38
  return { content: [{ type: "text", text: "OneBot 未连接" }] };
37
39
  }
40
+ const cfg = api?.config;
41
+ const textToSend = getRenderMarkdownToPlain(cfg) ? markdownToPlain(params.text) : params.text;
38
42
  const t = params.target.replace(/^onebot:/i, "");
39
43
  try {
40
44
  if (t.startsWith("group:")) {
41
- await sendGroupMsg(parseInt(t.slice(6), 10), params.text);
45
+ await sendGroupMsg(parseInt(t.slice(6), 10), textToSend);
42
46
  }
43
47
  else {
44
48
  const id = parseInt(t.replace(/^user:/, ""), 10);
45
- await sendPrivateMsg(id, params.text);
49
+ await sendPrivateMsg(id, textToSend);
46
50
  }
47
51
  return { content: [{ type: "text", text: "发送成功" }] };
48
52
  }
@@ -1,7 +1,7 @@
1
1
  {
2
- "id": "@kirigaya/openclaw-onebot",
2
+ "id": "openclaw-onebot",
3
3
  "name": "OneBot Channel",
4
- "version": "1.0.0",
4
+ "version": "1.0.1",
5
5
  "description": "OneBot v11 protocol channel (QQ/Lagrange.Core via WebSocket)",
6
6
  "author": "Lagrange.Onebot",
7
7
  "channels": ["onebot"],
@@ -23,6 +23,16 @@
23
23
  "default": true,
24
24
  "description": "群聊是否必须 @ 机器人才回复"
25
25
  },
26
+ "whitelistUserIds": {
27
+ "type": "array",
28
+ "items": { "type": "number" },
29
+ "description": "白名单 QQ 号,非空时仅白名单内用户可触发 AI;为空则所有人可回复"
30
+ },
31
+ "renderMarkdownToPlain": {
32
+ "type": "boolean",
33
+ "default": true,
34
+ "description": "是否将机器人回复中的 Markdown 渲染为纯文本再发送(去除 **、# 等标记)"
35
+ },
26
36
  "groupIncrease": {
27
37
  "type": "object",
28
38
  "properties": {
@@ -31,9 +41,13 @@
31
41
  "type": "string",
32
42
  "description": "欢迎语模板。占位符:{name} 新成员昵称,{userId} QQ号,{groupName} 群名,{groupId} 群号,{avatarUrl} 头像链接"
33
43
  },
34
- "handler": {
44
+ "command": {
45
+ "type": "string",
46
+ "description": "在 cwd 下用系统 shell 执行的命令。调用方自动追加 --userId、--username、--groupId 参数。命令可自行发送(如调用 openclaw message send),或向 stdout 输出 JSON 行供 handler 发送"
47
+ },
48
+ "cwd": {
35
49
  "type": "string",
36
- "description": "自定义脚本路径(.mjs/.ts/.mts),接收 ctx,返回 { text?, imagePath?, imageUrl? }。TS 需安装 tsx。优先级高于 message"
50
+ "description": "命令执行的工作目录(绝对路径)"
37
51
  }
38
52
  }
39
53
  },
@@ -42,6 +56,21 @@
42
56
  "default": 60,
43
57
  "description": "表情 ID(Lagrange/QQ NT set_msg_emoji_like),收到消息时添加,回复完成后取消"
44
58
  },
59
+ "onReplySessionEnd": {
60
+ "type": "string",
61
+ "description": "回复会话结束时的钩子脚本路径(.mjs/.ts/.mts)。脚本接收 ctx: { replySessionId, sessionId, to, chunks, userMessage },可对同一问题下的多次发送做统一处理(如日志、合并、上报等)"
62
+ },
63
+ "longMessageMode": {
64
+ "type": "string",
65
+ "enum": ["normal", "og_image", "forward"],
66
+ "default": "normal",
67
+ "description": "长消息处理模式:normal 正常发送;og_image 生成图片(需 node-html-to-image);forward 合并转发"
68
+ },
69
+ "longMessageThreshold": {
70
+ "type": "number",
71
+ "default": 300,
72
+ "description": "长消息阈值(字符数),超过则启用 longMessageMode"
73
+ },
45
74
  "cronJobs": {
46
75
  "type": "array",
47
76
  "description": "内置定时任务(无 AI 介入,直接执行脚本并推送到群聊)",
@@ -67,6 +96,7 @@
67
96
  "accessToken": { "label": "Access Token", "sensitive": true },
68
97
  "host": { "label": "主机地址", "placeholder": "127.0.0.1" },
69
98
  "port": { "label": "端口", "placeholder": "8080" },
70
- "requireMention": { "label": "群聊需 @ 回复" }
99
+ "requireMention": { "label": "群聊需 @ 回复" },
100
+ "renderMarkdownToPlain": { "label": "Markdown 转纯文本" }
71
101
  }
72
102
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kirigaya/openclaw-onebot",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "OneBot v11 protocol channel plugin for OpenClaw (QQ/Lagrange.Core/go-cqhttp)",
5
5
  "license": "MIT",
6
6
  "publishConfig": { "access": "public" },
@@ -46,6 +46,8 @@
46
46
  "scripts": {
47
47
  "build": "tsc",
48
48
  "test:connect": "npx tsx scripts/test-connect.ts",
49
+ "test:group-welcome": "npx tsx scripts/test-group-welcome.ts",
50
+ "test:group-increase-handler": "npx tsx scripts/test-group-increase-handler.ts",
49
51
  "prepublishOnly": "npm run build",
50
52
  "pub": "npm run build && npm publish"
51
53
  },
@@ -53,7 +55,12 @@
53
55
  "ws": "^8.17.0",
54
56
  "@clack/prompts": "^1.0.0",
55
57
  "tsx": "^4.0.0",
56
- "cron": "^4.4.0"
58
+ "cron": "^4.4.0",
59
+ "marked": "^15.0.0",
60
+ "highlight.js": "^11.10.0"
61
+ },
62
+ "optionalDependencies": {
63
+ "node-html-to-image": "^3.0.0"
57
64
  },
58
65
  "peerDependencies": {
59
66
  "openclaw": "*",
@@ -32,7 +32,7 @@ openclaw plugins install ./openclaw-onebot
32
32
 
33
33
  - Gateway 已启动:`openclaw gateway`
34
34
  - OneBot 实现(Lagrange.Core / go-cqhttp)已运行并暴露 WebSocket
35
- - 在 `openclaw.json` 中配置 `channels.onebot` 或通过 `LAGRANGE_WS_*` 环境变量
35
+ - 在 `openclaw.json` 中配置 `channels.onebot` 或通过 `ONEBOT_WS_*` 环境变量
36
36
 
37
37
  ## OneBot 协议能力
38
38