koishi-plugin-echo-cave 1.29.9 → 1.29.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.
package/README.md CHANGED
@@ -23,7 +23,9 @@
23
23
  - 🎯 **按人随机抽取**:支持通过 `@用户` 或用户 ID 定向随机抽取该用户相关的回声洞,且不影响全局抽取权重
24
24
  - 🧭 **转发绑定体验优化**:转发消息关联用户的详细输入指南仅在用户首次成功完成选择前显示,选择完成或超时后会尝试撤回提示消息
25
25
  - 🤖 **特殊转发用户处理**:可配置在检测到转发记录中包含特殊用户 `1094950020` 时直接拒绝存储,或要求二次确认后再存储
26
- - 🛠️ **回声洞 ID 重排**:提供管理员维护命令,可在写入备份后安全重排现有回声洞 ID
26
+ - 🛠️ **回声洞 ID 重排**:提供管理员维护命令,可在写入备份后安全重排现有回声洞 public ID,而不抬高数据库内部自增主键
27
+ - ♻️ **回声洞备份恢复**:提供管理员维护命令,可从 ID 重排生成的 JSON 备份自动恢复数据库
28
+ - ⏰ **自动重排维护**:可选开启每日定时自动重排,并在执行时输出日志
27
29
  - ⚖️ **加权随机抽取**:根据消息被抽取次数动态调整抽取概率,被抽取次数越多,概率越低。使用公式:
