@openclaw/qqbot 2026.6.11 → 2026.7.1-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/api.js CHANGED
@@ -1,8 +1,8 @@
1
- import { t as qqbotPlugin } from "./channel-DvWrqcZB.js";
1
+ import { t as qqbotPlugin } from "./channel-Bh0MrYvU.js";
2
2
  import { a as resolveQQBotAccount, i as resolveDefaultQQBotAccountId, n as applyQQBotAccountConfig, r as listQQBotAccountIds, t as DEFAULT_ACCOUNT_ID } from "./config-C1qZbh0K.js";
3
3
  import { t as qqbotSetupPlugin } from "./channel.setup-CldhUlTO.js";
4
- import { o as getFrameworkCommands } from "./group-CvVXke_k.js";
5
- import { n as registerRemindTool, r as registerChannelTool, t as registerQQBotTools } from "./tools-CeUI9pG-.js";
6
- import { t as registerQQBotFull } from "./channel-entry-BGXFRmbg.js";
7
- import { C as getMessageReplyConfig, D as OUTBOUND_ERROR_CODES, E as DEFAULT_MEDIA_SEND_ERROR, O as setOutboundAudioPort, S as checkMessageReplyLimit, T as recordMessageReply, _ as sendVideoMsg, a as sendText, b as resolveUserFacingMediaError, f as buildMediaTarget, g as sendPhoto, h as sendDocument, i as sendProactiveMessage, m as resolveOutboundMediaPath, n as sendCronMessage, p as parseTarget, r as sendMedia, v as sendVoice, w as getMessageReplyStats, x as MESSAGE_REPLY_LIMIT } from "./outbound-BQcPCo-z.js";
4
+ import { o as getFrameworkCommands } from "./group-DQQsZ6Y0.js";
5
+ import { C as getMessageReplyConfig, D as OUTBOUND_ERROR_CODES, E as DEFAULT_MEDIA_SEND_ERROR, O as setOutboundAudioPort, S as checkMessageReplyLimit, T as recordMessageReply, _ as sendVideoMsg, a as sendText, b as resolveUserFacingMediaError, f as buildMediaTarget, g as sendPhoto, h as sendDocument, i as sendProactiveMessage, m as resolveOutboundMediaPath, n as sendCronMessage, p as parseTarget, r as sendMedia, v as sendVoice, w as getMessageReplyStats, x as MESSAGE_REPLY_LIMIT } from "./outbound-Dfa2uOEp.js";
6
+ import { n as registerRemindTool, r as registerChannelTool, t as registerQQBotTools } from "./tools-BvXe7bTq.js";
7
+ import { t as registerQQBotFull } from "./channel-entry-LMtNY-J6.js";
8
8
  export { DEFAULT_ACCOUNT_ID, DEFAULT_MEDIA_SEND_ERROR, MESSAGE_REPLY_LIMIT, OUTBOUND_ERROR_CODES, applyQQBotAccountConfig, buildMediaTarget, checkMessageReplyLimit, getFrameworkCommands, getMessageReplyConfig, getMessageReplyStats, listQQBotAccountIds, parseTarget, qqbotPlugin, qqbotSetupPlugin, recordMessageReply, registerChannelTool, registerQQBotFull, registerQQBotTools, registerRemindTool, resolveDefaultQQBotAccountId, resolveOutboundMediaPath, resolveQQBotAccount, resolveUserFacingMediaError, sendCronMessage, sendDocument, sendMedia, sendPhoto, sendProactiveMessage, sendText, sendVideoMsg, sendVoice, setOutboundAudioPort };
@@ -1,9 +1,9 @@
1
1
  import { t as asOptionalObjectRecord } from "./string-normalize-R_0cKO7Q.js";
2
2
  import { a as qqbotSetupAdapterShared, i as qqbotMeta, n as qqbotSetupWizard, o as ensurePlatformAdapter, r as qqbotConfigAdapter, s as getBridgeLogger, t as qqbotChannelConfigSchema } from "./config-schema-gMW9zKvQ.js";
