@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.
Files changed (45) hide show
  1. package/README.md +216 -49
  2. package/README.zh.md +216 -4
  3. package/dist/index.d.ts +1 -0
  4. package/dist/index.js +1 -0
  5. package/dist/src/api.d.ts +6 -0
  6. package/dist/src/api.js +33 -4
  7. package/dist/src/approval-handler.d.ts +47 -0
  8. package/dist/src/approval-handler.js +372 -0
  9. package/dist/src/channel.js +72 -0
  10. package/dist/src/config.d.ts +5 -1
  11. package/dist/src/config.js +12 -2
  12. package/dist/src/gateway.js +175 -170
  13. package/dist/src/slash-commands.d.ts +7 -2
  14. package/dist/src/slash-commands.js +354 -3
  15. package/dist/src/tools/channel.js +1 -4
  16. package/dist/src/tools/remind.js +0 -1
  17. package/dist/src/transport/index.d.ts +10 -0
  18. package/dist/src/transport/index.js +9 -0
  19. package/dist/src/transport/webhook-transport.d.ts +67 -0
  20. package/dist/src/transport/webhook-transport.js +245 -0
  21. package/dist/src/transport/webhook-verify.d.ts +48 -0
  22. package/dist/src/transport/webhook-verify.js +98 -0
  23. package/dist/src/types.d.ts +85 -0
  24. package/dist/src/utils/audio-convert.js +37 -9
  25. package/index.ts +1 -0
  26. package/package.json +1 -1
  27. package/scripts/postinstall-link-sdk.js +44 -0
  28. package/scripts/upgrade-via-npm.sh +358 -62
  29. package/scripts/upgrade-via-source.sh +122 -85
  30. package/src/api.ts +50 -5
  31. package/src/approval-handler.ts +505 -0
  32. package/src/channel.ts +76 -0
  33. package/src/config.ts +15 -2
  34. package/src/gateway.ts +181 -169
  35. package/src/onboarding.ts +8 -0
  36. package/src/openclaw-plugin-sdk.d.ts +127 -2
  37. package/src/slash-commands.ts +390 -5
  38. package/src/tools/channel.ts +1 -7
  39. package/src/tools/remind.ts +0 -2
  40. package/src/transport/index.ts +11 -0
  41. package/src/transport/webhook-transport.ts +332 -0
  42. package/src/transport/webhook-verify.ts +119 -0
  43. package/src/types.ts +100 -1
  44. package/src/typings/openclaw-webhook-ingress.d.ts +66 -0
  45. package/src/utils/audio-convert.ts +37 -9
@@ -2,12 +2,14 @@ import WebSocket from "ws";
2
2
  import path from "node:path";
3
3
  import fs from "node:fs";
4
4
  import { MSG_TYPE_QUOTE } from "./types.js";
5
+ import { startWebhookTransport } from "./transport/index.js";
5
6
  import { getAccessToken, getGatewayUrl, sendC2CMessage, sendChannelMessage, sendGroupMessage, clearTokenCache, initApiConfig, startBackgroundTokenRefresh, stopBackgroundTokenRefresh, sendC2CInputNotify, onMessageSent, getPluginUserAgent, acknowledgeInteraction, getApiPluginVersion, setApiLogger } from "./api.js";
6
7
  import { loadSession, saveSession, clearSession } from "./session-store.js";
7
8
  import { recordKnownUser, flushKnownUsers } from "./known-users.js";
8
9
  import { getQQBotRuntime } from "./runtime.js";
9
10
  import { isGroupAllowed, resolveGroupName, resolveGroupPrompt, resolveHistoryLimit, resolveGroupPolicy, resolveGroupConfig, resolveIgnoreOtherMentions, resolveMentionPatterns } from "./config.js";
10
11
  import { qqbotPlugin, stripMentionText, detectWasMentioned } from "./channel.js";
12
+ import { QQBotApprovalHandler, registerApprovalHandler, unregisterApprovalHandler, getApprovalHandler } from "./approval-handler.js";
11
13
  import { recordPendingHistoryEntry, buildPendingHistoryContext, buildMergedMessageContext, clearPendingHistory, formatAttachmentTags, formatMessageContent, toAttachmentSummaries, } from "./group-history.js";