28
30
  ```
29
31
  权重 = 1 / (1 + drawCount * α)
@@ -47,6 +49,7 @@
47
49
  | `cave.bind <id> <...userIds>` | 将用户绑定到特定 ID 的回声洞 | 所有人 |
48
50
  | `cave.rank [period]` | 查看回声洞排行榜,支持多种时间段 | 所有人 |
49
51
  | `cave.admin.reindex` | 安全重排所有现有回声洞 ID,并先写入备份 | 管理员(私聊) |
52
+ | `cave.admin.restore-reindex <backupPath>` | 从重排生成的 JSON 备份恢复回声洞数据 | 管理员(私聊) |
50
53
 
51
54
  ## 🚀 使用指南
52
55
 
@@ -79,6 +82,8 @@ npm install koishi-plugin-echo-cave
79
82
  | `autoBindSingleForwardUser` | boolean | `false` | 转发消息仅识别到一个用户时,是否默认自动绑定该用户 |
80
83
  | `forwardSpecialUserHandlingMode` | `'ignore' \| 'reject' \| 'confirm'` | `'ignore'` | 检测到转发消息中包含用户 `1094950020` 时的处理方式:忽略、提醒后拒绝存储、或提醒并要求确认后再存储 |
81
84
  | `alpha` | number | `0.2` | 加权随机抽取的调整因子,控制抽取次数对概率的影响程度,值越大影响越明显 |
85
+ | `enableAutoReindex` | boolean | `false` | 是否启用每日自动重整回声洞 public ID |
86
+ | `autoReindexTime` | string | `00:00` | 自动重整的每日执行时间,格式为 `HH:mm` |
82
87
 
83
88
  ## 📝 注意事项
84
89
 
@@ -93,8 +98,12 @@ npm install koishi-plugin-echo-cave
93
98
  - 转发消息可选择相关用户进行绑定,超时后自动跳过
94
99
  - 转发消息关联用户的详细示例只会在用户首次成功完成一次选择前显示;如果只是超时,则下次仍会显示
95
100
  - 可通过配置项禁用转发消息用户选择、开启单用户自动绑定,或配置检测到特殊用户 `1094950020` 时的处理模式
96
- - `cave.admin.reindex` 会先在 `logs/` 下写入备份文件,再执行 ID 重排;建议仅在单实例维护时段执行
101
+ - 当前运行时使用 v3 数据表:数据库内部自增主键与对外展示的回声洞 ID 已分离,因此重整不会再把后续插入的主键抬到很大
102
+ - 插件启动后会自动执行 v2 → v3 的 1:1 数据迁移,保留现有回声洞 ID,不会在迁移阶段重排编号
103
+ - `cave.admin.reindex` 会先在 `logs/` 下写入备份文件,再执行 public ID 重排;建议仅在单实例维护时段执行
97
104
  - 如果重排前备份写入失败,命令会直接终止,不会开始改写数据库
105
+ - 若开启 `enableAutoReindex`,插件会在 `autoReindexTime` 指定的每日时间自动执行同样的 public ID 重排,并输出日志
106
+ - 如果需要恢复,可执行 `cave.admin.restore-reindex <备份路径>` 读取对应的 JSON 备份并回写数据库
98
107
 
99
108
  ## 🤝 贡献指南
100
109
 
@@ -34,5 +34,7 @@ export interface Config {
34
34
  sendFailureSummaryAdminId?: string;
35
35
  sendFailureSummaryTime?: string;
36
36
  oversizedMediaCleanupMode?: OversizedMediaCleanupMode;
37
+ enableAutoReindex?: boolean;
38
+ autoReindexTime?: string;
37
39
  }
38
40
  export declare const Config: Schema<Config>;
@@ -0,0 +1,29 @@
1
+ import { Context } from 'koishi';
2
+ import { EchoCave } from '../index';
3
+ export declare const ACTIVE_CAVE_TABLE: "echo_cave_v3";
4
+ export interface CaveSnapshotRecord {
5
+ id: number;
6
+ channelId: string;
7
+ createTime: Date;
8
+ userId: string;
9
+ originUserId: string;
10
+ type: 'forward' | 'msg';
11
+ content: string;
12
+ relatedUsers: string[];
13
+ drawCount: number;
14
+ }
15
+ export interface CaveBackupRecord extends CaveSnapshotRecord {
16
+ entryId?: number;
17
+ }
18
+ export declare function toCaveSnapshotRecord<T extends CaveSnapshotRecord>(cave: T): CaveSnapshotRecord;
19
+ export declare function toCaveBackupRecord(cave: EchoCave | CaveSnapshotRecord): CaveBackupRecord;
20
+ export declare function getNextCavePublicId(ctx: Context): Promise<number>;
21
+ export declare function getCaveByPublicId(ctx: Context, id: number, channelId?: string): Promise<EchoCave>;
22
+ export declare function getCavesByPublicIds(ctx: Context, ids: number[]): Promise<EchoCave[]>;
23
+ export declare function removeCaveByEntryId(ctx: Context, entryId: number): Promise<void>;
24
+ export declare function createCaveRecord(ctx: Context, cave: CaveSnapshotRecord): Promise<EchoCave>;
25
+ export declare function updateCaveByEntryId(ctx: Context, entryId: number, data: Partial<Omit<CaveSnapshotRecord, 'id'>> & {
26
+ id?: number;
27
+ }): Promise<void>;
28
+ export declare function getAllCaves(ctx: Context): Promise<EchoCave[]>;
29
+ export declare function getCavesByChannel(ctx: Context, channelId: string): Promise<EchoCave[]>;
@@ -6,3 +6,5 @@ export declare function migrateLegacyLocalMedia(ctx: Context, session: Session,
6
6
  export declare function migrateMediaToS3(ctx: Context, session: Session, cfg: Config, keepLocalOption?: string): Promise<string>;
7
7
  export declare function inspectMediaRefsForMigration(ctx: Context, session: Session, cfg: Config, idRangesOption?: string): Promise<string>;
8
8
  export declare function reindexCaveIds(ctx: Context, session: Session, cfg: Config): Promise<string>;
9
+ export declare function restoreReindexBackup(ctx: Context, session: Session, cfg: Config, backupPathInput?: string): Promise<string>;
10
+ export declare function registerAutoReindexScheduler(ctx: Context, cfg: Config): void;
package/lib/index.cjs CHANGED
@@ -18017,9 +18017,9 @@ var require_dist_cjs47 = __commonJS({
18017
18017
  }
18018
18018
  });
18019
18019
 
18020
- // node_modules/.pnpm/@aws-sdk+client-s3@3.1027.0/node_modules/@aws-sdk/client-s3/dist-cjs/endpoint/ruleset.js
18020
+ // node_modules/.pnpm/@aws-sdk+client-s3@3.1029.0/node_modules/@aws-sdk/client-s3/dist-cjs/endpoint/ruleset.js
18021
18021
  var require_ruleset = __commonJS({
18022
- "node_modules/.pnpm/@aws-sdk+client-s3@3.1027.0/node_modules/@aws-sdk/client-s3/dist-cjs/endpoint/ruleset.js"(exports2) {
18022
+ "node_modules/.pnpm/@aws-sdk+client-s3@3.1029.0/node_modules/@aws-sdk/client-s3/dist-cjs/endpoint/ruleset.js"(exports2) {
18023
18023
  "use strict";
18024
18024
  Object.defineProperty(exports2, "__esModule", { value: true });
18025
18025
  exports2.ruleSet = void 0;
@@ -18219,9 +18219,9 @@ var require_ruleset = __commonJS({
18219
18219
  }
18220
18220
  });
18221
18221
 
18222
- // node_modules/.pnpm/@aws-sdk+client-s3@3.1027.0/node_modules/@aws-sdk/client-s3/dist-cjs/endpoint/endpointResolver.js
18222
+ // node_modules/.pnpm/@aws-sdk+client-s3@3.1029.0/node_modules/@aws-sdk/client-s3/dist-cjs/endpoint/endpointResolver.js
18223
18223
  var require_endpointResolver = __commonJS({
18224
- "node_modules/.pnpm/@aws-sdk+client-s3@3.1027.0/node_modules/@aws-sdk/client-s3/dist-cjs/endpoint/endpointResolver.js"(exports2) {
18224
+ "node_modules/.pnpm/@aws-sdk+client-s3@3.1029.0/node_modules/@aws-sdk/client-s3/dist-cjs/endpoint/endpointResolver.js"(exports2) {
18225
18225
  "use strict";
18226
18226
  Object.defineProperty(exports2, "__esModule", { value: true });
18227
18227
  exports2.defaultEndpointResolver = void 0;
@@ -18258,9 +18258,9 @@ var require_endpointResolver = __commonJS({
18258
18258
  }
18259
18259
  });
18260
18260
 
18261
- // node_modules/.pnpm/@aws-sdk+client-s3@3.1027.0/node_modules/@aws-sdk/client-s3/dist-cjs/auth/httpAuthSchemeProvider.js
18261
+ // node_modules/.pnpm/@aws-sdk+client-s3@3.1029.0/node_modules/@aws-sdk/client-s3/dist-cjs/auth/httpAuthSchemeProvider.js
18262
18262
  var require_httpAuthSchemeProvider = __commonJS({
18263
- "node_modules/.pnpm/@aws-sdk+client-s3@3.1027.0/node_modules/@aws-sdk/client-s3/dist-cjs/auth/httpAuthSchemeProvider.js"(exports2) {
18263
+ "node_modules/.pnpm/@aws-sdk+client-s3@3.1029.0/node_modules/@aws-sdk/client-s3/dist-cjs/auth/httpAuthSchemeProvider.js"(exports2) {
18264
18264
  "use strict";
18265
18265
  Object.defineProperty(exports2, "__esModule", { value: true });
18266
18266
  exports2.resolveHttpAuthSchemeConfig = exports2.defaultS3HttpAuthSchemeProvider = exports2.defaultS3HttpAuthSchemeParametersProvider = void 0;
@@ -18387,9 +18387,9 @@ var require_httpAuthSchemeProvider = __commonJS({
18387
18387
  }
18388
18388
  });
18389
18389
 
18390
- // node_modules/.pnpm/@aws-sdk+client-s3@3.1027.0/node_modules/@aws-sdk/client-s3/dist-cjs/models/S3ServiceException.js
18390
+ // node_modules/.pnpm/@aws-sdk+client-s3@3.1029.0/node_modules/@aws-sdk/client-s3/dist-cjs/models/S3ServiceException.js
18391
18391
  var require_S3ServiceException = __commonJS({
18392
- "node_modules/.pnpm/@aws-sdk+client-s3@3.1027.0/node_modules/@aws-sdk/client-s3/dist-cjs/models/S3ServiceException.js"(exports2) {
18392
+ "node_modules/.pnpm/@aws-sdk+client-s3@3.1029.0/node_modules/@aws-sdk/client-s3/dist-cjs/models/S3ServiceException.js"(exports2) {
18393
18393
  "use strict";
18394
18394
  Object.defineProperty(exports2, "__esModule", { value: true });
18395
18395
  exports2.S3ServiceException = exports2.__ServiceException = void 0;
@@ -18407,9 +18407,9 @@ var require_S3ServiceException = __commonJS({
18407
18407
  }
18408
18408
  });
18409
18409
 
18410
- // node_modules/.pnpm/@aws-sdk+client-s3@3.1027.0/node_modules/@aws-sdk/client-s3/dist-cjs/models/errors.js
18410
+ // node_modules/.pnpm/@aws-sdk+client-s3@3.1029.0/node_modules/@aws-sdk/client-s3/dist-cjs/models/errors.js
18411
18411
  var require_errors = __commonJS({
18412
- "node_modules/.pnpm/@aws-sdk+client-s3@3.1027.0/node_modules/@aws-sdk/client-s3/dist-cjs/models/errors.js"(exports2) {
18412
+ "node_modules/.pnpm/@aws-sdk+client-s3@3.1029.0/node_modules/@aws-sdk/client-s3/dist-cjs/models/errors.js"(exports2) {
18413
18413
  "use strict";
18414
18414
  Object.defineProperty(exports2, "__esModule", { value: true });
18415
18415
  exports2.ObjectAlreadyInActiveTierError = exports2.IdempotencyParameterMismatch = exports2.TooManyParts = exports2.InvalidWriteOffset = exports2.InvalidRequest = exports2.EncryptionTypeMismatch = exports2.NotFound = exports2.NoSuchKey = exports2.InvalidObjectState = exports2.NoSuchBucket = exports2.BucketAlreadyOwnedByYou = exports2.BucketAlreadyExists = exports2.ObjectNotInActiveTierError = exports2.AccessDenied = exports2.NoSuchUpload = void 0;
@@ -18616,9 +18616,9 @@ var require_errors = __commonJS({
18616
18616
  }
18617
18617
  });
18618
18618
 
18619
- // node_modules/.pnpm/@aws-sdk+client-s3@3.1027.0/node_modules/@aws-sdk/client-s3/dist-cjs/schemas/schemas_0.js
18619
+ // node_modules/.pnpm/@aws-sdk+client-s3@3.1029.0/node_modules/@aws-sdk/client-s3/dist-cjs/schemas/schemas_0.js
18620
18620
  var require_schemas_0 = __commonJS({
18621
- "node_modules/.pnpm/@aws-sdk+client-s3@3.1027.0/node_modules/@aws-sdk/client-s3/dist-cjs/schemas/schemas_0.js"(exports2) {
18621
+ "node_modules/.pnpm/@aws-sdk+client-s3@3.1029.0/node_modules/@aws-sdk/client-s3/dist-cjs/schemas/schemas_0.js"(exports2) {
18622
18622
  "use strict";
18623
18623
  Object.defineProperty(exports2, "__esModule", { value: true });
18624
18624
  exports2.CreateBucketMetadataTableConfigurationRequest$ = exports2.CreateBucketMetadataConfigurationRequest$ = exports2.CreateBucketConfiguration$ = exports2.CORSRule$ = exports2.CORSConfiguration$ = exports2.CopyPartResult$ = exports2.CopyObjectResult$ = exports2.CopyObjectRequest$ = exports2.CopyObjectOutput$ = exports2.ContinuationEvent$ = exports2.Condition$ = exports2.CompleteMultipartUploadRequest$ = exports2.CompleteMultipartUploadOutput$ = exports2.CompletedPart$ = exports2.CompletedMultipartUpload$ = exports2.CommonPrefix$ = exports2.Checksum$ = exports2.BucketLoggingStatus$ = exports2.BucketLifecycleConfiguration$ = exports2.BucketInfo$ = exports2.Bucket$ = exports2.BlockedEncryptionTypes$ = exports2.AnalyticsS3BucketDestination$ = exports2.AnalyticsExportDestination$ = exports2.AnalyticsConfiguration$ = exports2.AnalyticsAndOperator$ = exports2.AccessControlTranslation$ = exports2.AccessControlPolicy$ = exports2.AccelerateConfiguration$ = exports2.AbortMultipartUploadRequest$ = exports2.AbortMultipartUploadOutput$ = exports2.AbortIncompleteMultipartUpload$ = exports2.AbacStatus$ = exports2.errorTypeRegistries = exports2.TooManyParts$ = exports2.ObjectNotInActiveTierError$ = exports2.ObjectAlreadyInActiveTierError$ = exports2.NotFound$ = exports2.NoSuchUpload$ = exports2.NoSuchKey$ = exports2.NoSuchBucket$ = exports2.InvalidWriteOffset$ = exports2.InvalidRequest$ = exports2.InvalidObjectState$ = exports2.IdempotencyParameterMismatch$ = exports2.EncryptionTypeMismatch$ = exports2.BucketAlreadyOwnedByYou$ = exports2.BucketAlreadyExists$ = exports2.AccessDenied$ = exports2.S3ServiceException$ = void 0;
@@ -23815,13 +23815,13 @@ var require_schemas_0 = __commonJS({
23815
23815
  }
23816
23816
  });
23817
23817
 
23818
- // node_modules/.pnpm/@aws-sdk+client-s3@3.1027.0/node_modules/@aws-sdk/client-s3/package.json
23818
+ // node_modules/.pnpm/@aws-sdk+client-s3@3.1029.0/node_modules/@aws-sdk/client-s3/package.json
23819
23819
  var require_package = __commonJS({
23820
- "node_modules/.pnpm/@aws-sdk+client-s3@3.1027.0/node_modules/@aws-sdk/client-s3/package.json"(exports2, module2) {
23820
+ "node_modules/.pnpm/@aws-sdk+client-s3@3.1029.0/node_modules/@aws-sdk/client-s3/package.json"(exports2, module2) {
23821
23821
  module2.exports = {
23822
23822
  name: "@aws-sdk/client-s3",
23823
23823
  description: "AWS SDK for JavaScript S3 Client for Node.js, Browser and React Native",
23824
- version: "3.1027.0",
23824
+ version: "3.1029.0",
23825
23825
  scripts: {
23826
23826
  build: "concurrently 'yarn:build:types' 'yarn:build:es' && yarn build:cjs",
23827
23827
  "build:cjs": "node ../../scripts/compilation/inline client-s3",
@@ -23904,7 +23904,7 @@ var require_package = __commonJS({
23904
23904
  tslib: "^2.6.2"
23905
23905
  },
23906
23906
  devDependencies: {
23907
- "@aws-sdk/signature-v4-crt": "3.1027.0",
23907
+ "@aws-sdk/signature-v4-crt": "3.1029.0",
23908
23908
  "@smithy/snapshot-testing": "^2.0.5",
23909
23909
  "@tsconfig/node20": "20.1.8",
23910
23910
  "@types/node": "^20.14.8",
@@ -30686,9 +30686,9 @@ var require_dist_cjs67 = __commonJS({
30686
30686
  }
30687
30687
  });
30688
30688
 
30689
- // node_modules/.pnpm/@aws-sdk+client-s3@3.1027.0/node_modules/@aws-sdk/client-s3/dist-cjs/runtimeConfig.shared.js
30689
+ // node_modules/.pnpm/@aws-sdk+client-s3@3.1029.0/node_modules/@aws-sdk/client-s3/dist-cjs/runtimeConfig.shared.js
30690
30690
  var require_runtimeConfig_shared = __commonJS({
30691
- "node_modules/.pnpm/@aws-sdk+client-s3@3.1027.0/node_modules/@aws-sdk/client-s3/dist-cjs/runtimeConfig.shared.js"(exports2) {
30691
+ "node_modules/.pnpm/@aws-sdk+client-s3@3.1029.0/node_modules/@aws-sdk/client-s3/dist-cjs/runtimeConfig.shared.js"(exports2) {
30692
30692
  "use strict";
30693
30693
  Object.defineProperty(exports2, "__esModule", { value: true });
30694
30694
  exports2.getRuntimeConfig = void 0;
@@ -30748,9 +30748,9 @@ var require_runtimeConfig_shared = __commonJS({
30748
30748
  }
30749
30749
  });
30750
30750
 
30751
- // node_modules/.pnpm/@aws-sdk+client-s3@3.1027.0/node_modules/@aws-sdk/client-s3/dist-cjs/runtimeConfig.js
30751
+ // node_modules/.pnpm/@aws-sdk+client-s3@3.1029.0/node_modules/@aws-sdk/client-s3/dist-cjs/runtimeConfig.js
30752
30752
  var require_runtimeConfig = __commonJS({
30753
- "node_modules/.pnpm/@aws-sdk+client-s3@3.1027.0/node_modules/@aws-sdk/client-s3/dist-cjs/runtimeConfig.js"(exports2) {
30753
+ "node_modules/.pnpm/@aws-sdk+client-s3@3.1029.0/node_modules/@aws-sdk/client-s3/dist-cjs/runtimeConfig.js"(exports2) {
30754
30754
  "use strict";
30755
30755
  Object.defineProperty(exports2, "__esModule", { value: true });
30756
30756
  exports2.getRuntimeConfig = void 0;
@@ -31098,9 +31098,9 @@ var require_dist_cjs70 = __commonJS({
31098
31098
  }
31099
31099
  });
31100
31100
 
31101
- // node_modules/.pnpm/@aws-sdk+client-s3@3.1027.0/node_modules/@aws-sdk/client-s3/dist-cjs/index.js
31101
+ // node_modules/.pnpm/@aws-sdk+client-s3@3.1029.0/node_modules/@aws-sdk/client-s3/dist-cjs/index.js
31102
31102
  var require_dist_cjs71 = __commonJS({
31103
- "node_modules/.pnpm/@aws-sdk+client-s3@3.1027.0/node_modules/@aws-sdk/client-s3/dist-cjs/index.js"(exports2) {
31103
+ "node_modules/.pnpm/@aws-sdk+client-s3@3.1029.0/node_modules/@aws-sdk/client-s3/dist-cjs/index.js"(exports2) {
31104
31104
  "use strict";
31105
31105
  var middlewareExpectContinue = require_dist_cjs3();
31106
31106
  var middlewareFlexibleChecksums = require_dist_cjs19();
@@ -33321,9 +33321,9 @@ var require_dist_cjs72 = __commonJS({
33321
33321
  }
33322
33322
  });
33323
33323
 
33324
- // node_modules/.pnpm/@aws-sdk+s3-request-presigner@3.1027.0/node_modules/@aws-sdk/s3-request-presigner/dist-cjs/index.js
33324
+ // node_modules/.pnpm/@aws-sdk+s3-request-presigner@3.1029.0/node_modules/@aws-sdk/s3-request-presigner/dist-cjs/index.js
33325
33325
  var require_dist_cjs73 = __commonJS({
33326
- "node_modules/.pnpm/@aws-sdk+s3-request-presigner@3.1027.0/node_modules/@aws-sdk/s3-request-presigner/dist-cjs/index.js"(exports2) {
33326
+ "node_modules/.pnpm/@aws-sdk+s3-request-presigner@3.1029.0/node_modules/@aws-sdk/s3-request-presigner/dist-cjs/index.js"(exports2) {
33327
33327
  "use strict";
33328
33328
  var utilFormatUrl = require_dist_cjs72();
33329
33329
  var middlewareEndpoint = require_dist_cjs45();
@@ -33458,6 +33458,70 @@ __export(index_exports, {
33458
33458
  });
33459
33459
  module.exports = __toCommonJS(index_exports);
33460
33460
 
33461
+ // src/core/cave-store.ts
33462
+ var ACTIVE_CAVE_TABLE = "echo_cave_v3";
33463
+ function toCaveSnapshotRecord(cave) {
33464
+ return {
33465
+ id: cave.id,
33466
+ channelId: cave.channelId,
33467
+ createTime: new Date(cave.createTime),
33468
+ userId: cave.userId,
33469
+ originUserId: cave.originUserId,
33470
+ type: cave.type,
33471
+ content: cave.content,
33472
+ relatedUsers: [...cave.relatedUsers],
33473
+ drawCount: cave.drawCount
33474
+ };
33475
+ }
33476
+ function toCaveBackupRecord(cave) {
33477
+ return {
33478
+ entryId: "entryId" in cave ? cave.entryId : void 0,
33479
+ ...toCaveSnapshotRecord(cave)
33480
+ };
33481
+ }
33482
+ async function getNextCavePublicId(ctx) {
33483
+ const caves = await ctx.database.get(ACTIVE_CAVE_TABLE, {}, ["id"]);
33484
+ return caves.reduce((maxId, cave) => Math.max(maxId, cave.id), 0) + 1;
33485
+ }
33486
+ async function getCaveByPublicId(ctx, id, channelId) {
33487
+ const query = channelId ? { id, channelId } : { id };
33488
+ const caves = await ctx.database.get(ACTIVE_CAVE_TABLE, query);
33489
+ return caves[0] || null;
33490
+ }
33491
+ async function getCavesByPublicIds(ctx, ids) {
33492
+ if (ids.length === 0) {
33493
+ return [];
33494
+ }
33495
+ const idSet = new Set(ids);
33496
+ const caves = await ctx.database.get(ACTIVE_CAVE_TABLE, {});
33497
+ return caves.filter((cave) => idSet.has(cave.id));
33498
+ }
33499
+ async function removeCaveByEntryId(ctx, entryId) {
33500
+ await ctx.database.remove(ACTIVE_CAVE_TABLE, entryId);
33501
+ }
33502
+ async function createCaveRecord(ctx, cave) {
33503
+ return await ctx.database.create(ACTIVE_CAVE_TABLE, {
33504
+ id: cave.id,
33505
+ channelId: cave.channelId,
33506
+ createTime: new Date(cave.createTime),
33507
+ userId: cave.userId,
33508
+ originUserId: cave.originUserId,
33509
+ type: cave.type,
33510
+ content: cave.content,
33511
+ relatedUsers: [...cave.relatedUsers],
33512
+ drawCount: cave.drawCount
33513
+ });
33514
+ }
33515
+ async function updateCaveByEntryId(ctx, entryId, data2) {
33516
+ await ctx.database.set(ACTIVE_CAVE_TABLE, { entryId }, data2);
33517
+ }
33518
+ async function getAllCaves(ctx) {
33519
+ return await ctx.database.get(ACTIVE_CAVE_TABLE, {});
33520
+ }
33521
+ async function getCavesByChannel(ctx, channelId) {
33522
+ return await ctx.database.get(ACTIVE_CAVE_TABLE, { channelId });
33523
+ }
33524
+
33461
33525
  // src/adapters/onebot/user.ts
33462
33526
  async function getUserIdFromNickname(session, nickname, userId) {
33463
33527
  const memberInfos = await session.onebot.getGroupMemberList(session.channelId);
@@ -34204,7 +34268,7 @@ async function collectMessageMediaRefs(ctx, content) {
34204
34268
  }
34205
34269
  async function collectCavesReferencingFiles(ctx, targetPaths) {
34206
34270
  const targetSet = new Set(targetPaths.map((filePath) => normalizePathForComparison(filePath)));
34207
- const caves = await ctx.database.get("echo_cave_v2", {});
34271
+ const caves = await getAllCaves(ctx);
34208
34272
  const matched = [];
34209
34273
  for (const cave of caves) {
34210
34274
  try {
@@ -34228,7 +34292,10 @@ async function deleteCavesForOversizedMedia(ctx, caves, cfg) {
34228
34292
  for (const cave of caves) {
34229
34293
  try {
34230
34294
  await deleteMediaFilesFromMessage(ctx, cave.content, cfg);
34231
- await ctx.database.remove("echo_cave_v2", cave.id);
34295
+ if (typeof cave.entryId !== "number") {
34296
+ throw new Error(`missing_entry_id_for_cave_${cave.id}`);
34297
+ }
34298
+ await removeCaveByEntryId(ctx, cave.entryId);
34232
34299
  ctx.logger.info(`Deleted cave #${cave.id} because its media exceeded the size limit.`);