3
3
  import { a as resolveQQBotAccount, n as applyQQBotAccountConfig, r as listQQBotAccountIds, t as DEFAULT_ACCOUNT_ID$1 } from "./config-C1qZbh0K.js";
4
- import { t as getQQBotRuntime } from "./runtime-D1hn91-s.js";
5
- import { n as normalizeCompatibilityConfig, t as legacyConfigRules } from "./doctor-contract-BYTS8-ia.js";
6
- import { n as normalizeTarget, s as getQQBotDataPath, t as looksLikeQQBotTarget } from "./target-parser-BdCUmxK7.js";
4
+ import { t as getQQBotRuntime } from "./runtime-BL0xZzWe.js";
5
+ import { n as legacyConfigRules, r as normalizeCompatibilityConfig, t as buildQQBotStateKey } from "./state-keys-CQKlAFyo.js";
6
+ import { n as normalizeTarget, t as looksLikeQQBotTarget } from "./target-parser-DagYRQkP.js";
7
7
  import { getExecApprovalReplyMetadata } from "openclaw/plugin-sdk/approval-runtime";
8
8
  import { createMessageReceiptFromOutboundResults, defineChannelMessageAdapter } from "openclaw/plugin-sdk/channel-outbound";
9
9
  import { sanitizeAssistantVisibleText } from "openclaw/plugin-sdk/text-chunking";
@@ -14,11 +14,6 @@ import { normalizeLowercaseStringOrEmpty, normalizeOptionalString } from "opencl
14
14
  import { markImplicitSameChatApprovalAuthorization, resolveApprovalApprovers } from "openclaw/plugin-sdk/approval-auth-runtime";
15
15
  import { createChannelExecApprovalProfile, isChannelExecApprovalClientEnabledFromConfig, matchesApprovalRequestFilters } from "openclaw/plugin-sdk/approval-client-runtime";
16
16
  import { normalizeAccountId } from "openclaw/plugin-sdk/routing";
17
- import fs from "node:fs";
18
- import crypto from "node:crypto";
19
- import path from "node:path";
20
- import { loadJsonFile } from "openclaw/plugin-sdk/json-store";
21
- import { resolveStateDir } from "openclaw/plugin-sdk/state-paths";
22
17
  import { resolveChannelGroupToolsPolicy } from "openclaw/plugin-sdk/channel-policy";
23
18
  //#region extensions/qqbot/src/engine/approval/index.ts
24
19
  function buildExecApprovalText(request) {
@@ -370,7 +365,7 @@ function createQQBotApprovalCapability() {
370
365
  },
371
366
  load: async () => {
372
367
  ensurePlatformAdapter();
373
- return (await import("./handler-runtime--OTMkFWa.js")).qqbotApprovalNativeRuntime;
368
+ return (await import("./handler-runtime-BQ72YPsH.js")).qqbotApprovalNativeRuntime;
374
369
  }
375
370
  })
376
371
  });
@@ -412,41 +407,6 @@ const qqbotDoctor = {
412
407
  normalizeCompatibilityConfig
413
408
  };
414
409
  //#endregion
415
- //#region extensions/qqbot/src/engine/utils/data-paths.ts
416
- /**
417
- * Centralised filename helpers for persisted QQBot state.
418
- *
419
- * Every persistence module routes file paths through these helpers so the
420
- * naming convention stays in sync and legacy migrations are handled
421
- * consistently.
422
- *
423
- * Key design decisions:
424
- * - Credential backup is keyed only by `accountId` because recovery runs
425
- * exactly when the appId is missing from config.
426
- */
427
- /**
428
- * Normalise an identifier so it is safe to embed in a filename.
429
- * Keeps alphanumerics, dot, underscore, dash; everything else becomes `_`.
430
- */
431
- function safeName$1(id) {
432
- return id.replace(/[^a-zA-Z0-9._-]/g, "_");
433
- }
434
- function getCredentialBackupRoot() {
435
- return path.join(resolveStateDir(process.env), "qqbot", "data");
436
- }
437
- /**
438
- * Per-accountId credential backup file. Not keyed by appId because the
439
- * whole point of this file is to recover credentials when appId is
440
- * missing from the live config.
441
- */
442
- function getCredentialBackupFile(accountId) {
443
- return path.join(getCredentialBackupRoot(), `credential-backup-${safeName$1(accountId)}.json`);
444
- }
445
- /** Legacy single-file credential backup (pre-multi-account-isolation). */
446
- function getLegacyCredentialBackupFile() {
447
- return path.join(getCredentialBackupRoot(), "credential-backup.json");
448
- }
449
- //#endregion
450
410
  //#region extensions/qqbot/src/engine/utils/sqlite-state.ts
