@tencent-connect/openclaw-qqbot 1.7.0 → 1.7.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 +216 -49
- package/README.zh.md +216 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/src/api.d.ts +6 -0
- package/dist/src/api.js +33 -4
- package/dist/src/approval-handler.d.ts +47 -0
- package/dist/src/approval-handler.js +372 -0
- package/dist/src/channel.js +72 -0
- package/dist/src/config.d.ts +5 -1
- package/dist/src/config.js +12 -2
- package/dist/src/gateway.js +175 -170
- package/dist/src/slash-commands.d.ts +7 -2
- package/dist/src/slash-commands.js +354 -3
- package/dist/src/tools/channel.js +1 -4
- package/dist/src/tools/remind.js +0 -1
- package/dist/src/transport/index.d.ts +10 -0
- package/dist/src/transport/index.js +9 -0
- package/dist/src/transport/webhook-transport.d.ts +67 -0
- package/dist/src/transport/webhook-transport.js +245 -0
- package/dist/src/transport/webhook-verify.d.ts +48 -0
- package/dist/src/transport/webhook-verify.js +98 -0
- package/dist/src/types.d.ts +85 -0
- package/dist/src/utils/audio-convert.js +37 -9
- package/index.ts +1 -0
- package/package.json +1 -1
- package/scripts/postinstall-link-sdk.js +44 -0
- package/scripts/upgrade-via-npm.sh +358 -62
- package/scripts/upgrade-via-source.sh +122 -85
- package/src/api.ts +50 -5
- package/src/approval-handler.ts +505 -0
- package/src/channel.ts +76 -0
- package/src/config.ts +15 -2
- package/src/gateway.ts +181 -169
- package/src/onboarding.ts +8 -0
- package/src/openclaw-plugin-sdk.d.ts +127 -2
- package/src/slash-commands.ts +390 -5
- package/src/tools/channel.ts +1 -7
- package/src/tools/remind.ts +0 -2
- package/src/transport/index.ts +11 -0
- package/src/transport/webhook-transport.ts +332 -0
- package/src/transport/webhook-verify.ts +119 -0
- package/src/types.ts +100 -1
- package/src/typings/openclaw-webhook-ingress.d.ts +66 -0
- package/src/utils/audio-convert.ts +37 -9
package/src/gateway.ts
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import WebSocket from "ws";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import fs from "node:fs";
|
|
4
|
-
import type { ResolvedQQBotAccount, WSPayload, C2CMessageEvent, GuildMessageEvent, GroupMessageEvent, InteractionEvent, MsgElement } from "./types.js";
|
|
4
|
+
import type { ResolvedQQBotAccount, WSPayload, C2CMessageEvent, GuildMessageEvent, GroupMessageEvent, InteractionEvent, MsgElement, TransportMode } from "./types.js";
|
|
5
5
|
import { MSG_TYPE_QUOTE } from "./types.js";
|
|
6
|
+
import { startWebhookTransport } from "./transport/index.js";
|
|
6
7
|
import { getAccessToken, getGatewayUrl, sendC2CMessage, sendChannelMessage, sendGroupMessage, clearTokenCache, initApiConfig, startBackgroundTokenRefresh, stopBackgroundTokenRefresh, sendC2CInputNotify, onMessageSent, getPluginUserAgent, sendProactiveGroupMessage, acknowledgeInteraction, getApiPluginVersion, setApiLogger } from "./api.js";
|
|
7
8
|
import { loadSession, saveSession, clearSession } from "./session-store.js";
|
|
8
9
|
import { recordKnownUser, flushKnownUsers } from "./known-users.js";
|
|
9
10
|
import { getQQBotRuntime } from "./runtime.js";
|
|
10
11
|
import { isGroupAllowed, resolveGroupName, resolveGroupPrompt, resolveHistoryLimit, resolveGroupPolicy, resolveGroupConfig, resolveIgnoreOtherMentions, resolveMentionPatterns } from "./config.js";
|
|
11
12
|
import { qqbotPlugin, stripMentionText, detectWasMentioned } from "./channel.js";
|
|
13
|
+
import { QQBotApprovalHandler, registerApprovalHandler, unregisterApprovalHandler, getApprovalHandler } from "./approval-handler.js";
|
|
12
14
|
import {
|
|
13
15
|
recordPendingHistoryEntry,
|
|
14
16
|
buildPendingHistoryContext,
|
|
@@ -21,7 +23,7 @@ import {
|
|
|
21
23
|
} from "./group-history.js";
|
|
22
24
|
|
|
23
25
|
import { setRefIndex, getRefIndex, formatRefEntryForAgent, formatMessageReferenceForAgent, flushRefIndex, type RefAttachmentSummary } from "./ref-index-store.js";
|
|
24
|
-
import { matchSlashCommand, getFrameworkVersion, parseFrameworkDateVersion, type SlashCommandContext, type SlashCommandFileResult } from "./slash-commands.js";
|
|
26
|
+
import { matchSlashCommand, getFrameworkVersion, parseFrameworkDateVersion, type SlashCommandContext, type SlashCommandFileResult, type SlashCommandDelegateResult } from "./slash-commands.js";
|
|
25
27
|
import { createMessageQueue, type QueuedMessage } from "./message-queue.js";
|
|
26
28
|
import { triggerUpdateCheck } from "./update-checker.js";
|
|
27
29
|
import { startImageServer, isImageServerRunning, type ImageServerConfig } from "./image-server.js";
|
|
@@ -174,9 +176,27 @@ async function handleInteractionCreate(params: {
|
|
|
174
176
|
await acknowledgeInteraction(token, event.id, 0, { claw_cfg: ackClawCfg });
|
|
175
177
|
log?.info(`[qqbot:${account.accountId}] Interaction ACK (type=${INTERACTION_TYPE_CONFIG_UPDATE}) sent: ${event.id}, claw_cfg=${JSON.stringify(ackClawCfg)}`);
|
|
176
178
|
} else {
|
|
177
|
-
//
|
|
179
|
+
// 普通按钮交互:先 ACK
|
|
178
180
|
await acknowledgeInteraction(token, event.id);
|
|
179
181
|
log?.debug?.(`[qqbot:${account.accountId}] Interaction ACK sent: ${event.id}`);
|
|
182
|
+
|
|
183
|
+
// Inline Keyboard 审批按钮(type=1 Callback)
|
|
184
|
+
// button_data 格式:approve:<approvalId>:<decision>
|
|
185
|
+
// approvalId 可能是 "exec:uuid" / "plugin:uuid"(带前缀)或纯 "uuid"(无前缀)
|
|
186
|
+
const buttonData = event.data?.resolved?.button_data ?? "";
|
|
187
|
+
const m = buttonData.match(/^approve:((?:(?:exec|plugin):)?[0-9a-f-]+):(allow-once|allow-always|deny)$/i);
|
|
188
|
+
if (m) {
|
|
189
|
+
const approvalId = m[1]!;
|
|
190
|
+
const decision = m[2] as "allow-once" | "allow-always" | "deny";
|
|
191
|
+
const userId = event.group_member_openid || event.user_openid || event.data?.resolved?.user_id || "unknown";
|
|
192
|
+
log?.info(`[qqbot:${account.accountId}] Approval button clicked: approvalId=${approvalId}, decision=${decision}, user=${userId}, buttonData=${buttonData}`);
|
|
193
|
+
const handler = getApprovalHandler(account.accountId);
|
|
194
|
+
if (handler) {
|
|
195
|
+
void handler.resolveApproval(approvalId, decision);
|
|
196
|
+
} else {
|
|
197
|
+
log?.error(`[qqbot:${account.accountId}] Approval button: no handler found for accountId=${account.accountId}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
180
200
|
}
|
|
181
201
|
}
|
|
182
202
|
|
|
@@ -399,6 +419,21 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
|
|
|
399
419
|
throw new Error("QQBot not configured (missing appId or clientSecret)");
|
|
400
420
|
}
|
|
401
421
|
|
|
422
|
+
// 安全网:捕获 approval-handler / SDK 内部 WS 握手异步错误(如 403),避免进程崩溃
|
|
423
|
+
const wsUncaughtHandler = (err: Error) => {
|
|
424
|
+
if (err.message?.includes("Unexpected server response")) {
|
|
425
|
+
log?.error(`[qqbot:${account.accountId}] Caught WS handshake error (non-fatal): ${err.message}`);
|
|
426
|
+
// 不重新抛出,防止进程退出
|
|
427
|
+
} else {
|
|
428
|
+
// 非 WS 握手错误,重新抛出交给上层处理
|
|
429
|
+
throw err;
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
process.on("uncaughtException", wsUncaughtHandler);
|
|
433
|
+
abortSignal.addEventListener("abort", () => {
|
|
434
|
+
process.removeListener("uncaughtException", wsUncaughtHandler);
|
|
435
|
+
}, { once: true });
|
|
436
|
+
|
|
402
437
|
// 启动环境诊断(首次连接时执行)
|
|
403
438
|
const diag = await runDiagnostics();
|
|
404
439
|
if (diag.warnings.length > 0) {
|
|
@@ -492,6 +527,13 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
|
|
|
492
527
|
log?.info(`[qqbot:${account.accountId}] Image server disabled (no imageServerBaseUrl configured)`);
|
|
493
528
|
}
|
|
494
529
|
|
|
530
|
+
// ============ Transport 模式标记 ============
|
|
531
|
+
const transportMode: TransportMode = account.config.transport ?? "websocket";
|
|
532
|
+
if (transportMode === "webhook") {
|
|
533
|
+
log?.info(`[qqbot:${account.accountId}] Using webhook transport mode`);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// ============ WebSocket / Webhook 公共初始化 ============
|
|
495
537
|
let reconnectAttempts = 0;
|
|
496
538
|
let isAborted = false;
|
|
497
539
|
let currentWs: WebSocket | null = null;
|
|
@@ -517,6 +559,19 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
|
|
|
517
559
|
log?.info(`[qqbot:${account.accountId}] Restored session from storage: sessionId=${sessionId}, lastSeq=${lastSeq}`);
|
|
518
560
|
}
|
|
519
561
|
|
|
562
|
+
// ============ 审批 Handler ============
|
|
563
|
+
const approvalHandler = new QQBotApprovalHandler({
|
|
564
|
+
accountId: account.accountId,
|
|
565
|
+
appId: account.appId,
|
|
566
|
+
clientSecret: account.clientSecret,
|
|
567
|
+
cfg: cfg as any,
|
|
568
|
+
log,
|
|
569
|
+
});
|
|
570
|
+
registerApprovalHandler(account.accountId, approvalHandler);
|
|
571
|
+
approvalHandler.start().catch((err) => {
|
|
572
|
+
log?.error(`[qqbot:${account.accountId}] approval-handler: uncaught start error: ${err}`);
|
|
573
|
+
});
|
|
574
|
+
|
|
520
575
|
// ============ 消息队列(复用 createMessageQueue,内置群消息合并/淘汰策略) ============
|
|
521
576
|
const msgQueue = createMessageQueue({
|
|
522
577
|
accountId: account.accountId,
|
|
@@ -526,7 +581,9 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
|
|
|
526
581
|
|
|
527
582
|
// 斜杠指令拦截:在入队前匹配插件级指令,命中则直接回复,不入队
|
|
528
583
|
// 紧急命令列表:这些命令会立即执行,不进入斜杠匹配流程
|
|
529
|
-
|
|
584
|
+
// /stop — 停止当前 agent run,清空队列
|
|
585
|
+
// /approve — 审批决策,必须在 agent 等待审批时立即执行,否则死锁
|
|
586
|
+
const URGENT_COMMANDS = ["/stop", "/approve"];
|
|
530
587
|
|
|
531
588
|
const trySlashCommandOrEnqueue = async (msg: QueuedMessage): Promise<void> => {
|
|
532
589
|
const content = (msg.content ?? "").trim();
|
|
@@ -577,6 +634,16 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
|
|
|
577
634
|
return;
|
|
578
635
|
}
|
|
579
636
|
|
|
637
|
+
// 委托给 AI 模型:用加工后的 prompt 替换原始消息入队
|
|
638
|
+
const isDelegateResult = typeof reply === "object" && reply !== null && "delegatePrompt" in reply;
|
|
639
|
+
if (isDelegateResult) {
|
|
640
|
+
const delegatePrompt = (reply as SlashCommandDelegateResult).delegatePrompt;
|
|
641
|
+
log?.info(`[qqbot:${account.accountId}] Slash command delegated to AI: ${content.slice(0, 40)}`);
|
|
642
|
+
msg.content = delegatePrompt;
|
|
643
|
+
msgQueue.enqueue(msg);
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
|
|
580
647
|
// 命中插件级指令,直接回复
|
|
581
648
|
log?.info(`[qqbot:${account.accountId}] Slash command matched: ${content}, replying directly`);
|
|
582
649
|
const token = await getAccessToken(account.appId, account.clientSecret);
|
|
@@ -635,6 +702,9 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
|
|
|
635
702
|
flushKnownUsers();
|
|
636
703
|
// P1-4: 保存引用索引数据
|
|
637
704
|
flushRefIndex();
|
|
705
|
+
// 停止审批 handler
|
|
706
|
+
void approvalHandler.stop();
|
|
707
|
+
unregisterApprovalHandler(account.accountId);
|
|
638
708
|
});
|
|
639
709
|
|
|
640
710
|
const cleanup = () => {
|
|
@@ -694,15 +764,6 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
|
|
|
694
764
|
clearTokenCache(account.appId);
|
|
695
765
|
shouldRefreshToken = false;
|
|
696
766
|
}
|
|
697
|
-
|
|
698
|
-
const accessToken = await getAccessToken(account.appId, account.clientSecret);
|
|
699
|
-
log?.info(`[qqbot:${account.accountId}] ✅ Access token obtained successfully`);
|
|
700
|
-
const gatewayUrl = await getGatewayUrl(accessToken);
|
|
701
|
-
|
|
702
|
-
log?.info(`[qqbot:${account.accountId}] Connecting to ${gatewayUrl}`);
|
|
703
|
-
|
|
704
|
-
const ws = new WebSocket(gatewayUrl, { headers: { "User-Agent": getPluginUserAgent() } });
|
|
705
|
-
currentWs = ws;
|
|
706
767
|
|
|
707
768
|
const pluginRuntime = getQQBotRuntime();
|
|
708
769
|
|
|
@@ -1457,6 +1518,8 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
|
|
|
1457
1518
|
});
|
|
1458
1519
|
}
|
|
1459
1520
|
|
|
1521
|
+
// 打印 runId 用于调试
|
|
1522
|
+
log?.info?.(`[qqbot:${account.accountId}] Dispatching with runId: ${event.messageId}`);
|
|
1460
1523
|
|
|
1461
1524
|
const dispatchPromise = pluginRuntime.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
1462
1525
|
ctx: ctxPayload,
|
|
@@ -1728,7 +1791,10 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
|
|
|
1728
1791
|
}
|
|
1729
1792
|
},
|
|
1730
1793
|
},
|
|
1794
|
+
|
|
1731
1795
|
replyOptions: {
|
|
1796
|
+
// 使用消息ID作为 runId,用于追踪一次完整的 AI 对话运行
|
|
1797
|
+
runId: event.messageId,
|
|
1732
1798
|
// 流式模式时禁用 block streaming
|
|
1733
1799
|
disableBlockStreaming: !useStreaming,
|
|
1734
1800
|
// 流式模式下注册 onPartialReply 回调,接收流式文本增量
|
|
@@ -1824,6 +1890,99 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
|
|
|
1824
1890
|
}); // end runWithRequestContext
|
|
1825
1891
|
};
|
|
1826
1892
|
|
|
1893
|
+
// ============ 统一事件分发(WebSocket/Webhook 共用) ============
|
|
1894
|
+
const dispatchInboundEvent = async (t: string, d: unknown): Promise<void> => {
|
|
1895
|
+
if (t === "C2C_MESSAGE_CREATE") {
|
|
1896
|
+
const ev = d as C2CMessageEvent;
|
|
1897
|
+
recordKnownUser({ openid: ev.author.user_openid, type: "c2c", accountId: account.accountId });
|
|
1898
|
+
const refs = parseRefIndices(ev.message_scene?.ext, ev.message_type, ev.msg_elements);
|
|
1899
|
+
await trySlashCommandOrEnqueue({ type: "c2c", senderId: ev.author.user_openid, content: ev.content, messageId: ev.id, timestamp: ev.timestamp, attachments: ev.attachments, refMsgIdx: refs.refMsgIdx, msgIdx: refs.msgIdx, msgElements: ev.msg_elements, msgType: ev.message_type });
|
|
1900
|
+
} else if (t === "AT_MESSAGE_CREATE") {
|
|
1901
|
+
const ev = d as GuildMessageEvent;
|
|
1902
|
+
recordKnownUser({ openid: ev.author.id, type: "c2c", nickname: ev.author.username, accountId: account.accountId });
|
|
1903
|
+
const refs = parseRefIndices((ev as any).message_scene?.ext, (ev as any).message_type, (ev as any).msg_elements);
|
|
1904
|
+
await trySlashCommandOrEnqueue({ type: "guild", senderId: ev.author.id, senderName: ev.author.username, content: ev.content, messageId: ev.id, timestamp: ev.timestamp, channelId: ev.channel_id, guildId: ev.guild_id, attachments: ev.attachments, refMsgIdx: refs.refMsgIdx, msgIdx: refs.msgIdx, msgType: (ev as any).message_type });
|
|
1905
|
+
} else if (t === "DIRECT_MESSAGE_CREATE") {
|
|
1906
|
+
const ev = d as GuildMessageEvent;
|
|
1907
|
+
recordKnownUser({ openid: ev.author.id, type: "c2c", nickname: ev.author.username, accountId: account.accountId });
|
|
1908
|
+
const refs = parseRefIndices((ev as any).message_scene?.ext, (ev as any).message_type, (ev as any).msg_elements);
|
|
1909
|
+
await trySlashCommandOrEnqueue({ type: "dm", senderId: ev.author.id, senderName: ev.author.username, content: ev.content, messageId: ev.id, timestamp: ev.timestamp, guildId: ev.guild_id, attachments: ev.attachments, refMsgIdx: refs.refMsgIdx, msgIdx: refs.msgIdx, msgType: (ev as any).message_type });
|
|
1910
|
+
} else if (t === "GROUP_AT_MESSAGE_CREATE" || t === "GROUP_MESSAGE_CREATE") {
|
|
1911
|
+
const ev = d as GroupMessageEvent;
|
|
1912
|
+
recordKnownUser({ openid: ev.author.member_openid, type: "group", nickname: ev.author.username, groupOpenid: ev.group_openid, accountId: account.accountId });
|
|
1913
|
+
const refs = parseRefIndices(ev.message_scene?.ext, ev.message_type, ev.msg_elements);
|
|
1914
|
+
await trySlashCommandOrEnqueue({ type: "group", senderId: ev.author.member_openid, senderName: ev.author.username, senderIsBot: ev.author.bot, content: ev.content, messageId: ev.id, timestamp: ev.timestamp, groupOpenid: ev.group_openid, attachments: ev.attachments, refMsgIdx: refs.refMsgIdx, msgIdx: refs.msgIdx, eventType: t, mentions: ev.mentions, messageScene: ev.message_scene, msgElements: ev.msg_elements, msgType: ev.message_type });
|
|
1915
|
+
} else if (t === "GROUP_ADD_ROBOT") {
|
|
1916
|
+
const ev = d as { timestamp: string; group_openid: string; op_member_openid: string };
|
|
1917
|
+
log?.info(`[qqbot:${account.accountId}] Bot added to group: ${ev.group_openid} by ${ev.op_member_openid}`);
|
|
1918
|
+
recordKnownUser({ openid: ev.op_member_openid, type: "group", groupOpenid: ev.group_openid, accountId: account.accountId });
|
|
1919
|
+
} else if (t === "GROUP_DEL_ROBOT") {
|
|
1920
|
+
const ev = d as { timestamp: string; group_openid: string; op_member_openid: string };
|
|
1921
|
+
log?.info(`[qqbot:${account.accountId}] Bot removed from group: ${ev.group_openid} by ${ev.op_member_openid}`);
|
|
1922
|
+
} else if (t === "GROUP_MSG_REJECT") {
|
|
1923
|
+
const ev = d as { timestamp: number; group_openid: string; op_member_openid: string };
|
|
1924
|
+
log?.info(`[qqbot:${account.accountId}] Group ${ev.group_openid} rejected bot proactive messages (by ${ev.op_member_openid})`);
|
|
1925
|
+
} else if (t === "GROUP_MSG_RECEIVE") {
|
|
1926
|
+
const ev = d as { timestamp: number; group_openid: string; op_member_openid: string };
|
|
1927
|
+
log?.info(`[qqbot:${account.accountId}] Group ${ev.group_openid} accepted bot proactive messages (by ${ev.op_member_openid})`);
|
|
1928
|
+
} else if (t === "INTERACTION_CREATE") {
|
|
1929
|
+
const ev = d as InteractionEvent;
|
|
1930
|
+
const resolved = ev.data?.resolved;
|
|
1931
|
+
const sceneDesc = ev.scene ?? (ev.chat_type === 0 ? "guild" : ev.chat_type === 1 ? "group" : "c2c");
|
|
1932
|
+
log?.info(`[qqbot:${account.accountId}] Interaction: scene=${sceneDesc}, type=${ev.data?.type}, button_id=${resolved?.button_id}, button_data=${resolved?.button_data}`);
|
|
1933
|
+
handleInteractionCreate({ event: ev, account, cfg, log }).catch((err) => {
|
|
1934
|
+
log?.error(`[qqbot:${account.accountId}] Failed to handle interaction ${ev.id}: ${err}`);
|
|
1935
|
+
});
|
|
1936
|
+
}
|
|
1937
|
+
};
|
|
1938
|
+
|
|
1939
|
+
// ============ Webhook 模式:共享 handleMessage,不走 WS ============
|
|
1940
|
+
if (transportMode === "webhook") {
|
|
1941
|
+
isConnecting = false;
|
|
1942
|
+
msgQueue.startProcessor(handleMessage);
|
|
1943
|
+
startBackgroundTokenRefresh(account.appId, account.clientSecret, {
|
|
1944
|
+
log: log as { info: (msg: string) => void; error: (msg: string) => void; debug?: (msg: string) => void },
|
|
1945
|
+
});
|
|
1946
|
+
|
|
1947
|
+
await startWebhookTransport({
|
|
1948
|
+
account,
|
|
1949
|
+
abortSignal,
|
|
1950
|
+
onEvent: async (event) => {
|
|
1951
|
+
const { eventType: t, data: d } = event;
|
|
1952
|
+
log?.info(`[qqbot:${account.accountId}:webhook] 📩 Dispatch event: t=${t}, d=${JSON.stringify(d)}`);
|
|
1953
|
+
await dispatchInboundEvent(t, d);
|
|
1954
|
+
},
|
|
1955
|
+
onReady: () => {
|
|
1956
|
+
log?.info(`[qqbot:${account.accountId}:webhook] Transport ready`);
|
|
1957
|
+
log?.info(`[qqbot:${account.accountId}] ✅ Webhook transport started successfully (path: ${account.config.webhook?.path ?? "/qqbot/webhook"})`);
|
|
1958
|
+
onReady?.({ transport: "webhook" });
|
|
1959
|
+
if (_pendingFirstReady.has(account.accountId)) {
|
|
1960
|
+
_pendingFirstReady.delete(account.accountId);
|
|
1961
|
+
sendStartupGreetings(adminCtx, "READY");
|
|
1962
|
+
}
|
|
1963
|
+
},
|
|
1964
|
+
onError: (error) => {
|
|
1965
|
+
log?.error(`[qqbot:${account.accountId}:webhook] Error: ${error.message}`);
|
|
1966
|
+
onError?.(error);
|
|
1967
|
+
},
|
|
1968
|
+
log,
|
|
1969
|
+
});
|
|
1970
|
+
|
|
1971
|
+
stopBackgroundTokenRefresh();
|
|
1972
|
+
unregisterApprovalHandler(account.accountId);
|
|
1973
|
+
return; // webhook transport 结束,不继续 WS 逻辑
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
// ============ WebSocket 模式:获取 token 并建立 WS 连接 ============
|
|
1977
|
+
const accessToken = await getAccessToken(account.appId, account.clientSecret);
|
|
1978
|
+
log?.info(`[qqbot:${account.accountId}] ✅ Access token obtained successfully`);
|
|
1979
|
+
const gatewayUrl = await getGatewayUrl(accessToken);
|
|
1980
|
+
|
|
1981
|
+
log?.info(`[qqbot:${account.accountId}] Connecting to ${gatewayUrl}`);
|
|
1982
|
+
|
|
1983
|
+
const ws = new WebSocket(gatewayUrl, { headers: { "User-Agent": getPluginUserAgent() } });
|
|
1984
|
+
currentWs = ws;
|
|
1985
|
+
|
|
1827
1986
|
ws.on("open", () => {
|
|
1828
1987
|
log?.info(`[qqbot:${account.accountId}] WebSocket connected`);
|
|
1829
1988
|
isConnecting = false; // 连接完成,释放锁
|
|
@@ -1946,159 +2105,10 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
|
|
|
1946
2105
|
appId: account.appId,
|
|
1947
2106
|
});
|
|
1948
2107
|
}
|
|
1949
|
-
} else
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
openid: event.author.user_openid,
|
|
1954
|
-
type: "c2c",
|
|
1955
|
-
accountId: account.accountId,
|
|
1956
|
-
});
|
|
1957
|
-
// 解析引用索引
|
|
1958
|
-
const c2cRefs = parseRefIndices(event.message_scene?.ext, event.message_type, event.msg_elements);
|
|
1959
|
-
// 斜杠指令拦截 → 不匹配则入队
|
|
1960
|
-
trySlashCommandOrEnqueue({
|
|
1961
|
-
type: "c2c",
|
|
1962
|
-
senderId: event.author.user_openid,
|
|
1963
|
-
content: event.content,
|
|
1964
|
-
messageId: event.id,
|
|
1965
|
-
timestamp: event.timestamp,
|
|
1966
|
-
attachments: event.attachments,
|
|
1967
|
-
refMsgIdx: c2cRefs.refMsgIdx,
|
|
1968
|
-
msgIdx: c2cRefs.msgIdx,
|
|
1969
|
-
msgElements: event.msg_elements,
|
|
1970
|
-
msgType: event.message_type,
|
|
1971
|
-
});
|
|
1972
|
-
} else if (t === "AT_MESSAGE_CREATE") {
|
|
1973
|
-
const event = d as GuildMessageEvent;
|
|
1974
|
-
// P1-3: 记录已知用户(频道用户)
|
|
1975
|
-
recordKnownUser({
|
|
1976
|
-
openid: event.author.id,
|
|
1977
|
-
type: "c2c", // 频道用户按 c2c 类型存储
|
|
1978
|
-
nickname: event.author.username,
|
|
1979
|
-
accountId: account.accountId,
|
|
1980
|
-
});
|
|
1981
|
-
const guildRefs = parseRefIndices((event as any).message_scene?.ext, (event as any).message_type, (event as any).msg_elements);
|
|
1982
|
-
trySlashCommandOrEnqueue({
|
|
1983
|
-
type: "guild",
|
|
1984
|
-
senderId: event.author.id,
|
|
1985
|
-
senderName: event.author.username,
|
|
1986
|
-
content: event.content,
|
|
1987
|
-
messageId: event.id,
|
|
1988
|
-
timestamp: event.timestamp,
|
|
1989
|
-
channelId: event.channel_id,
|
|
1990
|
-
guildId: event.guild_id,
|
|
1991
|
-
attachments: event.attachments,
|
|
1992
|
-
refMsgIdx: guildRefs.refMsgIdx,
|
|
1993
|
-
msgIdx: guildRefs.msgIdx,
|
|
1994
|
-
msgType: (event as any).message_type,
|
|
1995
|
-
});
|
|
1996
|
-
} else if (t === "DIRECT_MESSAGE_CREATE") {
|
|
1997
|
-
const event = d as GuildMessageEvent;
|
|
1998
|
-
// P1-3: 记录已知用户(频道私信用户)
|
|
1999
|
-
recordKnownUser({
|
|
2000
|
-
openid: event.author.id,
|
|
2001
|
-
type: "c2c",
|
|
2002
|
-
nickname: event.author.username,
|
|
2003
|
-
accountId: account.accountId,
|
|
2004
|
-
});
|
|
2005
|
-
const dmRefs = parseRefIndices((event as any).message_scene?.ext, (event as any).message_type, (event as any).msg_elements);
|
|
2006
|
-
trySlashCommandOrEnqueue({
|
|
2007
|
-
type: "dm",
|
|
2008
|
-
senderId: event.author.id,
|
|
2009
|
-
senderName: event.author.username,
|
|
2010
|
-
content: event.content,
|
|
2011
|
-
messageId: event.id,
|
|
2012
|
-
timestamp: event.timestamp,
|
|
2013
|
-
guildId: event.guild_id,
|
|
2014
|
-
attachments: event.attachments,
|
|
2015
|
-
refMsgIdx: dmRefs.refMsgIdx,
|
|
2016
|
-
msgIdx: dmRefs.msgIdx,
|
|
2017
|
-
msgType: (event as any).message_type,
|
|
2018
|
-
});
|
|
2019
|
-
} else if (t === "GROUP_AT_MESSAGE_CREATE") {
|
|
2020
|
-
const event = d as GroupMessageEvent;
|
|
2021
|
-
// 被 @ 的消息,直接入队回复
|
|
2022
|
-
recordKnownUser({
|
|
2023
|
-
openid: event.author.member_openid,
|
|
2024
|
-
type: "group",
|
|
2025
|
-
nickname: event.author.username,
|
|
2026
|
-
groupOpenid: event.group_openid,
|
|
2027
|
-
accountId: account.accountId,
|
|
2028
|
-
});
|
|
2029
|
-
const groupRefs = parseRefIndices(event.message_scene?.ext, event.message_type, event.msg_elements);
|
|
2030
|
-
trySlashCommandOrEnqueue({
|
|
2031
|
-
type: "group",
|
|
2032
|
-
senderId: event.author.member_openid,
|
|
2033
|
-
senderName: event.author.username,
|
|
2034
|
-
content: event.content,
|
|
2035
|
-
messageId: event.id,
|
|
2036
|
-
timestamp: event.timestamp,
|
|
2037
|
-
groupOpenid: event.group_openid,
|
|
2038
|
-
attachments: event.attachments,
|
|
2039
|
-
refMsgIdx: groupRefs.refMsgIdx,
|
|
2040
|
-
msgIdx: groupRefs.msgIdx,
|
|
2041
|
-
eventType: "GROUP_AT_MESSAGE_CREATE",
|
|
2042
|
-
mentions: event.mentions,
|
|
2043
|
-
messageScene: event.message_scene,
|
|
2044
|
-
msgElements: event.msg_elements,
|
|
2045
|
-
msgType: event.message_type,
|
|
2046
|
-
});
|
|
2047
|
-
} else if (t === "GROUP_MESSAGE_CREATE") {
|
|
2048
|
-
const event = d as GroupMessageEvent;
|
|
2049
|
-
recordKnownUser({
|
|
2050
|
-
openid: event.author.member_openid,
|
|
2051
|
-
type: "group",
|
|
2052
|
-
nickname: event.author.username,
|
|
2053
|
-
groupOpenid: event.group_openid,
|
|
2054
|
-
accountId: account.accountId,
|
|
2055
|
-
});
|
|
2056
|
-
const groupRefs = parseRefIndices(event.message_scene?.ext, event.message_type, event.msg_elements);
|
|
2057
|
-
trySlashCommandOrEnqueue({
|
|
2058
|
-
type: "group",
|
|
2059
|
-
senderId: event.author.member_openid,
|
|
2060
|
-
senderName: event.author.username,
|
|
2061
|
-
senderIsBot: event.author.bot,
|
|
2062
|
-
content: event.content,
|
|
2063
|
-
messageId: event.id,
|
|
2064
|
-
timestamp: event.timestamp,
|
|
2065
|
-
groupOpenid: event.group_openid,
|
|
2066
|
-
attachments: event.attachments,
|
|
2067
|
-
refMsgIdx: groupRefs.refMsgIdx,
|
|
2068
|
-
msgIdx: groupRefs.msgIdx,
|
|
2069
|
-
eventType: "GROUP_MESSAGE_CREATE",
|
|
2070
|
-
mentions: event.mentions,
|
|
2071
|
-
messageScene: event.message_scene,
|
|
2072
|
-
msgElements: event.msg_elements,
|
|
2073
|
-
msgType: event.message_type,
|
|
2074
|
-
});
|
|
2075
|
-
} else if (t === "GROUP_ADD_ROBOT") {
|
|
2076
|
-
const event = d as { timestamp: string; group_openid: string; op_member_openid: string };
|
|
2077
|
-
log?.info(`[qqbot:${account.accountId}] Bot added to group: ${event.group_openid} by ${event.op_member_openid}`);
|
|
2078
|
-
recordKnownUser({
|
|
2079
|
-
openid: event.op_member_openid,
|
|
2080
|
-
type: "group",
|
|
2081
|
-
groupOpenid: event.group_openid,
|
|
2082
|
-
accountId: account.accountId,
|
|
2083
|
-
});
|
|
2084
|
-
|
|
2085
|
-
} else if (t === "GROUP_DEL_ROBOT") {
|
|
2086
|
-
const event = d as { timestamp: string; group_openid: string; op_member_openid: string };
|
|
2087
|
-
log?.info(`[qqbot:${account.accountId}] Bot removed from group: ${event.group_openid} by ${event.op_member_openid}`);
|
|
2088
|
-
} else if (t === "GROUP_MSG_REJECT") {
|
|
2089
|
-
const event = d as { timestamp: number; group_openid: string; op_member_openid: string };
|
|
2090
|
-
log?.info(`[qqbot:${account.accountId}] Group ${event.group_openid} rejected bot proactive messages (by ${event.op_member_openid})`);
|
|
2091
|
-
} else if (t === "GROUP_MSG_RECEIVE") {
|
|
2092
|
-
const event = d as { timestamp: number; group_openid: string; op_member_openid: string };
|
|
2093
|
-
log?.info(`[qqbot:${account.accountId}] Group ${event.group_openid} accepted bot proactive messages (by ${event.op_member_openid})`);
|
|
2094
|
-
} else if (t === "INTERACTION_CREATE") {
|
|
2095
|
-
const event = d as InteractionEvent;
|
|
2096
|
-
const resolved = event.data?.resolved;
|
|
2097
|
-
const sceneDesc = event.scene ?? (event.chat_type === 0 ? "guild" : event.chat_type === 1 ? "group" : "c2c");
|
|
2098
|
-
log?.info(`[qqbot:${account.accountId}] Interaction: scene=${sceneDesc}, type=${event.data?.type}, button_id=${resolved?.button_id}, button_data=${resolved?.button_data}, user=${event.group_member_openid || event.user_openid || resolved?.user_id || "unknown"}`);
|
|
2099
|
-
|
|
2100
|
-
handleInteractionCreate({ event, account, cfg, log }).catch((err) => {
|
|
2101
|
-
log?.error(`[qqbot:${account.accountId}] Failed to handle interaction ${event.id}: ${err}`);
|
|
2108
|
+
} else {
|
|
2109
|
+
// 所有其他事件统一分发
|
|
2110
|
+
dispatchInboundEvent(t!, d).catch((err) => {
|
|
2111
|
+
log?.error(`[qqbot:${account.accountId}] Event dispatch error (t=${t}): ${err}`);
|
|
2102
2112
|
});
|
|
2103
2113
|
}
|
|
2104
2114
|
break;
|
|
@@ -2253,8 +2263,10 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
|
|
|
2253
2263
|
// 开始连接
|
|
2254
2264
|
await connect();
|
|
2255
2265
|
|
|
2256
|
-
// 等待 abort
|
|
2257
|
-
|
|
2258
|
-
|
|
2266
|
+
// 等待 abort 信号(如果 connect() 返回时 signal 已经 aborted,直接 resolve)
|
|
2267
|
+
if (abortSignal.aborted) return;
|
|
2268
|
+
return new Promise<void>((resolve) => {
|
|
2269
|
+
abortSignal.addEventListener("abort", () => resolve(), { once: true });
|
|
2259
2270
|
});
|
|
2260
2271
|
}
|
|
2272
|
+
|
package/src/onboarding.ts
CHANGED
|
@@ -23,6 +23,14 @@ interface QQBotChannelConfig {
|
|
|
23
23
|
imageServerBaseUrl?: string;
|
|
24
24
|
markdownSupport?: boolean;
|
|
25
25
|
allowFrom?: string[];
|
|
26
|
+
/** HTTP/WebSocket User-Agent 追加后缀 */
|
|
27
|
+
userAgentSuffix?: string;
|
|
28
|
+
/** 群消息是否默认需要 @提及才触发回复 */
|
|
29
|
+
defaultRequireMention?: boolean;
|
|
30
|
+
/** 消息接收传输方式:websocket(默认)| webhook */
|
|
31
|
+
transport?: string;
|
|
32
|
+
/** webhook 传输配置(transport="webhook" 时生效) */
|
|
33
|
+
webhook?: { path?: string };
|
|
26
34
|
accounts?: Record<string, {
|
|
27
35
|
enabled?: boolean;
|
|
28
36
|
appId?: string;
|
|
@@ -369,19 +369,34 @@ declare module "openclaw/plugin-sdk" {
|
|
|
369
369
|
/** 群消息策略适配器(resolveRequireMention / resolveToolPolicy / resolveGroupIntroHint) */
|
|
370
370
|
export interface ChannelGroupAdapter {
|
|
371
371
|
/** 是否需要 @机器人才响应 */
|
|
372
|
-
resolveRequireMention?: (ctx: { cfg: OpenClawConfig; accountId?: string; groupId: string }) => boolean;
|
|
372
|
+
resolveRequireMention?: (ctx: { cfg: OpenClawConfig; accountId?: string; groupId: string }) => boolean | undefined;
|
|
373
373
|
/** 群聊 AI 工具使用范围 */
|
|
374
|
-
resolveToolPolicy?: (
|
|
374
|
+
resolveToolPolicy?: (
|
|
375
|
+
ctx: { cfg: OpenClawConfig; accountId?: string; groupId: string; senderId?: string }
|
|
376
|
+
) =>
|
|
377
|
+
| "full"
|
|
378
|
+
| "restricted"
|
|
379
|
+
| "none"
|
|
380
|
+
| GroupToolPolicyConfig
|
|
381
|
+
| undefined;
|
|
375
382
|
/** 平台特有的群聊行为提示 */
|
|
376
383
|
resolveGroupIntroHint?: (ctx: { cfg: OpenClawConfig; accountId?: string; groupId: string }) => string | undefined;
|
|
377
384
|
/** 其他适配器方法 */
|
|
378
385
|
[key: string]: unknown;
|
|
379
386
|
}
|
|
380
387
|
|
|
388
|
+
/** 工具策略配置(用于把 none/restricted/full 映射为 allow/deny) */
|
|
389
|
+
export type GroupToolPolicyConfig = {
|
|
390
|
+
allow: string[];
|
|
391
|
+
deny?: string[];
|
|
392
|
+
};
|
|
393
|
+
|
|
381
394
|
/** @mention 检测与清理适配器(stripMentionText / detectWasMentioned) */
|
|
382
395
|
export interface ChannelMentionAdapter {
|
|
383
396
|
/** 清理 @mention 文本:平台格式→可读格式,去除 @机器人自身 */
|
|
384
397
|
stripMentionText?: (text: string, mentions?: Array<{ member_openid?: string; nickname?: string; is_you?: boolean }>) => string;
|
|
398
|
+
/** stripMentions:框架回调型(一次性拿到 text + ctx) */
|
|
399
|
+
stripMentions?: (params: { text: string; ctx: unknown }) => string;
|
|
385
400
|
/** 检测当前消息是否 @了机器人 */
|
|
386
401
|
detectWasMentioned?: (ctx: {
|
|
387
402
|
eventType?: string;
|
|
@@ -393,6 +408,14 @@ declare module "openclaw/plugin-sdk" {
|
|
|
393
408
|
[key: string]: unknown;
|
|
394
409
|
}
|
|
395
410
|
|
|
411
|
+
/** 状态摘要构建(仅覆盖当前项目用到的字段) */
|
|
412
|
+
export interface ChannelPluginStatus {
|
|
413
|
+
defaultRuntime: Record<string, unknown>;
|
|
414
|
+
buildChannelSummary?: (ctx: { snapshot: any }) => Record<string, unknown>;
|
|
415
|
+
buildAccountSnapshot?: (ctx: { account: any; runtime: any }) => Record<string, unknown>;
|
|
416
|
+
[key: string]: unknown;
|
|
417
|
+
}
|
|
418
|
+
|
|
396
419
|
/**
|
|
397
420
|
* 频道插件接口(泛型)
|
|
398
421
|
*/
|
|
@@ -423,6 +446,8 @@ declare module "openclaw/plugin-sdk" {
|
|
|
423
446
|
groups?: ChannelGroupAdapter;
|
|
424
447
|
/** @mention 检测与清理适配器 */
|
|
425
448
|
mentions?: ChannelMentionAdapter;
|
|
449
|
+
/** 状态摘要构建(可选) */
|
|
450
|
+
status?: ChannelPluginStatus;
|
|
426
451
|
/** 启动函数 */
|
|
427
452
|
start?: (runtime: PluginRuntime) => void | Promise<void>;
|
|
428
453
|
/** 停止函数 */
|
|
@@ -579,6 +604,54 @@ declare module "openclaw/plugin-sdk" {
|
|
|
579
604
|
hasMatchInput?: boolean;
|
|
580
605
|
}): MatchedGroupAccessDecision;
|
|
581
606
|
|
|
607
|
+
// ============ 审批运行时类型(minimal) ============
|
|
608
|
+
// 这些类型只覆盖本项目真实使用到的字段,
|
|
609
|
+
// 用来消除 strict/noImplicitAny 下的连锁报错。
|
|
610
|
+
|
|
611
|
+
export interface ExecApprovalRequest {
|
|
612
|
+
id: string;
|
|
613
|
+
expiresAtMs: number;
|
|
614
|
+
request: {
|
|
615
|
+
commandPreview?: string;
|
|
616
|
+
command?: string;
|
|
617
|
+
cwd?: string;
|
|
618
|
+
agentId?: string;
|
|
619
|
+
turnSourceAccountId?: string;
|
|
620
|
+
sessionKey?: string;
|
|
621
|
+
turnSourceTo?: string;
|
|
622
|
+
[key: string]: unknown;
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
export interface ExecApprovalResolved {
|
|
627
|
+
id: string;
|
|
628
|
+
decision: string;
|
|
629
|
+
[key: string]: unknown;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
export interface PluginApprovalRequest {
|
|
633
|
+
id: string;
|
|
634
|
+
request: {
|
|
635
|
+
timeoutMs?: number;
|
|
636
|
+
severity?: "critical" | "info" | string;
|
|
637
|
+
title: string;
|
|
638
|
+
description?: string;
|
|
639
|
+
toolName?: string;
|
|
640
|
+
pluginId?: string;
|
|
641
|
+
agentId?: string;
|
|
642
|
+
turnSourceAccountId?: string;
|
|
643
|
+
sessionKey?: string;
|
|
644
|
+
turnSourceTo?: string;
|
|
645
|
+
[key: string]: unknown;
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
export interface PluginApprovalResolved {
|
|
650
|
+
id: string;
|
|
651
|
+
decision: string;
|
|
652
|
+
[key: string]: unknown;
|
|
653
|
+
}
|
|
654
|
+
|
|
582
655
|
// ============ 其他导出 ============
|
|
583
656
|
|
|
584
657
|
/** 默认账户 ID 常量 */
|
|
@@ -587,3 +660,55 @@ declare module "openclaw/plugin-sdk" {
|
|
|
587
660
|
/** 规范化账户 ID */
|
|
588
661
|
export function normalizeAccountId(accountId: string | undefined | null): string;
|
|
589
662
|
}
|
|
663
|
+
|
|
664
|
+
declare module "openclaw/plugin-sdk/approval-runtime" {
|
|
665
|
+
export interface ExecApprovalReplyMetadata {
|
|
666
|
+
approvalId: string;
|
|
667
|
+
approvalSlug: string;
|
|
668
|
+
allowedDecisions?: string[];
|
|
669
|
+
}
|
|
670
|
+
export type ExecApprovalRequest = import("openclaw/plugin-sdk").ExecApprovalRequest;
|
|
671
|
+
export type ExecApprovalResolved = import("openclaw/plugin-sdk").ExecApprovalResolved;
|
|
672
|
+
export type PluginApprovalRequest = import("openclaw/plugin-sdk").PluginApprovalRequest;
|
|
673
|
+
export type PluginApprovalResolved = import("openclaw/plugin-sdk").PluginApprovalResolved;
|
|
674
|
+
export function getExecApprovalReplyMetadata(payload: { channelData?: unknown }): ExecApprovalReplyMetadata | null;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
declare module "openclaw/plugin-sdk/core" {
|
|
678
|
+
export type OpenClawConfig = import("openclaw/plugin-sdk").OpenClawConfig;
|
|
679
|
+
export type ChannelPlugin<TAccount = unknown> =
|
|
680
|
+
import("openclaw/plugin-sdk").ChannelPlugin<TAccount>;
|
|
681
|
+
|
|
682
|
+
export const applyAccountNameToChannelSection: typeof import("openclaw/plugin-sdk")
|
|
683
|
+
.applyAccountNameToChannelSection;
|
|
684
|
+
export const deleteAccountFromConfigSection: typeof import("openclaw/plugin-sdk")
|
|
685
|
+
.deleteAccountFromConfigSection;
|
|
686
|
+
export const setAccountEnabledInConfigSection: typeof import("openclaw/plugin-sdk")
|
|
687
|
+
.setAccountEnabledInConfigSection;
|
|
688
|
+
|
|
689
|
+
// 允许其它 core 导出被逐步补齐(避免引入大量严格类型时频繁改代码)
|
|
690
|
+
export * from "openclaw/plugin-sdk";
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
declare module "openclaw/plugin-sdk/gateway-runtime" {
|
|
694
|
+
export interface EventFrame {
|
|
695
|
+
event: string;
|
|
696
|
+
payload: unknown;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
export interface GatewayClient {
|
|
700
|
+
start: () => void | Promise<void>;
|
|
701
|
+
stop: () => void | Promise<void>;
|
|
702
|
+
request: (method: string, params: unknown) => Promise<unknown>;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
export function createOperatorApprovalsGatewayClient(options: {
|
|
706
|
+
config: import("openclaw/plugin-sdk").OpenClawConfig;
|
|
707
|
+
gatewayUrl?: string;
|
|
708
|
+
clientDisplayName?: string;
|
|
709
|
+
onEvent: (evt: EventFrame) => void;
|
|
710
|
+
onHelloOk?: () => void;
|
|
711
|
+
onConnectError?: (err: { message: string }) => void;
|
|
712
|
+
onClose?: (code: number, reason: string) => void;
|
|
713
|
+
}): Promise<GatewayClient>;
|
|
714
|
+
}
|