@ryantest/openclaw-qqbot 0.0.1
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 +22 -0
- package/README.md +483 -0
- package/README.zh.md +478 -0
- package/bin/qqbot-cli.js +243 -0
- package/clawdbot.plugin.json +16 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +26 -0
- package/dist/src/admin-resolver.d.ts +27 -0
- package/dist/src/admin-resolver.js +122 -0
- package/dist/src/api.d.ts +156 -0
- package/dist/src/api.js +599 -0
- package/dist/src/channel.d.ts +11 -0
- package/dist/src/channel.js +354 -0
- package/dist/src/config.d.ts +25 -0
- package/dist/src/config.js +161 -0
- package/dist/src/credential-backup.d.ts +31 -0
- package/dist/src/credential-backup.js +66 -0
- package/dist/src/gateway.d.ts +18 -0
- package/dist/src/gateway.js +1265 -0
- package/dist/src/image-server.d.ts +68 -0
- package/dist/src/image-server.js +462 -0
- package/dist/src/inbound-attachments.d.ts +58 -0
- package/dist/src/inbound-attachments.js +234 -0
- package/dist/src/known-users.d.ts +100 -0
- package/dist/src/known-users.js +263 -0
- package/dist/src/message-queue.d.ts +50 -0
- package/dist/src/message-queue.js +115 -0
- package/dist/src/onboarding.d.ts +10 -0
- package/dist/src/onboarding.js +203 -0
- package/dist/src/outbound-deliver.d.ts +48 -0
- package/dist/src/outbound-deliver.js +462 -0
- package/dist/src/outbound.d.ts +203 -0
- package/dist/src/outbound.js +1102 -0
- package/dist/src/proactive.d.ts +170 -0
- package/dist/src/proactive.js +399 -0
- package/dist/src/ref-index-store.d.ts +70 -0
- package/dist/src/ref-index-store.js +273 -0
- package/dist/src/reply-dispatcher.d.ts +35 -0
- package/dist/src/reply-dispatcher.js +311 -0
- package/dist/src/runtime.d.ts +3 -0
- package/dist/src/runtime.js +10 -0
- package/dist/src/session-store.d.ts +52 -0
- package/dist/src/session-store.js +254 -0
- package/dist/src/slash-commands.d.ts +71 -0
- package/dist/src/slash-commands.js +1179 -0
- package/dist/src/startup-greeting.d.ts +30 -0
- package/dist/src/startup-greeting.js +78 -0
- package/dist/src/stt.d.ts +21 -0
- package/dist/src/stt.js +70 -0
- package/dist/src/tools/channel.d.ts +16 -0
- package/dist/src/tools/channel.js +234 -0
- package/dist/src/tools/remind.d.ts +2 -0
- package/dist/src/tools/remind.js +247 -0
- package/dist/src/types.d.ts +175 -0
- package/dist/src/types.js +1 -0
- package/dist/src/typing-keepalive.d.ts +27 -0
- package/dist/src/typing-keepalive.js +64 -0
- package/dist/src/update-checker.d.ts +34 -0
- package/dist/src/update-checker.js +166 -0
- package/dist/src/user-messages.d.ts +8 -0
- package/dist/src/user-messages.js +8 -0
- package/dist/src/utils/audio-convert.d.ts +89 -0
- package/dist/src/utils/audio-convert.js +704 -0
- package/dist/src/utils/file-utils.d.ts +55 -0
- package/dist/src/utils/file-utils.js +150 -0
- package/dist/src/utils/image-size.d.ts +51 -0
- package/dist/src/utils/image-size.js +234 -0
- package/dist/src/utils/media-tags.d.ts +14 -0
- package/dist/src/utils/media-tags.js +164 -0
- package/dist/src/utils/payload.d.ts +112 -0
- package/dist/src/utils/payload.js +186 -0
- package/dist/src/utils/platform.d.ts +137 -0
- package/dist/src/utils/platform.js +390 -0
- package/dist/src/utils/text-parsing.d.ts +32 -0
- package/dist/src/utils/text-parsing.js +80 -0
- package/dist/src/utils/upload-cache.d.ts +34 -0
- package/dist/src/utils/upload-cache.js +93 -0
- package/index.ts +31 -0
- package/moltbot.plugin.json +16 -0
- package/node_modules/@eshaz/web-worker/LICENSE +201 -0
- package/node_modules/@eshaz/web-worker/README.md +134 -0
- package/node_modules/@eshaz/web-worker/browser.js +17 -0
- package/node_modules/@eshaz/web-worker/cjs/browser.js +16 -0
- package/node_modules/@eshaz/web-worker/cjs/node.js +219 -0
- package/node_modules/@eshaz/web-worker/index.d.ts +4 -0
- package/node_modules/@eshaz/web-worker/node.js +223 -0
- package/node_modules/@eshaz/web-worker/package.json +54 -0
- package/node_modules/@wasm-audio-decoders/common/index.js +5 -0
- package/node_modules/@wasm-audio-decoders/common/package.json +36 -0
- package/node_modules/@wasm-audio-decoders/common/src/WASMAudioDecoderCommon.js +231 -0
- package/node_modules/@wasm-audio-decoders/common/src/WASMAudioDecoderWorker.js +129 -0
- package/node_modules/@wasm-audio-decoders/common/src/puff/README +67 -0
- package/node_modules/@wasm-audio-decoders/common/src/puff/build_puff.js +31 -0
- package/node_modules/@wasm-audio-decoders/common/src/puff/puff.c +863 -0
- package/node_modules/@wasm-audio-decoders/common/src/puff/puff.h +35 -0
- package/node_modules/@wasm-audio-decoders/common/src/utilities.js +3 -0
- package/node_modules/@wasm-audio-decoders/common/types.d.ts +7 -0
- package/node_modules/mpg123-decoder/README.md +265 -0
- package/node_modules/mpg123-decoder/dist/mpg123-decoder.min.js +185 -0
- package/node_modules/mpg123-decoder/dist/mpg123-decoder.min.js.map +1 -0
- package/node_modules/mpg123-decoder/index.js +8 -0
- package/node_modules/mpg123-decoder/package.json +58 -0
- package/node_modules/mpg123-decoder/src/EmscriptenWasm.js +464 -0
- package/node_modules/mpg123-decoder/src/MPEGDecoder.js +200 -0
- package/node_modules/mpg123-decoder/src/MPEGDecoderWebWorker.js +21 -0
- package/node_modules/mpg123-decoder/types.d.ts +30 -0
- package/node_modules/silk-wasm/LICENSE +21 -0
- package/node_modules/silk-wasm/README.md +85 -0
- package/node_modules/silk-wasm/lib/index.cjs +16 -0
- package/node_modules/silk-wasm/lib/index.d.ts +70 -0
- package/node_modules/silk-wasm/lib/index.mjs +16 -0
- package/node_modules/silk-wasm/lib/silk.wasm +0 -0
- package/node_modules/silk-wasm/lib/utils.d.ts +4 -0
- package/node_modules/silk-wasm/package.json +39 -0
- package/node_modules/simple-yenc/.github/FUNDING.yml +1 -0
- package/node_modules/simple-yenc/.prettierignore +1 -0
- package/node_modules/simple-yenc/LICENSE +7 -0
- package/node_modules/simple-yenc/README.md +163 -0
- package/node_modules/simple-yenc/dist/esm.js +1 -0
- package/node_modules/simple-yenc/dist/index.js +1 -0
- package/node_modules/simple-yenc/package.json +50 -0
- package/node_modules/simple-yenc/rollup.config.js +27 -0
- package/node_modules/simple-yenc/src/simple-yenc.js +302 -0
- package/node_modules/ws/LICENSE +20 -0
- package/node_modules/ws/README.md +548 -0
- package/node_modules/ws/browser.js +8 -0
- package/node_modules/ws/index.js +13 -0
- package/node_modules/ws/lib/buffer-util.js +131 -0
- package/node_modules/ws/lib/constants.js +19 -0
- package/node_modules/ws/lib/event-target.js +292 -0
- package/node_modules/ws/lib/extension.js +203 -0
- package/node_modules/ws/lib/limiter.js +55 -0
- package/node_modules/ws/lib/permessage-deflate.js +528 -0
- package/node_modules/ws/lib/receiver.js +706 -0
- package/node_modules/ws/lib/sender.js +602 -0
- package/node_modules/ws/lib/stream.js +161 -0
- package/node_modules/ws/lib/subprotocol.js +62 -0
- package/node_modules/ws/lib/validation.js +152 -0
- package/node_modules/ws/lib/websocket-server.js +554 -0
- package/node_modules/ws/lib/websocket.js +1393 -0
- package/node_modules/ws/package.json +69 -0
- package/node_modules/ws/wrapper.mjs +8 -0
- package/openclaw.plugin.json +16 -0
- package/package.json +76 -0
- package/scripts/cleanup-legacy-plugins.sh +124 -0
- package/scripts/proactive-api-server.ts +369 -0
- package/scripts/send-proactive.ts +293 -0
- package/scripts/set-markdown.sh +156 -0
- package/scripts/test-sendmedia.ts +116 -0
- package/scripts/upgrade-via-alt-pkg.sh +307 -0
- package/scripts/upgrade-via-npm.ps1 +296 -0
- package/scripts/upgrade-via-npm.sh +301 -0
- package/scripts/upgrade-via-source.sh +774 -0
- package/skills/qqbot-channel/SKILL.md +263 -0
- package/skills/qqbot-channel/references/api_references.md +521 -0
- package/skills/qqbot-media/SKILL.md +56 -0
- package/skills/qqbot-remind/SKILL.md +149 -0
- package/src/admin-resolver.ts +140 -0
- package/src/api.ts +819 -0
- package/src/bot-logs-2026-03-21T11-21-47(2).txt +46 -0
- package/src/channel.ts +381 -0
- package/src/config.ts +187 -0
- package/src/credential-backup.ts +72 -0
- package/src/gateway.log +43 -0
- package/src/gateway.ts +1404 -0
- package/src/image-server.ts +539 -0
- package/src/inbound-attachments.ts +304 -0
- package/src/known-users.ts +353 -0
- package/src/message-queue.ts +169 -0
- package/src/onboarding.ts +274 -0
- package/src/openclaw-2026-03-21.log +3729 -0
- package/src/openclaw-plugin-sdk.d.ts +522 -0
- package/src/outbound-deliver.ts +552 -0
- package/src/outbound.ts +1266 -0
- package/src/proactive.ts +530 -0
- package/src/ref-index-store.ts +357 -0
- package/src/reply-dispatcher.ts +334 -0
- package/src/runtime.ts +14 -0
- package/src/session-store.ts +303 -0
- package/src/slash-commands.ts +1305 -0
- package/src/startup-greeting.ts +98 -0
- package/src/stt.ts +86 -0
- package/src/tools/channel.ts +281 -0
- package/src/tools/remind.ts +296 -0
- package/src/types.ts +183 -0
- package/src/typing-keepalive.ts +59 -0
- package/src/update-checker.ts +179 -0
- package/src/user-messages.ts +7 -0
- package/src/utils/audio-convert.ts +803 -0
- package/src/utils/file-utils.ts +167 -0
- package/src/utils/image-size.ts +266 -0
- package/src/utils/media-tags.ts +182 -0
- package/src/utils/payload.ts +265 -0
- package/src/utils/platform.ts +435 -0
- package/src/utils/text-parsing.ts +82 -0
- package/src/utils/upload-cache.ts +128 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// 消息队列配置
|
|
2
|
+
const MESSAGE_QUEUE_SIZE = 1000;
|
|
3
|
+
const PER_USER_QUEUE_SIZE = 20;
|
|
4
|
+
const MAX_CONCURRENT_USERS = 10;
|
|
5
|
+
/**
|
|
6
|
+
* 创建按用户并发的消息队列(同用户串行,跨用户并行)
|
|
7
|
+
*/
|
|
8
|
+
export function createMessageQueue(ctx) {
|
|
9
|
+
const { accountId, log } = ctx;
|
|
10
|
+
const userQueues = new Map();
|
|
11
|
+
const activeUsers = new Set();
|
|
12
|
+
let messagesProcessed = 0;
|
|
13
|
+
let handleMessageFnRef = null;
|
|
14
|
+
let totalEnqueued = 0;
|
|
15
|
+
const getMessagePeerId = (msg) => {
|
|
16
|
+
if (msg.type === "guild")
|
|
17
|
+
return `guild:${msg.channelId ?? "unknown"}`;
|
|
18
|
+
if (msg.type === "group")
|
|
19
|
+
return `group:${msg.groupOpenid ?? "unknown"}`;
|
|
20
|
+
return `dm:${msg.senderId}`;
|
|
21
|
+
};
|
|
22
|
+
const drainUserQueue = async (peerId) => {
|
|
23
|
+
if (activeUsers.has(peerId))
|
|
24
|
+
return;
|
|
25
|
+
if (activeUsers.size >= MAX_CONCURRENT_USERS) {
|
|
26
|
+
log?.info(`[qqbot:${accountId}] Max concurrent users (${MAX_CONCURRENT_USERS}) reached, ${peerId} will wait`);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const queue = userQueues.get(peerId);
|
|
30
|
+
if (!queue || queue.length === 0) {
|
|
31
|
+
userQueues.delete(peerId);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
activeUsers.add(peerId);
|
|
35
|
+
try {
|
|
36
|
+
while (queue.length > 0 && !ctx.isAborted()) {
|
|
37
|
+
const msg = queue.shift();
|
|
38
|
+
totalEnqueued = Math.max(0, totalEnqueued - 1);
|
|
39
|
+
try {
|
|
40
|
+
if (handleMessageFnRef) {
|
|
41
|
+
await handleMessageFnRef(msg);
|
|
42
|
+
messagesProcessed++;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
log?.error(`[qqbot:${accountId}] Message processor error for ${peerId}: ${err}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
activeUsers.delete(peerId);
|
|
52
|
+
userQueues.delete(peerId);
|
|
53
|
+
for (const [waitingPeerId, waitingQueue] of userQueues) {
|
|
54
|
+
if (activeUsers.size >= MAX_CONCURRENT_USERS)
|
|
55
|
+
break;
|
|
56
|
+
if (waitingQueue.length > 0 && !activeUsers.has(waitingPeerId)) {
|
|
57
|
+
drainUserQueue(waitingPeerId);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
const enqueue = (msg) => {
|
|
63
|
+
const peerId = getMessagePeerId(msg);
|
|
64
|
+
let queue = userQueues.get(peerId);
|
|
65
|
+
if (!queue) {
|
|
66
|
+
queue = [];
|
|
67
|
+
userQueues.set(peerId, queue);
|
|
68
|
+
}
|
|
69
|
+
if (queue.length >= PER_USER_QUEUE_SIZE) {
|
|
70
|
+
const dropped = queue.shift();
|
|
71
|
+
log?.error(`[qqbot:${accountId}] Per-user queue full for ${peerId}, dropping oldest message ${dropped?.messageId}`);
|
|
72
|
+
}
|
|
73
|
+
totalEnqueued++;
|
|
74
|
+
if (totalEnqueued > MESSAGE_QUEUE_SIZE) {
|
|
75
|
+
log?.error(`[qqbot:${accountId}] Global queue limit reached (${totalEnqueued}), message from ${peerId} may be delayed`);
|
|
76
|
+
}
|
|
77
|
+
queue.push(msg);
|
|
78
|
+
log?.debug?.(`[qqbot:${accountId}] Message enqueued for ${peerId}, user queue: ${queue.length}, active users: ${activeUsers.size}`);
|
|
79
|
+
drainUserQueue(peerId);
|
|
80
|
+
};
|
|
81
|
+
const startProcessor = (handleMessageFn) => {
|
|
82
|
+
handleMessageFnRef = handleMessageFn;
|
|
83
|
+
log?.info(`[qqbot:${accountId}] Message processor started (per-user concurrency, max ${MAX_CONCURRENT_USERS} users)`);
|
|
84
|
+
};
|
|
85
|
+
const getSnapshot = (senderPeerId) => {
|
|
86
|
+
let totalPending = 0;
|
|
87
|
+
for (const [, q] of userQueues) {
|
|
88
|
+
totalPending += q.length;
|
|
89
|
+
}
|
|
90
|
+
const senderQueue = userQueues.get(senderPeerId);
|
|
91
|
+
return {
|
|
92
|
+
totalPending,
|
|
93
|
+
activeUsers: activeUsers.size,
|
|
94
|
+
maxConcurrentUsers: MAX_CONCURRENT_USERS,
|
|
95
|
+
senderPending: senderQueue ? senderQueue.length : 0,
|
|
96
|
+
};
|
|
97
|
+
};
|
|
98
|
+
const clearUserQueue = (peerId) => {
|
|
99
|
+
const queue = userQueues.get(peerId);
|
|
100
|
+
if (!queue || queue.length === 0)
|
|
101
|
+
return 0;
|
|
102
|
+
const droppedCount = queue.length;
|
|
103
|
+
queue.length = 0;
|
|
104
|
+
totalEnqueued = Math.max(0, totalEnqueued - droppedCount);
|
|
105
|
+
return droppedCount;
|
|
106
|
+
};
|
|
107
|
+
const executeImmediate = (msg) => {
|
|
108
|
+
if (handleMessageFnRef) {
|
|
109
|
+
handleMessageFnRef(msg).catch(err => {
|
|
110
|
+
log?.error(`[qqbot:${accountId}] Immediate execution error: ${err}`);
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
return { enqueue, startProcessor, getSnapshot, getMessagePeerId, clearUserQueue, executeImmediate };
|
|
115
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QQBot CLI Onboarding Adapter
|
|
3
|
+
*
|
|
4
|
+
* 提供 openclaw onboard 命令的交互式配置支持
|
|
5
|
+
*/
|
|
6
|
+
import type { ChannelOnboardingAdapter } from "openclaw/plugin-sdk";
|
|
7
|
+
/**
|
|
8
|
+
* QQBot Onboarding Adapter
|
|
9
|
+
*/
|
|
10
|
+
export declare const qqbotOnboardingAdapter: ChannelOnboardingAdapter;
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { DEFAULT_ACCOUNT_ID, listQQBotAccountIds, resolveQQBotAccount } from "./config.js";
|
|
2
|
+
/**
|
|
3
|
+
* 解析默认账户 ID
|
|
4
|
+
*/
|
|
5
|
+
function resolveDefaultQQBotAccountId(cfg) {
|
|
6
|
+
const ids = listQQBotAccountIds(cfg);
|
|
7
|
+
return ids[0] ?? DEFAULT_ACCOUNT_ID;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* QQBot Onboarding Adapter
|
|
11
|
+
*/
|
|
12
|
+
export const qqbotOnboardingAdapter = {
|
|
13
|
+
channel: "qqbot",
|
|
14
|
+
getStatus: async (ctx) => {
|
|
15
|
+
const cfg = ctx.cfg;
|
|
16
|
+
const configured = listQQBotAccountIds(cfg).some((accountId) => {
|
|
17
|
+
const account = resolveQQBotAccount(cfg, accountId);
|
|
18
|
+
return Boolean(account.appId && account.clientSecret);
|
|
19
|
+
});
|
|
20
|
+
return {
|
|
21
|
+
channel: "qqbot",
|
|
22
|
+
configured,
|
|
23
|
+
statusLines: [`QQ Bot: ${configured ? "已配置" : "需要 AppID 和 ClientSecret"}`],
|
|
24
|
+
selectionHint: configured ? "已配置" : "支持 QQ 群聊和私聊(流式消息)",
|
|
25
|
+
quickstartScore: configured ? 1 : 20,
|
|
26
|
+
};
|
|
27
|
+
},
|
|
28
|
+
configure: async (ctx) => {
|
|
29
|
+
const cfg = ctx.cfg;
|
|
30
|
+
const prompter = ctx.prompter;
|
|
31
|
+
const accountOverrides = ctx.accountOverrides;
|
|
32
|
+
const shouldPromptAccountIds = ctx.shouldPromptAccountIds;
|
|
33
|
+
const qqbotOverride = accountOverrides?.qqbot?.trim();
|
|
34
|
+
const defaultAccountId = resolveDefaultQQBotAccountId(cfg);
|
|
35
|
+
let accountId = qqbotOverride ?? defaultAccountId;
|
|
36
|
+
// 是否需要提示选择账户
|
|
37
|
+
if (shouldPromptAccountIds && !qqbotOverride) {
|
|
38
|
+
const existingIds = listQQBotAccountIds(cfg);
|
|
39
|
+
if (existingIds.length > 1) {
|
|
40
|
+
accountId = await prompter.select({
|
|
41
|
+
message: "选择 QQBot 账户",
|
|
42
|
+
options: existingIds.map((id) => ({
|
|
43
|
+
value: id,
|
|
44
|
+
label: id === DEFAULT_ACCOUNT_ID ? "默认账户" : id,
|
|
45
|
+
})),
|
|
46
|
+
initialValue: accountId,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
let next = cfg;
|
|
51
|
+
const resolvedAccount = resolveQQBotAccount(next, accountId);
|
|
52
|
+
const accountConfigured = Boolean(resolvedAccount.appId && resolvedAccount.clientSecret);
|
|
53
|
+
const allowEnv = accountId === DEFAULT_ACCOUNT_ID;
|
|
54
|
+
const envAppId = typeof process !== "undefined" ? process.env?.QQBOT_APP_ID?.trim() : undefined;
|
|
55
|
+
const envSecret = typeof process !== "undefined" ? process.env?.QQBOT_CLIENT_SECRET?.trim() : undefined;
|
|
56
|
+
const canUseEnv = allowEnv && Boolean(envAppId && envSecret);
|
|
57
|
+
const hasConfigCredentials = Boolean(resolvedAccount.config.appId && resolvedAccount.config.clientSecret);
|
|
58
|
+
let appId = null;
|
|
59
|
+
let clientSecret = null;
|
|
60
|
+
// 显示帮助
|
|
61
|
+
if (!accountConfigured) {
|
|
62
|
+
await prompter.note([
|
|
63
|
+
"1) 打开 QQ 开放平台: https://q.qq.com/",
|
|
64
|
+
"2) 创建机器人应用,获取 AppID 和 ClientSecret",
|
|
65
|
+
"3) 在「开发设置」中添加沙箱成员(测试阶段)",
|
|
66
|
+
"4) 你也可以设置环境变量 QQBOT_APP_ID 和 QQBOT_CLIENT_SECRET",
|
|
67
|
+
"",
|
|
68
|
+
"文档: https://bot.q.qq.com/wiki/",
|
|
69
|
+
"",
|
|
70
|
+
"此版本支持流式消息发送!",
|
|
71
|
+
].join("\n"), "QQ Bot 配置");
|
|
72
|
+
}
|
|
73
|
+
// 检测环境变量
|
|
74
|
+
if (canUseEnv && !hasConfigCredentials) {
|
|
75
|
+
const keepEnv = await prompter.confirm({
|
|
76
|
+
message: "检测到环境变量 QQBOT_APP_ID 和 QQBOT_CLIENT_SECRET,是否使用?",
|
|
77
|
+
initialValue: true,
|
|
78
|
+
});
|
|
79
|
+
if (keepEnv) {
|
|
80
|
+
next = {
|
|
81
|
+
...next,
|
|
82
|
+
channels: {
|
|
83
|
+
...next.channels,
|
|
84
|
+
qqbot: {
|
|
85
|
+
...(next.channels?.qqbot || {}),
|
|
86
|
+
enabled: true,
|
|
87
|
+
allowFrom: resolvedAccount.config?.allowFrom ?? ["*"],
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
// 手动输入
|
|
94
|
+
appId = String(await prompter.text({
|
|
95
|
+
message: "请输入 QQ Bot AppID",
|
|
96
|
+
placeholder: "例如: 102146862",
|
|
97
|
+
initialValue: resolvedAccount.appId || undefined,
|
|
98
|
+
validate: (value) => (value?.trim() ? undefined : "AppID 不能为空"),
|
|
99
|
+
})).trim();
|
|
100
|
+
clientSecret = String(await prompter.text({
|
|
101
|
+
message: "请输入 QQ Bot ClientSecret",
|
|
102
|
+
placeholder: "你的 ClientSecret",
|
|
103
|
+
validate: (value) => (value?.trim() ? undefined : "ClientSecret 不能为空"),
|
|
104
|
+
})).trim();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
else if (hasConfigCredentials) {
|
|
108
|
+
// 已有配置
|
|
109
|
+
const keep = await prompter.confirm({
|
|
110
|
+
message: "QQ Bot 已配置,是否保留当前配置?",
|
|
111
|
+
initialValue: true,
|
|
112
|
+
});
|
|
113
|
+
if (!keep) {
|
|
114
|
+
appId = String(await prompter.text({
|
|
115
|
+
message: "请输入 QQ Bot AppID",
|
|
116
|
+
placeholder: "例如: 102146862",
|
|
117
|
+
initialValue: resolvedAccount.appId || undefined,
|
|
118
|
+
validate: (value) => (value?.trim() ? undefined : "AppID 不能为空"),
|
|
119
|
+
})).trim();
|
|
120
|
+
clientSecret = String(await prompter.text({
|
|
121
|
+
message: "请输入 QQ Bot ClientSecret",
|
|
122
|
+
placeholder: "你的 ClientSecret",
|
|
123
|
+
validate: (value) => (value?.trim() ? undefined : "ClientSecret 不能为空"),
|
|
124
|
+
})).trim();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
// 没有配置,需要输入
|
|
129
|
+
appId = String(await prompter.text({
|
|
130
|
+
message: "请输入 QQ Bot AppID",
|
|
131
|
+
placeholder: "例如: 102146862",
|
|
132
|
+
initialValue: resolvedAccount.appId || undefined,
|
|
133
|
+
validate: (value) => (value?.trim() ? undefined : "AppID 不能为空"),
|
|
134
|
+
})).trim();
|
|
135
|
+
clientSecret = String(await prompter.text({
|
|
136
|
+
message: "请输入 QQ Bot ClientSecret",
|
|
137
|
+
placeholder: "你的 ClientSecret",
|
|
138
|
+
validate: (value) => (value?.trim() ? undefined : "ClientSecret 不能为空"),
|
|
139
|
+
})).trim();
|
|
140
|
+
}
|
|
141
|
+
// 默认允许所有人执行命令(用户无感知)
|
|
142
|
+
const allowFrom = resolvedAccount.config?.allowFrom ?? ["*"];
|
|
143
|
+
// 应用配置(markdownSupport 默认开启,如需关闭可用 set-markdown.sh)
|
|
144
|
+
if (appId && clientSecret) {
|
|
145
|
+
const existingQQBot = next.channels?.qqbot || {};
|
|
146
|
+
// 保留已有的 markdownSupport 设置,新装默认 true
|
|
147
|
+
const markdownSupport = existingQQBot.markdownSupport ?? true;
|
|
148
|
+
if (accountId === DEFAULT_ACCOUNT_ID) {
|
|
149
|
+
next = {
|
|
150
|
+
...next,
|
|
151
|
+
channels: {
|
|
152
|
+
...next.channels,
|
|
153
|
+
qqbot: {
|
|
154
|
+
...existingQQBot,
|
|
155
|
+
enabled: true,
|
|
156
|
+
appId,
|
|
157
|
+
clientSecret,
|
|
158
|
+
markdownSupport,
|
|
159
|
+
allowFrom,
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
const existingAccounts = (next.channels?.qqbot?.accounts || {});
|
|
166
|
+
const existingAccount = existingAccounts[accountId] || {};
|
|
167
|
+
const acctMarkdown = existingAccount.markdownSupport ?? true;
|
|
168
|
+
next = {
|
|
169
|
+
...next,
|
|
170
|
+
channels: {
|
|
171
|
+
...next.channels,
|
|
172
|
+
qqbot: {
|
|
173
|
+
...existingQQBot,
|
|
174
|
+
enabled: true,
|
|
175
|
+
accounts: {
|
|
176
|
+
...existingAccounts,
|
|
177
|
+
[accountId]: {
|
|
178
|
+
...existingAccount,
|
|
179
|
+
enabled: true,
|
|
180
|
+
appId,
|
|
181
|
+
clientSecret,
|
|
182
|
+
markdownSupport: acctMarkdown,
|
|
183
|
+
allowFrom,
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return { success: true, cfg: next, accountId };
|
|
192
|
+
},
|
|
193
|
+
disable: (cfg) => {
|
|
194
|
+
const config = cfg;
|
|
195
|
+
return {
|
|
196
|
+
...config,
|
|
197
|
+
channels: {
|
|
198
|
+
...config.channels,
|
|
199
|
+
qqbot: { ...(config.channels?.qqbot || {}), enabled: false },
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
},
|
|
203
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 出站消息投递模块
|
|
3
|
+
*
|
|
4
|
+
* 从 gateway deliver 回调中提取的两大发送管线:
|
|
5
|
+
* 1. parseAndSendMediaTags — 解析 <qqimg/qqvoice/qqvideo/qqfile/qqmedia> 标签并按顺序发送
|
|
6
|
+
* 2. sendPlainReply — 处理不含媒体标签的普通回复(markdown 图片/纯文本+图片)
|
|
7
|
+
*/
|
|
8
|
+
import type { ResolvedQQBotAccount } from "./types.js";
|
|
9
|
+
export interface DeliverEventContext {
|
|
10
|
+
type: "c2c" | "guild" | "dm" | "group";
|
|
11
|
+
senderId: string;
|
|
12
|
+
messageId: string;
|
|
13
|
+
channelId?: string;
|
|
14
|
+
groupOpenid?: string;
|
|
15
|
+
msgIdx?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface DeliverAccountContext {
|
|
18
|
+
account: ResolvedQQBotAccount;
|
|
19
|
+
qualifiedTarget: string;
|
|
20
|
+
log?: {
|
|
21
|
+
info: (msg: string) => void;
|
|
22
|
+
error: (msg: string) => void;
|
|
23
|
+
debug?: (msg: string) => void;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/** token 重试包装 */
|
|
27
|
+
export type SendWithRetryFn = <T>(sendFn: (token: string) => Promise<T>) => Promise<T>;
|
|
28
|
+
/** 一次性消费引用 ref */
|
|
29
|
+
export type ConsumeQuoteRefFn = () => string | undefined;
|
|
30
|
+
/**
|
|
31
|
+
* 解析回复文本中的媒体标签并按顺序发送。
|
|
32
|
+
*
|
|
33
|
+
* @returns true 如果检测到媒体标签并已处理;false 表示无媒体标签,调用方继续走普通文本管线
|
|
34
|
+
*/
|
|
35
|
+
export declare function parseAndSendMediaTags(replyText: string, event: DeliverEventContext, actx: DeliverAccountContext, sendWithRetry: SendWithRetryFn, consumeQuoteRef: ConsumeQuoteRefFn): Promise<{
|
|
36
|
+
handled: boolean;
|
|
37
|
+
normalizedText: string;
|
|
38
|
+
}>;
|
|
39
|
+
export interface PlainReplyPayload {
|
|
40
|
+
text?: string;
|
|
41
|
+
mediaUrls?: string[];
|
|
42
|
+
mediaUrl?: string;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* 发送不含媒体标签的普通回复。
|
|
46
|
+
* 处理 markdown 图片嵌入、Base64 富媒体、纯文本分块、本地媒体自动路由。
|
|
47
|
+
*/
|
|
48
|
+
export declare function sendPlainReply(payload: PlainReplyPayload, replyText: string, event: DeliverEventContext, actx: DeliverAccountContext, sendWithRetry: SendWithRetryFn, consumeQuoteRef: ConsumeQuoteRefFn, toolMediaUrls: string[]): Promise<void>;
|