451
411
  function resolveStoreEnv(options) {
452
412
  if (!options.stateDir) return options.env;
@@ -463,9 +423,6 @@ function openQQBotSyncKeyedStore(options) {
463
423
  ...resolveStoreEnv(options) ? { env: resolveStoreEnv(options) } : {}
464
424
  });
465
425
  }
466
- function buildQQBotStateKey(...parts) {
467
- return crypto.createHash("sha256").update(JSON.stringify(parts)).digest("hex");
468
- }
469
426
  //#endregion
470
427
  //#region extensions/qqbot/src/engine/config/credential-backup.ts
471
428
  /**
@@ -481,8 +438,8 @@ function buildQQBotStateKey(...parts) {
481
438
  * - During plugin startup, if the live config has an empty appId or
482
439
  * secret, the gateway consults the backup and restores the values
483
440
  * via the config mutation API.
484
- * - Legacy JSON backups are imported on first read, then removed after
485
- * SQLite has the canonical copy.
441
+ * - Legacy JSON backups are imported by `openclaw doctor --fix`, not by
442
+ * runtime startup.
486
443
  *
487
444
  * Safety notes:
488
445
  * - Only restore when credentials are **actually empty** — never
@@ -498,53 +455,16 @@ function createCredentialBackupStore() {
498
455
  maxEntries: MAX_CREDENTIAL_BACKUPS
499
456
  });
500
457
  }
501
- function safeName(id) {
502
- return id.replace(/[^a-zA-Z0-9._-]/g, "_");
503
- }
504
- function getLegacyOsHomeCredentialBackupFile(accountId) {
505
- return path.join(getQQBotDataPath("data"), `credential-backup-${safeName(accountId)}.json`);
506
- }
507
- function getLegacyOsHomeCredentialBackupFileWithoutAccount() {
508
- return path.join(getQQBotDataPath("data"), "credential-backup.json");
509
- }
510
458
  function credentialBackupKey(accountId) {
511
459
  return buildQQBotStateKey("credential-backup", accountId);
512
460
  }
513
461
  function isUsableBackup(data) {
514
462
  return Boolean(data?.accountId && data.appId && data.clientSecret);
515
463
  }
516
- function loadUsableBackupFromFile(filePath) {
517
- const data = loadJsonFile(filePath);
518
- return isUsableBackup(data) ? data : null;
519
- }
520
- function removeFileQuietly(filePath) {
521
- try {
522
- fs.unlinkSync(filePath);
523
- } catch {}
524
- }
525
- function findLegacyBackup(accountId) {
526
- const candidates = accountId ? [
527
- getCredentialBackupFile(accountId),
528
- getLegacyCredentialBackupFile(),
529
- getLegacyOsHomeCredentialBackupFile(accountId),
530
- getLegacyOsHomeCredentialBackupFileWithoutAccount()
531
- ] : [getLegacyCredentialBackupFile(), getLegacyOsHomeCredentialBackupFileWithoutAccount()];
532
- for (const filePath of candidates) {
533
- const data = loadUsableBackupFromFile(filePath);
534
- if (!data) continue;
535
- if (accountId && data.accountId !== accountId) continue;
536
- return {
537
- data,
538
- filePath
539
- };
540
- }
541
- return null;
542
- }
543
464
  /** Persist a credential snapshot (called once gateway reaches READY). */
