@openclaw-china/qqbot 0.1.13 → 2026.3.3

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/dist/index.js CHANGED
@@ -11,6 +11,8 @@ import N2, { stdout, stdin } from 'process';
11
11
  import ot from 'readline';
12
12
  import 'tty';
13
13
  import 'util';
14
+ import { createRequire } from 'module';
15
+ import { execFileSync } from 'child_process';
14
16
  import WebSocket from 'ws';
15
17
 
16
18
  var __create = Object.create;
@@ -648,8 +650,8 @@ function getErrorMap() {
648
650
 
649
651
  // ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/parseUtil.js
650
652
  var makeIssue = (params) => {
651
- const { data, path: path3, errorMaps, issueData } = params;
652
- const fullPath = [...path3, ...issueData.path || []];
653
+ const { data, path: path4, errorMaps, issueData } = params;
654
+ const fullPath = [...path4, ...issueData.path || []];
653
655
  const fullIssue = {
654
656
  ...issueData,
655
657
  path: fullPath
@@ -765,11 +767,11 @@ var errorUtil;
765
767
 
766
768
  // ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/types.js
767
769
  var ParseInputLazyPath = class {
768
- constructor(parent, value, path3, key) {
770
+ constructor(parent, value, path4, key) {
769
771
  this._cachedPath = [];
770
772
  this.parent = parent;
771
773
  this.data = value;
772
- this._path = path3;
774
+ this._path = path4;
773
775
  this._key = key;
774
776
  }
775
777
  get path() {
@@ -4219,8 +4221,9 @@ var optionalCoercedString = external_exports.preprocess(
4219
4221
  },
4220
4222
  external_exports.string().min(1).optional()
4221
4223
  );
4222
- var QQBotConfigSchema = external_exports.object({
4223
- enabled: external_exports.boolean().optional().default(true),
4224
+ var QQBotAccountSchema = external_exports.object({
4225
+ name: external_exports.string().optional(),
4226
+ enabled: external_exports.boolean().optional(),
4224
4227
  appId: optionalCoercedString,
4225
4228
  clientSecret: optionalCoercedString,
4226
4229
  asr: external_exports.object({
@@ -4241,8 +4244,38 @@ var QQBotConfigSchema = external_exports.object({
4241
4244
  maxFileSizeMB: external_exports.number().positive().optional().default(100),
4242
4245
  mediaTimeoutMs: external_exports.number().int().positive().optional().default(3e4)
4243
4246
  });
4244
- function isConfigured(config) {
4245
- return Boolean(config?.appId && config?.clientSecret);
4247
+ QQBotAccountSchema.extend({
4248
+ defaultAccount: external_exports.string().optional(),
4249
+ accounts: external_exports.record(QQBotAccountSchema).optional()
4250
+ });
4251
+ var DEFAULT_ACCOUNT_ID = "default";
4252
+ function listConfiguredAccountIds(cfg) {
4253
+ const accounts = cfg.channels?.qqbot?.accounts;
4254
+ if (!accounts || typeof accounts !== "object") return [];
4255
+ return Object.keys(accounts).filter(Boolean);
4256
+ }
4257
+ function listQQBotAccountIds(cfg) {
4258
+ const ids = listConfiguredAccountIds(cfg);
4259
+ if (ids.length === 0) return [DEFAULT_ACCOUNT_ID];
4260
+ return ids.sort((a, b) => a.localeCompare(b));
4261
+ }
4262
+ function resolveDefaultQQBotAccountId(cfg) {
4263
+ const qqbotConfig = cfg.channels?.qqbot;
4264
+ if (qqbotConfig?.defaultAccount?.trim()) return qqbotConfig.defaultAccount.trim();
4265
+ const ids = listQQBotAccountIds(cfg);
4266
+ if (ids.includes(DEFAULT_ACCOUNT_ID)) return DEFAULT_ACCOUNT_ID;
4267
+ return ids[0] ?? DEFAULT_ACCOUNT_ID;
4268
+ }
4269
+ function resolveAccountConfig(cfg, accountId) {
4270
+ const accounts = cfg.channels?.qqbot?.accounts;
4271
+ if (!accounts || typeof accounts !== "object") return void 0;
4272
+ return accounts[accountId];
4273
+ }
4274
+ function mergeQQBotAccountConfig(cfg, accountId) {
4275
+ const base = cfg.channels?.qqbot ?? {};
4276
+ const { accounts: _ignored, defaultAccount: _ignored2, ...baseConfig } = base;
4277
+ const account = resolveAccountConfig(cfg, accountId) ?? {};
4278
+ return { ...baseConfig, ...account };
4246
4279
  }
4247
4280
  function resolveQQBotCredentials(config) {
4248
4281
  if (!config?.appId || !config?.clientSecret) return void 0;
@@ -6920,9 +6953,9 @@ function showChinaInstallHint(api) {
6920
6953
  // src/client.ts
6921
6954
  var API_BASE = "https://api.sgroup.qq.com";
6922
6955
  var TOKEN_URL = "https://bots.qq.com/app/getAppAccessToken";
6923
- var tokenCache = null;
6924
- var tokenPromise = null;
6925
- var MSG_SEQ_BASE = Math.floor(Date.now() / 1e3) % 1e8;
6956
+ var MSG_SEQ_BASE = 1e6;
6957
+ var tokenCacheMap = /* @__PURE__ */ new Map();
6958
+ var tokenPromiseMap = /* @__PURE__ */ new Map();
6926
6959
  var msgSeqMap = /* @__PURE__ */ new Map();
6927
6960
  function nextMsgSeq(messageId) {
6928
6961
  if (!messageId) return MSG_SEQ_BASE + 1;
@@ -6937,17 +6970,25 @@ function nextMsgSeq(messageId) {
6937
6970
  }
6938
6971
  return MSG_SEQ_BASE + next;
6939
6972
  }
6940
- function clearTokenCache() {
6941
- tokenCache = null;
6973
+ function clearTokenCache(appId) {
6974
+ if (appId) {
6975
+ tokenCacheMap.delete(appId);
6976
+ tokenPromiseMap.delete(appId);
6977
+ } else {
6978
+ tokenCacheMap.clear();
6979
+ tokenPromiseMap.clear();
6980
+ }
6942
6981
  }
6943
6982
  async function getAccessToken(appId, clientSecret, options) {
6944
- if (tokenCache && Date.now() < tokenCache.expiresAt - 5 * 60 * 1e3) {
6945
- return tokenCache.token;
6983
+ const cached = tokenCacheMap.get(appId);
6984
+ if (cached && Date.now() < cached.expiresAt - 5 * 60 * 1e3) {
6985
+ return cached.token;
6946
6986
  }
6947
- if (tokenPromise) {
6948
- return tokenPromise;
6987
+ const existingPromise = tokenPromiseMap.get(appId);
6988
+ if (existingPromise) {
6989
+ return existingPromise;
6949
6990
  }
6950
- tokenPromise = (async () => {
6991
+ const promise = (async () => {
6951
6992
  try {
6952
6993
  const data = await httpPost(
6953
6994
  TOKEN_URL,
@@ -6957,19 +6998,20 @@ async function getAccessToken(appId, clientSecret, options) {
6957
6998
  if (!data.access_token) {
6958
6999
  throw new Error("access_token missing from QQ response");
6959
7000
  }
6960
- tokenCache = {
7001
+ tokenCacheMap.set(appId, {
6961
7002
  token: data.access_token,
6962
7003
  expiresAt: Date.now() + (data.expires_in ?? 7200) * 1e3
6963
- };
6964
- return tokenCache.token;
7004
+ });
7005
+ return data.access_token;
6965
7006
  } finally {
6966
- tokenPromise = null;
7007
+ tokenPromiseMap.delete(appId);
6967
7008
  }
6968
7009
  })();
6969
- return tokenPromise;
7010
+ tokenPromiseMap.set(appId, promise);
7011
+ return promise;
6970
7012
  }
6971
- async function apiGet(accessToken, path3, options) {
6972
- const url = `${API_BASE}${path3}`;
7013
+ async function apiGet(accessToken, path4, options) {
7014
+ const url = `${API_BASE}${path4}`;
6973
7015
  return httpGet(url, {
6974
7016
  ...options,
6975
7017
  headers: {
@@ -6978,8 +7020,8 @@ async function apiGet(accessToken, path3, options) {
6978
7020
  }
6979
7021
  });
6980
7022
  }
6981
- async function apiPost(accessToken, path3, body, options) {
6982
- const url = `${API_BASE}${path3}`;
7023
+ async function apiPost(accessToken, path4, body, options) {
7024
+ const url = `${API_BASE}${path4}`;
6983
7025
  return httpPost(url, body, {
6984
7026
  ...options,
6985
7027
  headers: {
@@ -7116,9 +7158,8 @@ async function sendGroupMediaMessage(params) {
7116
7158
  { timeout: 15e3 }
7117
7159
  );
7118
7160
  }
7119
-
7120
- // src/send.ts
7121
7161
  var QQBOT_UNSUPPORTED_FILE_TYPE_MESSAGE = "QQ official C2C/group media API does not support generic files (file_type=4, e.g. PDF). Images and other supported media types are unaffected.";
7162
+ var require2 = createRequire(import.meta.url);
7122
7163
  function resolveQQBotMediaFileType(fileName) {
7123
7164
  const mediaType = detectMediaType(fileName);
7124
7165
  switch (mediaType) {
@@ -7153,6 +7194,30 @@ async function uploadQQBotFile(params) {
7153
7194
  }
7154
7195
  return upload.file_info;
7155
7196
  }
7197
+ async function convertAudioToSilk(audioPath) {
7198
+ const ffmpegPath = require2("ffmpeg-static");
7199
+ if (!ffmpegPath) {
7200
+ throw new Error("ffmpeg-static not found");
7201
+ }
7202
+ const silkWasm = require2("silk-wasm");
7203
+ const tmpDir = fs3.mkdtempSync(path.join(os.tmpdir(), "qqbot-silk-"));
7204
+ const pcmPath = path.join(tmpDir, "audio.pcm");
7205
+ try {
7206
+ execFileSync(
7207
+ ffmpegPath,
7208
+ ["-y", "-i", audioPath, "-f", "s16le", "-ar", "24000", "-ac", "1", pcmPath],
7209
+ { timeout: 3e4, stdio: "pipe" }
7210
+ );
7211
+ const pcmBuffer = fs3.readFileSync(pcmPath);
7212
+ const result = await silkWasm.encode(pcmBuffer, 24e3);
7213
+ return result.data;
7214
+ } finally {
7215
+ try {
7216
+ fs3.rmSync(tmpDir, { recursive: true, force: true });
7217
+ } catch {
7218
+ }
7219
+ }
7220
+ }
7156
7221
  async function sendFileQQBot(params) {
7157
7222
  const { cfg, target, mediaUrl, messageId } = params;
7158
7223
  if (!cfg.appId || !cfg.clientSecret) {
@@ -7178,10 +7243,25 @@ async function sendFileQQBot(params) {
7178
7243
  url: src
7179
7244
  });
7180
7245
  } else {
7181
- const { buffer } = await readMediaWithConfig(src, {
7182
- timeout: mediaTimeoutMs,
7183
- maxSize: maxSizeBytes
7184
- });
7246
+ let buffer;
7247
+ if (fileType === 3 /* VOICE */) {
7248
+ try {
7249
+ const silkData = await convertAudioToSilk(src);
7250
+ buffer = Buffer.from(silkData);
7251
+ } catch {
7252
+ const local = await readMediaWithConfig(src, {
7253
+ timeout: mediaTimeoutMs,
7254
+ maxSize: maxSizeBytes
7255
+ });
7256
+ buffer = local.buffer;
7257
+ }
7258
+ } else {
7259
+ const local = await readMediaWithConfig(src, {
7260
+ timeout: mediaTimeoutMs,
7261
+ maxSize: maxSizeBytes
7262
+ });
7263
+ buffer = local.buffer;
7264
+ }
7185
7265
  fileInfo = await uploadQQBotFile({
7186
7266
  accessToken,
7187
7267
  target,
@@ -7280,13 +7360,8 @@ var qqbotOutbound = {
7280
7360
  textChunkLimit: 1500,
7281
7361
  chunkerMode: "markdown",
7282
7362
  sendText: async (params) => {
7283
- const { cfg, to, text, replyToId } = params;
7284
- const rawCfg = cfg.channels?.qqbot;
7285
- const parsed = rawCfg ? QQBotConfigSchema.safeParse(rawCfg) : null;
7286
- const qqCfg = parsed?.success ? parsed.data : rawCfg;
7287
- if (!qqCfg) {
7288
- return { channel: "qqbot", error: "QQBot channel not configured" };
7289
- }
7363
+ const { cfg, to, text, replyToId, accountId } = params;
7364
+ const qqCfg = mergeQQBotAccountConfig(cfg, accountId ?? DEFAULT_ACCOUNT_ID);
7290
7365
  if (!qqCfg.appId || !qqCfg.clientSecret) {
7291
7366
  return { channel: "qqbot", error: "QQBot not configured (missing appId/clientSecret)" };
7292
7367
  }
@@ -7327,20 +7402,15 @@ var qqbotOutbound = {
7327
7402
  }
7328
7403
  },
7329
7404
  sendMedia: async (params) => {
7330
- const { cfg, to, mediaUrl, text, replyToId } = params;
7405
+ const { cfg, to, mediaUrl, text, replyToId, accountId } = params;
7331
7406
  if (!mediaUrl) {
7332
7407
  const fallbackText = text?.trim() ?? "";
7333
7408
  if (!fallbackText) {
7334
7409
  return { channel: "qqbot", error: "mediaUrl is required for sendMedia" };
7335
7410
  }
7336
- return qqbotOutbound.sendText({ cfg, to, text: fallbackText, replyToId });
7337
- }
7338
- const rawCfg = cfg.channels?.qqbot;
7339
- const parsed = rawCfg ? QQBotConfigSchema.safeParse(rawCfg) : null;
7340
- const qqCfg = parsed?.success ? parsed.data : rawCfg;
7341
- if (!qqCfg) {
7342
- return { channel: "qqbot", error: "QQBot channel not configured" };
7411
+ return qqbotOutbound.sendText({ cfg, to, text: fallbackText, replyToId, accountId });
7343
7412
  }
7413
+ const qqCfg = mergeQQBotAccountConfig(cfg, accountId ?? DEFAULT_ACCOUNT_ID);
7344
7414
  if (!qqCfg.appId || !qqCfg.clientSecret) {
7345
7415
  return { channel: "qqbot", error: "QQBot not configured (missing appId/clientSecret)" };
7346
7416
  }
@@ -7364,13 +7434,8 @@ ${mediaUrl}` : mediaUrl;
7364
7434
  }
7365
7435
  },
7366
7436
  sendTyping: async (params) => {
7367
- const { cfg, to, replyToId, inputSecond } = params;
7368
- const rawCfg = cfg.channels?.qqbot;
7369
- const parsed = rawCfg ? QQBotConfigSchema.safeParse(rawCfg) : null;
7370
- const qqCfg = parsed?.success ? parsed.data : rawCfg;
7371
- if (!qqCfg) {
7372
- return { channel: "qqbot", error: "QQBot channel not configured" };
7373
- }
7437
+ const { cfg, to, replyToId, inputSecond, accountId } = params;
7438
+ const qqCfg = mergeQQBotAccountConfig(cfg, accountId ?? DEFAULT_ACCOUNT_ID);
7374
7439
  if (!qqCfg.appId || !qqCfg.clientSecret) {
7375
7440
  return { channel: "qqbot", error: "QQBot not configured (missing appId/clientSecret)" };
7376
7441
  }
@@ -7830,6 +7895,78 @@ function buildMediaFallbackText(mediaUrl, errorMessage) {
7830
7895
  }
7831
7896
  return `\u{1F4CE} ${mediaUrl}`;
7832
7897
  }
7898
+ var THINK_BLOCK_RE = /<think\b[^>]*>[\s\S]*?<\/think>/gi;
7899
+ var FINAL_BLOCK_RE = /<final\b[^>]*>([\s\S]*?)<\/final>/gi;
7900
+ var RAW_THINK_OR_FINAL_TAG_RE = /<\/?(?:think|final)\b[^>]*>/gi;
7901
+ var DIRECTIVE_TAG_RE = /\[\[\s*(?:reply_to_current|reply_to\s*:[^\]]+|audio_as_voice|tts(?::text)?|\/tts(?::text)?)\s*\]\]/gi;
7902
+ var VOICE_EMOTION_TAG_RE = /\[(?:happy|excited|calm|sad|angry|frustrated|softly|whispers|loudly|cheerfully|deadpan|sarcastically|laughs|sighs|chuckles|gasps|pause|slowly|rushed|hesitates|playfully|warmly|gently)\]/gi;
7903
+ var TTS_LIKE_RAW_TEXT_RE = /\[\[\s*(?:tts(?::text)?|\/tts(?::text)?|audio_as_voice|reply_to_current|reply_to\s*:)/i;
7904
+ function extractFinalBlocks(text) {
7905
+ const matches = Array.from(text.matchAll(FINAL_BLOCK_RE));
7906
+ if (matches.length === 0) return void 0;
7907
+ return matches.map((match) => (match[1] ?? "").trim()).filter(Boolean).join("\n");
7908
+ }
7909
+ function sanitizeQQBotOutboundText(rawText) {
7910
+ if (!rawText) return "";
7911
+ let next = rawText.replace(/\r\n/g, "\n");
7912
+ const finalOnly = extractFinalBlocks(next);
7913
+ if (typeof finalOnly === "string") {
7914
+ next = finalOnly;
7915
+ }
7916
+ next = next.replace(THINK_BLOCK_RE, "");
7917
+ next = next.replace(RAW_THINK_OR_FINAL_TAG_RE, "");
7918
+ next = next.replace(DIRECTIVE_TAG_RE, " ");
7919
+ next = next.replace(VOICE_EMOTION_TAG_RE, " ");
7920
+ next = next.replace(/[ \t]+\n/g, "\n");
7921
+ next = next.replace(/\n{3,}/g, "\n\n");
7922
+ next = next.trim();
7923
+ if (!next) return "";
7924
+ if (/^NO_REPLY$/i.test(next)) return "";
7925
+ return next;
7926
+ }
7927
+ function shouldSuppressQQBotTextWhenMediaPresent(rawText, sanitizedText) {
7928
+ const raw = rawText.trim();
7929
+ if (!raw) return false;
7930
+ if (TTS_LIKE_RAW_TEXT_RE.test(raw)) return true;
7931
+ if (/<(?:think|final)\b/i.test(raw)) return true;
7932
+ if (!sanitizedText) return true;
7933
+ return !/[A-Za-z0-9\u4e00-\u9fff]/.test(sanitizedText);
7934
+ }
7935
+ function evaluateReplyFinalOnlyDelivery(params) {
7936
+ const { replyFinalOnly, kind, hasMedia } = params;
7937
+ if (!replyFinalOnly || !kind || kind === "final") {
7938
+ return { skipDelivery: false, suppressText: false };
7939
+ }
7940
+ if (hasMedia) {
7941
+ return { skipDelivery: false, suppressText: true };
7942
+ }
7943
+ return { skipDelivery: true, suppressText: false };
7944
+ }
7945
+ async function sendQQBotMediaWithFallback(params) {
7946
+ const { qqCfg, to, mediaQueue, replyToId, logger } = params;
7947
+ const outbound = params.outbound ?? qqbotOutbound;
7948
+ for (const mediaUrl of mediaQueue) {
7949
+ const result = await outbound.sendMedia({
7950
+ cfg: { channels: { qqbot: qqCfg } },
7951
+ to,
7952
+ mediaUrl,
7953
+ replyToId
7954
+ });
7955
+ if (result.error) {
7956
+ logger.error(`sendMedia failed: ${result.error}`);
7957
+ const fallback = buildMediaFallbackText(mediaUrl, result.error);
7958
+ const fallbackResult = await outbound.sendText({
7959
+ cfg: { channels: { qqbot: qqCfg } },
7960
+ to,
7961
+ text: fallback,
7962
+ replyToId
7963
+ });
7964
+ if (fallbackResult.error) {
7965
+ logger.error(`sendText fallback failed: ${fallbackResult.error}`);
7966
+ }
7967
+ }
7968
+ }
7969
+ }
7833
7970
  function buildInboundContext(params) {
7834
7971
  const { event, sessionKey, accountId } = params;
7835
7972
  const body = params.body ?? event.content;
@@ -8015,18 +8152,16 @@ async function dispatchToAgent(params) {
8015
8152
  };
8016
8153
  const replyFinalOnly = qqCfg.replyFinalOnly ?? false;
8017
8154
  const deliver = async (payload, info) => {
8018
- if (replyFinalOnly && info?.kind && info.kind !== "final") return;
8019
8155
  const typed = payload;
8020
- const rawText = typed?.text ?? "";
8021
8156
  const mediaLineResult = extractMediaLinesFromText({
8022
- text: rawText,
8157
+ text: typed?.text ?? "",
8023
8158
  logger
8024
8159
  });
8025
8160
  const localMediaResult = extractLocalMediaFromText({
8026
8161
  text: mediaLineResult.text,
8027
8162
  logger
8028
8163
  });
8029
- const trimmed = localMediaResult.text.trim();
8164
+ const cleanedText = sanitizeQQBotOutboundText(localMediaResult.text);
8030
8165
  const payloadMediaUrls = Array.isArray(typed?.mediaUrls) ? typed?.mediaUrls : typed?.mediaUrl ? [typed.mediaUrl] : [];
8031
8166
  const mediaQueue = [];
8032
8167
  const seenMedia = /* @__PURE__ */ new Set();
@@ -8040,8 +8175,16 @@ async function dispatchToAgent(params) {
8040
8175
  for (const url of payloadMediaUrls) addMedia(url);
8041
8176
  for (const url of mediaLineResult.mediaUrls) addMedia(url);
8042
8177
  for (const url of localMediaResult.mediaUrls) addMedia(url);
8043
- if (trimmed) {
8044
- const converted = textApi?.convertMarkdownTables ? textApi.convertMarkdownTables(trimmed, resolvedTableMode) : trimmed;
8178
+ const deliveryDecision = evaluateReplyFinalOnlyDelivery({
8179
+ replyFinalOnly,
8180
+ kind: info?.kind,
8181
+ hasMedia: mediaQueue.length > 0});
8182
+ if (deliveryDecision.skipDelivery) return;
8183
+ const suppressEchoText = mediaQueue.length > 0 && shouldSuppressQQBotTextWhenMediaPresent(localMediaResult.text, cleanedText);
8184
+ const suppressText = deliveryDecision.suppressText || suppressEchoText;
8185
+ const textToSend = suppressText ? "" : cleanedText;
8186
+ if (textToSend) {
8187
+ const converted = textApi?.convertMarkdownTables ? textApi.convertMarkdownTables(textToSend, resolvedTableMode) : textToSend;
8045
8188
  const chunks = chunkText(converted);
8046
8189
  for (const chunk of chunks) {
8047
8190
  const result = await qqbotOutbound.sendText({
@@ -8055,27 +8198,13 @@ async function dispatchToAgent(params) {
8055
8198
  }
8056
8199
  }
8057
8200
  }
8058
- for (const mediaUrl of mediaQueue) {
8059
- const result = await qqbotOutbound.sendMedia({
8060
- cfg: { channels: { qqbot: qqCfg } },
8061
- to: target.to,
8062
- mediaUrl,
8063
- replyToId: inbound.messageId
8064
- });
8065
- if (result.error) {
8066
- logger.error(`sendMedia failed: ${result.error}`);
8067
- const fallback = buildMediaFallbackText(mediaUrl, result.error);
8068
- const fallbackResult = await qqbotOutbound.sendText({
8069
- cfg: { channels: { qqbot: qqCfg } },
8070
- to: target.to,
8071
- text: fallback,
8072
- replyToId: inbound.messageId
8073
- });
8074
- if (fallbackResult.error) {
8075
- logger.error(`sendText fallback failed: ${fallbackResult.error}`);
8076
- }
8077
- }
8078
- }
8201
+ await sendQQBotMediaWithFallback({
8202
+ qqCfg,
8203
+ to: target.to,
8204
+ mediaQueue,
8205
+ replyToId: inbound.messageId,
8206
+ logger
8207
+ });
8079
8208
  };
8080
8209
  const humanDelay = replyApi.resolveHumanDelayConfig?.(cfg, route.agentId);
8081
8210
  const dispatchBuffered = replyApi.dispatchReplyWithBufferedBlockDispatcher;
@@ -8162,14 +8291,13 @@ async function handleQQBotDispatch(params) {
8162
8291
  if (!inbound) {
8163
8292
  return;
8164
8293
  }
8165
- const rawCfg = params.cfg?.channels?.qqbot;
8166
- const parsedCfg = rawCfg ? QQBotConfigSchema.safeParse(rawCfg) : null;
8167
- const qqCfg = parsedCfg?.success ? parsedCfg.data : rawCfg;
8294
+ const accountId = params.accountId ?? DEFAULT_ACCOUNT_ID;
8295
+ const qqCfg = params.cfg ? mergeQQBotAccountConfig(params.cfg, accountId) : void 0;
8168
8296
  if (!qqCfg) {
8169
8297
  logger.warn("qqbot config missing, ignoring inbound message");
8170
8298
  return;
8171
8299
  }
8172
- if (!qqCfg.enabled) {
8300
+ if (qqCfg.enabled === false) {
8173
8301
  logger.info("qqbot disabled, ignoring inbound message");
8174
8302
  return;
8175
8303
  }
@@ -8180,7 +8308,7 @@ async function handleQQBotDispatch(params) {
8180
8308
  attachments: inbound.attachments
8181
8309
  })
8182
8310
  );
8183
- logger.info(`[inbound-user] senderId=${inbound.senderId} content=${inboundLogContent}`);
8311
+ logger.info(`[inbound-user] accountId=${accountId} senderId=${inbound.senderId} content=${inboundLogContent}`);
8184
8312
  if (!shouldHandleMessage(inbound, qqCfg, logger)) {
8185
8313
  return;
8186
8314
  }
@@ -8195,7 +8323,7 @@ async function handleQQBotDispatch(params) {
8195
8323
  inbound: { ...inbound, content },
8196
8324
  cfg: params.cfg,
8197
8325
  qqCfg,
8198
- accountId: params.accountId,
8326
+ accountId,
8199
8327
  logger
8200
8328
  });
8201
8329
  }
@@ -8208,72 +8336,80 @@ var INTENTS = {
8208
8336
  };
8209
8337
  var DEFAULT_INTENTS = INTENTS.GUILD_MESSAGES | INTENTS.DIRECT_MESSAGE | INTENTS.GROUP_AND_C2C;
8210
8338
  var RECONNECT_DELAYS_MS = [1e3, 2e3, 5e3, 1e4, 2e4, 3e4];
8211
- var activeSocket = null;
8212
- var activeAccountId = null;
8213
- var activePromise = null;
8214
- var activeStop = null;
8339
+ var activeConnections = /* @__PURE__ */ new Map();
8340
+ function getOrCreateConnection(accountId) {
8341
+ let conn = activeConnections.get(accountId);
8342
+ if (!conn) {
8343
+ conn = {
8344
+ socket: null,
8345
+ promise: null,
8346
+ stop: null,
8347
+ sessionId: null,
8348
+ lastSeq: null,
8349
+ heartbeatTimer: null,
8350
+ reconnectTimer: null,
8351
+ reconnectAttempt: 0,
8352
+ connecting: false
8353
+ };
8354
+ activeConnections.set(accountId, conn);
8355
+ }
8356
+ return conn;
8357
+ }
8358
+ function clearTimers(conn) {
8359
+ if (conn.heartbeatTimer) {
8360
+ clearInterval(conn.heartbeatTimer);
8361
+ conn.heartbeatTimer = null;
8362
+ }
8363
+ if (conn.reconnectTimer) {
8364
+ clearTimeout(conn.reconnectTimer);
8365
+ conn.reconnectTimer = null;
8366
+ }
8367
+ }
8368
+ function cleanupSocket(conn) {
8369
+ clearTimers(conn);
8370
+ if (conn.socket) {
8371
+ try {
8372
+ if (conn.socket.readyState === WebSocket.OPEN) {
8373
+ conn.socket.close();
8374
+ }
8375
+ } catch {
8376
+ }
8377
+ conn.socket = null;
8378
+ }
8379
+ }
8215
8380
  async function monitorQQBotProvider(opts = {}) {
8216
- const { config, runtime: runtime2, abortSignal, accountId = "default" } = opts;
8381
+ const { config, runtime: runtime2, abortSignal, accountId = DEFAULT_ACCOUNT_ID } = opts;
8217
8382
  const logger = createLogger("qqbot", {
8218
8383
  log: runtime2?.log,
8219
8384
  error: runtime2?.error
8220
8385
  });
8221
- if (activeSocket) {
8222
- if (activeAccountId && activeAccountId !== accountId) {
8223
- throw new Error(`QQBot already running for account ${activeAccountId}`);
8224
- }
8225
- if (activePromise) {
8226
- return activePromise;
8386
+ const conn = getOrCreateConnection(accountId);
8387
+ if (conn.socket) {
8388
+ if (conn.promise) {
8389
+ return conn.promise;
8227
8390
  }
8228
- throw new Error("QQBot monitor state invalid: active socket without promise");
8391
+ throw new Error(`QQBot monitor state invalid for account ${accountId}: active socket without promise`);
8229
8392
  }
8230
- const rawCfg = config?.channels?.qqbot;
8231
- const parsed = rawCfg ? QQBotConfigSchema.safeParse(rawCfg) : null;
8232
- const qqCfg = parsed?.success ? parsed.data : rawCfg;
8393
+ const qqCfg = config ? mergeQQBotAccountConfig(config, accountId) : void 0;
8233
8394
  if (!qqCfg) {
8234
8395
  throw new Error("QQBot configuration not found");
8235
8396
  }
8236
8397
  if (!qqCfg.appId || !qqCfg.clientSecret) {
8237
- throw new Error("QQBot not configured (missing appId or clientSecret)");
8398
+ throw new Error(`QQBot not configured for account ${accountId} (missing appId or clientSecret)`);
8238
8399
  }
8239
- activePromise = new Promise((resolve3, reject) => {
8400
+ conn.promise = new Promise((resolve3, reject) => {
8240
8401
  let stopped = false;
8241
- let reconnectAttempt = 0;
8242
- let heartbeatTimer = null;
8243
- let reconnectTimer = null;
8244
- let sessionId = null;
8245
- let lastSeq = null;
8246
- let connecting = false;
8247
- const clearTimers = () => {
8248
- if (heartbeatTimer) {
8249
- clearInterval(heartbeatTimer);
8250
- heartbeatTimer = null;
8251
- }
8252
- if (reconnectTimer) {
8253
- clearTimeout(reconnectTimer);
8254
- reconnectTimer = null;
8255
- }
8256
- };
8257
- const cleanupSocket = () => {
8258
- clearTimers();
8259
- if (activeSocket) {
8260
- try {
8261
- if (activeSocket.readyState === WebSocket.OPEN) {
8262
- activeSocket.close();
8263
- }
8264
- } catch {
8265
- }
8266
- }
8267
- activeSocket = null;
8268
- };
8269
8402
  const finish = (err) => {
8270
8403
  if (stopped) return;
8271
8404
  stopped = true;
8272
8405
  abortSignal?.removeEventListener("abort", onAbort);
8273
- cleanupSocket();
8274
- activeAccountId = null;
8275
- activePromise = null;
8276
- activeStop = null;
8406
+ cleanupSocket(conn);
8407
+ conn.sessionId = null;
8408
+ conn.lastSeq = null;
8409
+ conn.promise = null;
8410
+ conn.stop = null;
8411
+ conn.reconnectAttempt = 0;
8412
+ activeConnections.delete(accountId);
8277
8413
  {
8278
8414
  resolve3();
8279
8415
  }
@@ -8282,33 +8418,33 @@ async function monitorQQBotProvider(opts = {}) {
8282
8418
  logger.info("abort signal received, stopping gateway");
8283
8419
  finish();
8284
8420
  };
8285
- activeStop = () => {
8421
+ conn.stop = () => {
8286
8422
  logger.info("stop requested");
8287
8423
  finish();
8288
8424
  };
8289
8425
  const scheduleReconnect = (reason) => {
8290
8426
  if (stopped) return;
8291
- if (reconnectTimer) return;
8292
- const delay = RECONNECT_DELAYS_MS[Math.min(reconnectAttempt, RECONNECT_DELAYS_MS.length - 1)];
8293
- reconnectAttempt += 1;
8427
+ if (conn.reconnectTimer) return;
8428
+ const delay = RECONNECT_DELAYS_MS[Math.min(conn.reconnectAttempt, RECONNECT_DELAYS_MS.length - 1)];
8429
+ conn.reconnectAttempt += 1;
8294
8430
  logger.warn(`[reconnect] ${reason}; retry in ${delay}ms`);
8295
- reconnectTimer = setTimeout(() => {
8296
- reconnectTimer = null;
8431
+ conn.reconnectTimer = setTimeout(() => {
8432
+ conn.reconnectTimer = null;
8297
8433
  void connect();
8298
8434
  }, delay);
8299
8435
  };
8300
8436
  const startHeartbeat = (intervalMs) => {
8301
- if (heartbeatTimer) {
8302
- clearInterval(heartbeatTimer);
8437
+ if (conn.heartbeatTimer) {
8438
+ clearInterval(conn.heartbeatTimer);
8303
8439
  }
8304
- heartbeatTimer = setInterval(() => {
8305
- if (!activeSocket || activeSocket.readyState !== WebSocket.OPEN) return;
8306
- const payload = JSON.stringify({ op: 1, d: lastSeq });
8307
- activeSocket.send(payload);
8440
+ conn.heartbeatTimer = setInterval(() => {
8441
+ if (!conn.socket || conn.socket.readyState !== WebSocket.OPEN) return;
8442
+ const payload = JSON.stringify({ op: 1, d: conn.lastSeq });
8443
+ conn.socket.send(payload);
8308
8444
  }, intervalMs);
8309
8445
  };
8310
8446
  const sendIdentify = (token) => {
8311
- if (!activeSocket || activeSocket.readyState !== WebSocket.OPEN) return;
8447
+ if (!conn.socket || conn.socket.readyState !== WebSocket.OPEN) return;
8312
8448
  const payload = {
8313
8449
  op: 2,
8314
8450
  d: {
@@ -8317,10 +8453,10 @@ async function monitorQQBotProvider(opts = {}) {
8317
8453
  shard: [0, 1]
8318
8454
  }
8319
8455
  };
8320
- activeSocket.send(JSON.stringify(payload));
8456
+ conn.socket.send(JSON.stringify(payload));
8321
8457
  };
8322
8458
  const sendResume = (token, session, seq) => {
8323
- if (!activeSocket || activeSocket.readyState !== WebSocket.OPEN) return;
8459
+ if (!conn.socket || conn.socket.readyState !== WebSocket.OPEN) return;
8324
8460
  const payload = {
8325
8461
  op: 6,
8326
8462
  d: {
@@ -8329,11 +8465,11 @@ async function monitorQQBotProvider(opts = {}) {
8329
8465
  seq
8330
8466
  }
8331
8467
  };
8332
- activeSocket.send(JSON.stringify(payload));
8468
+ conn.socket.send(JSON.stringify(payload));
8333
8469
  };
8334
8470
  const handleGatewayPayload = async (payload) => {
8335
8471
  if (typeof payload.s === "number") {
8336
- lastSeq = payload.s;
8472
+ conn.lastSeq = payload.s;
8337
8473
  }
8338
8474
  switch (payload.op) {
8339
8475
  case 10: {
@@ -8341,8 +8477,8 @@ async function monitorQQBotProvider(opts = {}) {
8341
8477
  const interval = hello?.heartbeat_interval ?? 3e4;
8342
8478
  startHeartbeat(interval);
8343
8479
  const token = await getAccessToken(qqCfg.appId, qqCfg.clientSecret);
8344
- if (sessionId && typeof lastSeq === "number") {
8345
- sendResume(token, sessionId, lastSeq);
8480
+ if (conn.sessionId && typeof conn.lastSeq === "number") {
8481
+ sendResume(token, conn.sessionId, conn.lastSeq);
8346
8482
  } else {
8347
8483
  sendIdentify(token);
8348
8484
  }
@@ -8351,14 +8487,14 @@ async function monitorQQBotProvider(opts = {}) {
8351
8487
  case 11:
8352
8488
  return;
8353
8489
  case 7:
8354
- cleanupSocket();
8490
+ cleanupSocket(conn);
8355
8491
  scheduleReconnect("server requested reconnect");
8356
8492
  return;
8357
8493
  case 9:
8358
- sessionId = null;
8359
- lastSeq = null;
8360
- clearTokenCache();
8361
- cleanupSocket();
8494
+ conn.sessionId = null;
8495
+ conn.lastSeq = null;
8496
+ clearTokenCache(qqCfg.appId);
8497
+ cleanupSocket(conn);
8362
8498
  scheduleReconnect("invalid session");
8363
8499
  return;
8364
8500
  case 0: {
@@ -8366,14 +8502,14 @@ async function monitorQQBotProvider(opts = {}) {
8366
8502
  if (eventType === "READY") {
8367
8503
  const ready = payload.d;
8368
8504
  if (ready?.session_id) {
8369
- sessionId = ready.session_id;
8505
+ conn.sessionId = ready.session_id;
8370
8506
  }
8371
- reconnectAttempt = 0;
8507
+ conn.reconnectAttempt = 0;
8372
8508
  logger.info("gateway ready");
8373
8509
  return;
8374
8510
  }
8375
8511
  if (eventType === "RESUMED") {
8376
- reconnectAttempt = 0;
8512
+ conn.reconnectAttempt = 0;
8377
8513
  logger.info("gateway resumed");
8378
8514
  return;
8379
8515
  }
@@ -8393,16 +8529,15 @@ async function monitorQQBotProvider(opts = {}) {
8393
8529
  }
8394
8530
  };
8395
8531
  const connect = async () => {
8396
- if (stopped || connecting) return;
8397
- connecting = true;
8532
+ if (stopped || conn.connecting) return;
8533
+ conn.connecting = true;
8398
8534
  try {
8399
- cleanupSocket();
8535
+ cleanupSocket(conn);
8400
8536
  const token = await getAccessToken(qqCfg.appId, qqCfg.clientSecret);
8401
8537
  const gatewayUrl = await getGatewayUrl(token);
8402
8538
  logger.info(`connecting gateway: ${gatewayUrl}`);
8403
8539
  const ws = new WebSocket(gatewayUrl);
8404
- activeSocket = ws;
8405
- activeAccountId = accountId;
8540
+ conn.socket = ws;
8406
8541
  ws.on("open", () => {
8407
8542
  logger.info("gateway socket opened");
8408
8543
  });
@@ -8421,7 +8556,7 @@ async function monitorQQBotProvider(opts = {}) {
8421
8556
  });
8422
8557
  ws.on("close", (code, reason) => {
8423
8558
  logger.warn(`gateway socket closed (${code}) ${String(reason)}`);
8424
- cleanupSocket();
8559
+ cleanupSocket(conn);
8425
8560
  scheduleReconnect("socket closed");
8426
8561
  });
8427
8562
  ws.on("error", (err) => {
@@ -8429,10 +8564,10 @@ async function monitorQQBotProvider(opts = {}) {
8429
8564
  });
8430
8565
  } catch (err) {
8431
8566
  logger.error(`gateway connect failed: ${String(err)}`);
8432
- cleanupSocket();
8567
+ cleanupSocket(conn);
8433
8568
  scheduleReconnect("connect failed");
8434
8569
  } finally {
8435
- connecting = false;
8570
+ conn.connecting = false;
8436
8571
  }
8437
8572
  };
8438
8573
  if (abortSignal?.aborted) {
@@ -8442,27 +8577,20 @@ async function monitorQQBotProvider(opts = {}) {
8442
8577
  abortSignal?.addEventListener("abort", onAbort, { once: true });
8443
8578
  void connect();
8444
8579
  });
8445
- return activePromise;
8580
+ return conn.promise;
8446
8581
  }
8447
- function stopQQBotMonitor() {
8448
- if (activeStop) {
8449
- activeStop();
8582
+ function stopQQBotMonitorForAccount(accountId = DEFAULT_ACCOUNT_ID) {
8583
+ const conn = activeConnections.get(accountId);
8584
+ if (!conn) return;
8585
+ if (conn.stop) {
8586
+ conn.stop();
8450
8587
  return;
8451
8588
  }
8452
- if (activeSocket) {
8453
- try {
8454
- activeSocket.close();
8455
- } catch {
8456
- }
8457
- activeSocket = null;
8458
- activeAccountId = null;
8459
- activePromise = null;
8460
- activeStop = null;
8461
- }
8589
+ cleanupSocket(conn);
8590
+ activeConnections.delete(accountId);
8462
8591
  }
8463
8592
 
8464
8593
  // src/channel.ts
8465
- var DEFAULT_ACCOUNT_ID = "default";
8466
8594
  var meta = {
8467
8595
  id: "qqbot",
8468
8596
  label: "QQ Bot",
@@ -8475,17 +8603,17 @@ var meta = {
8475
8603
  };
8476
8604
  function resolveQQBotAccount(params) {
8477
8605
  const { cfg, accountId = DEFAULT_ACCOUNT_ID } = params;
8478
- const qqCfg = cfg.channels?.qqbot;
8479
- const parsed = qqCfg ? QQBotConfigSchema.safeParse(qqCfg) : null;
8480
- const config = parsed?.success ? parsed.data : void 0;
8481
- const credentials = resolveQQBotCredentials(config);
8606
+ const merged = mergeQQBotAccountConfig(cfg, accountId);
8607
+ const baseEnabled = cfg.channels?.qqbot?.enabled !== false;
8608
+ const enabled = baseEnabled && merged.enabled !== false;
8609
+ const credentials = resolveQQBotCredentials(merged);
8482
8610
  const configured = Boolean(credentials);
8483
8611
  return {
8484
8612
  accountId,
8485
- enabled: config?.enabled ?? true,
8613
+ enabled,
8486
8614
  configured,
8487
8615
  appId: credentials?.appId,
8488
- markdownSupport: config?.markdownSupport ?? true
8616
+ markdownSupport: merged.markdownSupport ?? true
8489
8617
  };
8490
8618
  }
8491
8619
  var qqbotPlugin = {
@@ -8558,6 +8686,8 @@ var qqbotPlugin = {
8558
8686
  additionalProperties: false,
8559
8687
  properties: {
8560
8688
  enabled: { type: "boolean" },
8689
+ name: { type: "string" },
8690
+ defaultAccount: { type: "string" },
8561
8691
  appId: { type: "string" },
8562
8692
  clientSecret: { type: "string" },
8563
8693
  asr: {
@@ -8578,46 +8708,121 @@ var qqbotPlugin = {
8578
8708
  groupAllowFrom: { type: "array", items: { type: "string" } },
8579
8709
  historyLimit: { type: "integer", minimum: 0 },
8580
8710
  textChunkLimit: { type: "integer", minimum: 1 },
8581
- replyFinalOnly: { type: "boolean" }
8711
+ replyFinalOnly: { type: "boolean" },
8712
+ maxFileSizeMB: { type: "number" },
8713
+ mediaTimeoutMs: { type: "number" },
8714
+ accounts: {
8715
+ type: "object",
8716
+ additionalProperties: {
8717
+ type: "object",
8718
+ additionalProperties: false,
8719
+ properties: {
8720
+ name: { type: "string" },
8721
+ enabled: { type: "boolean" },
8722
+ appId: { type: "string" },
8723
+ clientSecret: { type: "string" },
8724
+ asr: {
8725
+ type: "object",
8726
+ additionalProperties: false,
8727
+ properties: {
8728
+ enabled: { type: "boolean" },
8729
+ appId: { type: "string" },
8730
+ secretId: { type: "string" },
8731
+ secretKey: { type: "string" }
8732
+ }
8733
+ },
8734
+ markdownSupport: { type: "boolean" },
8735
+ dmPolicy: { type: "string", enum: ["open", "pairing", "allowlist"] },
8736
+ groupPolicy: { type: "string", enum: ["open", "allowlist", "disabled"] },
8737
+ requireMention: { type: "boolean" },
8738
+ allowFrom: { type: "array", items: { type: "string" } },
8739
+ groupAllowFrom: { type: "array", items: { type: "string" } },
8740
+ historyLimit: { type: "integer", minimum: 0 },
8741
+ textChunkLimit: { type: "integer", minimum: 1 },
8742
+ replyFinalOnly: { type: "boolean" },
8743
+ maxFileSizeMB: { type: "number" },
8744
+ mediaTimeoutMs: { type: "number" }
8745
+ }
8746
+ }
8747
+ }
8582
8748
  }
8583
8749
  }
8584
8750
  },
8585
8751
  reload: { configPrefixes: ["channels.qqbot"] },
8586
8752
  config: {
8587
- listAccountIds: (_cfg) => [DEFAULT_ACCOUNT_ID],
8753
+ listAccountIds: (cfg) => listQQBotAccountIds(cfg),
8588
8754
  resolveAccount: (cfg, accountId) => resolveQQBotAccount({ cfg, accountId }),
8589
8755
  defaultAccountId: () => DEFAULT_ACCOUNT_ID,
8590
8756
  setAccountEnabled: (params) => {
8757
+ const accountId = params.accountId ?? DEFAULT_ACCOUNT_ID;
8591
8758
  const existing = params.cfg.channels?.qqbot ?? {};
8759
+ if (accountId === DEFAULT_ACCOUNT_ID) {
8760
+ return {
8761
+ ...params.cfg,
8762
+ channels: {
8763
+ ...params.cfg.channels,
8764
+ qqbot: { ...existing, enabled: params.enabled }
8765
+ }
8766
+ };
8767
+ }
8768
+ const accounts = existing.accounts ?? {};
8769
+ const account = accounts[accountId] ?? {};
8592
8770
  return {
8593
8771
  ...params.cfg,
8594
8772
  channels: {
8595
8773
  ...params.cfg.channels,
8596
8774
  qqbot: {
8597
8775
  ...existing,
8598
- enabled: params.enabled
8776
+ accounts: {
8777
+ ...accounts,
8778
+ [accountId]: { ...account, enabled: params.enabled }
8779
+ }
8599
8780
  }
8600
8781
  }
8601
8782
  };
8602
8783
  },
8603
8784
  deleteAccount: (params) => {
8604
- const next = { ...params.cfg };
8605
- const nextChannels = { ...params.cfg.channels };
8606
- delete nextChannels.qqbot;
8607
- if (Object.keys(nextChannels).length > 0) {
8608
- next.channels = nextChannels;
8609
- } else {
8610
- delete next.channels;
8785
+ const accountId = params.accountId ?? DEFAULT_ACCOUNT_ID;
8786
+ if (accountId === DEFAULT_ACCOUNT_ID) {
8787
+ const next = { ...params.cfg };
8788
+ const nextChannels = { ...params.cfg.channels };
8789
+ delete nextChannels.qqbot;
8790
+ if (Object.keys(nextChannels).length > 0) {
8791
+ next.channels = nextChannels;
8792
+ } else {
8793
+ delete next.channels;
8794
+ }
8795
+ return next;
8611
8796
  }
8612
- return next;
8797
+ const existing = params.cfg.channels?.qqbot;
8798
+ if (!existing?.accounts?.[accountId]) return params.cfg;
8799
+ const { [accountId]: _removed, ...remainingAccounts } = existing.accounts;
8800
+ return {
8801
+ ...params.cfg,
8802
+ channels: {
8803
+ ...params.cfg.channels,
8804
+ qqbot: {
8805
+ ...existing,
8806
+ accounts: Object.keys(remainingAccounts).length > 0 ? remainingAccounts : void 0
8807
+ }
8808
+ }
8809
+ };
8810
+ },
8811
+ isConfigured: (_account, cfg, accountId) => {
8812
+ const id = accountId ?? _account.accountId;
8813
+ const merged = mergeQQBotAccountConfig(cfg, id);
8814
+ return Boolean(merged.appId && merged.clientSecret);
8613
8815
  },
8614
- isConfigured: (_account, cfg) => isConfigured(cfg.channels?.qqbot),
8615
8816
  describeAccount: (account) => ({
8616
8817
  accountId: account.accountId,
8617
8818
  enabled: account.enabled,
8618
8819
  configured: account.configured
8619
8820
  }),
8620
- resolveAllowFrom: (params) => params.cfg.channels?.qqbot?.allowFrom ?? [],
8821
+ resolveAllowFrom: (params) => {
8822
+ const accountId = params.accountId ?? DEFAULT_ACCOUNT_ID;
8823
+ const merged = mergeQQBotAccountConfig(params.cfg, accountId);
8824
+ return merged.allowFrom ?? [];
8825
+ },
8621
8826
  formatAllowFrom: (params) => params.allowFrom.map((entry) => String(entry).trim()).filter(Boolean).map((entry) => entry.toLowerCase())
8622
8827
  },
8623
8828
  security: {
@@ -8631,16 +8836,30 @@ var qqbotPlugin = {
8631
8836
  }
8632
8837
  },
8633
8838
  setup: {
8634
- resolveAccountId: () => DEFAULT_ACCOUNT_ID,
8839
+ resolveAccountId: (params) => params.accountId ?? resolveDefaultQQBotAccountId(params.cfg),
8635
8840
  applyAccountConfig: (params) => {
8841
+ const accountId = params.accountId ?? DEFAULT_ACCOUNT_ID;
8636
8842
  const existing = params.cfg.channels?.qqbot ?? {};
8843
+ if (accountId === DEFAULT_ACCOUNT_ID) {
8844
+ return {
8845
+ ...params.cfg,
8846
+ channels: {
8847
+ ...params.cfg.channels,
8848
+ qqbot: { ...existing, ...params.config, enabled: true }
8849
+ }
8850
+ };
8851
+ }
8852
+ const accounts = existing.accounts ?? {};
8637
8853
  return {
8638
8854
  ...params.cfg,
8639
8855
  channels: {
8640
8856
  ...params.cfg.channels,
8641
8857
  qqbot: {
8642
8858
  ...existing,
8643
- enabled: true
8859
+ accounts: {
8860
+ ...accounts,
8861
+ [accountId]: { ...accounts[accountId], ...params.config, enabled: true }
8862
+ }
8644
8863
  }
8645
8864
  }
8646
8865
  };
@@ -8667,8 +8886,8 @@ var qqbotPlugin = {
8667
8886
  accountId: ctx.accountId
8668
8887
  });
8669
8888
  },
8670
- stopAccount: async (_ctx) => {
8671
- stopQQBotMonitor();
8889
+ stopAccount: async (ctx) => {
8890
+ stopQQBotMonitorForAccount(ctx.accountId);
8672
8891
  },
8673
8892
  getStatus: () => ({ connected: true })
8674
8893
  }
@@ -8684,6 +8903,8 @@ var plugin = {
8684
8903
  additionalProperties: false,
8685
8904
  properties: {
8686
8905
  enabled: { type: "boolean" },
8906
+ name: { type: "string" },
8907
+ defaultAccount: { type: "string" },
8687
8908
  appId: { type: "string" },
8688
8909
  clientSecret: { type: "string" },
8689
8910
  asr: {
@@ -8704,7 +8925,43 @@ var plugin = {
8704
8925
  groupAllowFrom: { type: "array", items: { type: "string" } },
8705
8926
  historyLimit: { type: "integer", minimum: 0 },
8706
8927
  textChunkLimit: { type: "integer", minimum: 1 },
8707
- replyFinalOnly: { type: "boolean" }
8928
+ replyFinalOnly: { type: "boolean" },
8929
+ maxFileSizeMB: { type: "number" },
8930
+ mediaTimeoutMs: { type: "number" },
8931
+ accounts: {
8932
+ type: "object",
8933
+ additionalProperties: {
8934
+ type: "object",
8935
+ additionalProperties: false,
8936
+ properties: {
8937
+ name: { type: "string" },
8938
+ enabled: { type: "boolean" },
8939
+ appId: { type: "string" },
8940
+ clientSecret: { type: "string" },
8941
+ asr: {
8942
+ type: "object",
8943
+ additionalProperties: false,
8944
+ properties: {
8945
+ enabled: { type: "boolean" },
8946
+ appId: { type: "string" },
8947
+ secretId: { type: "string" },
8948
+ secretKey: { type: "string" }
8949
+ }
8950
+ },
8951
+ markdownSupport: { type: "boolean" },
8952
+ dmPolicy: { type: "string", enum: ["open", "pairing", "allowlist"] },
8953
+ groupPolicy: { type: "string", enum: ["open", "allowlist", "disabled"] },
8954
+ requireMention: { type: "boolean" },
8955
+ allowFrom: { type: "array", items: { type: "string" } },
8956
+ groupAllowFrom: { type: "array", items: { type: "string" } },
8957
+ historyLimit: { type: "integer", minimum: 0 },
8958
+ textChunkLimit: { type: "integer", minimum: 1 },
8959
+ replyFinalOnly: { type: "boolean" },
8960
+ maxFileSizeMB: { type: "number" },
8961
+ mediaTimeoutMs: { type: "number" }
8962
+ }
8963
+ }
8964
+ }
8708
8965
  }
8709
8966
  },
8710
8967
  register(api) {