@ssfxx44533/onebot-qq 0.1.2
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 +119 -0
- package/index.ts +3 -0
- package/openclaw.plugin.json +192 -0
- package/package.json +47 -0
- package/setup-entry.ts +3 -0
- package/src/channel.ts +549 -0
- package/src/config.js +380 -0
- package/src/inbound.ts +587 -0
- package/src/message.js +235 -0
- package/src/onebot-client.js +226 -0
- package/src/plugin.ts +16 -0
- package/src/runtime.js +85 -0
package/src/config.js
ADDED
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
export const CHANNEL_ID = "onebot-qq";
|
|
4
|
+
export const DEFAULT_ACCOUNT_ID = "default";
|
|
5
|
+
export const DEFAULT_WS_URL = "ws://localhost:3001/?access_token=napcat_ws";
|
|
6
|
+
export const DEFAULT_CALL_TIMEOUT_MS = 10_000;
|
|
7
|
+
export const DEFAULT_RECONNECT_BASE_MS = 2_000;
|
|
8
|
+
export const DEFAULT_RECONNECT_MAX_MS = 30_000;
|
|
9
|
+
export const DEFAULT_MAX_CHUNK_LENGTH = 1200;
|
|
10
|
+
export const DEFAULT_DM_POLICY = "open";
|
|
11
|
+
export const DEFAULT_GROUP_POLICY = "open";
|
|
12
|
+
export const DEFAULT_GROUP_REQUIRE_MENTION = true;
|
|
13
|
+
|
|
14
|
+
export const ONEBOT_QQ_CHANNEL_SCHEMA = {
|
|
15
|
+
type: "object",
|
|
16
|
+
additionalProperties: false,
|
|
17
|
+
properties: {
|
|
18
|
+
enabled: { type: "boolean" },
|
|
19
|
+
name: { type: "string" },
|
|
20
|
+
wsUrl: { type: "string" },
|
|
21
|
+
accessToken: { type: "string" },
|
|
22
|
+
selfId: { type: "string" },
|
|
23
|
+
defaultTo: { type: "string" },
|
|
24
|
+
dmPolicy: {
|
|
25
|
+
type: "string",
|
|
26
|
+
enum: ["open", "allowlist", "pairing", "disabled"],
|
|
27
|
+
},
|
|
28
|
+
groupPolicy: {
|
|
29
|
+
type: "string",
|
|
30
|
+
enum: ["open", "allowlist", "disabled"],
|
|
31
|
+
},
|
|
32
|
+
allowFrom: {
|
|
33
|
+
type: "array",
|
|
34
|
+
items: { type: "string" },
|
|
35
|
+
},
|
|
36
|
+
groupAllowFrom: {
|
|
37
|
+
type: "array",
|
|
38
|
+
items: { type: "string" },
|
|
39
|
+
},
|
|
40
|
+
requireMention: { type: "boolean" },
|
|
41
|
+
mediaMaxMb: { type: "number", minimum: 0 },
|
|
42
|
+
maxChunkLength: { type: "integer", minimum: 1 },
|
|
43
|
+
callTimeoutMs: { type: "integer", minimum: 1 },
|
|
44
|
+
reconnectBaseMs: { type: "integer", minimum: 1 },
|
|
45
|
+
reconnectMaxMs: { type: "integer", minimum: 1 },
|
|
46
|
+
groups: {
|
|
47
|
+
type: "object",
|
|
48
|
+
additionalProperties: {
|
|
49
|
+
type: "object",
|
|
50
|
+
additionalProperties: false,
|
|
51
|
+
properties: {
|
|
52
|
+
enabled: { type: "boolean" },
|
|
53
|
+
requireMention: { type: "boolean" },
|
|
54
|
+
systemPrompt: { type: "string" },
|
|
55
|
+
allowFrom: {
|
|
56
|
+
type: "array",
|
|
57
|
+
items: { type: "string" },
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
accounts: {
|
|
63
|
+
type: "object",
|
|
64
|
+
additionalProperties: {
|
|
65
|
+
type: "object",
|
|
66
|
+
additionalProperties: false,
|
|
67
|
+
properties: {
|
|
68
|
+
enabled: { type: "boolean" },
|
|
69
|
+
name: { type: "string" },
|
|
70
|
+
wsUrl: { type: "string" },
|
|
71
|
+
accessToken: { type: "string" },
|
|
72
|
+
selfId: { type: "string" },
|
|
73
|
+
defaultTo: { type: "string" },
|
|
74
|
+
dmPolicy: {
|
|
75
|
+
type: "string",
|
|
76
|
+
enum: ["open", "allowlist", "pairing", "disabled"],
|
|
77
|
+
},
|
|
78
|
+
groupPolicy: {
|
|
79
|
+
type: "string",
|
|
80
|
+
enum: ["open", "allowlist", "disabled"],
|
|
81
|
+
},
|
|
82
|
+
allowFrom: {
|
|
83
|
+
type: "array",
|
|
84
|
+
items: { type: "string" },
|
|
85
|
+
},
|
|
86
|
+
groupAllowFrom: {
|
|
87
|
+
type: "array",
|
|
88
|
+
items: { type: "string" },
|
|
89
|
+
},
|
|
90
|
+
requireMention: { type: "boolean" },
|
|
91
|
+
mediaMaxMb: { type: "number", minimum: 0 },
|
|
92
|
+
maxChunkLength: { type: "integer", minimum: 1 },
|
|
93
|
+
callTimeoutMs: { type: "integer", minimum: 1 },
|
|
94
|
+
reconnectBaseMs: { type: "integer", minimum: 1 },
|
|
95
|
+
reconnectMaxMs: { type: "integer", minimum: 1 },
|
|
96
|
+
groups: {
|
|
97
|
+
type: "object",
|
|
98
|
+
additionalProperties: {
|
|
99
|
+
type: "object",
|
|
100
|
+
additionalProperties: false,
|
|
101
|
+
properties: {
|
|
102
|
+
enabled: { type: "boolean" },
|
|
103
|
+
requireMention: { type: "boolean" },
|
|
104
|
+
systemPrompt: { type: "string" },
|
|
105
|
+
allowFrom: {
|
|
106
|
+
type: "array",
|
|
107
|
+
items: { type: "string" },
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
function toPositiveInt(value, fallback) {
|
|
119
|
+
const parsed = Number.parseInt(String(value ?? ""), 10);
|
|
120
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function toIdList(value) {
|
|
124
|
+
if (!Array.isArray(value)) {
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
127
|
+
return value
|
|
128
|
+
.map((entry) => String(entry ?? "").trim())
|
|
129
|
+
.filter(Boolean);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function resolveAccountSource(channelConfig, accountId) {
|
|
133
|
+
if (
|
|
134
|
+
channelConfig?.accounts &&
|
|
135
|
+
typeof channelConfig.accounts === "object" &&
|
|
136
|
+
!Array.isArray(channelConfig.accounts) &&
|
|
137
|
+
accountId &&
|
|
138
|
+
accountId !== DEFAULT_ACCOUNT_ID
|
|
139
|
+
) {
|
|
140
|
+
const candidate = channelConfig.accounts[accountId];
|
|
141
|
+
if (candidate && typeof candidate === "object" && !Array.isArray(candidate)) {
|
|
142
|
+
return candidate;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return {};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function listOneBotAccountIds(cfg) {
|
|
149
|
+
const channelConfig = cfg?.channels?.[CHANNEL_ID];
|
|
150
|
+
if (!channelConfig || typeof channelConfig !== "object" || Array.isArray(channelConfig)) {
|
|
151
|
+
return [DEFAULT_ACCOUNT_ID];
|
|
152
|
+
}
|
|
153
|
+
const accountIds = Object.keys(channelConfig.accounts ?? {}).filter(Boolean);
|
|
154
|
+
return accountIds.length > 0 ? accountIds : [DEFAULT_ACCOUNT_ID];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function buildOneBotWsUrl({ wsUrl, accessToken }) {
|
|
158
|
+
const finalUrl = String(wsUrl ?? "").trim() || DEFAULT_WS_URL;
|
|
159
|
+
const token = String(accessToken ?? "").trim();
|
|
160
|
+
if (!token) {
|
|
161
|
+
return finalUrl;
|
|
162
|
+
}
|
|
163
|
+
const parsed = new URL(finalUrl);
|
|
164
|
+
if (!parsed.searchParams.has("access_token")) {
|
|
165
|
+
parsed.searchParams.set("access_token", token);
|
|
166
|
+
}
|
|
167
|
+
return parsed.toString();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function resolveOneBotAccount(cfg, accountId = DEFAULT_ACCOUNT_ID) {
|
|
171
|
+
const channelConfig = cfg?.channels?.[CHANNEL_ID];
|
|
172
|
+
const base =
|
|
173
|
+
channelConfig && typeof channelConfig === "object" && !Array.isArray(channelConfig)
|
|
174
|
+
? channelConfig
|
|
175
|
+
: {};
|
|
176
|
+
const scoped = resolveAccountSource(base, accountId);
|
|
177
|
+
const merged = { ...base, ...scoped };
|
|
178
|
+
const resolvedAccountId = String(accountId || DEFAULT_ACCOUNT_ID);
|
|
179
|
+
const name = String(merged.name ?? "OneBot QQ").trim() || "OneBot QQ";
|
|
180
|
+
const groups =
|
|
181
|
+
merged.groups && typeof merged.groups === "object" && !Array.isArray(merged.groups)
|
|
182
|
+
? merged.groups
|
|
183
|
+
: {};
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
accountId: resolvedAccountId,
|
|
187
|
+
name,
|
|
188
|
+
enabled: merged.enabled !== false,
|
|
189
|
+
configured: true,
|
|
190
|
+
wsUrl: buildOneBotWsUrl({
|
|
191
|
+
wsUrl: merged.wsUrl,
|
|
192
|
+
accessToken: merged.accessToken,
|
|
193
|
+
}),
|
|
194
|
+
selfId: String(merged.selfId ?? "").trim() || null,
|
|
195
|
+
defaultTo: String(merged.defaultTo ?? "").trim() || null,
|
|
196
|
+
dmPolicy: String(merged.dmPolicy ?? DEFAULT_DM_POLICY).trim() || DEFAULT_DM_POLICY,
|
|
197
|
+
groupPolicy: String(merged.groupPolicy ?? DEFAULT_GROUP_POLICY).trim() || DEFAULT_GROUP_POLICY,
|
|
198
|
+
requireMention:
|
|
199
|
+
typeof merged.requireMention === "boolean"
|
|
200
|
+
? merged.requireMention
|
|
201
|
+
: DEFAULT_GROUP_REQUIRE_MENTION,
|
|
202
|
+
mediaMaxMb:
|
|
203
|
+
typeof merged.mediaMaxMb === "number" && merged.mediaMaxMb > 0
|
|
204
|
+
? merged.mediaMaxMb
|
|
205
|
+
: undefined,
|
|
206
|
+
allowFrom: toIdList(merged.allowFrom),
|
|
207
|
+
groupAllowFrom: toIdList(merged.groupAllowFrom),
|
|
208
|
+
maxChunkLength: toPositiveInt(merged.maxChunkLength, DEFAULT_MAX_CHUNK_LENGTH),
|
|
209
|
+
callTimeoutMs: toPositiveInt(merged.callTimeoutMs, DEFAULT_CALL_TIMEOUT_MS),
|
|
210
|
+
reconnectBaseMs: toPositiveInt(merged.reconnectBaseMs, DEFAULT_RECONNECT_BASE_MS),
|
|
211
|
+
reconnectMaxMs: Math.max(
|
|
212
|
+
toPositiveInt(merged.reconnectMaxMs, DEFAULT_RECONNECT_MAX_MS),
|
|
213
|
+
toPositiveInt(merged.reconnectBaseMs, DEFAULT_RECONNECT_BASE_MS),
|
|
214
|
+
),
|
|
215
|
+
groups,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function resolveOneBotGroupConfig(account, groupId) {
|
|
220
|
+
const key = String(groupId ?? "").trim();
|
|
221
|
+
if (!key) {
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
const candidate = account?.groups?.[key];
|
|
225
|
+
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
return {
|
|
229
|
+
enabled: candidate.enabled !== false,
|
|
230
|
+
requireMention:
|
|
231
|
+
typeof candidate.requireMention === "boolean"
|
|
232
|
+
? candidate.requireMention
|
|
233
|
+
: undefined,
|
|
234
|
+
systemPrompt:
|
|
235
|
+
typeof candidate.systemPrompt === "string" && candidate.systemPrompt.trim()
|
|
236
|
+
? candidate.systemPrompt.trim()
|
|
237
|
+
: undefined,
|
|
238
|
+
allowFrom: toIdList(candidate.allowFrom),
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function normalizeAllowList(values) {
|
|
243
|
+
return toIdList(values)
|
|
244
|
+
.map((entry) => normalizeOneBotSenderEntry(entry))
|
|
245
|
+
.filter(Boolean);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function matchAllowedSender(senderId, allowList) {
|
|
249
|
+
const normalizedSenderId = normalizeOneBotUserId(senderId);
|
|
250
|
+
if (!normalizedSenderId) {
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
const normalizedAllowList = normalizeAllowList(allowList);
|
|
254
|
+
return normalizedAllowList.includes("*") || normalizedAllowList.includes(normalizedSenderId);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export function resolveLocalFilePath(mediaUrl) {
|
|
258
|
+
const raw = String(mediaUrl ?? "").trim();
|
|
259
|
+
if (!raw) {
|
|
260
|
+
return raw;
|
|
261
|
+
}
|
|
262
|
+
if (raw.startsWith("file://")) {
|
|
263
|
+
return new URL(raw).pathname;
|
|
264
|
+
}
|
|
265
|
+
if (path.isAbsolute(raw)) {
|
|
266
|
+
return raw;
|
|
267
|
+
}
|
|
268
|
+
return path.resolve(raw);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function isRemoteUrl(value) {
|
|
272
|
+
return /^https?:\/\//i.test(String(value ?? "").trim());
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function resolveMediaKind(mediaUrl) {
|
|
276
|
+
const raw = String(mediaUrl ?? "").trim();
|
|
277
|
+
if (!raw) {
|
|
278
|
+
return "file";
|
|
279
|
+
}
|
|
280
|
+
let pathname = raw;
|
|
281
|
+
if (isRemoteUrl(raw) || raw.startsWith("file://")) {
|
|
282
|
+
try {
|
|
283
|
+
pathname = new URL(raw).pathname;
|
|
284
|
+
} catch {
|
|
285
|
+
pathname = raw;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
const ext = path.extname(pathname).toLowerCase();
|
|
289
|
+
if ([".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp"].includes(ext)) {
|
|
290
|
+
return "image";
|
|
291
|
+
}
|
|
292
|
+
if ([".mp3", ".wav", ".ogg", ".m4a", ".aac", ".flac", ".amr", ".silk"].includes(ext)) {
|
|
293
|
+
return "audio";
|
|
294
|
+
}
|
|
295
|
+
if ([".mp4", ".mov", ".webm", ".mkv"].includes(ext)) {
|
|
296
|
+
return "video";
|
|
297
|
+
}
|
|
298
|
+
return "file";
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export function resolveUploadName(mediaUrl) {
|
|
302
|
+
const raw = String(mediaUrl ?? "").trim();
|
|
303
|
+
if (!raw) {
|
|
304
|
+
return "attachment";
|
|
305
|
+
}
|
|
306
|
+
if (isRemoteUrl(raw) || raw.startsWith("file://")) {
|
|
307
|
+
try {
|
|
308
|
+
return path.basename(new URL(raw).pathname) || "attachment";
|
|
309
|
+
} catch {
|
|
310
|
+
return path.basename(raw) || "attachment";
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return path.basename(raw) || "attachment";
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export function parseOutboundTarget(rawTarget, fallbackTarget = null) {
|
|
317
|
+
const raw = String(rawTarget ?? "").trim();
|
|
318
|
+
if (!raw) {
|
|
319
|
+
return fallbackTarget;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const groupId = normalizeOneBotGroupId(raw);
|
|
323
|
+
if (groupId) {
|
|
324
|
+
return {
|
|
325
|
+
chatType: "group",
|
|
326
|
+
targetId: groupId,
|
|
327
|
+
groupId,
|
|
328
|
+
userId: null,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const userId = normalizeOneBotUserId(raw);
|
|
333
|
+
if (userId) {
|
|
334
|
+
return {
|
|
335
|
+
chatType: "private",
|
|
336
|
+
targetId: userId,
|
|
337
|
+
groupId: null,
|
|
338
|
+
userId,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export function normalizeOneBotUserId(value) {
|
|
346
|
+
const raw = String(value ?? "").trim();
|
|
347
|
+
if (!raw) {
|
|
348
|
+
return "";
|
|
349
|
+
}
|
|
350
|
+
const normalized = raw
|
|
351
|
+
.replace(/^onebot-qq:/i, "")
|
|
352
|
+
.replace(/^qq-private:/i, "")
|
|
353
|
+
.replace(/^(?:private|user|qq|qq-private|u):/i, "")
|
|
354
|
+
.trim();
|
|
355
|
+
return /^\d+$/.test(normalized) ? normalized : "";
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
export function normalizeOneBotGroupId(value) {
|
|
359
|
+
const raw = String(value ?? "").trim();
|
|
360
|
+
if (!raw) {
|
|
361
|
+
return "";
|
|
362
|
+
}
|
|
363
|
+
const normalized = raw
|
|
364
|
+
.replace(/^onebot-qq:/i, "")
|
|
365
|
+
.replace(/^qq-group:/i, "")
|
|
366
|
+
.replace(/^(?:group|qq-group|g):/i, "")
|
|
367
|
+
.trim();
|
|
368
|
+
return /^\d+$/.test(normalized) ? normalized : "";
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
export function normalizeOneBotSenderEntry(value) {
|
|
372
|
+
const raw = String(value ?? "").trim();
|
|
373
|
+
if (!raw) {
|
|
374
|
+
return "";
|
|
375
|
+
}
|
|
376
|
+
if (raw === "*") {
|
|
377
|
+
return "*";
|
|
378
|
+
}
|
|
379
|
+
return normalizeOneBotUserId(raw).toLowerCase();
|
|
380
|
+
}
|