544
465
  function saveCredentialBackup(accountId, appId, clientSecret) {
545
466
  if (!appId || !clientSecret) return;
546
467
  try {
547
- const backupPath = getCredentialBackupFile(accountId);
548
468
  const data = {
549
469
  accountId,
550
470
  appId,
@@ -552,14 +472,13 @@ function saveCredentialBackup(accountId, appId, clientSecret) {
552
472
  savedAt: (/* @__PURE__ */ new Date()).toISOString()
553
473
  };
554
474
  createCredentialBackupStore().register(credentialBackupKey(accountId), data);
555
- removeFileQuietly(backupPath);
556
475
  } catch {}
557
476
  }
558
477
  /**
559
478
  * Load a credential snapshot for `accountId`.
560
479
  *
561
- * Consults SQLite first; falls back to shipped JSON backups and imports
562
- * them when the embedded `accountId` matches the request.
480
+ * Reads SQLite only. Legacy JSON backup import is owned by doctor/setup
481
+ * migration so runtime startup stays canonical-state-only.
563
482
  */
564
483
  function loadCredentialBackup(accountId) {
565
484
  try {
@@ -567,12 +486,6 @@ function loadCredentialBackup(accountId) {
567
486
  const data = createCredentialBackupStore().lookup(credentialBackupKey(accountId));
568
487
  if (isUsableBackup(data)) return data;
569
488
  }
570
- const legacy = findLegacyBackup(accountId);
571
- if (legacy) {
572
- createCredentialBackupStore().register(credentialBackupKey(legacy.data.accountId), legacy.data);
573
- removeFileQuietly(legacy.filePath);
574
- return legacy.data;
575
- }
576
489
  } catch {}
577
490
  return null;
578
491
  }
@@ -934,11 +847,32 @@ function isTableSeparatorLine(line) {
934
847
  const cells = splitTableCells(line);
935
848
  return cells.length > 0 && cells.every((cell) => /^:?-+:?$/.test(cell.trim()));
936
849
  }
850
+ function splitTableRowCells(inner) {
851
+ const cells = [];
852
+ let current = "";
853
+ for (let i = 0; i < inner.length; i++) {
854
+ const char = inner[i];
855
+ if (char === "\\" && i + 1 < inner.length) {
856
+ const next = inner[i + 1];
857
+ current += next === "|" || next === "\\" ? next : `\\${next}`;
858
+ i++;
859
+ continue;
860
+ }
861
+ if (char === "|") {
862
+ cells.push(current);
863
+ current = "";
864
+ continue;
865
+ }
866
+ current += char;
867
+ }
868
+ cells.push(current);
869
+ return cells;
870
+ }
937
871
  function splitTableCells(line) {
938
- return line.trim().slice(1, -1).split("|").map((cell) => cell.trim());
872
+ return splitTableRowCells(line.trim().slice(1, -1)).map((cell) => cell.trim());
939
873
  }
940
874
  function splitPartialTableCells(line) {
941
- return line.trim().replace(/^\|/, "").split("|").map((cell) => cell.trim()).filter((cell) => cell.length > 0);
875
+ return splitTableRowCells(line.trim().replace(/^\|/, "")).map((cell) => cell.trim()).filter((cell) => cell.length > 0);
942
876
  }
943
877
  function mergeRowFragments(pending, next) {
944
878
  return pending ? `${pending}${next}` : next;
@@ -1066,12 +1000,12 @@ function resolveQQBotGroupToolPolicy(params) {
1066
1000
  //#region extensions/qqbot/src/channel.ts
1067
1001
  let gatewayModulePromise;
1068
1002
  function loadGatewayModule() {
1069
- gatewayModulePromise ??= import("./gateway-CCkrdgaK.js");
1003
+ gatewayModulePromise ??= import("./gateway-BgfQAYxB.js");
1070
1004
  return gatewayModulePromise;
1071
1005
  }
1072
1006
  let outboundMessagingModulePromise;
1073
1007
  function loadOutboundMessagingModule() {
1074
- outboundMessagingModulePromise ??= import("./outbound-BQcPCo-z.js").then((n) => n.t);
1008
+ outboundMessagingModulePromise ??= import("./outbound-Dfa2uOEp.js").then((n) => n.t);
1075
1009
  return outboundMessagingModulePromise;
1076
1010
  }
1077
1011
  function createQQBotSendReceipt(params) {
@@ -1349,4 +1283,4 @@ const qqbotPlugin = {
1349
1283
  }
1350
1284
  };
1351
1285
  //#endregion
1352
- export { toGatewayAccount as a, matchesQQBotApprovalAccount as c, buildApprovalKeyboard as d, buildExecApprovalText as f, resolveApprovalTarget as h, openQQBotSyncKeyedStore as i, resolveQQBotExecApprovalConfig as l, parseApprovalButtonData as m, createQQBotMarkdownChunker as n, authorizeQQBotApprovalAction as o, buildPluginApprovalText as p, buildQQBotStateKey as r, isQQBotExecApprovalClientEnabled as s, qqbotPlugin as t, shouldHandleQQBotExecApprovalRequest as u };
1286
+ export { authorizeQQBotApprovalAction as a, resolveQQBotExecApprovalConfig as c, buildExecApprovalText as d, buildPluginApprovalText as f, toGatewayAccount as i, shouldHandleQQBotExecApprovalRequest as l, resolveApprovalTarget as m, createQQBotMarkdownChunker as n, isQQBotExecApprovalClientEnabled as o, parseApprovalButtonData as p, openQQBotSyncKeyedStore as r, matchesQQBotApprovalAccount as s, qqbotPlugin as t, buildApprovalKeyboard as u };
@@ -1,7 +1,7 @@
1
1
  import { a as resolveQQBotAccount } from "./config-C1qZbh0K.js";
2
- import { d as PRIVATE_CHAT_ONLY_TEXT, n as resolveGroupCommandLevelFromAccountConfig, o as getFrameworkCommands } from "./group-CvVXke_k.js";
3
- import { t as registerQQBotTools } from "./tools-CeUI9pG-.js";
4
- import { h as sendDocument } from "./outbound-BQcPCo-z.js";
2
+ import { d as PRIVATE_CHAT_ONLY_TEXT, n as resolveGroupCommandLevelFromAccountConfig, o as getFrameworkCommands } from "./group-DQQsZ6Y0.js";
3
+ import { h as sendDocument } from "./outbound-Dfa2uOEp.js";
4
+ import { t as registerQQBotTools } from "./tools-BvXe7bTq.js";
5
5
  //#region extensions/qqbot/src/bridge/commands/framework-context-adapter.ts
6
6
  /**
7
7
  * Default queue snapshot used for framework-registered commands.
@@ -1,2 +1,2 @@
1
- import { t as registerQQBotFull } from "./channel-entry-BGXFRmbg.js";
1
+ import { t as registerQQBotFull } from "./channel-entry-LMtNY-J6.js";
2
2
  export { registerQQBotFull };
@@ -1,2 +1,2 @@
1
- import { t as qqbotPlugin } from "./channel-DvWrqcZB.js";
1
+ import { t as qqbotPlugin } from "./channel-Bh0MrYvU.js";
2
2
  export { qqbotPlugin };
@@ -1,2 +1,168 @@
1
- import { n as normalizeCompatibilityConfig, t as legacyConfigRules } from "./doctor-contract-BYTS8-ia.js";
2
- export { legacyConfigRules, normalizeCompatibilityConfig };
1
+ import { n as legacyConfigRules, r as normalizeCompatibilityConfig, t as buildQQBotStateKey } from "./state-keys-CQKlAFyo.js";
2
+ import path from "node:path";
3
+ import fs from "node:fs/promises";
4
+ //#region extensions/qqbot/src/state-migrations.ts
5
+ const CREDENTIAL_BACKUPS_NAMESPACE = "credential-backups";
6
+ const MAX_CREDENTIAL_BACKUPS = 1e3;
7
+ function safeName(id) {
8
+ return id.replace(/[^a-zA-Z0-9._-]/g, "_");
9
+ }
10
+ async function fileExists(filePath) {
11
+ try {
12
+ return (await fs.lstat(filePath)).isFile();
13
+ } catch {
14
+ return false;
15
+ }
16
+ }
17
+ async function readCredentialBackup(filePath) {
18
+ try {
19
+ const parsed = JSON.parse(await fs.readFile(filePath, "utf8"));
20
+ if (typeof parsed.accountId !== "string" || typeof parsed.appId !== "string" || typeof parsed.clientSecret !== "string" || !parsed.accountId || !parsed.appId || !parsed.clientSecret) return null;
21
+ return {
22
+ accountId: parsed.accountId,
23
+ appId: parsed.appId,
24
+ clientSecret: parsed.clientSecret,
25
+ savedAt: typeof parsed.savedAt === "string" && parsed.savedAt ? parsed.savedAt : (/* @__PURE__ */ new Date(0)).toISOString()
26
+ };
27
+ } catch {
28
+ return null;
29
+ }
30
+ }
31
+ function credentialBackupKey(accountId) {
32
+ return buildQQBotStateKey("credential-backup", accountId);
33
+ }
34
+ async function credentialBackupCandidates(stateDir) {
35
+ const dataDir = path.join(stateDir, "qqbot", "data");
36
+ const accountFiles = [];
37
+ try {
38
+ for (const entry of await fs.readdir(dataDir, { withFileTypes: true })) if (entry.isFile() && entry.name.startsWith("credential-backup-") && entry.name.endsWith(".json")) accountFiles.push({
39
+ sourcePath: path.join(dataDir, entry.name),
40
+ expectedSafeAccountId: entry.name.slice(18, -5)
41
+ });
42
+ } catch {}
43
+ accountFiles.sort((left, right) => left.sourcePath.localeCompare(right.sourcePath));
44
+ const singlePath = path.join(dataDir, "credential-backup.json");
45
+ return await fileExists(singlePath) ? [...accountFiles, { sourcePath: singlePath }] : accountFiles;
46
+ }
47
+ async function readLegacyCredentialBackups(stateDir) {
48
+ const backups = [];
49
+ for (const candidate of await credentialBackupCandidates(stateDir)) {
50
+ const value = await readCredentialBackup(candidate.sourcePath);
51
+ if (!value || candidate.expectedSafeAccountId !== void 0 && safeName(value.accountId) !== candidate.expectedSafeAccountId) continue;
52
+ backups.push({
53
+ sourcePath: candidate.sourcePath,
54
+ key: credentialBackupKey(value.accountId),
55
+ value
56
+ });
57
+ }
58
+ return backups;
59
+ }
60
+ async function archiveLegacySource(params) {
61
+ const archivedPath = `${params.sourcePath}.migrated`;
62
+ if (await fileExists(archivedPath)) {
63
+ params.warnings.push(`Left QQBot credential backup in place because ${archivedPath} already exists`);
64
+ return;
65
+ }
66
+ try {
67
+ await fs.chmod(params.sourcePath, 384);
68
+ } catch (err) {
69
+ params.warnings.push(`Failed securing QQBot credential backup legacy source: ${String(err)}`);
70
+ return;
71
+ }
72
+ try {
73
+ await fs.rename(params.sourcePath, archivedPath);
74
+ try {
75
+ await fs.chmod(archivedPath, 384);
76
+ } catch (err) {
77
+ params.warnings.push(`Failed securing archived QQBot credential backup legacy source: ${String(err)}`);
78
+ }
79
+ params.changes.push(`Archived QQBot credential backup legacy source -> ${archivedPath}`);
80
+ } catch (err) {
81
+ params.warnings.push(`Failed archiving QQBot credential backup: ${String(err)}`);
82
+ }
83
+ }
84
+ function sameCredentialBackup(left, right) {
85
+ return left?.accountId === right.accountId && left.appId === right.appId && left.clientSecret === right.clientSecret && left.savedAt === right.savedAt;
86
+ }
87
+ async function rollbackCredentialImports(store, inserted, existing) {
88
+ for (const [key, value] of [...inserted].toReversed()) if (sameCredentialBackup(await store.lookup(key), value)) await store.delete(key);
89
+ for (const [key, value] of existing) if (await store.lookup(key) === void 0) await store.registerIfAbsent(key, value);
90
+ }
91
+ function findMissingKey(expected, actual) {
92
+ for (const key of expected) if (!actual.has(key)) return key;
93
+ return null;
94
+ }
95
+ const stateMigrations = [{
96
+ id: "qqbot-credential-backups-json-to-plugin-state",
97
+ label: "QQBot credential backups",
98
+ async detectLegacyState(params) {
99
+ const backups = await readLegacyCredentialBackups(params.stateDir);
100
+ if (backups.length === 0) return null;
101
+ return { preview: [`- QQBot credential backups: ${backups.length} ${backups.length === 1 ? "file" : "files"} -> plugin state (${CREDENTIAL_BACKUPS_NAMESPACE})`] };
102
+ },
103
+ async migrateLegacyState(params) {
104
+ const changes = [];
105
+ const warnings = [];
106
+ const backups = await readLegacyCredentialBackups(params.stateDir);
107
+ if (backups.length === 0) return {
108
+ changes,
109
+ warnings
110
+ };
111
+ const selectedByKey = /* @__PURE__ */ new Map();
112
+ for (const backup of backups) if (!selectedByKey.has(backup.key)) selectedByKey.set(backup.key, backup);
113
+ const store = params.context.openPluginStateKeyedStore({
114
+ namespace: CREDENTIAL_BACKUPS_NAMESPACE,
115
+ maxEntries: MAX_CREDENTIAL_BACKUPS
116
+ });
117
+ const existingEntries = await store.entries();
118
+ const existingValues = new Map(existingEntries.map((entry) => [entry.key, entry.value]));
119
+ const existingKeys = new Set(existingValues.keys());
120
+ const missing = [...selectedByKey.values()].filter((backup) => !existingKeys.has(backup.key));
121
+ const available = MAX_CREDENTIAL_BACKUPS - existingKeys.size;
122
+ if (missing.length > available) {
123
+ warnings.push(`Skipped QQBot credential backup migration because plugin state has room for ${available} of ${missing.length} missing entries; left legacy sources in place`);
124
+ return {
125
+ changes,
126
+ warnings
127
+ };
128
+ }
129
+ const expectedKeys = new Set(existingKeys);
130
+ const inserted = /* @__PURE__ */ new Map();
131
+ for (const backup of missing) try {
132
+ if (await store.registerIfAbsent(backup.key, backup.value)) inserted.set(backup.key, backup.value);
133
+ const missingKey = findMissingKey(new Set(expectedKeys).add(backup.key), new Set((await store.entries()).map((entry) => entry.key)));
134
+ if (missingKey) {
135
+ await rollbackCredentialImports(store, inserted, existingValues);
136
+ warnings.push(`Stopped QQBot credential backup migration because plugin state capacity evicted ${missingKey}; restored credential state and left legacy sources in place`);
137
+ return {
138
+ changes,
139
+ warnings
140
+ };
141
+ }
142
+ expectedKeys.add(backup.key);
143
+ } catch (err) {
144
+ try {
145
+ await rollbackCredentialImports(store, inserted, existingValues);
146
+ } catch (rollbackErr) {
147
+ warnings.push(`Failed restoring QQBot credential state after migration error: ${String(rollbackErr)}`);
148
+ }
149
+ warnings.push(`Failed migrating QQBot credential backup: ${String(err)}; left legacy sources in place`);
150
+ return {
151
+ changes,
152
+ warnings
153
+ };
154
+ }
155
+ if (inserted.size > 0) changes.push(`Migrated ${inserted.size} QQBot credential ${inserted.size === 1 ? "backup" : "backups"} -> plugin state`);
156
+ for (const backup of backups) await archiveLegacySource({
157
+ sourcePath: backup.sourcePath,
158
+ changes,
159
+ warnings
160
+ });
161
+ return {
162
+ changes,
163
+ warnings
164
+ };
165
+ }
166
+ }];
167
+ //#endregion
168
+ export { legacyConfigRules, normalizeCompatibilityConfig, stateMigrations };