34233
34300
  } catch (error2) {
34234
34301
  ctx.logger.warn(`Failed to delete cave #${cave.id} during oversized media cleanup: ${error2}`);
@@ -34236,7 +34303,7 @@ async function deleteCavesForOversizedMedia(ctx, caves, cfg) {
34236
34303
  }
34237
34304
  }
34238
34305
  async function inspectCaveMediaRefs(ctx, shouldInclude) {
34239
- const caves = await ctx.database.get("echo_cave_v2", {});
34306
+ const caves = await getAllCaves(ctx);
34240
34307
  const results = [];
34241
34308
  for (const cave of caves) {
34242
34309
  if (shouldInclude && !shouldInclude(cave.id)) {
@@ -34631,7 +34698,7 @@ async function deleteMediaFilesFromMessage(ctx, content, cfg) {
34631
34698
  });
34632
34699
  }
34633
34700
  async function migrateLocalMediaToV2(ctx, cfg) {
34634
- const caves = await ctx.database.get("echo_cave_v2", {});
34701
+ const caves = await getAllCaves(ctx);
34635
34702
  const stats = createEmptyStats();
34636
34703
  const state2 = {
34637
34704
  transferPlans: /* @__PURE__ */ new Map()
@@ -34652,7 +34719,10 @@ async function migrateLocalMediaToV2(ctx, cfg) {
34652
34719
  (fileRef, type) => isLegacyLocalMediaRef(ctx, fileRef, type)
34653
34720
  );
34654
34721
  if (rewritten.content !== cave.content) {
34655
- await ctx.database.set("echo_cave_v2", cave.id, { content: rewritten.content });
34722
+ if (typeof cave.entryId !== "number") {
34723
+ throw new Error(`missing_entry_id_for_cave_${cave.id}`);
34724
+ }
34725
+ await updateCaveByEntryId(ctx, cave.entryId, { content: rewritten.content });
34656
34726
  await runTransferPlans(rewritten.plans, "commit");
34657
34727
  }
34658
34728
  } catch (error2) {
@@ -34670,7 +34740,7 @@ async function migrateLocalMediaToV2(ctx, cfg) {
34670
34740
  };
34671
34741
  }
34672
34742
  async function migrateLocalMediaToS3(ctx, cfg, keepLocal, createHooks) {
34673
- const caves = await ctx.database.get("echo_cave_v2", {});
34743
+ const caves = await getAllCaves(ctx);
34674
34744
  const stats = createEmptyStats();
34675
34745
  const state2 = {
34676
34746
  transferPlans: /* @__PURE__ */ new Map()
@@ -34693,7 +34763,10 @@ async function migrateLocalMediaToS3(ctx, cfg, keepLocal, createHooks) {
34693
34763
  hooks
34694
34764
  );
34695
34765
  if (rewritten.content !== cave.content) {
34696
- await ctx.database.set("echo_cave_v2", cave.id, { content: rewritten.content });
34766
+ if (typeof cave.entryId !== "number") {
34767
+ throw new Error(`missing_entry_id_for_cave_${cave.id}`);
34768
+ }
34769
+ await updateCaveByEntryId(ctx, cave.entryId, { content: rewritten.content });
34697
34770
  await runTransferPlans(rewritten.plans, "commit");
34698
34771
  await hooks?.onMigrationCommitted?.();
34699
34772
  }
@@ -34712,7 +34785,7 @@ async function migrateLocalMediaToS3(ctx, cfg, keepLocal, createHooks) {
34712
34785
  };
34713
34786
  }
34714
34787
  async function mergeChannelCaves(ctx, cfg, sourceChannelId, targetChannelId, keepSource) {
34715
- const sourceCaves = await ctx.database.get("echo_cave_v2", { channelId: sourceChannelId });
34788
+ const sourceCaves = await getCavesByChannel(ctx, sourceChannelId);
34716
34789
  const targetMode = getStorageMode(cfg);
34717
34790
  const transferMode = keepSource ? "copy" : "move";
34718
34791
  const stats = createEmptyStats();
@@ -34735,7 +34808,9 @@ async function mergeChannelCaves(ctx, cfg, sourceChannelId, targetChannelId, kee
34735
34808
  stats
34736
34809
  );
34737
34810
  if (keepSource) {
34738
- await ctx.database.create("echo_cave_v2", {
34811
+ const nextId = await getNextCavePublicId(ctx);
34812
+ await createCaveRecord(ctx, {
34813
+ id: nextId,
34739
34814
  channelId: targetChannelId,
34740
34815
  createTime: cave.createTime,
34741
34816
  userId: cave.userId,
@@ -34746,7 +34821,10 @@ async function mergeChannelCaves(ctx, cfg, sourceChannelId, targetChannelId, kee
34746
34821
  drawCount: cave.drawCount
34747
34822
  });
34748
34823
  } else {
34749
- await ctx.database.set("echo_cave_v2", cave.id, {
34824
+ if (typeof cave.entryId !== "number") {
34825
+ throw new Error(`missing_entry_id_for_cave_${cave.id}`);
34826
+ }
34827
+ await updateCaveByEntryId(ctx, cave.entryId, {
34750
34828
  channelId: targetChannelId,
34751
34829
  content: rewritten.content
34752
34830
  });
@@ -34852,12 +34930,143 @@ var import_node_fs2 = require("node:fs");
34852
34930
  var import_node_path2 = __toESM(require("node:path"), 1);
34853
34931
  var REINDEX_SPECIAL_OFFSET = 1e6;
34854
34932
  var caveMaintenanceLock = false;
34933
+ function cloneCaveRecord(cave, id) {
34934
+ return {
34935
+ ...cave,
34936
+ createTime: new Date(cave.createTime),
34937
+ id,
34938
+ relatedUsers: [...cave.relatedUsers]
34939
+ };
34940
+ }
34941
+ async function removeCavesByEntryIds(ctx, entryIds) {
34942
+ for (const entryId of entryIds) {
34943
+ await removeCaveByEntryId(ctx, entryId);
34944
+ }
34945
+ }
34946
+ async function upsertCaves(ctx, caves) {
34947
+ if (caves.length === 0) {
34948
+ return;
34949
+ }
34950
+ for (const cave of caves) {
34951
+ if (typeof cave.entryId === "number") {
34952
+ await ctx.database.upsert(ACTIVE_CAVE_TABLE, [cave], "entryId");
34953
+ continue;
34954
+ }
34955
+ await createCaveRecord(ctx, toCaveSnapshotRecord(cave));
34956
+ }
34957
+ }
34958
+ async function rollbackCaveReplacement(ctx, currentCaves, nextCaves) {
34959
+ const nextEntryIds = nextCaves.map((cave) => cave.entryId).filter((entryId) => typeof entryId === "number");
34960
+ await removeCavesByEntryIds(ctx, nextEntryIds);
34961
+ await upsertCaves(ctx, currentCaves);
34962
+ }
34963
+ async function replaceCaveSnapshot(ctx, currentCaves, nextCaves) {
34964
+ try {
34965
+ const currentEntryIds = currentCaves.map((cave) => cave.entryId).filter((entryId) => typeof entryId === "number");
34966
+ await removeCavesByEntryIds(ctx, currentEntryIds);
34967
+ await upsertCaves(ctx, nextCaves);
34968
+ } catch (error2) {
34969
+ await rollbackCaveReplacement(ctx, currentCaves, nextCaves);
34970
+ throw error2;
34971
+ }
34972
+ }
34973
+ function buildSequentialCaveSnapshot(caves) {
34974
+ return [...caves].sort((a5, b5) => a5.id - b5.id).map((cave, index) => cloneCaveRecord(cave, index + 1));
34975
+ }
34976
+ function normalizeCaveRecord(cave) {
34977
+ return {
34978
+ channelId: cave.channelId,
34979
+ content: cave.content,
34980
+ createTime: new Date(cave.createTime).toISOString(),
34981
+ drawCount: cave.drawCount,
34982
+ id: cave.id,
34983
+ originUserId: cave.originUserId,
34984
+ relatedUsers: [...cave.relatedUsers],
34985
+ type: cave.type,
34986
+ userId: cave.userId
34987
+ };
34988
+ }
34989
+ function normalizeBackupRecord(cave) {
34990
+ return {
34991
+ ...normalizeCaveRecord(cave),
34992
+ entryId: cave.entryId
34993
+ };
34994
+ }
34995
+ function isRecordObject(value) {
34996
+ return typeof value === "object" && value !== null;
34997
+ }
34998
+ function isEchoCaveRecord(value) {
34999
+ if (!isRecordObject(value)) {
35000
+ return false;
35001
+ }
35002
+ return typeof value.id === "number" && typeof value.channelId === "string" && (value.createTime instanceof Date || typeof value.createTime === "string" || typeof value.createTime === "number") && typeof value.userId === "string" && typeof value.originUserId === "string" && (value.type === "forward" || value.type === "msg") && typeof value.content === "string" && Array.isArray(value.relatedUsers) && value.relatedUsers.every((user) => typeof user === "string") && typeof value.drawCount === "number";
35003
+ }
35004
+ function isCaveBackupRecord(value) {
35005
+ if (!isEchoCaveRecord(value)) {
35006
+ return false;
35007
+ }
35008
+ return typeof value.entryId === "number" || typeof value.entryId === "undefined";
35009
+ }
35010
+ function isReindexPlanItem(value) {
35011
+ if (!isRecordObject(value)) {
35012
+ return false;
35013
+ }
35014
+ return typeof value.oldId === "number" && typeof value.newId === "number" && typeof value.tempId === "number";
35015
+ }
35016
+ function parseReindexBackupPayload(content) {
35017
+ const parsed = JSON.parse(content);
35018
+ if (!isRecordObject(parsed)) {
35019
+ throw new Error("invalid_backup_payload");
35020
+ }
35021
+ if (typeof parsed.createdAt !== "string") {
35022
+ throw new Error("invalid_backup_created_at");
35023
+ }
35024
+ if (!Array.isArray(parsed.records) || !parsed.records.every((record) => isCaveBackupRecord(record))) {
35025
+ throw new Error("invalid_backup_records");
35026
+ }
35027
+ if (!Array.isArray(parsed.mapping) || !parsed.mapping.every((item) => isReindexPlanItem(item))) {
35028
+ throw new Error("invalid_backup_mapping");
35029
+ }
35030
+ return {
35031
+ createdAt: parsed.createdAt,
35032
+ mapping: parsed.mapping,
35033
+ records: parsed.records.map((record) => ({
35034
+ ...toCaveBackupRecord(record),
35035
+ createTime: new Date(record.createTime)
35036
+ }))
35037
+ };
35038
+ }
35039
+ function resolveBackupPath(backupPath) {
35040
+ return import_node_path2.default.isAbsolute(backupPath) ? backupPath : import_node_path2.default.resolve(process.cwd(), backupPath);
35041
+ }
34855
35042
  function getCaveMaintenanceMessage(session) {
34856
35043
  return caveMaintenanceLock ? session.text("echo-cave.general.maintenanceLocked") : null;
34857
35044
  }
34858
35045
  function setCaveMaintenanceLock(value) {
34859
35046
  caveMaintenanceLock = value;
34860
35047
  }
35048
+ function parseDailyTime(value) {
35049
+ const match = value.trim().match(/^(\d{1,2}):(\d{2})$/);
35050
+ if (!match) {
35051
+ return null;
35052
+ }
35053
+ const hour = Number(match[1]);
35054
+ const minute = Number(match[2]);
35055
+ if (hour < 0 || hour > 23 || minute < 0 || minute > 59) {
35056
+ return null;
35057
+ }
35058
+ return { hour, minute };
35059
+ }
35060
+ function getDelayUntilNextRun(time2) {
35061
+ const now = /* @__PURE__ */ new Date();
35062
+ const nextRun = new Date(now);
35063
+ nextRun.setSeconds(0, 0);
35064
+ nextRun.setHours(time2.hour, time2.minute, 0, 0);
35065
+ if (nextRun.getTime() <= now.getTime()) {
35066
+ nextRun.setDate(nextRun.getDate() + 1);
35067
+ }
35068
+ return nextRun.getTime() - now.getTime();
35069
+ }
34861
35070
  function ensureAdminPrivateAccess(session, cfg) {
34862
35071
  const maintenanceMessage = getCaveMaintenanceMessage(session);
34863
35072
  if (maintenanceMessage) {
@@ -35010,11 +35219,11 @@ async function mergeCavesBetweenChannels(ctx, session, cfg, sourceChannelId, tar
35010
35219
  if (sourceChannelId === targetChannelId) {
35011
35220
  return session.text("commands.cave.admin.merge.messages.sameChannel");
35012
35221
  }
35013
- const sourceCaves = await ctx.database.get("echo_cave_v2", { channelId: sourceChannelId });
35222
+ const sourceCaves = await getCavesByChannel(ctx, sourceChannelId);
35014
35223
  if (sourceCaves.length === 0) {
35015
35224
  return session.text("commands.cave.admin.merge.messages.noSourceCaves");
35016
35225
  }
35017
- const targetCaves = await ctx.database.get("echo_cave_v2", { channelId: targetChannelId });
35226
+ const targetCaves = await getCavesByChannel(ctx, targetChannelId);
35018
35227
  const confirmed = await requestSecondConfirmation(
35019
35228
  ctx,
35020
35229
  session,
@@ -35160,62 +35369,39 @@ async function writeReindexBackup(backupDir, caves, mapping) {
35160
35369
  const backupPath = import_node_path2.default.join(backupDir, `echo-cave-reindex-${timestamp}.json`);
35161
35370
  const payload2 = {
35162
35371
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
35163
- records: caves,
35372
+ records: caves.map((cave) => toCaveBackupRecord(cave)),
35164
35373
  mapping
35165
35374
  };
35166
35375
  await import_node_fs2.promises.writeFile(backupPath, JSON.stringify(payload2, null, 2), "utf8");
35167
35376
  return backupPath;
35168
35377
  }
35169
- async function applyReindexPlan(ctx, plan) {
35170
- const descendingPlan = [...plan].sort((a5, b5) => b5.oldId - a5.oldId);
35171
- for (const item of descendingPlan) {
35172
- if (item.oldId === item.newId) {
35173
- continue;
35174
- }
35175
- await ctx.database.set("echo_cave_v2", { id: item.oldId }, { id: item.tempId });
35378
+ async function verifyCaveSnapshot(ctx, expectedCaves) {
35379
+ const actualCaves = (await getAllCaves(ctx)).sort((a5, b5) => a5.id - b5.id);
35380
+ const normalizedExpected = [...expectedCaves].sort((a5, b5) => a5.id - b5.id).map(normalizeCaveRecord);
35381
+ const normalizedActual = actualCaves.map(normalizeCaveRecord);
35382
+ if (normalizedActual.length !== normalizedExpected.length) {
35383
+ throw new Error("record_count_mismatch");
35176
35384
  }
35177
- const ascendingPlan = [...plan].sort((a5, b5) => a5.newId - b5.newId);
35178
- for (const item of ascendingPlan) {
35179
- if (item.oldId === item.newId) {
35180
- continue;
35385
+ for (let index = 0; index < normalizedActual.length; index++) {
35386
+ const actual = normalizedActual[index];
35387
+ const expected = normalizedExpected[index];
35388
+ if (actual.id !== index + 1) {
35389
+ throw new Error("id_sequence_mismatch");
35390
+ }
35391
+ if (JSON.stringify(actual) !== JSON.stringify(expected)) {
35392
+ throw new Error("record_content_mismatch");
35181
35393
  }
35182
- await ctx.database.set("echo_cave_v2", { id: item.tempId }, { id: item.newId });
35183
35394
  }
35184
35395
  }
35185
- async function verifyReindexPlan(ctx, originalCaves) {
35186
- const reindexedCaves = (await ctx.database.get("echo_cave_v2", {})).sort((a5, b5) => a5.id - b5.id);
35187
- if (reindexedCaves.length !== originalCaves.length) {
35396
+ async function verifyRestoredSnapshot(ctx, expectedCaves) {
35397
+ const actualCaves = (await getAllCaves(ctx)).sort((a5, b5) => a5.id - b5.id);
35398
+ const normalizedExpected = [...expectedCaves].sort((a5, b5) => a5.id - b5.id).map(normalizeBackupRecord);
35399
+ const normalizedActual = actualCaves.map(normalizeCaveRecord);
35400
+ if (normalizedActual.length !== normalizedExpected.length) {
35188
35401
  throw new Error("record_count_mismatch");
35189
35402
  }
35190
- const normalizedOriginal = [...originalCaves].sort((a5, b5) => a5.id - b5.id).map((cave) => ({
35191
- channelId: cave.channelId,
35192
- content: cave.content,
35193
- createTime: new Date(cave.createTime).toISOString(),
35194
- drawCount: cave.drawCount,
35195
- originUserId: cave.originUserId,
35196
- relatedUsers: [...cave.relatedUsers],
35197
- type: cave.type,
35198
- userId: cave.userId
35199
- }));
35200
- const normalizedReindexed = reindexedCaves.map((cave, index) => ({
35201
- channelId: cave.channelId,
35202
- content: cave.content,
35203
- createTime: new Date(cave.createTime).toISOString(),
35204
- drawCount: cave.drawCount,
35205
- id: cave.id,
35206
- originUserId: cave.originUserId,
35207
- relatedUsers: [...cave.relatedUsers],
35208
- type: cave.type,
35209
- userId: cave.userId,
35210
- expectedId: index + 1
35211
- }));
35212
- for (let index = 0; index < normalizedReindexed.length; index++) {
35213
- const reindexed = normalizedReindexed[index];
35214
- const original = normalizedOriginal[index];
35215
- if (reindexed.id !== reindexed.expectedId) {
35216
- throw new Error("id_sequence_mismatch");
35217
- }
35218
- if (reindexed.channelId !== original.channelId || reindexed.content !== original.content || reindexed.createTime !== original.createTime || reindexed.drawCount !== original.drawCount || reindexed.originUserId !== original.originUserId || reindexed.relatedUsers.join(",") !== original.relatedUsers.join(",") || reindexed.type !== original.type || reindexed.userId !== original.userId) {
35403
+ for (let index = 0; index < normalizedActual.length; index++) {
35404
+ if (JSON.stringify(normalizedActual[index]) !== JSON.stringify(normalizedExpected[index])) {
35219
35405
  throw new Error("record_content_mismatch");
35220
35406
  }
35221
35407
  }
@@ -35230,7 +35416,7 @@ async function reindexCaveIds(ctx, session, cfg) {
35230
35416
  if (!cfg.adminIds?.includes(session.userId)) {
35231
35417
  return session.text("echo-cave.general.adminPermissionDenied");
35232
35418
  }
35233
- const caves = (await ctx.database.get("echo_cave_v2", {})).sort((a5, b5) => a5.id - b5.id);
35419
+ const caves = (await getAllCaves(ctx)).sort((a5, b5) => a5.id - b5.id);
35234
35420
  if (caves.length === 0) {
35235
35421
  return session.text("commands.cave.admin.reindex.messages.noCaves");
35236
35422
  }
@@ -35265,8 +35451,14 @@ async function reindexCaveIds(ctx, session, cfg) {
35265
35451
  }
35266
35452
  setCaveMaintenanceLock(true);
35267
35453
  try {
35268
- await applyReindexPlan(ctx, plan);
35269
- await verifyReindexPlan(ctx, caves);
35454
+ const reindexedCaves = buildSequentialCaveSnapshot(caves);
35455
+ for (const cave of reindexedCaves) {
35456
+ if (typeof cave.entryId !== "number") {
35457
+ throw new Error(`missing_entry_id_for_cave_${cave.id}`);
35458
+ }
35459
+ await updateCaveByEntryId(ctx, cave.entryId, { id: cave.id });
35460
+ }
35461
+ await verifyCaveSnapshot(ctx, reindexedCaves);
35270
35462
  return session.text("commands.cave.admin.reindex.messages.reindexDone", {
35271
35463
  caveCount: caves.length,
35272
35464
  backupPath
@@ -35281,6 +35473,122 @@ async function reindexCaveIds(ctx, session, cfg) {
35281
35473
  setCaveMaintenanceLock(false);
35282
35474
  }
35283
35475
  }
35476
+ async function restoreReindexBackup(ctx, session, cfg, backupPathInput) {
35477
+ const accessError = ensureAdminPrivateAccess(session, cfg);
35478
+ if (accessError) {
35479
+ return accessError;
35480
+ }
35481
+ if (!backupPathInput?.trim()) {
35482
+ return session.text("commands.cave.admin.restore-reindex.messages.missingBackupPath");
35483
+ }
35484
+ const backupPath = resolveBackupPath(backupPathInput.trim());
35485
+ let backup;
35486
+ try {
35487
+ const content = await import_node_fs2.promises.readFile(backupPath, "utf8");
35488
+ backup = parseReindexBackupPayload(content);
35489
+ } catch (error2) {
35490
+ ctx.logger.error(`Failed to read cave reindex backup: ${error2}`);
35491
+ return session.text("commands.cave.admin.restore-reindex.messages.backupReadFailed", {
35492
+ backupPath,
35493
+ error: error2 instanceof Error ? error2.message : String(error2)
35494
+ });
35495
+ }
35496
+ const currentCaves = (await getAllCaves(ctx)).sort((a5, b5) => a5.id - b5.id);
35497
+ const confirmed = await requestSecondConfirmation(
35498
+ ctx,
35499
+ session,
35500
+ session.text("commands.cave.admin.restore-reindex.messages.confirmSummary", {
35501
+ backupPath,
35502
+ currentCount: currentCaves.length,
35503
+ backupCount: backup.records.length,
35504
+ backupCreatedAt: backup.createdAt
35505
+ }),
35506
+ session.text("commands.cave.admin.restore-reindex.messages.confirmRetry"),
35507
+ session.text("commands.cave.admin.restore-reindex.messages.confirmTimeout"),
35508
+ session.text("commands.cave.admin.restore-reindex.messages.confirmCancelled")
35509
+ );
35510
+ if (!confirmed) {
35511
+ return;
35512
+ }
35513
+ setCaveMaintenanceLock(true);
35514
+ try {
35515
+ const restoreRecords = backup.records.map((record) => ({
35516
+ ...toCaveSnapshotRecord(record),
35517
+ entryId: record.entryId
35518
+ }));
35519
+ await replaceCaveSnapshot(ctx, currentCaves, restoreRecords);
35520
+ await verifyRestoredSnapshot(ctx, backup.records);
35521
+ return session.text("commands.cave.admin.restore-reindex.messages.restoreDone", {
35522
+ backupPath,
35523
+ caveCount: backup.records.length
35524
+ });
35525
+ } catch (error2) {
35526
+ ctx.logger.error(`Failed to restore cave reindex backup: ${error2}`);
35527
+ return session.text("commands.cave.admin.restore-reindex.messages.restoreFailed", {
35528
+ backupPath,
35529
+ error: error2 instanceof Error ? error2.message : String(error2)
35530
+ });
35531
+ } finally {
35532
+ setCaveMaintenanceLock(false);
35533
+ }
35534
+ }
35535
+ async function runScheduledReindex(ctx, cfg) {
35536
+ if (caveMaintenanceLock) {
35537
+ ctx.logger.warn("Skipped scheduled cave reindex because another maintenance task is running.");
35538
+ return;
35539
+ }
35540
+ const caves = (await getAllCaves(ctx)).sort((a5, b5) => a5.id - b5.id);
35541
+ if (caves.length === 0) {
35542
+ ctx.logger.info("Skipped scheduled cave reindex because there are no cave records.");
35543
+ return;
35544
+ }
35545
+ const plan = buildReindexPlan(caves);
35546
+ if (!hasIdGaps(plan)) {
35547
+ ctx.logger.info("Skipped scheduled cave reindex because IDs are already sequential.");
35548
+ return;
35549
+ }
35550
+ const backupPath = await writeReindexBackup(import_node_path2.default.resolve(process.cwd(), "logs"), caves, plan);
35551
+ setCaveMaintenanceLock(true);
35552
+ try {
35553
+ const reindexedCaves = buildSequentialCaveSnapshot(caves);
35554
+ for (const cave of reindexedCaves) {
35555
+ if (typeof cave.entryId !== "number") {
35556
+ throw new Error(`missing_entry_id_for_cave_${cave.id}`);
35557
+ }
35558
+ await updateCaveByEntryId(ctx, cave.entryId, { id: cave.id });
35559
+ }
35560
+ await verifyCaveSnapshot(ctx, reindexedCaves);
35561
+ ctx.logger.info(
35562
+ `Scheduled cave reindex completed. Processed ${caves.length} records. Backup: ${backupPath}`
35563
+ );
35564
+ } finally {
35565
+ setCaveMaintenanceLock(false);
35566
+ }
35567
+ }
35568
+ function scheduleNextAutoReindex(ctx, cfg) {
35569
+ const parsedTime = parseDailyTime(cfg.autoReindexTime || "00:00");
35570
+ if (!parsedTime) {
35571
+ ctx.logger.warn(`Invalid autoReindexTime: ${cfg.autoReindexTime}. Expected HH:mm.`);
35572
+ return;
35573
+ }
35574
+ const delay = getDelayUntilNextRun(parsedTime);
35575
+ ctx.setTimeout(async () => {
35576
+ try {
35577
+ await runScheduledReindex(ctx, cfg);
35578
+ } catch (error2) {
35579
+ ctx.logger.error(`Failed to run scheduled cave reindex: ${error2}`);
35580
+ }
35581
+ scheduleNextAutoReindex(ctx, cfg);
35582
+ }, delay);
35583
+ }
35584
+ function registerAutoReindexScheduler(ctx, cfg) {
35585
+ if (!cfg.enableAutoReindex) {
35586
+ return;
35587
+ }
35588
+ ctx.on("ready", () => {
35589
+ scheduleNextAutoReindex(ctx, cfg);
35590
+ });
35591
+ }
35284
35592
 
35285
35593
  // src/core/parser/forward-parser.ts
35286
35594
  async function reconstructForwardMsg(ctx, session, message, cfg, processMedia = true) {
@@ -35449,7 +35757,9 @@ async function addCave(ctx, session, cfg, userIds) {
35449
35757
  originName
35450
35758
  );
35451
35759
  try {
35452
- const result = await ctx.database.create("echo_cave_v2", {
35760
+ const nextId = await getNextCavePublicId(ctx);
35761
+ const result = await ctx.database.create(ACTIVE_CAVE_TABLE, {
35762
+ id: nextId,
35453
35763
  channelId,
35454
35764
  createTime: /* @__PURE__ */ new Date(),
35455
35765
  userId,
@@ -35649,7 +35959,10 @@ async function deleteStoredCave(ctx, cfg, caveMsg) {
35649
35959
  if (cfg.deleteMediaWhenDeletingMsg) {
35650
35960
  await deleteMediaFilesFromMessage(ctx, caveMsg.content, cfg);
35651
35961
  }
35652
- await ctx.database.remove("echo_cave_v2", caveMsg.id);
35962
+ if (typeof caveMsg.entryId !== "number") {
35963
+ throw new Error(`missing_entry_id_for_cave_${caveMsg.id}`);
35964
+ }
35965
+ await removeCaveByEntryId(ctx, caveMsg.entryId);
35653
35966
  }
35654
35967
  async function deleteCave(ctx, session, cfg, id) {
35655
35968
  const guildAccessError = ensureGuildSession(session);
@@ -35663,11 +35976,11 @@ async function deleteCave(ctx, session, cfg, id) {
35663
35976
  if (!id) {
35664
35977
  return session.text(".noIdProvided");
35665
35978
  }
35666
- const caves = await ctx.database.get("echo_cave_v2", id);
35667
- if (caves.length === 0) {
35979
+ const cave = await getCaveByPublicId(ctx, id, session.channelId);
35980
+ if (!cave) {
35668
35981
  return session.text("echo-cave.general.noMsgWithId");
35669
35982
  }
35670
- const caveMsg = caves[0];
35983
+ const caveMsg = cave;
35671
35984
  const actor = await getDeleteActor(ctx, session);
35672
35985
  const permissionFailure = await getDeletePermissionFailure(ctx, session, cfg, caveMsg, actor);
35673
35986
  if (permissionFailure) {
@@ -35690,7 +36003,7 @@ async function deleteCaves(ctx, session, cfg, ids) {
35690
36003
  }
35691
36004
  const failedIds = [];
35692
36005
  const actor = await getDeleteActor(ctx, session);
35693
- const caves = await ctx.database.get("echo_cave_v2", ids);
36006
+ const caves = (await getCavesByPublicIds(ctx, ids)).filter((cave) => cave.channelId === session.channelId);
35694
36007
  for (const cave of caves) {
35695
36008
  const permissionFailure = await getDeletePermissionFailure(ctx, session, cfg, cave, actor);
35696
36009
  if (permissionFailure) {
@@ -35791,7 +36104,7 @@ function scheduleNextSendFailureSummary(ctx, cfg) {
35791
36104
  );
35792
36105
  return;
35793
36106
  }
35794
- const delay = getDelayUntilNextRun(summaryTime);
36107
+ const delay = getDelayUntilNextRun2(summaryTime);
35795
36108
  ctx.setTimeout(async () => {
35796
36109
  try {
35797
36110
  await flushSendFailureSummary(ctx, cfg);
@@ -35938,7 +36251,7 @@ function parseSummaryTime(value) {
35938
36251
  }
35939
36252
  return { hour, minute };
35940
36253
  }
35941
- function getDelayUntilNextRun(summaryTime) {
36254
+ function getDelayUntilNextRun2(summaryTime) {
35942
36255
  const now = /* @__PURE__ */ new Date();
35943
36256
  const nextRun = new Date(now);
35944
36257
  nextRun.setSeconds(0, 0);
@@ -36107,7 +36420,7 @@ async function getCaveIdsByField(ctx, session, field, emptyText) {
36107
36420
  return maintenanceMessage;
36108
36421
  }
36109
36422
  const caves = await ctx.database.get(
36110
- "echo_cave_v2",
36423
+ ACTIVE_CAVE_TABLE,
36111
36424
  {
36112
36425
  [field]: session.userId,
36113
36426
  channelId: session.channelId
@@ -36121,9 +36434,9 @@ async function getCaveIdsByField(ctx, session, field, emptyText) {
36121
36434
  }
36122
36435
  async function incrementDrawCount(ctx, caveMsg) {
36123
36436
  await ctx.database.set(
36124
- "echo_cave_v2",
36437
+ ACTIVE_CAVE_TABLE,
36125
36438
  {
36126
- id: caveMsg.id,
36439
+ entryId: caveMsg.entryId,
36127
36440
  channelId: caveMsg.channelId
36128
36441
  },
36129
36442
  {
@@ -36156,7 +36469,7 @@ async function getTargetedUserCave(ctx, session, targetUserId) {
36156
36469
  if (!isUserInGroup) {
36157
36470
  return session.text("echo-cave.user.userNotInGroup");
36158
36471
  }
36159
- const caves = await ctx.database.get("echo_cave_v2", {
36472
+ const caves = await ctx.database.get(ACTIVE_CAVE_TABLE, {
36160
36473
  channelId: session.channelId
36161
36474
  });
36162
36475
  const matchingCaves = caves.filter(
@@ -36180,7 +36493,7 @@ async function getCave(ctx, session, cfg, target) {
36180
36493
  let shouldIncrementDrawCount = true;
36181
36494
  const { channelId } = session;
36182
36495
  if (!target) {
36183
- const caves = await ctx.database.get("echo_cave_v2", {
36496
+ const caves = await ctx.database.get(ACTIVE_CAVE_TABLE, {
36184
36497
  channelId
36185
36498
  });
36186
36499
  if (caves.length === 0) {
@@ -36197,7 +36510,7 @@ async function getCave(ctx, session, cfg, target) {
36197
36510
  const isExactNumericTarget = /^\d+$/.test(trimmedTarget);
36198
36511
  if (isExactNumericTarget) {
36199
36512
  const id = Number(trimmedTarget);
36200
- const caves = await ctx.database.get("echo_cave_v2", {
36513
+ const caves = await ctx.database.get(ACTIVE_CAVE_TABLE, {
36201
36514
  id,
36202
36515
  channelId
36203
36516
  });
@@ -36258,15 +36571,15 @@ async function bindUsersToCave(ctx, session, id, userIds) {
36258
36571
  if (parsedUserIds.length === 0) {
36259
36572
  return session.text(".noValidUserIdProvided");
36260
36573
  }
36261
- const caves = await ctx.database.get("echo_cave_v2", id);
36262
- if (caves.length === 0) {
36574
+ const cave = await getCaveByPublicId(ctx, id, session.channelId);
36575
+ if (!cave) {
36263
36576
  return session.text("echo-cave.general.noMsgWithId");
36264
36577
  }
36265
36578
  const isAllUsersInGroup = await checkUsersInGroup(ctx, session, parsedUserIds);
36266
36579
  if (!isAllUsersInGroup) {
36267
36580
  return session.text("echo-cave.user.userNotInGroup");
36268
36581
  }
36269
- await ctx.database.set("echo_cave_v2", id, {
36582
+ await ctx.database.set(ACTIVE_CAVE_TABLE, { entryId: cave.entryId }, {
36270
36583
  relatedUsers: parsedUserIds
36271
36584
  });
36272
36585
  return session.text(".userBoundSuccess", [id]);
@@ -36297,7 +36610,11 @@ function parseCustomTime(timeStr) {
36297
36610
  let totalMs = 0;
36298
36611
  const unitOrder = ["m", "w", "d", "h"];
36299
36612
  let lastIndex = -1;
36300
- while ((match = regex.exec(s5)) !== null) {
36613
+ while (true) {
36614
+ match = regex.exec(s5);
36615
+ if (match === null) {
36616
+ break;
36617
+ }
36301
36618
  const value = parseInt(match[1], 10);
36302
36619
  const unit = match[2].toLowerCase();
36303
36620
  const currentIndex = unitOrder.indexOf(unit);
@@ -36333,12 +36650,13 @@ function getStartTime(period, customTimeMs) {
36333
36650
  case "day":
36334
36651
  startTime.setHours(0, 0, 0, 0);
36335
36652
  break;
36336
- case "week":
36653
+ case "week": {
36337
36654
  const dayOfWeek = now.getDay();
36338
36655
  const daysToMonday = dayOfWeek === 0 ? 6 : dayOfWeek - 1;
36339
36656
  startTime.setDate(now.getDate() - daysToMonday);
36340
36657
  startTime.setHours(0, 0, 0, 0);
36341
36658
  break;
36659
+ }
36342
36660
  case "month":
36343
36661
  startTime.setDate(1);
36344
36662
  startTime.setHours(0, 0, 0, 0);
@@ -36420,7 +36738,7 @@ async function getRanking(ctx, session, cfg, period = "all") {
36420
36738
  const { channelId } = session;
36421
36739
  const startTime = getStartTime(normalizedPeriod, customTimeMs);
36422
36740
  const topCount = cfg.rankingTopCount || 10;
36423
- const caves = await ctx.database.get("echo_cave_v2", {
36741
+ const caves = await ctx.database.get(ACTIVE_CAVE_TABLE, {
36424
36742
  channelId,
36425
36743
  createTime: {
36426
36744
  $gte: startTime
@@ -36468,7 +36786,7 @@ async function searchCave(ctx, session, userIds) {
36468
36786
  }
36469
36787
  const targetUserId = parsedUserIds[0];
36470
36788
  const { channelId } = session;
36471
- const caves = await ctx.database.get("echo_cave_v2", {
36789
+ const caves = await ctx.database.get(ACTIVE_CAVE_TABLE, {
36472
36790
  channelId
36473
36791
  });
36474
36792
  const matchingCaves = caves.filter(
@@ -36694,17 +37012,30 @@ var zh_CN_default = {
36694
37012
  }
36695
37013
  },
36696
37014
  "cave.admin.reindex": {
36697
- description: "\u91CD\u6392\u6240\u6709\u56DE\u58F0\u6D1E ID\uFF0C\u4F7F\u73B0\u6709\u8BB0\u5F55\u91CD\u65B0\u8FDE\u7EED\u7F16\u53F7",
37015
+ description: "\u91CD\u6392\u6240\u6709\u56DE\u58F0\u6D1E public ID\uFF0C\u4F7F\u73B0\u6709\u8BB0\u5F55\u91CD\u65B0\u8FDE\u7EED\u7F16\u53F7\u800C\u4E0D\u6539\u52A8\u5185\u90E8\u4E3B\u952E",
36698
37016
  messages: {
36699
37017
  noCaves: "\u{1F50D} \u5F53\u524D\u6CA1\u6709\u4EFB\u4F55\u56DE\u58F0\u6D1E\u8BB0\u5F55\u53EF\u4F9B\u91CD\u6392\u3002",
36700
37018
  alreadySequential: "\u2705 \u5F53\u524D\u6240\u6709\u56DE\u58F0\u6D1E ID \u5DF2\u7ECF\u8FDE\u7EED\uFF0C\u65E0\u9700\u91CD\u6392\u3002",
36701
37019
  backupWriteFailed: "\u274C \u56DE\u58F0\u6D1E ID \u91CD\u6392\u524D\u7684\u5907\u4EFD\u5199\u5165\u5931\u8D25\uFF0C\u64CD\u4F5C\u5DF2\u7EC8\u6B62\u3002\u9519\u8BEF\uFF1A{error}",
36702
- confirmSummary: "\u26A0\uFE0F \u5373\u5C06\u6267\u884C\u56DE\u58F0\u6D1E ID \u91CD\u6392\n\u5F53\u524D\u8BB0\u5F55\u6570\uFF1A{caveCount}\n\u5F53\u524D\u6700\u5927 ID\uFF1A{currentMaxId}\n\u91CD\u6392\u540E\u6700\u5927 ID\uFF1A{nextMaxId}\n\u5907\u4EFD\u6587\u4EF6\uFF1A{backupPath}\n\n\u672C\u6B21\u64CD\u4F5C\u4F1A\u5148\u5199\u5165\u5B8C\u6574\u5907\u4EFD\uFF0C\u518D\u6309\u5B89\u5168\u987A\u5E8F\u91CD\u6392 ID\u3002\u6267\u884C\u671F\u95F4\u4F1A\u4E34\u65F6\u9501\u5B9A\u56DE\u58F0\u6D1E\u76F8\u5173\u547D\u4EE4\u3002\u8BF7\u5728 30 \u79D2\u5185\u8F93\u5165\u201C\u786E\u8BA4\u201D\u7EE7\u7EED\uFF0C\u8F93\u5165\u201C\u53D6\u6D88\u201D\u53EF\u7EC8\u6B62\u3002",
37020
+ confirmSummary: "\u26A0\uFE0F \u5373\u5C06\u6267\u884C\u56DE\u58F0\u6D1E public ID \u91CD\u6392\n\u5F53\u524D\u8BB0\u5F55\u6570\uFF1A{caveCount}\n\u5F53\u524D\u6700\u5927 ID\uFF1A{currentMaxId}\n\u91CD\u6392\u540E\u6700\u5927 ID\uFF1A{nextMaxId}\n\u5907\u4EFD\u6587\u4EF6\uFF1A{backupPath}\n\n\u672C\u6B21\u64CD\u4F5C\u4F1A\u5148\u5199\u5165\u5B8C\u6574\u5907\u4EFD\uFF0C\u518D\u4EC5\u91CD\u6392\u5BF9\u5916\u5C55\u793A\u7684 public ID\uFF0C\u4E0D\u6539\u52A8\u6570\u636E\u5E93\u5185\u90E8\u4E3B\u952E\u3002\u6267\u884C\u671F\u95F4\u4F1A\u4E34\u65F6\u9501\u5B9A\u56DE\u58F0\u6D1E\u76F8\u5173\u547D\u4EE4\u3002\u8BF7\u5728 30 \u79D2\u5185\u8F93\u5165\u201C\u786E\u8BA4\u201D\u7EE7\u7EED\uFF0C\u8F93\u5165\u201C\u53D6\u6D88\u201D\u53EF\u7EC8\u6B62\u3002",
36703
37021
  confirmRetry: "\u26A0\uFE0F \u672A\u6536\u5230\u201C\u786E\u8BA4\u201D\u3002\u8BF7\u5728\u5269\u4F59\u65F6\u95F4\u5185\u8F93\u5165\u201C\u786E\u8BA4\u201D\u7EE7\u7EED\uFF0C\u6216\u8F93\u5165\u201C\u53D6\u6D88\u201D\u7EC8\u6B62\u3002",
36704
37022
  confirmCancelled: "\u{1F6D1} \u5DF2\u53D6\u6D88\u672C\u6B21\u56DE\u58F0\u6D1E ID \u91CD\u6392\u3002",
36705
37023
  confirmTimeout: "\u231B \u4E8C\u6B21\u786E\u8BA4\u8D85\u65F6\uFF0C\u56DE\u58F0\u6D1E ID \u91CD\u6392\u672A\u6267\u884C\u3002",
36706
- reindexDone: "\u2705 \u56DE\u58F0\u6D1E ID \u91CD\u6392\u5B8C\u6210\uFF0C\u5171\u5904\u7406 {caveCount} \u6761\u8BB0\u5F55\u3002\u5907\u4EFD\u6587\u4EF6\uFF1A{backupPath}",
36707
- reindexFailed: "\u274C \u56DE\u58F0\u6D1E ID \u91CD\u6392\u5931\u8D25\uFF0C\u5DF2\u505C\u6B62\u7EE7\u7EED\u5199\u5165\u3002\u8BF7\u6839\u636E\u5907\u4EFD\u6587\u4EF6\u68C0\u67E5\u5E76\u6062\u590D\uFF1A{backupPath}\n\u9519\u8BEF\uFF1A{error}"
37024
+ reindexDone: "\u2705 \u56DE\u58F0\u6D1E public ID \u91CD\u6392\u5B8C\u6210\uFF0C\u5171\u5904\u7406 {caveCount} \u6761\u8BB0\u5F55\u3002\u5907\u4EFD\u6587\u4EF6\uFF1A{backupPath}",
37025
+ reindexFailed: "\u274C \u56DE\u58F0\u6D1E ID \u91CD\u6392\u5931\u8D25\uFF0C\u5DF2\u505C\u6B62\u7EE7\u7EED\u5199\u5165\u3002\u53EF\u4F7F\u7528 cave.admin.restore-reindex \u6062\u590D\u5907\u4EFD\uFF1A{backupPath}\n\u9519\u8BEF\uFF1A{error}"
37026
+ }
37027
+ },
37028
+ "cave.admin.restore-reindex": {
37029
+ description: "\u4ECE\u56DE\u58F0\u6D1E ID \u91CD\u6392\u751F\u6210\u7684 JSON \u5907\u4EFD\u6062\u590D\u6570\u636E\u5E93",
37030
+ messages: {
37031
+ missingBackupPath: "\u274C \u8BF7\u63D0\u4F9B\u8981\u6062\u590D\u7684\u5907\u4EFD\u6587\u4EF6\u8DEF\u5F84\u3002",
37032
+ backupReadFailed: "\u274C \u8BFB\u53D6\u56DE\u58F0\u6D1E\u91CD\u6392\u5907\u4EFD\u5931\u8D25\uFF1A{backupPath}\n\u9519\u8BEF\uFF1A{error}",
37033
+ confirmSummary: "\u26A0\uFE0F \u5373\u5C06\u4ECE\u5907\u4EFD\u6062\u590D\u56DE\u58F0\u6D1E\u6570\u636E\n\u5907\u4EFD\u6587\u4EF6\uFF1A{backupPath}\n\u5907\u4EFD\u521B\u5EFA\u65F6\u95F4\uFF1A{backupCreatedAt}\n\u5907\u4EFD\u8BB0\u5F55\u6570\uFF1A{backupCount}\n\u5F53\u524D\u6570\u636E\u5E93\u8BB0\u5F55\u6570\uFF1A{currentCount}\n\n\u672C\u6B21\u64CD\u4F5C\u4F1A\u5148\u4E34\u65F6\u817E\u632A\u5F53\u524D\u8BB0\u5F55\uFF0C\u518D\u6309\u5907\u4EFD\u5185\u5BB9\u5B8C\u6574\u6062\u590D\u3002\u6267\u884C\u671F\u95F4\u4F1A\u4E34\u65F6\u9501\u5B9A\u56DE\u58F0\u6D1E\u76F8\u5173\u547D\u4EE4\u3002\u8BF7\u5728 30 \u79D2\u5185\u8F93\u5165\u201C\u786E\u8BA4\u201D\u7EE7\u7EED\uFF0C\u8F93\u5165\u201C\u53D6\u6D88\u201D\u53EF\u7EC8\u6B62\u3002",
37034
+ confirmRetry: "\u26A0\uFE0F \u672A\u6536\u5230\u201C\u786E\u8BA4\u201D\u3002\u8BF7\u5728\u5269\u4F59\u65F6\u95F4\u5185\u8F93\u5165\u201C\u786E\u8BA4\u201D\u7EE7\u7EED\uFF0C\u6216\u8F93\u5165\u201C\u53D6\u6D88\u201D\u7EC8\u6B62\u3002",
37035
+ confirmCancelled: "\u{1F6D1} \u5DF2\u53D6\u6D88\u672C\u6B21\u56DE\u58F0\u6D1E\u5907\u4EFD\u6062\u590D\u3002",
37036
+ confirmTimeout: "\u231B \u4E8C\u6B21\u786E\u8BA4\u8D85\u65F6\uFF0C\u56DE\u58F0\u6D1E\u5907\u4EFD\u6062\u590D\u672A\u6267\u884C\u3002",
37037
+ restoreDone: "\u2705 \u56DE\u58F0\u6D1E\u5907\u4EFD\u6062\u590D\u5B8C\u6210\uFF0C\u5171\u6062\u590D {caveCount} \u6761\u8BB0\u5F55\u3002\u5907\u4EFD\u6587\u4EF6\uFF1A{backupPath}",
37038
+ restoreFailed: "\u274C \u56DE\u58F0\u6D1E\u5907\u4EFD\u6062\u590D\u5931\u8D25\uFF0C\u5DF2\u5C1D\u8BD5\u56DE\u6EDA\u5230\u6062\u590D\u524D\u72B6\u6001\uFF1A{backupPath}\n\u9519\u8BEF\uFF1A{error}"
36708
37039
  }
36709
37040
  }
36710
37041
  }
@@ -36742,7 +37073,9 @@ var zh_CN_default2 = {
36742
37073
  s3PresignExpiresIn: "S3 \u79C1\u6709\u5BF9\u8C61\u9884\u7B7E\u540D URL \u7684\u6709\u6548\u671F (\u79D2)",
36743
37074
  sendFailureHandlingMode: "\u56DE\u58F0\u6D1E\u53D1\u9001\u5931\u8D25\u540E\u7684\u5904\u7406\u65B9\u5F0F\uFF1A\u81EA\u52A8\u5220\u9664\u3001\u6309\u65E5\u6C47\u603B\u4E0A\u62A5\u6216\u4E0D\u989D\u5916\u5904\u7406",
36744
37075
  sendFailureSummaryAdminId: "\u53D1\u9001\u5931\u8D25\u65E5\u62A5\u7684\u5355\u72EC\u7BA1\u7406\u5458 ID\uFF08\u4E0D\u590D\u7528\u7BA1\u7406\u5458 ID \u5217\u8868\uFF09",
36745
- sendFailureSummaryTime: "\u53D1\u9001\u5931\u8D25\u65E5\u62A5\u7684\u6BCF\u65E5\u53D1\u9001\u65F6\u95F4\uFF0C\u683C\u5F0F\u4E3A HH:mm"
37076
+ sendFailureSummaryTime: "\u53D1\u9001\u5931\u8D25\u65E5\u62A5\u7684\u6BCF\u65E5\u53D1\u9001\u65F6\u95F4\uFF0C\u683C\u5F0F\u4E3A HH:mm",
37077
+ enableAutoReindex: "\u662F\u5426\u542F\u7528\u56DE\u58F0\u6D1E\u6570\u636E\u5E93\u81EA\u52A8\u91CD\u6574\uFF0C\u5F00\u542F\u540E\u4F1A\u5728\u6BCF\u65E5\u6307\u5B9A\u65F6\u95F4\u81EA\u52A8\u8865\u9F50 public ID \u95F4\u9699\u5E76\u5199\u5165\u65E5\u5FD7",
37078
+ autoReindexTime: "\u56DE\u58F0\u6D1E\u6570\u636E\u5E93\u81EA\u52A8\u91CD\u6574\u7684\u6BCF\u65E5\u6267\u884C\u65F6\u95F4\uFF0C\u683C\u5F0F\u4E3A HH:mm"
36746
37079
  };
36747
37080
 
36748
37081
  // src/config/config.ts
@@ -36788,7 +37121,11 @@ var Config = import_koishi2.Schema.intersect([
36788
37121
  sendFailureHandlingMode: import_koishi2.Schema.union(["auto-delete", "daily-report", "ignore"]).default("ignore"),
36789
37122
  sendFailureSummaryAdminId: import_koishi2.Schema.string().default(""),
36790
37123
  sendFailureSummaryTime: import_koishi2.Schema.string().default("09:00")
36791
- }).description("\u53D1\u9001\u5931\u8D25\u5904\u7406")
37124
+ }).description("\u53D1\u9001\u5931\u8D25\u5904\u7406"),
37125
+ import_koishi2.Schema.object({
37126
+ enableAutoReindex: import_koishi2.Schema.boolean().default(false),
37127
+ autoReindexTime: import_koishi2.Schema.string().default("00:00")
37128
+ }).description("\u81EA\u52A8\u7EF4\u62A4")
36792
37129
  ]).i18n({
36793
37130
  "zh-CN": zh_CN_default2
36794
37131
  });
@@ -36796,6 +37133,19 @@ var Config = import_koishi2.Schema.intersect([
36796
37133
  // src/index.ts
36797
37134
  var name = "echo-cave";
36798
37135
  var inject = ["database"];
37136
+ function toV3Record(record) {
37137
+ return {
37138
+ id: record.id,
37139
+ channelId: record.channelId,
37140
+ createTime: new Date(record.createTime),
37141
+ userId: record.userId,
37142
+ originUserId: record.originUserId,
37143
+ type: record.type,
37144
+ content: record.content,
37145
+ relatedUsers: [...record.relatedUsers],
37146
+ drawCount: record.drawCount
37147
+ };
37148
+ }
36799
37149
  function apply(ctx, cfg) {
36800
37150
  ctx.i18n.define("zh-CN", zh_CN_default);
36801
37151
  ctx.model.extend(
@@ -36816,6 +37166,26 @@ function apply(ctx, cfg) {
36816
37166
  autoInc: true
36817
37167
  }
36818
37168
  );
37169
+ ctx.model.extend(
37170
+ ACTIVE_CAVE_TABLE,
37171
+ {
37172
+ entryId: "unsigned",
37173
+ id: "unsigned",
37174
+ channelId: "string",
37175
+ createTime: "timestamp",
37176
+ userId: "string",
37177
+ originUserId: "string",
37178
+ type: "string",
37179
+ content: "text",
37180
+ relatedUsers: "list",
37181
+ drawCount: { type: "unsigned", initial: 0 }
37182
+ },
37183
+ {
37184
+ primary: "entryId",
37185
+ autoInc: true,
37186
+ unique: ["id"]
37187
+ }
37188
+ );
36819
37189
  ctx.model.extend(
36820
37190
  "echo_cave_v2",
36821
37191
  {
@@ -36871,6 +37241,22 @@ function apply(ctx, cfg) {
36871
37241
  await database.upsert("echo_cave_v2", data2);
36872
37242
  }
36873
37243
  );
37244
+ ctx.model.migrate(
37245
+ "echo_cave_v2",
37246
+ {
37247
+ entryId: "unsigned"
37248
+ },
37249
+ async (database) => {
37250
+ const existing = await database.get(ACTIVE_CAVE_TABLE, {});
37251
+ if (existing.length > 0) {
37252
+ return;
37253
+ }
37254
+ const data2 = (await database.get("echo_cave_v2", {})).sort((a5, b5) => a5.id - b5.id);
37255
+ for (const record of data2) {
37256
+ await database.create(ACTIVE_CAVE_TABLE, toV3Record(record));
37257
+ }
37258
+ }
37259
+ );
36874
37260
  ctx.command("cave [target:text]").action(
36875
37261
  async ({ session }, target) => await getCave(ctx, session, cfg, target)
36876
37262
  );
@@ -36920,7 +37306,11 @@ function apply(ctx, cfg) {
36920
37306
  ctx.command("cave.admin.reindex").action(
36921
37307
  async ({ session }) => await reindexCaveIds(ctx, session, cfg)
36922
37308
  );
37309
+ ctx.command("cave.admin.restore-reindex <backupPath:text>").action(
37310
+ async ({ session }, backupPath) => await restoreReindexBackup(ctx, session, cfg, backupPath)
37311
+ );
36923
37312
  registerSendFailureSummaryScheduler(ctx, cfg);
37313
+ registerAutoReindexScheduler(ctx, cfg);
36924
37314
  }
36925
37315
  // Annotate the CommonJS export names for ESM import in node:
36926
37316
  0 && (module.exports = {
package/lib/index.d.ts CHANGED
@@ -4,6 +4,7 @@ export declare const name = "echo-cave";
4
4
  export declare const inject: string[];
5
5
  export * from './config/config';
6
6
  export interface EchoCave {
7
+ entryId?: number;
7
8
  id: number;
8
9
  channelId: string;
9
10
  createTime: Date;
@@ -32,6 +33,7 @@ declare module 'koishi' {
32
33
  interface Tables {
33
34
  echo_cave: EchoCave;
34
35
  echo_cave_v2: EchoCave;
36
+ echo_cave_v3: EchoCave;
35
37
  echo_cave_send_failure: EchoCaveSendFailure;
36
38
  echo_cave_user_state: EchoCaveUserState;
37
39
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-echo-cave",
3
3
  "description": "Group echo cave",
4
- "version": "1.29.9",
4
+ "version": "1.29.11",
5
5
  "main": "lib/index.cjs",
6
6
  "typings": "lib/index.d.ts",
7
7
  "type": "module",
@@ -36,8 +36,8 @@
36
36
  "koishi": "^4.18.10"
37
37
  },
38
38
  "dependencies": {
39
- "@aws-sdk/client-s3": "^3.1027.0",
40
- "@aws-sdk/s3-request-presigner": "^3.1027.0",
39
+ "@aws-sdk/client-s3": "^3.1029.0",
40
+ "@aws-sdk/s3-request-presigner": "^3.1029.0",
41
41
  "@pynickle/koishi-plugin-adapter-onebot": "^1.0.0",
42
42
  "axios": "^1.15.0",
43
43
  "uuid": "^11.1.0"