@tencent-connect/openclaw-qqbot 1.6.4-alpha.1 → 1.6.4-alpha.11

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.
@@ -4,6 +4,7 @@ import { sendText, sendMedia } from "./outbound.js";
4
4
  import { startGateway } from "./gateway.js";
5
5
  import { qqbotOnboardingAdapter } from "./onboarding.js";
6
6
  import { getQQBotRuntime } from "./runtime.js";
7
+ import { saveCredentialBackup, loadCredentialBackup } from "./credential-backup.js";
7
8
  /** QQ Bot 单条消息文本长度上限 */
8
9
  export const TEXT_CHUNK_LIMIT = 5000;
9
10
  /**
@@ -218,7 +219,30 @@ export const qqbotPlugin = {
218
219
  },
219
220
  gateway: {
220
221
  startAccount: async (ctx) => {
221
- const { account, abortSignal, log, cfg } = ctx;
222
+ let { account } = ctx;
223
+ const { abortSignal, log, cfg } = ctx;
224
+ // 凭证恢复:如果 appId/secret 为空(热更新打断可能导致配置丢失),尝试从暂存文件恢复
225
+ if (!account.appId || !account.clientSecret) {
226
+ const backup = loadCredentialBackup(account.accountId);
227
+ if (backup) {
228
+ log?.info(`[qqbot:${account.accountId}] 配置中凭证为空,从暂存文件恢复 (appId=${backup.appId}, savedAt=${backup.savedAt})`);
229
+ try {
230
+ const runtime = getQQBotRuntime();
231
+ const restoredCfg = applyQQBotAccountConfig(cfg, account.accountId, {
232
+ appId: backup.appId,
233
+ clientSecret: backup.clientSecret,
234
+ });
235
+ const configApi = runtime.config;
236
+ await configApi.writeConfigFile(restoredCfg);
237
+ // 重新解析 account 以获取恢复后的值
238
+ account = resolveQQBotAccount(restoredCfg, account.accountId);
239
+ log?.info(`[qqbot:${account.accountId}] 凭证已恢复`);
240
+ }
241
+ catch (e) {
242
+ log?.error(`[qqbot:${account.accountId}] 凭证恢复失败: ${e}`);
243
+ }
244
+ }
245
+ }
222
246
  log?.info(`[qqbot:${account.accountId}] Starting gateway — appId=${account.appId}, enabled=${account.enabled}, name=${account.name ?? "unnamed"}`);
223
247
  console.log(`[qqbot:channel] startAccount: accountId=${account.accountId}, appId=${account.appId}, secretSource=${account.secretSource}`);
224
248
  await startGateway({
@@ -228,6 +252,8 @@ export const qqbotPlugin = {
228
252
  log,
229
253
  onReady: () => {
230
254
  log?.info(`[qqbot:${account.accountId}] Gateway ready`);
255
+ // 启动成功,保存凭证快照供后续恢复使用
256
+ saveCredentialBackup(account.accountId, account.appId, account.clientSecret);
231
257
  ctx.setStatus({
232
258
  ...ctx.getStatus(),
233
259
  running: true,
@@ -0,0 +1,31 @@
1
+ /**
2
+ * 凭证暂存与恢复
3
+ *
4
+ * 解决热更新被打断时 openclaw.json 中 appId/secret 丢失的问题。
5
+ *
6
+ * 原理:
7
+ * - 每次 gateway 成功启动后,把当前账户的 appId/secret 写入暂存文件
8
+ * - 插件启动时如果检测到配置中 appId/secret 为空,尝试从暂存文件恢复
9
+ * - 暂存文件存储在 ~/.openclaw/qqbot/data/ 下,不受插件目录替换影响
10
+ *
11
+ * 安全保障:
12
+ * - 只在 appId/secret **确实为空** 时才尝试恢复(不干扰正常配置变更)
13
+ * - 恢复后通过 openclaw 的 config API 写回配置文件,确保框架感知到变更
14
+ * - 暂存文件使用原子写入(先写 .tmp 再 rename)防止损坏
15
+ */
16
+ interface CredentialBackup {
17
+ accountId: string;
18
+ appId: string;
19
+ clientSecret: string;
20
+ savedAt: string;
21
+ }
22
+ /**
23
+ * 保存凭证快照到暂存文件(gateway 成功启动后调用)
24
+ */
25
+ export declare function saveCredentialBackup(accountId: string, appId: string, clientSecret: string): void;
26
+ /**
27
+ * 从暂存文件读取凭证(仅在配置为空时调用)
28
+ * 返回 null 表示无可用备份
29
+ */
30
+ export declare function loadCredentialBackup(accountId?: string): CredentialBackup | null;
31
+ export {};
@@ -0,0 +1,66 @@
1
+ /**
2
+ * 凭证暂存与恢复
3
+ *
4
+ * 解决热更新被打断时 openclaw.json 中 appId/secret 丢失的问题。
5
+ *
6
+ * 原理:
7
+ * - 每次 gateway 成功启动后,把当前账户的 appId/secret 写入暂存文件
8
+ * - 插件启动时如果检测到配置中 appId/secret 为空,尝试从暂存文件恢复
9
+ * - 暂存文件存储在 ~/.openclaw/qqbot/data/ 下,不受插件目录替换影响
10
+ *
11
+ * 安全保障:
12
+ * - 只在 appId/secret **确实为空** 时才尝试恢复(不干扰正常配置变更)
13
+ * - 恢复后通过 openclaw 的 config API 写回配置文件,确保框架感知到变更
14
+ * - 暂存文件使用原子写入(先写 .tmp 再 rename)防止损坏
15
+ */
16
+ import fs from "node:fs";
17
+ import path from "node:path";
18
+ import { getQQBotDataDir } from "./utils/platform.js";
19
+ const BACKUP_FILENAME = "credential-backup.json";
20
+ function getBackupPath() {
21
+ return path.join(getQQBotDataDir("data"), BACKUP_FILENAME);
22
+ }
23
+ /**
24
+ * 保存凭证快照到暂存文件(gateway 成功启动后调用)
25
+ */
26
+ export function saveCredentialBackup(accountId, appId, clientSecret) {
27
+ if (!appId || !clientSecret)
28
+ return; // 不保存空凭证
29
+ try {
30
+ const backupPath = getBackupPath();
31
+ const data = {
32
+ accountId,
33
+ appId,
34
+ clientSecret,
35
+ savedAt: new Date().toISOString(),
36
+ };
37
+ const tmpPath = backupPath + ".tmp";
38
+ fs.writeFileSync(tmpPath, JSON.stringify(data, null, 2) + "\n", "utf8");
39
+ fs.renameSync(tmpPath, backupPath);
40
+ }
41
+ catch {
42
+ // 非关键操作,静默忽略
43
+ }
44
+ }
45
+ /**
46
+ * 从暂存文件读取凭证(仅在配置为空时调用)
47
+ * 返回 null 表示无可用备份
48
+ */
49
+ export function loadCredentialBackup(accountId) {
50
+ try {
51
+ const backupPath = getBackupPath();
52
+ if (!fs.existsSync(backupPath))
53
+ return null;
54
+ const raw = fs.readFileSync(backupPath, "utf8");
55
+ const data = JSON.parse(raw);
56
+ if (!data.appId || !data.clientSecret)
57
+ return null;
58
+ // 如果指定了 accountId,校验是否匹配
59
+ if (accountId && data.accountId !== accountId)
60
+ return null;
61
+ return data;
62
+ }
63
+ catch {
64
+ return null;
65
+ }
66
+ }