@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.
- package/LICENSE +21 -0
- package/README.md +134 -0
- package/dist/channel.d.ts +100 -0
- package/dist/channel.js +173 -0
- package/dist/config.d.ts +6 -0
- package/dist/config.js +62 -0
- package/dist/connection.d.ts +94 -0
- package/dist/connection.js +426 -0
- package/dist/debug-log.d.ts +7 -0
- package/dist/debug-log.js +24 -0
- package/dist/gateway-proxy.d.ts +8 -0
- package/dist/gateway-proxy.js +36 -0
- package/dist/handlers/group-increase.d.ts +21 -0
- package/dist/handlers/group-increase.js +95 -0
- package/dist/handlers/process-inbound.d.ts +11 -0
- package/dist/handlers/process-inbound.js +224 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +33 -0
- package/dist/load-script.d.ts +5 -0
- package/dist/load-script.js +22 -0
- package/dist/message.d.ts +6 -0
- package/dist/message.js +24 -0
- package/dist/reply-context.d.ts +12 -0
- package/dist/reply-context.js +33 -0
- package/dist/scheduler.d.ts +19 -0
- package/dist/scheduler.js +70 -0
- package/dist/sdk.d.ts +9 -0
- package/dist/sdk.js +36 -0
- package/dist/send.d.ts +23 -0
- package/dist/send.js +98 -0
- package/dist/service.d.ts +4 -0
- package/dist/service.js +71 -0
- package/dist/setup.d.ts +1 -0
- package/dist/setup.js +65 -0
- package/dist/tools.d.ts +18 -0
- package/dist/tools.js +188 -0
- package/dist/types.d.ts +28 -0
- package/dist/types.js +4 -0
- package/openclaw.plugin.json +72 -0
- package/package.json +74 -0
- package/skills/onebot-ops/SKILL.md +61 -0
- package/skills/onebot-ops/config.md +55 -0
- package/skills/onebot-ops/receive.md +85 -0
- package/skills/onebot-ops/send.md +39 -0
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 入站消息处理
|
|
3
|
+
*/
|
|
4
|
+
import { getOneBotConfig } from "../config.js";
|
|
5
|
+
import { getRawText, isMentioned } from "../message.js";
|
|
6
|
+
import { sendPrivateMsg, sendGroupMsg, sendPrivateImage, sendGroupImage, setMsgEmojiLike, } from "../connection.js";
|
|
7
|
+
import { setActiveReplyTarget, clearActiveReplyTarget } from "../reply-context.js";
|
|
8
|
+
import { loadPluginSdk, getSdk } from "../sdk.js";
|
|
9
|
+
const DEFAULT_HISTORY_LIMIT = 20;
|
|
10
|
+
export const sessionHistories = new Map();
|
|
11
|
+
export async function processInboundMessage(api, msg) {
|
|
12
|
+
await loadPluginSdk();
|
|
13
|
+
const { buildPendingHistoryContextFromMap, recordPendingHistoryEntry, clearHistoryEntriesIfEnabled } = getSdk();
|
|
14
|
+
const runtime = api.runtime;
|
|
15
|
+
if (!runtime?.channel?.reply?.dispatchReplyWithBufferedBlockDispatcher) {
|
|
16
|
+
api.logger?.warn?.("[onebot] runtime.channel.reply not available");
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const config = getOneBotConfig(api);
|
|
20
|
+
if (!config) {
|
|
21
|
+
api.logger?.warn?.("[onebot] not configured");
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const cfg = api.config;
|
|
25
|
+
const messageText = getRawText(msg);
|
|
26
|
+
if (!messageText?.trim()) {
|
|
27
|
+
api.logger?.info?.(`[onebot] ignoring empty message`);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const isGroup = msg.message_type === "group";
|
|
31
|
+
const selfId = msg.self_id ?? 0;
|
|
32
|
+
const requireMention = cfg?.channels?.onebot?.requireMention ?? true;
|
|
33
|
+
if (isGroup && requireMention && !isMentioned(msg, selfId)) {
|
|
34
|
+
api.logger?.info?.(`[onebot] ignoring group message without @mention`);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const userId = msg.user_id;
|
|
38
|
+
const groupId = msg.group_id;
|
|
39
|
+
const sessionId = isGroup
|
|
40
|
+
? `onebot:group:${groupId}`.toLowerCase()
|
|
41
|
+
: `onebot:${userId}`.toLowerCase();
|
|
42
|
+
const route = runtime.channel.routing?.resolveAgentRoute?.({
|
|
43
|
+
cfg,
|
|
44
|
+
sessionKey: sessionId,
|
|
45
|
+
channel: "onebot",
|
|
46
|
+
accountId: config.accountId ?? "default",
|
|
47
|
+
}) ?? { agentId: "main" };
|
|
48
|
+
const storePath = runtime.channel.session?.resolveStorePath?.(cfg?.session?.store, {
|
|
49
|
+
agentId: route.agentId,
|
|
50
|
+
}) ?? "";
|
|
51
|
+
const envelopeOptions = runtime.channel.reply?.resolveEnvelopeFormatOptions?.(cfg) ?? {};
|
|
52
|
+
const chatType = isGroup ? "group" : "direct";
|
|
53
|
+
const fromLabel = String(userId);
|
|
54
|
+
const formattedBody = runtime.channel.reply?.formatInboundEnvelope?.({
|
|
55
|
+
channel: "OneBot",
|
|
56
|
+
from: fromLabel,
|
|
57
|
+
timestamp: Date.now(),
|
|
58
|
+
body: messageText,
|
|
59
|
+
chatType,
|
|
60
|
+
sender: { name: fromLabel, id: String(userId) },
|
|
61
|
+
envelope: envelopeOptions,
|
|
62
|
+
}) ?? { content: [{ type: "text", text: messageText }] };
|
|
63
|
+
const body = buildPendingHistoryContextFromMap
|
|
64
|
+
? buildPendingHistoryContextFromMap({
|
|
65
|
+
historyMap: sessionHistories,
|
|
66
|
+
historyKey: sessionId,
|
|
67
|
+
limit: DEFAULT_HISTORY_LIMIT,
|
|
68
|
+
currentMessage: formattedBody,
|
|
69
|
+
formatEntry: (entry) => runtime.channel.reply?.formatInboundEnvelope?.({
|
|
70
|
+
channel: "OneBot",
|
|
71
|
+
from: fromLabel,
|
|
72
|
+
timestamp: entry.timestamp,
|
|
73
|
+
body: entry.body,
|
|
74
|
+
chatType,
|
|
75
|
+
senderLabel: entry.sender,
|
|
76
|
+
envelope: envelopeOptions,
|
|
77
|
+
}) ?? { content: [{ type: "text", text: entry.body }] },
|
|
78
|
+
})
|
|
79
|
+
: formattedBody;
|
|
80
|
+
if (recordPendingHistoryEntry) {
|
|
81
|
+
recordPendingHistoryEntry({
|
|
82
|
+
historyMap: sessionHistories,
|
|
83
|
+
historyKey: sessionId,
|
|
84
|
+
entry: {
|
|
85
|
+
sender: fromLabel,
|
|
86
|
+
body: messageText,
|
|
87
|
+
timestamp: Date.now(),
|
|
88
|
+
messageId: `onebot-${Date.now()}`,
|
|
89
|
+
},
|
|
90
|
+
limit: DEFAULT_HISTORY_LIMIT,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
// 回复目标(参考 openclaw-feishu):群聊用 group:群号,私聊用 user:用户号
|
|
94
|
+
// To / OriginatingTo / ConversationLabel 均表示「发送目标」,Agent 的 message 工具会据此选择 target
|
|
95
|
+
const replyTarget = isGroup ? `onebot:group:${groupId}` : `onebot:${userId}`;
|
|
96
|
+
const ctxPayload = {
|
|
97
|
+
Body: body,
|
|
98
|
+
RawBody: messageText,
|
|
99
|
+
From: isGroup ? `onebot:group:${groupId}` : `onebot:${userId}`,
|
|
100
|
+
To: replyTarget,
|
|
101
|
+
SessionKey: sessionId,
|
|
102
|
+
AccountId: config.accountId ?? "default",
|
|
103
|
+
ChatType: chatType,
|
|
104
|
+
ConversationLabel: replyTarget, // 与 Feishu 一致:表示会话/回复目标,群聊时为 group:群号,非 SenderId
|
|
105
|
+
SenderName: fromLabel,
|
|
106
|
+
SenderId: String(userId),
|
|
107
|
+
Provider: "onebot",
|
|
108
|
+
Surface: "onebot",
|
|
109
|
+
MessageSid: `onebot-${Date.now()}`,
|
|
110
|
+
Timestamp: Date.now(),
|
|
111
|
+
OriginatingChannel: "onebot",
|
|
112
|
+
OriginatingTo: replyTarget,
|
|
113
|
+
CommandAuthorized: true,
|
|
114
|
+
DeliveryContext: {
|
|
115
|
+
channel: "onebot",
|
|
116
|
+
to: replyTarget,
|
|
117
|
+
accountId: config.accountId ?? "default",
|
|
118
|
+
},
|
|
119
|
+
_onebot: { userId, groupId, isGroup },
|
|
120
|
+
};
|
|
121
|
+
if (runtime.channel.session?.recordInboundSession) {
|
|
122
|
+
await runtime.channel.session.recordInboundSession({
|
|
123
|
+
storePath,
|
|
124
|
+
sessionKey: sessionId,
|
|
125
|
+
ctx: ctxPayload,
|
|
126
|
+
updateLastRoute: !isGroup ? { sessionKey: sessionId, channel: "onebot", to: String(userId), accountId: config.accountId ?? "default" } : undefined,
|
|
127
|
+
onRecordError: (err) => api.logger?.warn?.(`[onebot] recordInboundSession: ${err}`),
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
if (runtime.channel.activity?.record) {
|
|
131
|
+
runtime.channel.activity.record({ channel: "onebot", accountId: config.accountId ?? "default", direction: "inbound" });
|
|
132
|
+
}
|
|
133
|
+
const onebotCfg = cfg?.channels?.onebot ?? {};
|
|
134
|
+
const thinkingEmojiId = onebotCfg.thinkingEmojiId ?? 60;
|
|
135
|
+
const userMessageId = msg.message_id;
|
|
136
|
+
let emojiAdded = false;
|
|
137
|
+
const clearEmojiReaction = async () => {
|
|
138
|
+
if (emojiAdded && userMessageId != null) {
|
|
139
|
+
try {
|
|
140
|
+
await setMsgEmojiLike(userMessageId, thinkingEmojiId, false);
|
|
141
|
+
}
|
|
142
|
+
catch { }
|
|
143
|
+
emojiAdded = false;
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
if (userMessageId != null) {
|
|
147
|
+
try {
|
|
148
|
+
await setMsgEmojiLike(userMessageId, thinkingEmojiId, true);
|
|
149
|
+
emojiAdded = true;
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
api.logger?.warn?.("[onebot] setMsgEmojiLike failed (maybe OneBot doesn't support it)");
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
api.logger?.info?.(`[onebot] dispatching message for session ${sessionId}`);
|
|
156
|
+
setActiveReplyTarget(replyTarget);
|
|
157
|
+
try {
|
|
158
|
+
await runtime.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
159
|
+
ctx: ctxPayload,
|
|
160
|
+
cfg,
|
|
161
|
+
dispatcherOptions: {
|
|
162
|
+
deliver: async (payload, info) => {
|
|
163
|
+
await clearEmojiReaction();
|
|
164
|
+
const p = payload;
|
|
165
|
+
const replyText = typeof p === "string" ? p : (p?.text ?? p?.body ?? "");
|
|
166
|
+
const mediaUrl = typeof p === "string" ? undefined : (p?.mediaUrl ?? p?.mediaUrls?.[0]);
|
|
167
|
+
const trimmed = (replyText || "").trim();
|
|
168
|
+
if ((!trimmed || trimmed === "NO_REPLY" || trimmed.endsWith("NO_REPLY")) && !mediaUrl)
|
|
169
|
+
return;
|
|
170
|
+
const { userId: uid, groupId: gid, isGroup: ig } = ctxPayload._onebot || {};
|
|
171
|
+
// 兜底:sessionId 格式 onebot:group:群号 为权威来源,某些实现 _onebot.groupId 可能为空
|
|
172
|
+
const sessionKey = String(ctxPayload.SessionKey ?? sessionId);
|
|
173
|
+
const groupMatch = sessionKey.match(/^onebot:group:(\d+)$/i);
|
|
174
|
+
const effectiveIsGroup = groupMatch != null || Boolean(ig);
|
|
175
|
+
const effectiveGroupId = (groupMatch ? parseInt(groupMatch[1], 10) : undefined) ?? gid;
|
|
176
|
+
try {
|
|
177
|
+
if (trimmed) {
|
|
178
|
+
if (effectiveIsGroup && effectiveGroupId)
|
|
179
|
+
await sendGroupMsg(effectiveGroupId, trimmed);
|
|
180
|
+
else if (uid)
|
|
181
|
+
await sendPrivateMsg(uid, trimmed);
|
|
182
|
+
}
|
|
183
|
+
if (mediaUrl) {
|
|
184
|
+
if (effectiveIsGroup && effectiveGroupId)
|
|
185
|
+
await sendGroupImage(effectiveGroupId, mediaUrl);
|
|
186
|
+
else if (uid)
|
|
187
|
+
await sendPrivateImage(uid, mediaUrl);
|
|
188
|
+
}
|
|
189
|
+
if (info.kind === "final" && clearHistoryEntriesIfEnabled) {
|
|
190
|
+
clearHistoryEntriesIfEnabled({
|
|
191
|
+
historyMap: sessionHistories,
|
|
192
|
+
historyKey: sessionId,
|
|
193
|
+
limit: DEFAULT_HISTORY_LIMIT,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
catch (e) {
|
|
198
|
+
api.logger?.error?.(`[onebot] deliver failed: ${e?.message}`);
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
onError: async (err, info) => {
|
|
202
|
+
api.logger?.error?.(`[onebot] ${info?.kind} reply failed: ${err}`);
|
|
203
|
+
await clearEmojiReaction();
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
replyOptions: { disableBlockStreaming: true },
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
catch (err) {
|
|
210
|
+
await clearEmojiReaction();
|
|
211
|
+
api.logger?.error?.(`[onebot] dispatch failed: ${err?.message}`);
|
|
212
|
+
try {
|
|
213
|
+
const { userId: uid, groupId: gid, isGroup: ig } = ctxPayload._onebot || {};
|
|
214
|
+
if (ig && gid)
|
|
215
|
+
await sendGroupMsg(gid, `处理失败: ${err?.message?.slice(0, 80) || "未知错误"}`);
|
|
216
|
+
else if (uid)
|
|
217
|
+
await sendPrivateMsg(uid, `处理失败: ${err?.message?.slice(0, 80) || "未知错误"}`);
|
|
218
|
+
}
|
|
219
|
+
catch (_) { }
|
|
220
|
+
}
|
|
221
|
+
finally {
|
|
222
|
+
clearActiveReplyTarget();
|
|
223
|
+
}
|
|
224
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenClaw OneBot Channel Plugin
|
|
3
|
+
*
|
|
4
|
+
* 将 OneBot v11 协议(QQ/Lagrange.Core/go-cqhttp)接入 OpenClaw Gateway。
|
|
5
|
+
*
|
|
6
|
+
* 发送逻辑(参照飞书实现):
|
|
7
|
+
* - 由 OpenClaw 主包解析 `openclaw message send --channel onebot ...` 命令
|
|
8
|
+
* - 根据 --channel 查找已注册的 onebot 渠道,调用其 outbound.sendText / outbound.sendMedia
|
|
9
|
+
* - 不注册 Agent 工具,避免重复实现;Agent 回复时由 process-inbound 的 deliver 自动发送
|
|
10
|
+
*/
|
|
11
|
+
export default function register(api: any): void;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenClaw OneBot Channel Plugin
|
|
3
|
+
*
|
|
4
|
+
* 将 OneBot v11 协议(QQ/Lagrange.Core/go-cqhttp)接入 OpenClaw Gateway。
|
|
5
|
+
*
|
|
6
|
+
* 发送逻辑(参照飞书实现):
|
|
7
|
+
* - 由 OpenClaw 主包解析 `openclaw message send --channel onebot ...` 命令
|
|
8
|
+
* - 根据 --channel 查找已注册的 onebot 渠道,调用其 outbound.sendText / outbound.sendMedia
|
|
9
|
+
* - 不注册 Agent 工具,避免重复实现;Agent 回复时由 process-inbound 的 deliver 自动发送
|
|
10
|
+
*/
|
|
11
|
+
import { OneBotChannelPlugin } from "./channel.js";
|
|
12
|
+
import { registerService } from "./service.js";
|
|
13
|
+
import { startImageTempCleanup } from "./connection.js";
|
|
14
|
+
export default function register(api) {
|
|
15
|
+
globalThis.__onebotApi = api;
|
|
16
|
+
globalThis.__onebotGatewayConfig = api.config;
|
|
17
|
+
startImageTempCleanup();
|
|
18
|
+
api.registerChannel({ plugin: OneBotChannelPlugin });
|
|
19
|
+
if (typeof api.registerCli === "function") {
|
|
20
|
+
api.registerCli((ctx) => {
|
|
21
|
+
const prog = ctx.program;
|
|
22
|
+
if (prog && typeof prog.command === "function") {
|
|
23
|
+
const onebot = prog.command("onebot").description("OneBot 渠道配置");
|
|
24
|
+
onebot.command("setup").description("交互式配置 OneBot 连接参数").action(async () => {
|
|
25
|
+
const { runOneBotSetup } = await import("./setup.js");
|
|
26
|
+
await runOneBotSetup();
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
}, { commands: ["onebot"] });
|
|
30
|
+
}
|
|
31
|
+
registerService(api);
|
|
32
|
+
api.logger?.info?.("[onebot] plugin loaded");
|
|
33
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 动态加载用户脚本(支持 .js/.mjs/.ts/.mts)
|
|
3
|
+
* .ts/.mts 依赖 tsx 运行时
|
|
4
|
+
*/
|
|
5
|
+
import { resolve } from "path";
|
|
6
|
+
import { pathToFileURL } from "url";
|
|
7
|
+
import { extname } from "path";
|
|
8
|
+
const TS_EXT = [".ts", ".mts"];
|
|
9
|
+
export async function loadScript(scriptPath) {
|
|
10
|
+
const absPath = resolve(process.cwd(), scriptPath.trim());
|
|
11
|
+
const ext = extname(absPath).toLowerCase();
|
|
12
|
+
if (TS_EXT.includes(ext)) {
|
|
13
|
+
try {
|
|
14
|
+
await import("tsx/cjs");
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
throw new Error("执行 .ts/.mts 脚本需要安装 tsx 依赖:npm install tsx");
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
const url = pathToFileURL(absPath).href;
|
|
21
|
+
return import(url);
|
|
22
|
+
}
|
package/dist/message.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OneBot 消息解析
|
|
3
|
+
*/
|
|
4
|
+
export function getRawText(msg) {
|
|
5
|
+
if (!msg)
|
|
6
|
+
return "";
|
|
7
|
+
if (typeof msg.raw_message === "string" && msg.raw_message) {
|
|
8
|
+
return msg.raw_message;
|
|
9
|
+
}
|
|
10
|
+
const arr = msg.message;
|
|
11
|
+
if (!Array.isArray(arr))
|
|
12
|
+
return "";
|
|
13
|
+
return arr
|
|
14
|
+
.filter((m) => m?.type === "text")
|
|
15
|
+
.map((m) => m?.data?.text ?? "")
|
|
16
|
+
.join("");
|
|
17
|
+
}
|
|
18
|
+
export function isMentioned(msg, selfId) {
|
|
19
|
+
const arr = msg.message;
|
|
20
|
+
if (!Array.isArray(arr))
|
|
21
|
+
return false;
|
|
22
|
+
const selfStr = String(selfId);
|
|
23
|
+
return arr.some((m) => m?.type === "at" && String(m?.data?.qq || m?.data?.id) === selfStr);
|
|
24
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 当前回复目标上下文
|
|
3
|
+
* 当 process-inbound 处理群聊消息时设置,channel.sendMedia 可用其修正错误的 target
|
|
4
|
+
* (Agent 可能传入裸数字或 user:xxx,导致误发私聊)
|
|
5
|
+
*/
|
|
6
|
+
export declare function setActiveReplyTarget(to: string): void;
|
|
7
|
+
export declare function clearActiveReplyTarget(): void;
|
|
8
|
+
export declare function getActiveReplyTarget(): string | null;
|
|
9
|
+
/**
|
|
10
|
+
* 若当前有活跃群聊回复目标,且传入的 to 可能被误判为私聊(裸数字或 user:xxx 与群号相同),则返回修正后的 target
|
|
11
|
+
*/
|
|
12
|
+
export declare function resolveTargetForReply(to: string): string;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 当前回复目标上下文
|
|
3
|
+
* 当 process-inbound 处理群聊消息时设置,channel.sendMedia 可用其修正错误的 target
|
|
4
|
+
* (Agent 可能传入裸数字或 user:xxx,导致误发私聊)
|
|
5
|
+
*/
|
|
6
|
+
let activeReplyTarget = null;
|
|
7
|
+
export function setActiveReplyTarget(to) {
|
|
8
|
+
activeReplyTarget = to;
|
|
9
|
+
}
|
|
10
|
+
export function clearActiveReplyTarget() {
|
|
11
|
+
activeReplyTarget = null;
|
|
12
|
+
}
|
|
13
|
+
export function getActiveReplyTarget() {
|
|
14
|
+
return activeReplyTarget;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* 若当前有活跃群聊回复目标,且传入的 to 可能被误判为私聊(裸数字或 user:xxx 与群号相同),则返回修正后的 target
|
|
18
|
+
*/
|
|
19
|
+
export function resolveTargetForReply(to) {
|
|
20
|
+
const stored = activeReplyTarget;
|
|
21
|
+
if (!stored)
|
|
22
|
+
return to;
|
|
23
|
+
const m = stored.match(/group:(\d+)$/i) || stored.match(/onebot:group:(\d+)$/i);
|
|
24
|
+
if (!m)
|
|
25
|
+
return to;
|
|
26
|
+
const groupId = m[1];
|
|
27
|
+
const normalizedTo = to.replace(/^(onebot|qq|lagrange):/i, "").trim();
|
|
28
|
+
const numericPart = normalizedTo.replace(/^user:/i, "");
|
|
29
|
+
if (numericPart === groupId || normalizedTo === groupId) {
|
|
30
|
+
return stored;
|
|
31
|
+
}
|
|
32
|
+
return to;
|
|
33
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 内置定时任务调度器
|
|
3
|
+
* 根据配置在指定时间直接执行脚本,无需 AI 介入
|
|
4
|
+
*/
|
|
5
|
+
export interface CronJobConfig {
|
|
6
|
+
/** 任务名称,用于日志 */
|
|
7
|
+
name: string;
|
|
8
|
+
/** cron 表达式,如 "0 8 * * *" 表示每天 8:00 */
|
|
9
|
+
cron: string;
|
|
10
|
+
/** 时区,如 "Asia/Shanghai",默认 "Asia/Shanghai" */
|
|
11
|
+
timezone?: string;
|
|
12
|
+
/** 脚本路径,相对 process.cwd() 或绝对路径,支持 .mjs/.ts/.mts */
|
|
13
|
+
script: string;
|
|
14
|
+
/** 要推送的群号列表 */
|
|
15
|
+
groupIds: number[];
|
|
16
|
+
}
|
|
17
|
+
export declare function getCronJobsFromConfig(api: any): CronJobConfig[];
|
|
18
|
+
export declare function startScheduler(api: any): void;
|
|
19
|
+
export declare function stopScheduler(): void;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 内置定时任务调度器
|
|
3
|
+
* 根据配置在指定时间直接执行脚本,无需 AI 介入
|
|
4
|
+
*/
|
|
5
|
+
import { CronJob } from "cron";
|
|
6
|
+
import { loadScript } from "./load-script.js";
|
|
7
|
+
import { onebotClient } from "./tools.js";
|
|
8
|
+
import { getWs } from "./connection.js";
|
|
9
|
+
import WebSocket from "ws";
|
|
10
|
+
let scheduledJobs = [];
|
|
11
|
+
export function getCronJobsFromConfig(api) {
|
|
12
|
+
const cfg = api?.config ?? globalThis.__onebotGatewayConfig;
|
|
13
|
+
const jobs = cfg?.channels?.onebot?.cronJobs;
|
|
14
|
+
if (!Array.isArray(jobs) || jobs.length === 0)
|
|
15
|
+
return [];
|
|
16
|
+
return jobs.filter((j) => j &&
|
|
17
|
+
typeof j.name === "string" &&
|
|
18
|
+
typeof j.cron === "string" &&
|
|
19
|
+
typeof j.script === "string" &&
|
|
20
|
+
Array.isArray(j.groupIds));
|
|
21
|
+
}
|
|
22
|
+
async function runJob(api, job) {
|
|
23
|
+
const logger = api?.logger;
|
|
24
|
+
const w = getWs();
|
|
25
|
+
if (!w || w.readyState !== WebSocket.OPEN) {
|
|
26
|
+
logger?.warn?.(`[onebot] cron "${job.name}" 跳过:OneBot 未连接`);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
logger?.info?.(`[onebot] cron 执行: ${job.name}`);
|
|
30
|
+
try {
|
|
31
|
+
const mod = await loadScript(job.script);
|
|
32
|
+
const fn = mod?.default ?? mod?.run ?? mod?.execute;
|
|
33
|
+
if (typeof fn !== "function") {
|
|
34
|
+
logger?.error?.(`[onebot] cron "${job.name}": 脚本未导出 default/run/execute 函数`);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const ctx = {
|
|
38
|
+
onebot: onebotClient,
|
|
39
|
+
groupIds: job.groupIds,
|
|
40
|
+
};
|
|
41
|
+
const result = await fn(ctx);
|
|
42
|
+
logger?.info?.(`[onebot] cron "${job.name}" 完成: ${result != null ? String(result) : "ok"}`);
|
|
43
|
+
}
|
|
44
|
+
catch (e) {
|
|
45
|
+
logger?.error?.(`[onebot] cron "${job.name}" 失败: ${e?.message}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export function startScheduler(api) {
|
|
49
|
+
stopScheduler();
|
|
50
|
+
const jobs = getCronJobsFromConfig(api);
|
|
51
|
+
if (jobs.length === 0)
|
|
52
|
+
return;
|
|
53
|
+
const logger = api?.logger;
|
|
54
|
+
logger?.info?.(`[onebot] 启动 ${jobs.length} 个定时任务(无 AI 介入)`);
|
|
55
|
+
for (const job of jobs) {
|
|
56
|
+
try {
|
|
57
|
+
const cronJob = new CronJob(job.cron, () => runJob(api, job), null, true, job.timezone ?? "Asia/Shanghai");
|
|
58
|
+
scheduledJobs.push(cronJob);
|
|
59
|
+
}
|
|
60
|
+
catch (e) {
|
|
61
|
+
logger?.warn?.(`[onebot] cron "${job.name}" 注册失败: ${e?.message}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export function stopScheduler() {
|
|
66
|
+
for (const j of scheduledJobs) {
|
|
67
|
+
j.stop();
|
|
68
|
+
}
|
|
69
|
+
scheduledJobs = [];
|
|
70
|
+
}
|
package/dist/sdk.d.ts
ADDED
package/dist/sdk.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenClaw / ClawdBot Plugin SDK 懒加载
|
|
3
|
+
*/
|
|
4
|
+
let sdkLoaded = false;
|
|
5
|
+
let buildPendingHistoryContextFromMap;
|
|
6
|
+
let recordPendingHistoryEntry;
|
|
7
|
+
let clearHistoryEntriesIfEnabled;
|
|
8
|
+
export async function loadPluginSdk() {
|
|
9
|
+
if (sdkLoaded)
|
|
10
|
+
return;
|
|
11
|
+
try {
|
|
12
|
+
const sdk = await import("openclaw/plugin-sdk");
|
|
13
|
+
buildPendingHistoryContextFromMap = sdk.buildPendingHistoryContextFromMap;
|
|
14
|
+
recordPendingHistoryEntry = sdk.recordPendingHistoryEntry;
|
|
15
|
+
clearHistoryEntriesIfEnabled = sdk.clearHistoryEntriesIfEnabled;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
try {
|
|
19
|
+
const sdk = await import("clawdbot/plugin-sdk");
|
|
20
|
+
buildPendingHistoryContextFromMap = sdk.buildPendingHistoryContextFromMap;
|
|
21
|
+
recordPendingHistoryEntry = sdk.recordPendingHistoryEntry;
|
|
22
|
+
clearHistoryEntriesIfEnabled = sdk.clearHistoryEntriesIfEnabled;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
console.warn("[onebot] plugin-sdk not found, history features disabled");
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
sdkLoaded = true;
|
|
29
|
+
}
|
|
30
|
+
export function getSdk() {
|
|
31
|
+
return {
|
|
32
|
+
buildPendingHistoryContextFromMap,
|
|
33
|
+
recordPendingHistoryEntry,
|
|
34
|
+
clearHistoryEntriesIfEnabled,
|
|
35
|
+
};
|
|
36
|
+
}
|
package/dist/send.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OneBot 消息发送 — 文本与媒体
|
|
3
|
+
* 对应 Lagrange.onebot context.ts 的 sendPrivateMsg / sendGroupMsg / 图片消息
|
|
4
|
+
*/
|
|
5
|
+
export interface OneBotSendResult {
|
|
6
|
+
ok: boolean;
|
|
7
|
+
messageId?: string;
|
|
8
|
+
error?: string;
|
|
9
|
+
}
|
|
10
|
+
import type { OneBotAccountConfig } from "./types.js";
|
|
11
|
+
type OneBotConfigGetter = () => OneBotAccountConfig | null;
|
|
12
|
+
/**
|
|
13
|
+
* 发送文本消息到 OneBot 目标(私聊或群聊)
|
|
14
|
+
* @param getConfig 可选,用于按需连接(forward-websocket 下 message send 可独立运行)
|
|
15
|
+
*/
|
|
16
|
+
export declare function sendTextMessage(to: string, text: string, getConfig?: OneBotConfigGetter): Promise<OneBotSendResult>;
|
|
17
|
+
/**
|
|
18
|
+
* 发送媒体消息(图片等)到 OneBot 目标
|
|
19
|
+
* mediaUrl 支持 file:// 路径、http(s):// URL、base64://
|
|
20
|
+
* @param getConfig 可选,用于按需连接
|
|
21
|
+
*/
|
|
22
|
+
export declare function sendMediaMessage(to: string, mediaUrl: string, text?: string, getConfig?: OneBotConfigGetter): Promise<OneBotSendResult>;
|
|
23
|
+
export {};
|
package/dist/send.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OneBot 消息发送 — 文本与媒体
|
|
3
|
+
* 对应 Lagrange.onebot context.ts 的 sendPrivateMsg / sendGroupMsg / 图片消息
|
|
4
|
+
*/
|
|
5
|
+
import { sendPrivateMsg, sendGroupMsg, sendPrivateImage, sendGroupImage, } from "./connection.js";
|
|
6
|
+
import { resolveTargetForReply } from "./reply-context.js";
|
|
7
|
+
function parseTarget(to) {
|
|
8
|
+
const t = to.replace(/^(onebot|qq|lagrange):/i, "").trim();
|
|
9
|
+
if (!t)
|
|
10
|
+
return null;
|
|
11
|
+
if (t.startsWith("group:")) {
|
|
12
|
+
const id = parseInt(t.slice(6), 10);
|
|
13
|
+
if (isNaN(id))
|
|
14
|
+
return null;
|
|
15
|
+
return { type: "group", id };
|
|
16
|
+
}
|
|
17
|
+
const raw = t.replace(/^user:/, "");
|
|
18
|
+
const id = parseInt(raw, 10);
|
|
19
|
+
if (isNaN(id))
|
|
20
|
+
return null;
|
|
21
|
+
if (raw === t && !t.includes(":")) {
|
|
22
|
+
return { type: id > 100000000 ? "user" : "group", id };
|
|
23
|
+
}
|
|
24
|
+
return { type: "user", id };
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* 发送文本消息到 OneBot 目标(私聊或群聊)
|
|
28
|
+
* @param getConfig 可选,用于按需连接(forward-websocket 下 message send 可独立运行)
|
|
29
|
+
*/
|
|
30
|
+
export async function sendTextMessage(to, text, getConfig) {
|
|
31
|
+
const resolvedTo = resolveTargetForReply(to);
|
|
32
|
+
const target = parseTarget(resolvedTo);
|
|
33
|
+
if (!target) {
|
|
34
|
+
return { ok: false, error: `Invalid target: ${to}` };
|
|
35
|
+
}
|
|
36
|
+
if (!text?.trim()) {
|
|
37
|
+
return { ok: false, error: "No text provided" };
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
let messageId;
|
|
41
|
+
if (target.type === "group") {
|
|
42
|
+
messageId = await sendGroupMsg(target.id, text, getConfig);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
messageId = await sendPrivateMsg(target.id, text, getConfig);
|
|
46
|
+
}
|
|
47
|
+
return { ok: true, messageId: messageId != null ? String(messageId) : "" };
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
return {
|
|
51
|
+
ok: false,
|
|
52
|
+
error: err instanceof Error ? err.message : String(err),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* 发送媒体消息(图片等)到 OneBot 目标
|
|
58
|
+
* mediaUrl 支持 file:// 路径、http(s):// URL、base64://
|
|
59
|
+
* @param getConfig 可选,用于按需连接
|
|
60
|
+
*/
|
|
61
|
+
export async function sendMediaMessage(to, mediaUrl, text, getConfig) {
|
|
62
|
+
const resolvedTo = resolveTargetForReply(to);
|
|
63
|
+
const target = parseTarget(resolvedTo);
|
|
64
|
+
if (!target) {
|
|
65
|
+
return { ok: false, error: `Invalid target: ${to}` };
|
|
66
|
+
}
|
|
67
|
+
if (!mediaUrl?.trim()) {
|
|
68
|
+
return { ok: false, error: "No mediaUrl provided" };
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
let messageId;
|
|
72
|
+
if (text?.trim()) {
|
|
73
|
+
if (target.type === "group") {
|
|
74
|
+
messageId = await sendGroupMsg(target.id, text, getConfig);
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
messageId = await sendPrivateMsg(target.id, text, getConfig);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (target.type === "group") {
|
|
81
|
+
const id = await sendGroupImage(target.id, mediaUrl, undefined, getConfig);
|
|
82
|
+
if (id != null)
|
|
83
|
+
messageId = id;
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
const id = await sendPrivateImage(target.id, mediaUrl, undefined, getConfig);
|
|
87
|
+
if (id != null)
|
|
88
|
+
messageId = id;
|
|
89
|
+
}
|
|
90
|
+
return { ok: true, messageId: messageId != null ? String(messageId) : "" };
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
return {
|
|
94
|
+
ok: false,
|
|
95
|
+
error: err instanceof Error ? err.message : String(err),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
}
|