@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 +5 -5
- package/dist/{channel-DvWrqcZB.js → channel-Bh0MrYvU.js} +34 -100
- package/dist/{channel-entry-BGXFRmbg.js → channel-entry-LMtNY-J6.js} +3 -3
- package/dist/channel-entry-api.js +1 -1
- package/dist/channel-plugin-api.js +1 -1
- package/dist/doctor-contract-api.js +168 -2
- package/dist/{gateway-CCkrdgaK.js → gateway-BgfQAYxB.js} +11 -162
- package/dist/{group-CvVXke_k.js → group-DQQsZ6Y0.js} +2 -2
- package/dist/{handler-runtime--OTMkFWa.js → handler-runtime-BQ72YPsH.js} +2 -2
- package/dist/{outbound-BQcPCo-z.js → outbound-Dfa2uOEp.js} +216 -4
- package/dist/{runtime-D1hn91-s.js → runtime-BL0xZzWe.js} +1 -1
- package/dist/runtime-api.js +1 -1
- package/dist/{sender-CRZ2af7P.js → sender-BHWpE_yH.js} +2 -2
- package/dist/{doctor-contract-BYTS8-ia.js → state-keys-CQKlAFyo.js} +7 -1
- package/dist/target-parser-DagYRQkP.js +71 -0
- package/dist/{tools-CeUI9pG-.js → tools-BvXe7bTq.js} +58 -13
- package/dist/tools-api.js +1 -1
- package/npm-shrinkwrap.json +3 -3
- package/package.json +4 -4
- package/skills/qqbot-channel/SKILL.md +54 -41
- package/skills/qqbot-channel/references/api_references.md +16 -8
- package/skills/qqbot-media/SKILL.md +6 -3
- package/skills/qqbot-remind/SKILL.md +15 -14
- package/dist/target-parser-BdCUmxK7.js +0 -285
package/dist/api.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { t as qqbotPlugin } from "./channel-
|
|
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-
|
|
5
|
-
import { n as
|
|
6
|
-
import { t as
|
|
7
|
-
import {
|
|
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-
|
|
5
|
-
import { n as normalizeCompatibilityConfig, t as
|
|
6
|
-
import { n as normalizeTarget,
|
|
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
|
|
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
|
|
485
|
-
*
|
|
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
|
-
*
|
|
562
|
-
*
|
|
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)
|
|
872
|
+
return splitTableRowCells(line.trim().slice(1, -1)).map((cell) => cell.trim());
|
|
939
873
|
}
|
|
940
874
|
function splitPartialTableCells(line) {
|
|
941
|
-
return line.trim().replace(/^\|/, "")
|
|
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-
|
|
1003
|
+
gatewayModulePromise ??= import("./gateway-BgfQAYxB.js");
|
|
1070
1004
|
return gatewayModulePromise;
|
|
1071
1005
|
}
|
|
1072
1006
|
let outboundMessagingModulePromise;
|
|
1073
1007
|
function loadOutboundMessagingModule() {
|
|
1074
|
-
outboundMessagingModulePromise ??= import("./outbound-
|
|
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 {
|
|
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-
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
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-
|
|
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-
|
|
1
|
+
import { t as qqbotPlugin } from "./channel-Bh0MrYvU.js";
|
|
2
2
|
export { qqbotPlugin };
|
|
@@ -1,2 +1,168 @@
|
|
|
1
|
-
import { n as normalizeCompatibilityConfig, t as
|
|
2
|
-
|
|
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 };
|