12
14
  import { setRefIndex, getRefIndex, formatRefEntryForAgent, formatMessageReferenceForAgent, flushRefIndex } from "./ref-index-store.js";
13
15
  import { matchSlashCommand, getFrameworkVersion, parseFrameworkDateVersion } from "./slash-commands.js";
@@ -135,9 +137,27 @@ async function handleInteractionCreate(params) {
135
137
  log?.info(`[qqbot:${account.accountId}] Interaction ACK (type=${INTERACTION_TYPE_CONFIG_UPDATE}) sent: ${event.id}, claw_cfg=${JSON.stringify(ackClawCfg)}`);
136
138
  }
137
139
  else {
138
- // 其他类型:普通 ACK
140
+ // 普通按钮交互:先 ACK
139
141
  await acknowledgeInteraction(token, event.id);
140
142
  log?.debug?.(`[qqbot:${account.accountId}] Interaction ACK sent: ${event.id}`);
143
+ // Inline Keyboard 审批按钮(type=1 Callback)
144
+ // button_data 格式:approve:<approvalId>:<decision>
145
+ // approvalId 可能是 "exec:uuid" / "plugin:uuid"(带前缀)或纯 "uuid"(无前缀)
146
+ const buttonData = event.data?.resolved?.button_data ?? "";
147
+ const m = buttonData.match(/^approve:((?:(?:exec|plugin):)?[0-9a-f-]+):(allow-once|allow-always|deny)$/i);
148
+ if (m) {
149
+ const approvalId = m[1];
150
+ const decision = m[2];
151
+ const userId = event.group_member_openid || event.user_openid || event.data?.resolved?.user_id || "unknown";
152
+ log?.info(`[qqbot:${account.accountId}] Approval button clicked: approvalId=${approvalId}, decision=${decision}, user=${userId}, buttonData=${buttonData}`);
153
+ const handler = getApprovalHandler(account.accountId);
154
+ if (handler) {
155
+ void handler.resolveApproval(approvalId, decision);
156
+ }
157
+ else {
158
+ log?.error(`[qqbot:${account.accountId}] Approval button: no handler found for accountId=${account.accountId}`);
159
+ }
160
+ }
141
161
  }
142
162
  }
143
163
  /** 解析 session store 文件路径 */
@@ -313,6 +333,21 @@ export async function startGateway(ctx) {
313
333
  if (!account.appId || !account.clientSecret) {
314
334
  throw new Error("QQBot not configured (missing appId or clientSecret)");
315
335
  }
336
+ // 安全网:捕获 approval-handler / SDK 内部 WS 握手异步错误(如 403),避免进程崩溃
337
+ const wsUncaughtHandler = (err) => {
338
+ if (err.message?.includes("Unexpected server response")) {
339
+ log?.error(`[qqbot:${account.accountId}] Caught WS handshake error (non-fatal): ${err.message}`);
340
+ // 不重新抛出,防止进程退出
341
+ }
342
+ else {
343
+ // 非 WS 握手错误,重新抛出交给上层处理
344
+ throw err;
345
+ }
346
+ };
347
+ process.on("uncaughtException", wsUncaughtHandler);
348
+ abortSignal.addEventListener("abort", () => {
349
+ process.removeListener("uncaughtException", wsUncaughtHandler);
350
+ }, { once: true });
316
351
  // 启动环境诊断(首次连接时执行)
317
352
  const diag = await runDiagnostics();
318
353
  if (diag.warnings.length > 0) {
@@ -403,6 +438,12 @@ export async function startGateway(ctx) {
403
438
  else {
404
439
  log?.info(`[qqbot:${account.accountId}] Image server disabled (no imageServerBaseUrl configured)`);
405
440
  }
441
+ // ============ Transport 模式标记 ============
442
+ const transportMode = account.config.transport ?? "websocket";
443
+ if (transportMode === "webhook") {
444
+ log?.info(`[qqbot:${account.accountId}] Using webhook transport mode`);
445
+ }
446
+ // ============ WebSocket / Webhook 公共初始化 ============
406
447
  let reconnectAttempts = 0;
407
448
  let isAborted = false;
408
449
  let currentWs = null;
@@ -425,6 +466,18 @@ export async function startGateway(ctx) {
425
466
  lastSeq = savedSession.lastSeq;
426
467
  log?.info(`[qqbot:${account.accountId}] Restored session from storage: sessionId=${sessionId}, lastSeq=${lastSeq}`);
427
468
  }
469
+ // ============ 审批 Handler ============
470
+ const approvalHandler = new QQBotApprovalHandler({
471
+ accountId: account.accountId,
472
+ appId: account.appId,
473
+ clientSecret: account.clientSecret,
474
+ cfg: cfg,
475
+ log,
476
+ });
477
+ registerApprovalHandler(account.accountId, approvalHandler);
478
+ approvalHandler.start().catch((err) => {
479
+ log?.error(`[qqbot:${account.accountId}] approval-handler: uncaught start error: ${err}`);
480
+ });
428
481
  // ============ 消息队列(复用 createMessageQueue,内置群消息合并/淘汰策略) ============
429
482
  const msgQueue = createMessageQueue({
430
483
  accountId: account.accountId,
@@ -433,7 +486,9 @@ export async function startGateway(ctx) {
433
486
  });
434
487
  // 斜杠指令拦截:在入队前匹配插件级指令,命中则直接回复,不入队
435
488
  // 紧急命令列表:这些命令会立即执行,不进入斜杠匹配流程
436
- const URGENT_COMMANDS = ["/stop"];
489
+ // /stop — 停止当前 agent run,清空队列
490
+ // /approve — 审批决策,必须在 agent 等待审批时立即执行,否则死锁
491
+ const URGENT_COMMANDS = ["/stop", "/approve"];
437
492
  const trySlashCommandOrEnqueue = async (msg) => {
438
493
  const content = (msg.content ?? "").trim();
439
494
  if (!content.startsWith("/")) {
@@ -478,6 +533,15 @@ export async function startGateway(ctx) {
478
533
  msgQueue.enqueue(msg);
479
534
  return;
480
535
  }
536
+ // 委托给 AI 模型:用加工后的 prompt 替换原始消息入队
537
+ const isDelegateResult = typeof reply === "object" && reply !== null && "delegatePrompt" in reply;
538
+ if (isDelegateResult) {
539
+ const delegatePrompt = reply.delegatePrompt;
540
+ log?.info(`[qqbot:${account.accountId}] Slash command delegated to AI: ${content.slice(0, 40)}`);
541
+ msg.content = delegatePrompt;
542
+ msgQueue.enqueue(msg);
543
+ return;
544
+ }
481
545
  // 命中插件级指令,直接回复
482
546
  log?.info(`[qqbot:${account.accountId}] Slash command matched: ${content}, replying directly`);
483
547
  const token = await getAccessToken(account.appId, account.clientSecret);
@@ -537,6 +601,9 @@ export async function startGateway(ctx) {
537
601
  flushKnownUsers();
538
602
  // P1-4: 保存引用索引数据
539
603
  flushRefIndex();
604
+ // 停止审批 handler
605
+ void approvalHandler.stop();
606
+ unregisterApprovalHandler(account.accountId);
540
607
  });
541
608
  const cleanup = () => {
542
609
  if (heartbeatInterval) {
@@ -587,12 +654,6 @@ export async function startGateway(ctx) {
587
654
  clearTokenCache(account.appId);
588
655
  shouldRefreshToken = false;
589
656
  }
590
- const accessToken = await getAccessToken(account.appId, account.clientSecret);
591
- log?.info(`[qqbot:${account.accountId}] ✅ Access token obtained successfully`);
592
- const gatewayUrl = await getGatewayUrl(accessToken);
593
- log?.info(`[qqbot:${account.accountId}] Connecting to ${gatewayUrl}`);
594
- const ws = new WebSocket(gatewayUrl, { headers: { "User-Agent": getPluginUserAgent() } });
595
- currentWs = ws;
596
657
  const pluginRuntime = getQQBotRuntime();
597
658
  // 群历史消息缓存:非@消息写入此 Map,被@时一次性注入上下文后清空
598
659
  const groupHistories = new Map();
@@ -1244,6 +1305,8 @@ export async function startGateway(ctx) {
1244
1305
  },
1245
1306
  });
1246
1307
  }
1308
+ // 打印 runId 用于调试
1309
+ log?.info?.(`[qqbot:${account.accountId}] Dispatching with runId: ${event.messageId}`);
1247
1310
  const dispatchPromise = pluginRuntime.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
1248
1311
  ctx: ctxPayload,
1249
1312
  cfg,
@@ -1494,6 +1557,8 @@ export async function startGateway(ctx) {
1494
1557
  },
1495
1558
  },
1496
1559
  replyOptions: {
1560
+ // 使用消息ID作为 runId,用于追踪一次完整的 AI 对话运行
1561
+ runId: event.messageId,
1497
1562
  // 流式模式时禁用 block streaming
1498
1563
  disableBlockStreaming: !useStreaming,
1499
1564
  // 流式模式下注册 onPartialReply 回调,接收流式文本增量
@@ -1593,6 +1658,100 @@ export async function startGateway(ctx) {
1593
1658
  }
1594
1659
  }); // end runWithRequestContext
1595
1660
  };
1661
+ // ============ 统一事件分发(WebSocket/Webhook 共用) ============
1662
+ const dispatchInboundEvent = async (t, d) => {
1663
+ if (t === "C2C_MESSAGE_CREATE") {
1664
+ const ev = d;
1665
+ recordKnownUser({ openid: ev.author.user_openid, type: "c2c", accountId: account.accountId });
1666
+ const refs = parseRefIndices(ev.message_scene?.ext, ev.message_type, ev.msg_elements);
1667
+ 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 });
1668
+ }
1669
+ else if (t === "AT_MESSAGE_CREATE") {
1670
+ const ev = d;
1671
+ recordKnownUser({ openid: ev.author.id, type: "c2c", nickname: ev.author.username, accountId: account.accountId });
1672
+ const refs = parseRefIndices(ev.message_scene?.ext, ev.message_type, ev.msg_elements);
1673
+ 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.message_type });
1674
+ }
1675
+ else if (t === "DIRECT_MESSAGE_CREATE") {
1676
+ const ev = d;
1677
+ recordKnownUser({ openid: ev.author.id, type: "c2c", nickname: ev.author.username, accountId: account.accountId });
1678
+ const refs = parseRefIndices(ev.message_scene?.ext, ev.message_type, ev.msg_elements);
1679
+ 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.message_type });
1680
+ }
1681
+ else if (t === "GROUP_AT_MESSAGE_CREATE" || t === "GROUP_MESSAGE_CREATE") {
1682
+ const ev = d;
1683
+ recordKnownUser({ openid: ev.author.member_openid, type: "group", nickname: ev.author.username, groupOpenid: ev.group_openid, accountId: account.accountId });
1684
+ const refs = parseRefIndices(ev.message_scene?.ext, ev.message_type, ev.msg_elements);
1685
+ 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 });
1686
+ }
1687
+ else if (t === "GROUP_ADD_ROBOT") {
1688
+ const ev = d;
1689
+ log?.info(`[qqbot:${account.accountId}] Bot added to group: ${ev.group_openid} by ${ev.op_member_openid}`);
1690
+ recordKnownUser({ openid: ev.op_member_openid, type: "group", groupOpenid: ev.group_openid, accountId: account.accountId });
1691
+ }
1692
+ else if (t === "GROUP_DEL_ROBOT") {
1693
+ const ev = d;
1694
+ log?.info(`[qqbot:${account.accountId}] Bot removed from group: ${ev.group_openid} by ${ev.op_member_openid}`);
1695
+ }
1696
+ else if (t === "GROUP_MSG_REJECT") {
1697
+ const ev = d;
1698
+ log?.info(`[qqbot:${account.accountId}] Group ${ev.group_openid} rejected bot proactive messages (by ${ev.op_member_openid})`);
1699
+ }
1700
+ else if (t === "GROUP_MSG_RECEIVE") {
1701
+ const ev = d;
1702
+ log?.info(`[qqbot:${account.accountId}] Group ${ev.group_openid} accepted bot proactive messages (by ${ev.op_member_openid})`);
1703
+ }
1704
+ else if (t === "INTERACTION_CREATE") {
1705
+ const ev = d;
1706
+ const resolved = ev.data?.resolved;
1707
+ const sceneDesc = ev.scene ?? (ev.chat_type === 0 ? "guild" : ev.chat_type === 1 ? "group" : "c2c");
1708
+ log?.info(`[qqbot:${account.accountId}] Interaction: scene=${sceneDesc}, type=${ev.data?.type}, button_id=${resolved?.button_id}, button_data=${resolved?.button_data}`);
1709
+ handleInteractionCreate({ event: ev, account, cfg, log }).catch((err) => {
1710
+ log?.error(`[qqbot:${account.accountId}] Failed to handle interaction ${ev.id}: ${err}`);
1711
+ });
1712
+ }
1713
+ };
1714
+ // ============ Webhook 模式:共享 handleMessage,不走 WS ============
1715
+ if (transportMode === "webhook") {
1716
+ isConnecting = false;
1717
+ msgQueue.startProcessor(handleMessage);
1718
+ startBackgroundTokenRefresh(account.appId, account.clientSecret, {
1719
+ log: log,
1720
+ });
1721
+ await startWebhookTransport({
1722
+ account,
1723
+ abortSignal,
1724
+ onEvent: async (event) => {
1725
+ const { eventType: t, data: d } = event;
1726
+ log?.info(`[qqbot:${account.accountId}:webhook] 📩 Dispatch event: t=${t}, d=${JSON.stringify(d)}`);
1727
+ await dispatchInboundEvent(t, d);
1728
+ },
1729
+ onReady: () => {
1730
+ log?.info(`[qqbot:${account.accountId}:webhook] Transport ready`);
1731
+ log?.info(`[qqbot:${account.accountId}] ✅ Webhook transport started successfully (path: ${account.config.webhook?.path ?? "/qqbot/webhook"})`);
1732
+ onReady?.({ transport: "webhook" });
1733
+ if (_pendingFirstReady.has(account.accountId)) {
1734
+ _pendingFirstReady.delete(account.accountId);
1735
+ sendStartupGreetings(adminCtx, "READY");
1736
+ }
1737
+ },
1738
+ onError: (error) => {
1739
+ log?.error(`[qqbot:${account.accountId}:webhook] Error: ${error.message}`);
1740
+ onError?.(error);
1741
+ },
1742
+ log,
1743
+ });
1744
+ stopBackgroundTokenRefresh();
1745
+ unregisterApprovalHandler(account.accountId);
1746
+ return; // webhook transport 结束,不继续 WS 逻辑
1747
+ }
1748
+ // ============ WebSocket 模式:获取 token 并建立 WS 连接 ============
1749
+ const accessToken = await getAccessToken(account.appId, account.clientSecret);
1750
+ log?.info(`[qqbot:${account.accountId}] ✅ Access token obtained successfully`);
1751
+ const gatewayUrl = await getGatewayUrl(accessToken);
1752
+ log?.info(`[qqbot:${account.accountId}] Connecting to ${gatewayUrl}`);
1753
+ const ws = new WebSocket(gatewayUrl, { headers: { "User-Agent": getPluginUserAgent() } });
1754
+ currentWs = ws;
1596
1755
  ws.on("open", () => {
1597
1756
  log?.info(`[qqbot:${account.accountId}] WebSocket connected`);
1598
1757
  isConnecting = false; // 连接完成,释放锁
@@ -1712,166 +1871,10 @@ export async function startGateway(ctx) {
1712
1871
  });
1713
1872
  }
1714
1873
  }
1715
- else if (t === "C2C_MESSAGE_CREATE") {
1716
- const event = d;
1717
- // P1-3: 记录已知用户
1718
- recordKnownUser({
1719
- openid: event.author.user_openid,
1720
- type: "c2c",
1721
- accountId: account.accountId,
1722
- });
1723
- // 解析引用索引
1724
- const c2cRefs = parseRefIndices(event.message_scene?.ext, event.message_type, event.msg_elements);
1725
- // 斜杠指令拦截 → 不匹配则入队
1726
- trySlashCommandOrEnqueue({
1727
- type: "c2c",
1728
- senderId: event.author.user_openid,
1729
- content: event.content,
1730
- messageId: event.id,
1731
- timestamp: event.timestamp,
1732
- attachments: event.attachments,
1733
- refMsgIdx: c2cRefs.refMsgIdx,
1734
- msgIdx: c2cRefs.msgIdx,
1735
- msgElements: event.msg_elements,
1736
- msgType: event.message_type,
1737
- });
1738
- }
1739
- else if (t === "AT_MESSAGE_CREATE") {
1740
- const event = d;
1741
- // P1-3: 记录已知用户(频道用户)
1742
- recordKnownUser({
1743
- openid: event.author.id,
1744
- type: "c2c", // 频道用户按 c2c 类型存储
1745
- nickname: event.author.username,
1746
- accountId: account.accountId,
1747
- });
1748
- const guildRefs = parseRefIndices(event.message_scene?.ext, event.message_type, event.msg_elements);
1749
- trySlashCommandOrEnqueue({
1750
- type: "guild",
1751
- senderId: event.author.id,
1752
- senderName: event.author.username,
1753
- content: event.content,
1754
- messageId: event.id,
1755
- timestamp: event.timestamp,
1756
- channelId: event.channel_id,
1757
- guildId: event.guild_id,
1758
- attachments: event.attachments,
1759
- refMsgIdx: guildRefs.refMsgIdx,
1760
- msgIdx: guildRefs.msgIdx,
1761
- msgType: event.message_type,
1762
- });
1763
- }
1764
- else if (t === "DIRECT_MESSAGE_CREATE") {
1765
- const event = d;
1766
- // P1-3: 记录已知用户(频道私信用户)
1767
- recordKnownUser({
1768
- openid: event.author.id,
1769
- type: "c2c",
1770
- nickname: event.author.username,
1771
- accountId: account.accountId,
1772
- });
1773
- const dmRefs = parseRefIndices(event.message_scene?.ext, event.message_type, event.msg_elements);
1774
- trySlashCommandOrEnqueue({
1775
- type: "dm",
1776
- senderId: event.author.id,
1777
- senderName: event.author.username,
1778
- content: event.content,
1779
- messageId: event.id,
1780
- timestamp: event.timestamp,
1781
- guildId: event.guild_id,
1782
- attachments: event.attachments,
1783
- refMsgIdx: dmRefs.refMsgIdx,
1784
- msgIdx: dmRefs.msgIdx,
1785
- msgType: event.message_type,
1786
- });
1787
- }
1788
- else if (t === "GROUP_AT_MESSAGE_CREATE") {
1789
- const event = d;
1790
- // 被 @ 的消息,直接入队回复
1791
- recordKnownUser({
1792
- openid: event.author.member_openid,
1793
- type: "group",
1794
- nickname: event.author.username,
1795
- groupOpenid: event.group_openid,
1796
- accountId: account.accountId,
1797
- });
1798
- const groupRefs = parseRefIndices(event.message_scene?.ext, event.message_type, event.msg_elements);
1799
- trySlashCommandOrEnqueue({
1800
- type: "group",
1801
- senderId: event.author.member_openid,
1802
- senderName: event.author.username,
1803
- content: event.content,
1804
- messageId: event.id,
1805
- timestamp: event.timestamp,
1806
- groupOpenid: event.group_openid,
1807
- attachments: event.attachments,
1808
- refMsgIdx: groupRefs.refMsgIdx,
1809
- msgIdx: groupRefs.msgIdx,
1810
- eventType: "GROUP_AT_MESSAGE_CREATE",
1811
- mentions: event.mentions,
1812
- messageScene: event.message_scene,
1813
- msgElements: event.msg_elements,
1814
- msgType: event.message_type,
1815
- });
1816
- }
1817
- else if (t === "GROUP_MESSAGE_CREATE") {
1818
- const event = d;
1819
- recordKnownUser({
1820
- openid: event.author.member_openid,
1821
- type: "group",
1822
- nickname: event.author.username,
1823
- groupOpenid: event.group_openid,
1824
- accountId: account.accountId,
1825
- });
1826
- const groupRefs = parseRefIndices(event.message_scene?.ext, event.message_type, event.msg_elements);
1827
- trySlashCommandOrEnqueue({
1828
- type: "group",
1829
- senderId: event.author.member_openid,
1830
- senderName: event.author.username,
1831
- senderIsBot: event.author.bot,
1832
- content: event.content,
1833
- messageId: event.id,
1834
- timestamp: event.timestamp,
1835
- groupOpenid: event.group_openid,
1836
- attachments: event.attachments,
1837
- refMsgIdx: groupRefs.refMsgIdx,
1838
- msgIdx: groupRefs.msgIdx,
1839
- eventType: "GROUP_MESSAGE_CREATE",
1840
- mentions: event.mentions,
1841
- messageScene: event.message_scene,
1842
- msgElements: event.msg_elements,
1843
- msgType: event.message_type,
1844
- });
1845
- }
1846
- else if (t === "GROUP_ADD_ROBOT") {
1847
- const event = d;
1848
- log?.info(`[qqbot:${account.accountId}] Bot added to group: ${event.group_openid} by ${event.op_member_openid}`);
1849
- recordKnownUser({
1850
- openid: event.op_member_openid,
1851
- type: "group",
1852
- groupOpenid: event.group_openid,
1853
- accountId: account.accountId,
1854
- });
1855
- }
1856
- else if (t === "GROUP_DEL_ROBOT") {
1857
- const event = d;
1858
- log?.info(`[qqbot:${account.accountId}] Bot removed from group: ${event.group_openid} by ${event.op_member_openid}`);
1859
- }
1860
- else if (t === "GROUP_MSG_REJECT") {
1861
- const event = d;
1862
- log?.info(`[qqbot:${account.accountId}] Group ${event.group_openid} rejected bot proactive messages (by ${event.op_member_openid})`);
1863
- }
1864
- else if (t === "GROUP_MSG_RECEIVE") {
1865
- const event = d;
1866
- log?.info(`[qqbot:${account.accountId}] Group ${event.group_openid} accepted bot proactive messages (by ${event.op_member_openid})`);
1867
- }
1868
- else if (t === "INTERACTION_CREATE") {
1869
- const event = d;
1870
- const resolved = event.data?.resolved;
1871
- const sceneDesc = event.scene ?? (event.chat_type === 0 ? "guild" : event.chat_type === 1 ? "group" : "c2c");
1872
- 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"}`);
1873
- handleInteractionCreate({ event, account, cfg, log }).catch((err) => {
1874
- log?.error(`[qqbot:${account.accountId}] Failed to handle interaction ${event.id}: ${err}`);
1874
+ else {
1875
+ // 所有其他事件统一分发
1876
+ dispatchInboundEvent(t, d).catch((err) => {
1877
+ log?.error(`[qqbot:${account.accountId}] Event dispatch error (t=${t}): ${err}`);
1875
1878
  });
1876
1879
  }
1877
1880
  break;
@@ -2013,8 +2016,10 @@ export async function startGateway(ctx) {
2013
2016
  };
2014
2017
  // 开始连接
2015
2018
  await connect();
2016
- // 等待 abort 信号
2019
+ // 等待 abort 信号(如果 connect() 返回时 signal 已经 aborted,直接 resolve)
2020
+ if (abortSignal.aborted)
2021
+ return;
2017
2022
  return new Promise((resolve) => {
2018
- abortSignal.addEventListener("abort", () => resolve());
2023
+ abortSignal.addEventListener("abort", () => resolve(), { once: true });
2019
2024
  });
2020
2025
  }
@@ -59,14 +59,19 @@ export interface QueueSnapshot {
59
59
  /** 当前发送者在队列中的待处理消息数 */
60
60
  senderPending: number;
61
61
  }
62
- /** 斜杠指令返回值:文本、带文件的结果、或 null(不处理) */
63
- export type SlashCommandResult = string | SlashCommandFileResult | null;
62
+ /** 斜杠指令返回值:文本、带文件的结果、委托给模型、或 null(不处理) */
63
+ export type SlashCommandResult = string | SlashCommandFileResult | SlashCommandDelegateResult | null;
64
64
  /** 带文件的指令结果(先回复文本,再发送文件) */
65
65
  export interface SlashCommandFileResult {
66
66
  text: string;
67
67
  /** 要发送的本地文件路径 */
68
68
  filePath: string;
69
69
  }
70
+ /** 委托给 AI 模型处理:用加工后的 prompt 替换原始消息入队 */
71
+ export interface SlashCommandDelegateResult {
72
+ /** 替换原始消息内容的 prompt,交给 AI 模型执行 */
73
+ delegatePrompt: string;
74
+ }
70
75
  /**
71
76
  * 尝试匹配并执行插件级斜杠指令
72
77
  *