koishi-plugin-echo-cave 1.30.0 → 1.31.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/README.md +2 -0
- package/lib/config/config.d.ts +2 -0
- package/lib/core/cave-store.d.ts +9 -1
- package/lib/core/command/admin.d.ts +1 -0
- package/lib/index.cjs +405 -137
- package/lib/index.d.ts +4 -0
- package/lib/utils/media/media-helper.d.ts +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -85,6 +85,7 @@ npm install koishi-plugin-echo-cave
|
|
|
85
85
|
| `autoBindSingleForwardUser` | boolean | `false` | 转发消息仅识别到一个用户时,是否默认自动绑定该用户 |
|
|
86
86
|
| `forwardSpecialUserHandlingMode` | `'ignore' \| 'reject' \| 'confirm'` | `'ignore'` | 检测到转发消息中包含用户 `1094950020` 时的处理方式:忽略、提醒后拒绝存储、或提醒并要求确认后再存储 |
|
|
87
87
|
| `alpha` | number | `0.2` | 加权随机抽取的调整因子,控制抽取次数对概率的影响程度,值越大影响越明显 |
|
|
88
|
+
| `s3UploadFailureFallbackMode` | `'local' \| 'original-link'` | `'local'` | 仅在 `mediaStorage='s3'` 时生效:S3 上传失败后回退到本地存储,或保留原始链接 |
|
|
88
89
|
| `enableAutoReindex` | boolean | `false` | 是否启用每日自动重整回声洞 public ID |
|
|
89
90
|
| `autoReindexTime` | string | `00:00` | 自动重整的每日执行时间,格式为 `HH:mm` |
|
|
90
91
|
|
|
@@ -108,6 +109,7 @@ npm install koishi-plugin-echo-cave
|
|
|
108
109
|
- 如果重排前备份写入失败,命令会直接终止,不会开始改写数据库
|
|
109
110
|
- 若开启 `enableAutoReindex`,插件会在 `autoReindexTime` 指定的每日时间自动执行同样的 public ID 重排,并输出日志
|
|
110
111
|
- 如果需要恢复,可执行 `cave.admin.restore-reindex <备份路径>` 读取对应的 JSON 备份并回写数据库
|
|
112
|
+
- 若 `mediaStorage='s3'` 且上传失败,可通过 `s3UploadFailureFallbackMode` 指定回退到本地存储,或直接保留原始媒体链接
|
|
111
113
|
|
|
112
114
|
## 🤝 贡献指南
|
|
113
115
|
|
package/lib/config/config.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { Schema } from 'koishi';
|
|
|
2
2
|
export type SendFailureHandlingMode = 'auto-delete' | 'daily-report' | 'ignore';
|
|
3
3
|
export type OversizedMediaCleanupMode = 'delete-cave' | 'keep-cave';
|
|
4
4
|
export type ForwardSpecialUserHandlingMode = 'ignore' | 'reject' | 'confirm';
|
|
5
|
+
export type S3UploadFailureFallbackMode = 'local' | 'original-link';
|
|
5
6
|
export interface Config {
|
|
6
7
|
adminMessageProtection?: boolean;
|
|
7
8
|
adminIds?: string[];
|
|
@@ -22,6 +23,7 @@ export interface Config {
|
|
|
22
23
|
forwardSpecialUserHandlingMode?: ForwardSpecialUserHandlingMode;
|
|
23
24
|
alpha?: number;
|
|
24
25
|
mediaStorage?: 'local' | 's3';
|
|
26
|
+
s3UploadFailureFallbackMode?: S3UploadFailureFallbackMode;
|
|
25
27
|
s3Bucket?: string;
|
|
26
28
|
s3Region?: string;
|
|
27
29
|
s3Endpoint?: string;
|
package/lib/core/cave-store.d.ts
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import { Context } from 'koishi';
|
|
2
2
|
import { EchoCave } from '../index';
|
|
3
3
|
export declare const ACTIVE_CAVE_TABLE: "echo_cave_v3";
|
|
4
|
-
export interface
|
|
4
|
+
export interface CaveMediaUrlFields {
|
|
5
|
+
fileUrls: string[];
|
|
6
|
+
imageUrls: string[];
|
|
7
|
+
recordUrls: string[];
|
|
8
|
+
videoUrls: string[];
|
|
9
|
+
}
|
|
10
|
+
export declare function createEmptyCaveMediaUrlFields(): CaveMediaUrlFields;
|
|
11
|
+
export declare function normalizeCaveMediaUrlFields(cave?: Partial<CaveMediaUrlFields> | null): CaveMediaUrlFields;
|
|
12
|
+
export interface CaveSnapshotRecord extends CaveMediaUrlFields {
|
|
5
13
|
id: number;
|
|
6
14
|
channelId: string;
|
|
7
15
|
createTime: Date;
|
|
@@ -5,6 +5,7 @@ export declare function mergeCavesBetweenChannels(ctx: Context, session: Session
|
|
|
5
5
|
export declare function migrateLegacyLocalMedia(ctx: Context, session: Session, cfg: Config): Promise<string>;
|
|
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
|
+
export declare function backfillCaveMediaUrls(ctx: Context, session: Session, cfg: Config, idRangesOption?: string): Promise<string>;
|
|
8
9
|
export declare function reindexCaveIds(ctx: Context, session: Session, cfg: Config): Promise<string>;
|
|
9
10
|
export declare function restoreReindexBackup(ctx: Context, session: Session, cfg: Config, backupPathInput?: string): Promise<string>;
|
|
10
11
|
export declare function registerAutoReindexScheduler(ctx: Context, cfg: Config): void;
|
package/lib/index.cjs
CHANGED
|
@@ -33460,7 +33460,30 @@ module.exports = __toCommonJS(index_exports);
|
|
|
33460
33460
|
|
|
33461
33461
|
// src/core/cave-store.ts
|
|
33462
33462
|
var ACTIVE_CAVE_TABLE = "echo_cave_v3";
|
|
33463
|
+
function createEmptyCaveMediaUrlFields() {
|
|
33464
|
+
return {
|
|
33465
|
+
fileUrls: [],
|
|
33466
|
+
imageUrls: [],
|
|
33467
|
+
recordUrls: [],
|
|
33468
|
+
videoUrls: []
|
|
33469
|
+
};
|
|
33470
|
+
}
|
|
33471
|
+
function normalizeStringList(value) {
|
|
33472
|
+
if (!Array.isArray(value)) {
|
|
33473
|
+
return [];
|
|
33474
|
+
}
|
|
33475
|
+
return value.filter((item) => typeof item === "string");
|
|
33476
|
+
}
|
|
33477
|
+
function normalizeCaveMediaUrlFields(cave) {
|
|
33478
|
+
return {
|
|
33479
|
+
fileUrls: normalizeStringList(cave?.fileUrls),
|
|
33480
|
+
imageUrls: normalizeStringList(cave?.imageUrls),
|
|
33481
|
+
recordUrls: normalizeStringList(cave?.recordUrls),
|
|
33482
|
+
videoUrls: normalizeStringList(cave?.videoUrls)
|
|
33483
|
+
};
|
|
33484
|
+
}
|
|
33463
33485
|
function toCaveSnapshotRecord(cave) {
|
|
33486
|
+
const mediaUrlFields = normalizeCaveMediaUrlFields(cave);
|
|
33464
33487
|
return {
|
|
33465
33488
|
id: cave.id,
|
|
33466
33489
|
channelId: cave.channelId,
|
|
@@ -33471,7 +33494,8 @@ function toCaveSnapshotRecord(cave) {
|
|
|
33471
33494
|
content: cave.content,
|
|
33472
33495
|
relatedUsers: [...cave.relatedUsers],
|
|
33473
33496
|
drawCount: cave.drawCount,
|
|
33474
|
-
picDrawCount: cave.picDrawCount ?? 0
|
|
33497
|
+
picDrawCount: cave.picDrawCount ?? 0,
|
|
33498
|
+
...mediaUrlFields
|
|
33475
33499
|
};
|
|
33476
33500
|
}
|
|
33477
33501
|
function toCaveBackupRecord(cave) {
|
|
@@ -33501,6 +33525,7 @@ async function removeCaveByEntryId(ctx, entryId) {
|
|
|
33501
33525
|
await ctx.database.remove(ACTIVE_CAVE_TABLE, entryId);
|
|
33502
33526
|
}
|
|
33503
33527
|
async function createCaveRecord(ctx, cave) {
|
|
33528
|
+
const mediaUrlFields = normalizeCaveMediaUrlFields(cave);
|
|
33504
33529
|
return await ctx.database.create(ACTIVE_CAVE_TABLE, {
|
|
33505
33530
|
id: cave.id,
|
|
33506
33531
|
channelId: cave.channelId,
|
|
@@ -33511,7 +33536,8 @@ async function createCaveRecord(ctx, cave) {
|
|
|
33511
33536
|
content: cave.content,
|
|
33512
33537
|
relatedUsers: [...cave.relatedUsers],
|
|
33513
33538
|
drawCount: cave.drawCount,
|
|
33514
|
-
picDrawCount: cave.picDrawCount ?? 0
|
|
33539
|
+
picDrawCount: cave.picDrawCount ?? 0,
|
|
33540
|
+
...mediaUrlFields
|
|
33515
33541
|
});
|
|
33516
33542
|
}
|
|
33517
33543
|
async function updateCaveByEntryId(ctx, entryId, data2) {
|
|
@@ -33557,6 +33583,7 @@ async function checkUsersInGroup(ctx, session, userIds) {
|
|
|
33557
33583
|
var import_axios = __toESM(require("axios"), 1);
|
|
33558
33584
|
var import_client_s3 = __toESM(require_dist_cjs71(), 1);
|
|
33559
33585
|
var import_s3_request_presigner = __toESM(require_dist_cjs73(), 1);
|
|
33586
|
+
var import_node_crypto = require("node:crypto");
|
|
33560
33587
|
var import_node_fs = require("node:fs");
|
|
33561
33588
|
var import_node_path = __toESM(require("node:path"), 1);
|
|
33562
33589
|
var import_node_stream = require("node:stream");
|
|
@@ -33611,9 +33638,108 @@ function createEmptyStats() {
|
|
|
33611
33638
|
mediaFailed: 0
|
|
33612
33639
|
};
|
|
33613
33640
|
}
|
|
33641
|
+
function getMediaUrlFieldName(type) {
|
|
33642
|
+
switch (type) {
|
|
33643
|
+
case "image":
|
|
33644
|
+
return "imageUrls";
|
|
33645
|
+
case "video":
|
|
33646
|
+
return "videoUrls";
|
|
33647
|
+
case "record":
|
|
33648
|
+
return "recordUrls";
|
|
33649
|
+
default:
|
|
33650
|
+
return "fileUrls";
|
|
33651
|
+
}
|
|
33652
|
+
}
|
|
33653
|
+
function hasMediaUrlFieldChanges(current, next) {
|
|
33654
|
+
const normalizedCurrent = normalizeCaveMediaUrlFields(current);
|
|
33655
|
+
return JSON.stringify(normalizedCurrent.fileUrls) !== JSON.stringify(next.fileUrls) || JSON.stringify(normalizedCurrent.imageUrls) !== JSON.stringify(next.imageUrls) || JSON.stringify(normalizedCurrent.recordUrls) !== JSON.stringify(next.recordUrls) || JSON.stringify(normalizedCurrent.videoUrls) !== JSON.stringify(next.videoUrls);
|
|
33656
|
+
}
|
|
33657
|
+
function createUniqueStringList(values) {
|
|
33658
|
+
return [...new Set(values.filter(Boolean))];
|
|
33659
|
+
}
|
|
33660
|
+
function normalizeSourceIdentity(fileRef) {
|
|
33661
|
+
if (fileRef.startsWith("file:///")) {
|
|
33662
|
+
return normalizePathForComparison(fromFileUri(fileRef));
|
|
33663
|
+
}
|
|
33664
|
+
if (import_node_path.default.isAbsolute(fileRef) || /^[A-Za-z]:[\\/]/.test(fileRef)) {
|
|
33665
|
+
return normalizePathForComparison(fileRef);
|
|
33666
|
+
}
|
|
33667
|
+
if (fileRef.startsWith("s3://")) {
|
|
33668
|
+
return fileRef.toLowerCase();
|
|
33669
|
+
}
|
|
33670
|
+
return fileRef;
|
|
33671
|
+
}
|
|
33672
|
+
function getFileNameFromRef(fileRef, type) {
|
|
33673
|
+
if (fileRef.startsWith("file:///")) {
|
|
33674
|
+
return import_node_path.default.basename(fromFileUri(fileRef));
|
|
33675
|
+
}
|
|
33676
|
+
if (import_node_path.default.isAbsolute(fileRef) || /^[A-Za-z]:[\\/]/.test(fileRef)) {
|
|
33677
|
+
return import_node_path.default.basename(fileRef);
|
|
33678
|
+
}
|
|
33679
|
+
if (fileRef.startsWith("s3://")) {
|
|
33680
|
+
const location = parseS3Uri(fileRef);
|
|
33681
|
+
if (location) {
|
|
33682
|
+
return import_node_path.default.basename(location.key);
|
|
33683
|
+
}
|
|
33684
|
+
}
|
|
33685
|
+
if (/^https?:\/\//i.test(fileRef)) {
|
|
33686
|
+
try {
|
|
33687
|
+
return import_node_path.default.basename(new URL(fileRef).pathname) || `media.${getDefaultExtension(type)}`;
|
|
33688
|
+
} catch (error2) {
|
|
33689
|
+
return `media.${getDefaultExtension(type)}`;
|
|
33690
|
+
}
|
|
33691
|
+
}
|
|
33692
|
+
return `media.${getDefaultExtension(type)}`;
|
|
33693
|
+
}
|
|
33694
|
+
function buildStableTransferFileName(fileRef, type) {
|
|
33695
|
+
const sourceName = getFileNameFromRef(fileRef, type);
|
|
33696
|
+
const parsedName = import_node_path.default.parse(sourceName);
|
|
33697
|
+
const baseName = parsedName.name.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "media";
|
|
33698
|
+
const extension = parsedName.ext.replace(/^\./, "") || getDefaultExtension(type);
|
|
33699
|
+
const hash = (0, import_node_crypto.createHash)("sha1").update(normalizeSourceIdentity(fileRef)).digest("hex").slice(0, 12);
|
|
33700
|
+
return `${baseName}-${hash}.${extension}`;
|
|
33701
|
+
}
|
|
33702
|
+
async function removeFileIfExists(filePath) {
|
|
33703
|
+
try {
|
|
33704
|
+
await import_node_fs.promises.unlink(filePath);
|
|
33705
|
+
} catch (error2) {
|
|
33706
|
+
const fileError = error2;
|
|
33707
|
+
if (fileError.code !== "ENOENT") {
|
|
33708
|
+
throw error2;
|
|
33709
|
+
}
|
|
33710
|
+
}
|
|
33711
|
+
}
|
|
33712
|
+
async function writeTransferredLocalMedia(ctx, fileRef, channelId, type, buffer) {
|
|
33713
|
+
const targetDir = getLocalMediaV2Dir(ctx, channelId, type);
|
|
33714
|
+
await import_node_fs.promises.mkdir(targetDir, { recursive: true });
|
|
33715
|
+
const targetPath = import_node_path.default.join(targetDir, buildStableTransferFileName(fileRef, type));
|
|
33716
|
+
await removeFileIfExists(targetPath);
|
|
33717
|
+
await import_node_fs.promises.writeFile(targetPath, buffer);
|
|
33718
|
+
return targetPath;
|
|
33719
|
+
}
|
|
33720
|
+
async function uploadTransferredMediaToS3(cfg, fileRef, channelId, type, buffer, contentType) {
|
|
33721
|
+
const client = getS3Client(cfg);
|
|
33722
|
+
const bucket = cfg.s3Bucket;
|
|
33723
|
+
if (!bucket) {
|
|
33724
|
+
throw new Error("S3 bucket is not configured.");
|
|
33725
|
+
}
|
|
33726
|
+
const key = buildS3Key(cfg, channelId, type, buildStableTransferFileName(fileRef, type));
|
|
33727
|
+
await client.send(
|
|
33728
|
+
new import_client_s3.PutObjectCommand({
|
|
33729
|
+
Bucket: bucket,
|
|
33730
|
+
Key: key,
|
|
33731
|
+
Body: buffer,
|
|
33732
|
+
ContentType: contentType
|
|
33733
|
+
})
|
|
33734
|
+
);
|
|
33735
|
+
return { bucket, key };
|
|
33736
|
+
}
|
|
33614
33737
|
function getStorageMode(cfg) {
|
|
33615
33738
|
return cfg.mediaStorage === "s3" ? "s3" : "local";
|
|
33616
33739
|
}
|
|
33740
|
+
function getS3UploadFailureFallbackMode(cfg) {
|
|
33741
|
+
return cfg.s3UploadFailureFallbackMode === "original-link" ? "original-link" : "local";
|
|
33742
|
+
}
|
|
33617
33743
|
function hasS3Config(cfg) {
|
|
33618
33744
|
return Boolean(cfg.s3Bucket && cfg.s3Region);
|
|
33619
33745
|
}
|
|
@@ -33987,12 +34113,13 @@ async function createPresignedGetUrl(cfg, location) {
|
|
|
33987
34113
|
}
|
|
33988
34114
|
async function transferLocalFileToChannel(ctx, sourcePath, type, targetChannelId, mode) {
|
|
33989
34115
|
const targetDir = getLocalMediaV2Dir(ctx, targetChannelId, type);
|
|
33990
|
-
|
|
34116
|
+
await import_node_fs.promises.mkdir(targetDir, { recursive: true });
|
|
34117
|
+
const sourceRef = toFileUri(sourcePath);
|
|
34118
|
+
const targetPath = import_node_path.default.join(targetDir, buildStableTransferFileName(sourceRef, type));
|
|
34119
|
+
if (normalizePathForComparison(sourcePath) === normalizePathForComparison(targetPath)) {
|
|
33991
34120
|
return toFileUri(sourcePath);
|
|
33992
34121
|
}
|
|
33993
|
-
await
|
|
33994
|
-
const extension = import_node_path.default.extname(sourcePath).slice(1) || getDefaultExtension(type);
|
|
33995
|
-
const targetPath = import_node_path.default.join(targetDir, `${(0, import_uuid2.v4)().replace(/-/g, "")}.${extension}`);
|
|
34122
|
+
await removeFileIfExists(targetPath);
|
|
33996
34123
|
await import_node_fs.promises.copyFile(sourcePath, targetPath);
|
|
33997
34124
|
return toFileUri(targetPath);
|
|
33998
34125
|
}
|
|
@@ -34001,8 +34128,12 @@ async function transferS3ObjectToChannel(cfg, source, type, targetChannelId, mod
|
|
|
34001
34128
|
if (!bucket) {
|
|
34002
34129
|
throw new Error("S3 bucket is not configured.");
|
|
34003
34130
|
}
|
|
34004
|
-
const
|
|
34005
|
-
|
|
34131
|
+
const key = buildS3Key(
|
|
34132
|
+
cfg,
|
|
34133
|
+
targetChannelId,
|
|
34134
|
+
type,
|
|
34135
|
+
buildStableTransferFileName(toS3Uri(source), type)
|
|
34136
|
+
);
|
|
34006
34137
|
const target = { bucket, key };
|
|
34007
34138
|
if (source.bucket === target.bucket && source.key === target.key) {
|
|
34008
34139
|
return toS3Uri(source);
|
|
@@ -34051,19 +34182,19 @@ async function transferMediaRefToChannel(ctx, fileRef, type, targetChannelId, ta
|
|
|
34051
34182
|
if (targetMode === "local") {
|
|
34052
34183
|
if (isFileUri(fileRef)) {
|
|
34053
34184
|
const localSourcePath = fromFileUri(fileRef);
|
|
34054
|
-
const
|
|
34185
|
+
const nextRef = await transferLocalFileToChannel(
|
|
34055
34186
|
ctx,
|
|
34056
34187
|
localSourcePath,
|
|
34057
34188
|
type,
|
|
34058
34189
|
targetChannelId,
|
|
34059
34190
|
transferMode
|
|
34060
34191
|
);
|
|
34061
|
-
const
|
|
34062
|
-
nextRef
|
|
34063
|
-
rollback: createDeleteLocalFilePlan(fromFileUri(
|
|
34192
|
+
const plan2 = {
|
|
34193
|
+
nextRef,
|
|
34194
|
+
rollback: createDeleteLocalFilePlan(fromFileUri(nextRef)).rollback
|
|
34064
34195
|
};
|
|
34065
34196
|
if (transferMode === "move") {
|
|
34066
|
-
|
|
34197
|
+
plan2.commit = async () => {
|
|
34067
34198
|
try {
|
|
34068
34199
|
await import_node_fs.promises.unlink(localSourcePath);
|
|
34069
34200
|
base64Cache.delete(localSourcePath);
|
|
@@ -34076,20 +34207,25 @@ async function transferMediaRefToChannel(ctx, fileRef, type, targetChannelId, ta
|
|
|
34076
34207
|
} else {
|
|
34077
34208
|
stats.mediaCopied += 1;
|
|
34078
34209
|
}
|
|
34079
|
-
state2.transferPlans.set(cacheKey,
|
|
34080
|
-
return
|
|
34210
|
+
state2.transferPlans.set(cacheKey, plan2);
|
|
34211
|
+
return plan2;
|
|
34081
34212
|
}
|
|
34082
|
-
const
|
|
34083
|
-
const
|
|
34084
|
-
|
|
34085
|
-
|
|
34213
|
+
const loaded = await loadMediaBuffer(ctx, fileRef, cfg);
|
|
34214
|
+
const targetPath = await writeTransferredLocalMedia(
|
|
34215
|
+
ctx,
|
|
34216
|
+
fileRef,
|
|
34217
|
+
targetChannelId,
|
|
34218
|
+
type,
|
|
34219
|
+
loaded.buffer
|
|
34220
|
+
);
|
|
34221
|
+
const plan = {
|
|
34086
34222
|
nextRef: toFileUri(targetPath),
|
|
34087
34223
|
rollback: createDeleteLocalFilePlan(targetPath).rollback
|
|
34088
34224
|
};
|
|
34089
34225
|
if (transferMode === "move" && isS3Uri(fileRef)) {
|
|
34090
34226
|
const source = parseS3Uri(fileRef);
|
|
34091
34227
|
if (source) {
|
|
34092
|
-
|
|
34228
|
+
plan.commit = async () => {
|
|
34093
34229
|
try {
|
|
34094
34230
|
await deleteS3Object(cfg, source);
|
|
34095
34231
|
} catch (error2) {
|
|
@@ -34102,88 +34238,113 @@ async function transferMediaRefToChannel(ctx, fileRef, type, targetChannelId, ta
|
|
|
34102
34238
|
} else {
|
|
34103
34239
|
stats.mediaCopied += 1;
|
|
34104
34240
|
}
|
|
34105
|
-
state2.transferPlans.set(cacheKey,
|
|
34106
|
-
return
|
|
34241
|
+
state2.transferPlans.set(cacheKey, plan);
|
|
34242
|
+
return plan;
|
|
34107
34243
|
}
|
|
34108
|
-
|
|
34109
|
-
|
|
34110
|
-
|
|
34111
|
-
|
|
34244
|
+
try {
|
|
34245
|
+
if (isS3Uri(fileRef)) {
|
|
34246
|
+
const source = parseS3Uri(fileRef);
|
|
34247
|
+
if (!source) {
|
|
34248
|
+
throw new Error(`Invalid S3 uri: ${fileRef}`);
|
|
34249
|
+
}
|
|
34250
|
+
const nextRef2 = await transferS3ObjectToChannel(
|
|
34251
|
+
cfg,
|
|
34252
|
+
source,
|
|
34253
|
+
type,
|
|
34254
|
+
targetChannelId,
|
|
34255
|
+
transferMode
|
|
34256
|
+
);
|
|
34257
|
+
const targetLocation2 = parseS3Uri(nextRef2);
|
|
34258
|
+
const plan2 = {
|
|
34259
|
+
nextRef: nextRef2,
|
|
34260
|
+
rollback: targetLocation2 ? async () => {
|
|
34261
|
+
try {
|
|
34262
|
+
await deleteS3Object(cfg, targetLocation2);
|
|
34263
|
+
} catch (error2) {
|
|
34264
|
+
return;
|
|
34265
|
+
}
|
|
34266
|
+
} : void 0
|
|
34267
|
+
};
|
|
34268
|
+
if (transferMode === "move") {
|
|
34269
|
+
plan2.commit = async () => {
|
|
34270
|
+
try {
|
|
34271
|
+
await deleteS3Object(cfg, source);
|
|
34272
|
+
} catch (error2) {
|
|
34273
|
+
return;
|
|
34274
|
+
}
|
|
34275
|
+
};
|
|
34276
|
+
stats.mediaMoved += 1;
|
|
34277
|
+
stats.mediaDeleted += 1;
|
|
34278
|
+
} else {
|
|
34279
|
+
stats.mediaCopied += 1;
|
|
34280
|
+
}
|
|
34281
|
+
state2.transferPlans.set(cacheKey, plan2);
|
|
34282
|
+
return plan2;
|
|
34112
34283
|
}
|
|
34113
|
-
const
|
|
34284
|
+
const loaded = await loadMediaBuffer(ctx, fileRef, cfg);
|
|
34285
|
+
const location = await uploadTransferredMediaToS3(
|
|
34114
34286
|
cfg,
|
|
34115
|
-
|
|
34116
|
-
type,
|
|
34287
|
+
fileRef,
|
|
34117
34288
|
targetChannelId,
|
|
34118
|
-
|
|
34289
|
+
type,
|
|
34290
|
+
loaded.buffer,
|
|
34291
|
+
loaded.contentType
|
|
34119
34292
|
);
|
|
34120
|
-
const
|
|
34121
|
-
const
|
|
34122
|
-
|
|
34123
|
-
|
|
34293
|
+
const nextRef = toS3Uri(location);
|
|
34294
|
+
const targetLocation = parseS3Uri(nextRef);
|
|
34295
|
+
const plan = {
|
|
34296
|
+
nextRef,
|
|
34297
|
+
rollback: targetLocation ? async () => {
|
|
34124
34298
|
try {
|
|
34125
|
-
await deleteS3Object(cfg,
|
|
34299
|
+
await deleteS3Object(cfg, targetLocation);
|
|
34126
34300
|
} catch (error2) {
|
|
34127
34301
|
return;
|
|
34128
34302
|
}
|
|
34129
34303
|
} : void 0
|
|
34130
34304
|
};
|
|
34131
|
-
if (transferMode === "move") {
|
|
34132
|
-
|
|
34305
|
+
if (transferMode === "move" && isFileUri(fileRef)) {
|
|
34306
|
+
const currentPath = fromFileUri(fileRef);
|
|
34307
|
+
plan.commit = async () => {
|
|
34133
34308
|
try {
|
|
34134
|
-
await
|
|
34309
|
+
await import_node_fs.promises.unlink(currentPath);
|
|
34310
|
+
base64Cache.delete(currentPath);
|
|
34311
|
+
base64Cache.delete(fileRef);
|
|
34135
34312
|
} catch (error2) {
|
|
34136
34313
|
return;
|
|
34137
34314
|
}
|
|
34138
34315
|
};
|
|
34139
|
-
stats.mediaMoved += 1;
|
|
34140
34316
|
stats.mediaDeleted += 1;
|
|
34141
|
-
|
|
34142
|
-
stats.mediaCopied += 1;
|
|
34317
|
+
stats.mediaMoved += 1;
|
|
34143
34318
|
}
|
|
34144
|
-
state2.transferPlans.set(cacheKey,
|
|
34145
|
-
|
|
34146
|
-
|
|
34147
|
-
|
|
34148
|
-
|
|
34149
|
-
|
|
34150
|
-
|
|
34151
|
-
|
|
34152
|
-
|
|
34153
|
-
|
|
34154
|
-
|
|
34155
|
-
|
|
34156
|
-
|
|
34157
|
-
|
|
34158
|
-
|
|
34159
|
-
|
|
34160
|
-
|
|
34161
|
-
|
|
34162
|
-
|
|
34163
|
-
|
|
34164
|
-
|
|
34165
|
-
|
|
34166
|
-
|
|
34167
|
-
|
|
34168
|
-
|
|
34169
|
-
|
|
34170
|
-
|
|
34171
|
-
|
|
34172
|
-
try {
|
|
34173
|
-
await import_node_fs.promises.unlink(currentPath);
|
|
34174
|
-
base64Cache.delete(currentPath);
|
|
34175
|
-
base64Cache.delete(fileRef);
|
|
34176
|
-
} catch (error2) {
|
|
34177
|
-
return;
|
|
34178
|
-
}
|
|
34179
|
-
};
|
|
34180
|
-
stats.mediaDeleted += 1;
|
|
34181
|
-
stats.mediaMoved += 1;
|
|
34319
|
+
state2.transferPlans.set(cacheKey, plan);
|
|
34320
|
+
stats.mediaUploaded += 1;
|
|
34321
|
+
await hooks?.onS3Upload?.(type, nextRef);
|
|
34322
|
+
return plan;
|
|
34323
|
+
} catch (error2) {
|
|
34324
|
+
const fallbackMode = getS3UploadFailureFallbackMode(cfg);
|
|
34325
|
+
ctx.logger.warn(
|
|
34326
|
+
`Failed to store media in S3 for ${fileRef}, fallback mode=${fallbackMode}: ${error2}`
|
|
34327
|
+
);
|
|
34328
|
+
if (fallbackMode === "original-link") {
|
|
34329
|
+
const plan = { nextRef: fileRef };
|
|
34330
|
+
state2.transferPlans.set(cacheKey, plan);
|
|
34331
|
+
return plan;
|
|
34332
|
+
}
|
|
34333
|
+
const fallbackPlan = await transferMediaRefToChannel(
|
|
34334
|
+
ctx,
|
|
34335
|
+
fileRef,
|
|
34336
|
+
type,
|
|
34337
|
+
targetChannelId,
|
|
34338
|
+
"local",
|
|
34339
|
+
transferMode,
|
|
34340
|
+
cfg,
|
|
34341
|
+
state2,
|
|
34342
|
+
stats,
|
|
34343
|
+
hooks
|
|
34344
|
+
);
|
|
34345
|
+
state2.transferPlans.set(cacheKey, fallbackPlan);
|
|
34346
|
+
return fallbackPlan;
|
|
34182
34347
|
}
|
|
34183
|
-
state2.transferPlans.set(cacheKey, plan);
|
|
34184
|
-
stats.mediaUploaded += 1;
|
|
34185
|
-
await hooks?.onS3Upload?.(type, nextRef);
|
|
34186
|
-
return plan;
|
|
34187
34348
|
}
|
|
34188
34349
|
async function mutateMessageContent(ctx, content, handler) {
|
|
34189
34350
|
const parsed = JSON.parse(content);
|
|
@@ -34234,15 +34395,15 @@ async function processStoredMessageMedia(ctx, content, cfg, channelId, progressO
|
|
|
34234
34395
|
});
|
|
34235
34396
|
return rewritten.content;
|
|
34236
34397
|
}
|
|
34237
|
-
async function
|
|
34398
|
+
async function collectStoredMessageMediaUrls(ctx, content) {
|
|
34238
34399
|
const parsed = JSON.parse(content);
|
|
34239
34400
|
const elements = Array.isArray(parsed) ? parsed : [parsed];
|
|
34240
|
-
const refs =
|
|
34401
|
+
const refs = createEmptyCaveMediaUrlFields();
|
|
34241
34402
|
const visitElement = async (element) => {
|
|
34242
34403
|
if (isMediaType(element.type)) {
|
|
34243
34404
|
const fileRef = getElementFileRef(ctx, element, element.type);
|
|
34244
34405
|
if (fileRef) {
|
|
34245
|
-
refs.push(fileRef);
|
|
34406
|
+
refs[getMediaUrlFieldName(element.type)].push(fileRef);
|
|
34246
34407
|
}
|
|
34247
34408
|
}
|
|
34248
34409
|
const contentValue = element.data?.content;
|
|
@@ -34255,7 +34416,16 @@ async function collectMessageMediaRefs(ctx, content) {
|
|
|
34255
34416
|
for (const element of elements) {
|
|
34256
34417
|
await visitElement(element);
|
|
34257
34418
|
}
|
|
34258
|
-
return
|
|
34419
|
+
return {
|
|
34420
|
+
fileUrls: createUniqueStringList(refs.fileUrls),
|
|
34421
|
+
imageUrls: createUniqueStringList(refs.imageUrls),
|
|
34422
|
+
recordUrls: createUniqueStringList(refs.recordUrls),
|
|
34423
|
+
videoUrls: createUniqueStringList(refs.videoUrls)
|
|
34424
|
+
};
|
|
34425
|
+
}
|
|
34426
|
+
async function collectMessageMediaRefs(ctx, content) {
|
|
34427
|
+
const refs = await collectStoredMessageMediaUrls(ctx, content);
|
|
34428
|
+
return [...refs.fileUrls, ...refs.imageUrls, ...refs.recordUrls, ...refs.videoUrls];
|
|
34259
34429
|
}
|
|
34260
34430
|
async function collectCavesReferencingFiles(ctx, targetPaths) {
|
|
34261
34431
|
const targetSet = new Set(targetPaths.map((filePath) => normalizePathForComparison(filePath)));
|
|
@@ -34449,15 +34619,28 @@ async function saveMedia(ctx, mediaElement, type, cfg, channelId, progressOption
|
|
|
34449
34619
|
return mediaUrl;
|
|
34450
34620
|
}
|
|
34451
34621
|
if (getStorageMode(cfg) === "s3") {
|
|
34452
|
-
|
|
34453
|
-
|
|
34454
|
-
|
|
34455
|
-
|
|
34456
|
-
|
|
34457
|
-
|
|
34458
|
-
|
|
34459
|
-
|
|
34460
|
-
|
|
34622
|
+
try {
|
|
34623
|
+
const location = await uploadBufferToS3(
|
|
34624
|
+
cfg,
|
|
34625
|
+
channelId,
|
|
34626
|
+
type,
|
|
34627
|
+
buffer,
|
|
34628
|
+
extension,
|
|
34629
|
+
contentType || getMimeTypeByExtension(`.${extension}`, type)
|
|
34630
|
+
);
|
|
34631
|
+
return toS3Uri(location);
|
|
34632
|
+
} catch (error2) {
|
|
34633
|
+
const fallbackMode = getS3UploadFailureFallbackMode(cfg);
|
|
34634
|
+
ctx.logger.warn(
|
|
34635
|
+
`Failed to upload ${type} to S3 for ${mediaUrl}, fallback mode=${fallbackMode}: ${error2}`
|
|
34636
|
+
);
|
|
34637
|
+
if (fallbackMode === "original-link") {
|
|
34638
|
+
return mediaUrl;
|
|
34639
|
+
}
|
|
34640
|
+
const savedPath2 = await writeLocalMedia(ctx, channelId, type, buffer, extension);
|
|
34641
|
+
await debouncedCleanup(ctx, cfg, type, channelId);
|
|
34642
|
+
return savedPath2;
|
|
34643
|
+
}
|
|
34461
34644
|
}
|
|
34462
34645
|
const savedPath = await writeLocalMedia(ctx, channelId, type, buffer, extension);
|
|
34463
34646
|
await debouncedCleanup(ctx, cfg, type, channelId);
|
|
@@ -34707,11 +34890,16 @@ async function migrateLocalMediaToV2(ctx, cfg) {
|
|
|
34707
34890
|
stats,
|
|
34708
34891
|
(fileRef, type) => isLegacyLocalMediaRef(ctx, fileRef, type)
|
|
34709
34892
|
);
|
|
34710
|
-
|
|
34893
|
+
const mediaUrlFields = await collectStoredMessageMediaUrls(ctx, rewritten.content);
|
|
34894
|
+
const shouldUpdateMediaFields = hasMediaUrlFieldChanges(cave, mediaUrlFields);
|
|
34895
|
+
if (rewritten.content !== cave.content || shouldUpdateMediaFields) {
|
|
34711
34896
|
if (typeof cave.entryId !== "number") {
|
|
34712
34897
|
throw new Error(`missing_entry_id_for_cave_${cave.id}`);
|
|
34713
34898
|
}
|
|
34714
|
-
await updateCaveByEntryId(ctx, cave.entryId, {
|
|
34899
|
+
await updateCaveByEntryId(ctx, cave.entryId, {
|
|
34900
|
+
content: rewritten.content,
|
|
34901
|
+
...mediaUrlFields
|
|
34902
|
+
});
|
|
34715
34903
|
await runTransferPlans(rewritten.plans, "commit");
|
|
34716
34904
|
}
|
|
34717
34905
|
} catch (error2) {
|
|
@@ -34751,11 +34939,16 @@ async function migrateLocalMediaToS3(ctx, cfg, keepLocal, createHooks) {
|
|
|
34751
34939
|
(fileRef, type) => isFileUri(fileRef) || isLegacyLocalMediaRef(ctx, fileRef, type),
|
|
34752
34940
|
hooks
|
|
34753
34941
|
);
|
|
34754
|
-
|
|
34942
|
+
const mediaUrlFields = await collectStoredMessageMediaUrls(ctx, rewritten.content);
|
|
34943
|
+
const shouldUpdateMediaFields = hasMediaUrlFieldChanges(cave, mediaUrlFields);
|
|
34944
|
+
if (rewritten.content !== cave.content || shouldUpdateMediaFields) {
|
|
34755
34945
|
if (typeof cave.entryId !== "number") {
|
|
34756
34946
|
throw new Error(`missing_entry_id_for_cave_${cave.id}`);
|
|
34757
34947
|
}
|
|
34758
|
-
await updateCaveByEntryId(ctx, cave.entryId, {
|
|
34948
|
+
await updateCaveByEntryId(ctx, cave.entryId, {
|
|
34949
|
+
content: rewritten.content,
|
|
34950
|
+
...mediaUrlFields
|
|
34951
|
+
});
|
|
34759
34952
|
await runTransferPlans(rewritten.plans, "commit");
|
|
34760
34953
|
await hooks?.onMigrationCommitted?.();
|
|
34761
34954
|
}
|
|
@@ -34798,6 +34991,7 @@ async function mergeChannelCaves(ctx, cfg, sourceChannelId, targetChannelId, kee
|
|
|
34798
34991
|
);
|
|
34799
34992
|
if (keepSource) {
|
|
34800
34993
|
const nextId = await getNextCavePublicId(ctx);
|
|
34994
|
+
const mediaUrlFields = await collectStoredMessageMediaUrls(ctx, rewritten.content);
|
|
34801
34995
|
await createCaveRecord(ctx, {
|
|
34802
34996
|
id: nextId,
|
|
34803
34997
|
channelId: targetChannelId,
|
|
@@ -34808,15 +35002,18 @@ async function mergeChannelCaves(ctx, cfg, sourceChannelId, targetChannelId, kee
|
|
|
34808
35002
|
content: rewritten.content,
|
|
34809
35003
|
relatedUsers: cave.relatedUsers,
|
|
34810
35004
|
drawCount: cave.drawCount,
|
|
34811
|
-
picDrawCount: cave.picDrawCount ?? 0
|
|
35005
|
+
picDrawCount: cave.picDrawCount ?? 0,
|
|
35006
|
+
...mediaUrlFields
|
|
34812
35007
|
});
|
|
34813
35008
|
} else {
|
|
34814
35009
|
if (typeof cave.entryId !== "number") {
|
|
34815
35010
|
throw new Error(`missing_entry_id_for_cave_${cave.id}`);
|
|
34816
35011
|
}
|
|
35012
|
+
const mediaUrlFields = await collectStoredMessageMediaUrls(ctx, rewritten.content);
|
|
34817
35013
|
await updateCaveByEntryId(ctx, cave.entryId, {
|
|
34818
35014
|
channelId: targetChannelId,
|
|
34819
|
-
content: rewritten.content
|
|
35015
|
+
content: rewritten.content,
|
|
35016
|
+
...mediaUrlFields
|
|
34820
35017
|
});
|
|
34821
35018
|
}
|
|
34822
35019
|
await runTransferPlans(rewritten.plans, "commit");
|
|
@@ -34920,12 +35117,21 @@ var import_node_fs2 = require("node:fs");
|
|
|
34920
35117
|
var import_node_path2 = __toESM(require("node:path"), 1);
|
|
34921
35118
|
var REINDEX_SPECIAL_OFFSET = 1e6;
|
|
34922
35119
|
var caveMaintenanceLock = false;
|
|
35120
|
+
function hasStringArray(value) {
|
|
35121
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
35122
|
+
}
|
|
35123
|
+
function areMediaUrlFieldsEmpty(fields) {
|
|
35124
|
+
const normalized = normalizeCaveMediaUrlFields(fields);
|
|
35125
|
+
return normalized.fileUrls.length === 0 && normalized.imageUrls.length === 0 && normalized.recordUrls.length === 0 && normalized.videoUrls.length === 0;
|
|
35126
|
+
}
|
|
34923
35127
|
function cloneCaveRecord(cave, id) {
|
|
35128
|
+
const mediaUrlFields = normalizeCaveMediaUrlFields(cave);
|
|
34924
35129
|
return {
|
|
34925
35130
|
...cave,
|
|
34926
35131
|
createTime: new Date(cave.createTime),
|
|
34927
35132
|
id,
|
|
34928
|
-
relatedUsers: [...cave.relatedUsers]
|
|
35133
|
+
relatedUsers: [...cave.relatedUsers],
|
|
35134
|
+
...mediaUrlFields
|
|
34929
35135
|
};
|
|
34930
35136
|
}
|
|
34931
35137
|
async function removeCavesByEntryIds(ctx, entryIds) {
|
|
@@ -34964,6 +35170,7 @@ function buildSequentialCaveSnapshot(caves) {
|
|
|
34964
35170
|
return [...caves].sort((a5, b5) => a5.id - b5.id).map((cave, index) => cloneCaveRecord(cave, index + 1));
|
|
34965
35171
|
}
|
|
34966
35172
|
function normalizeCaveRecord(cave) {
|
|
35173
|
+
const mediaUrlFields = normalizeCaveMediaUrlFields(cave);
|
|
34967
35174
|
return {
|
|
34968
35175
|
channelId: cave.channelId,
|
|
34969
35176
|
content: cave.content,
|
|
@@ -34974,7 +35181,8 @@ function normalizeCaveRecord(cave) {
|
|
|
34974
35181
|
originUserId: cave.originUserId,
|
|
34975
35182
|
relatedUsers: [...cave.relatedUsers],
|
|
34976
35183
|
type: cave.type,
|
|
34977
|
-
userId: cave.userId
|
|
35184
|
+
userId: cave.userId,
|
|
35185
|
+
...mediaUrlFields
|
|
34978
35186
|
};
|
|
34979
35187
|
}
|
|
34980
35188
|
function normalizeBackupRecord(cave) {
|
|
@@ -34990,7 +35198,7 @@ function isEchoCaveRecord(value) {
|
|
|
34990
35198
|
if (!isRecordObject(value)) {
|
|
34991
35199
|
return false;
|
|
34992
35200
|
}
|
|
34993
|
-
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" && (typeof value.picDrawCount === "number" || typeof value.picDrawCount === "undefined");
|
|
35201
|
+
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" && (typeof value.picDrawCount === "number" || typeof value.picDrawCount === "undefined") && (typeof value.fileUrls === "undefined" || hasStringArray(value.fileUrls)) && (typeof value.imageUrls === "undefined" || hasStringArray(value.imageUrls)) && (typeof value.recordUrls === "undefined" || hasStringArray(value.recordUrls)) && (typeof value.videoUrls === "undefined" || hasStringArray(value.videoUrls));
|
|
34994
35202
|
}
|
|
34995
35203
|
function isCaveBackupRecord(value) {
|
|
34996
35204
|
if (!isEchoCaveRecord(value)) {
|
|
@@ -35342,6 +35550,69 @@ async function inspectMediaRefsForMigration(ctx, session, cfg, idRangesOption) {
|
|
|
35342
35550
|
);
|
|
35343
35551
|
}
|
|
35344
35552
|
}
|
|
35553
|
+
async function backfillCaveMediaUrls(ctx, session, cfg, idRangesOption) {
|
|
35554
|
+
const accessError = ensureAdminPrivateAccess(session, cfg);
|
|
35555
|
+
if (accessError) {
|
|
35556
|
+
return accessError;
|
|
35557
|
+
}
|
|
35558
|
+
const idRanges = parseIdRanges(idRangesOption);
|
|
35559
|
+
if (idRanges === null) {
|
|
35560
|
+
return session.text("commands.cave.admin.backfill-media-urls.messages.invalidRange");
|
|
35561
|
+
}
|
|
35562
|
+
const displayRanges = idRangesOption?.trim() || getAllRangesLabel(session);
|
|
35563
|
+
const caves = await getAllCaves(ctx);
|
|
35564
|
+
const candidates = caves.filter(
|
|
35565
|
+
(cave) => isIdInRanges(cave.id, idRanges) && areMediaUrlFieldsEmpty(cave)
|
|
35566
|
+
);
|
|
35567
|
+
if (candidates.length === 0) {
|
|
35568
|
+
return session.text("commands.cave.admin.backfill-media-urls.messages.noCandidates", {
|
|
35569
|
+
idRanges: displayRanges
|
|
35570
|
+
});
|
|
35571
|
+
}
|
|
35572
|
+
const confirmed = await requestSecondConfirmation(
|
|
35573
|
+
ctx,
|
|
35574
|
+
session,
|
|
35575
|
+
session.text("commands.cave.admin.backfill-media-urls.messages.confirmSummary", {
|
|
35576
|
+
idRanges: displayRanges,
|
|
35577
|
+
candidateCount: candidates.length
|
|
35578
|
+
}),
|
|
35579
|
+
session.text("commands.cave.admin.backfill-media-urls.messages.confirmRetry"),
|
|
35580
|
+
session.text("commands.cave.admin.backfill-media-urls.messages.confirmTimeout"),
|
|
35581
|
+
session.text("commands.cave.admin.backfill-media-urls.messages.confirmCancelled")
|
|
35582
|
+
);
|
|
35583
|
+
if (!confirmed) {
|
|
35584
|
+
return;
|
|
35585
|
+
}
|
|
35586
|
+
let updatedCount = 0;
|
|
35587
|
+
let skippedWithoutMedia = 0;
|
|
35588
|
+
const failedRecordIds = [];
|
|
35589
|
+
for (const cave of candidates) {
|
|
35590
|
+
try {
|
|
35591
|
+
if (typeof cave.entryId !== "number") {
|
|
35592
|
+
throw new Error(`missing_entry_id_for_cave_${cave.id}`);
|
|
35593
|
+
}
|
|
35594
|
+
const mediaUrlFields = await collectStoredMessageMediaUrls(ctx, cave.content);
|
|
35595
|
+
if (areMediaUrlFieldsEmpty(mediaUrlFields)) {
|
|
35596
|
+
skippedWithoutMedia += 1;
|
|
35597
|
+
continue;
|
|
35598
|
+
}
|
|
35599
|
+
await updateCaveByEntryId(ctx, cave.entryId, mediaUrlFields);
|
|
35600
|
+
updatedCount += 1;
|
|
35601
|
+
} catch (error2) {
|
|
35602
|
+
failedRecordIds.push(cave.id);
|
|
35603
|
+
ctx.logger.warn(`Failed to backfill media URLs for cave #${cave.id}: ${error2}`);
|
|
35604
|
+
}
|
|
35605
|
+
}
|
|
35606
|
+
return appendFailedRecordSummary(
|
|
35607
|
+
session,
|
|
35608
|
+
session.text("commands.cave.admin.backfill-media-urls.messages.backfillDone", {
|
|
35609
|
+
scannedRecords: candidates.length,
|
|
35610
|
+
updatedCount,
|
|
35611
|
+
skippedWithoutMedia
|
|
35612
|
+
}),
|
|
35613
|
+
failedRecordIds
|
|
35614
|
+
);
|
|
35615
|
+
}
|
|
35345
35616
|
function buildReindexPlan(caves) {
|
|
35346
35617
|
const maxId = caves.reduce((currentMax, cave) => Math.max(currentMax, cave.id), 0);
|
|
35347
35618
|
const offset = maxId + caves.length + REINDEX_SPECIAL_OFFSET;
|
|
@@ -35741,6 +36012,7 @@ async function addCave(ctx, session, cfg, userIds) {
|
|
|
35741
36012
|
finalParsedUserIds,
|
|
35742
36013
|
originName
|
|
35743
36014
|
);
|
|
36015
|
+
const mediaUrlFields = await collectStoredMessageMediaUrls(ctx, content);
|
|
35744
36016
|
try {
|
|
35745
36017
|
const nextId = await getNextCavePublicId(ctx);
|
|
35746
36018
|
const result = await ctx.database.create(ACTIVE_CAVE_TABLE, {
|
|
@@ -35752,7 +36024,9 @@ async function addCave(ctx, session, cfg, userIds) {
|
|
|
35752
36024
|
type,
|
|
35753
36025
|
content,
|
|
35754
36026
|
relatedUsers: finalParsedUserIds,
|
|
35755
|
-
|
|
36027
|
+
drawCount: 0,
|
|
36028
|
+
picDrawCount: 0,
|
|
36029
|
+
...mediaUrlFields
|
|
35756
36030
|
});
|
|
35757
36031
|
return session.text(".msgSaved", { id: result.id, relatedUsers: relatedUsersFormatted });
|
|
35758
36032
|
} catch (error2) {
|
|
@@ -37036,6 +37310,18 @@ var zh_CN_default = {
|
|
|
37036
37310
|
confirmTimeout: "\u231B \u4E8C\u6B21\u786E\u8BA4\u8D85\u65F6\uFF0C\u68C0\u6D4B\u672A\u6267\u884C\u3002"
|
|
37037
37311
|
}
|
|
37038
37312
|
},
|
|
37313
|
+
"cave.admin.backfill-media-urls": {
|
|
37314
|
+
description: "\u4E3A\u5386\u53F2\u56DE\u58F0\u6D1E\u56DE\u586B\u5A92\u4F53 URL \u5217",
|
|
37315
|
+
messages: {
|
|
37316
|
+
invalidRange: "\u274C \u8303\u56F4\u53C2\u6570\u683C\u5F0F\u65E0\u6548\u3002\u8BF7\u4F7F\u7528 0-100,105-240 \u6216 12,18-20 \u8FD9\u6837\u7684\u683C\u5F0F\u3002",
|
|
37317
|
+
noCandidates: "\u{1F50D} \u5728\u8303\u56F4 {idRanges} \u5185\u672A\u627E\u5230\u56DB\u5217\u5A92\u4F53 URL \u90FD\u4E3A\u7A7A\u7684\u8BB0\u5F55\u3002",
|
|
37318
|
+
confirmSummary: "\u26A0\uFE0F \u5373\u5C06\u4E3A\u5386\u53F2\u56DE\u58F0\u6D1E\u56DE\u586B\u5A92\u4F53 URL \u5217\n\u68C0\u6D4B\u8303\u56F4\uFF1A{idRanges}\n\u5019\u9009\u8BB0\u5F55\u6570\uFF1A{candidateCount}\n\n\u672C\u6B21\u64CD\u4F5C\u53EA\u4F1A\u5904\u7406 fileUrls\u3001imageUrls\u3001recordUrls\u3001videoUrls \u56DB\u5217\u90FD\u4E3A\u7A7A\u7684\u8BB0\u5F55\uFF0C\u5E76\u4ECE\u5B58\u50A8\u6D88\u606F\u5185\u5BB9\u4E2D\u63D0\u53D6\u5A92\u4F53\u5F15\u7528\u540E\u5199\u56DE\u6570\u636E\u5E93\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",
|
|
37319
|
+
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",
|
|
37320
|
+
confirmCancelled: "\u{1F6D1} \u5DF2\u53D6\u6D88\u672C\u6B21\u5A92\u4F53 URL \u56DE\u586B\u3002",
|
|
37321
|
+
confirmTimeout: "\u231B \u4E8C\u6B21\u786E\u8BA4\u8D85\u65F6\uFF0C\u5A92\u4F53 URL \u56DE\u586B\u672A\u6267\u884C\u3002",
|
|
37322
|
+
backfillDone: "\u2705 \u5A92\u4F53 URL \u56DE\u586B\u5B8C\u6210\uFF1A\u626B\u63CF {scannedRecords} \u6761\u5019\u9009\u8BB0\u5F55\uFF0C\u6210\u529F\u66F4\u65B0 {updatedCount} \u6761\uFF0C\u65E0\u5A92\u4F53\u53EF\u56DE\u586B {skippedWithoutMedia} \u6761\u3002"
|
|
37323
|
+
}
|
|
37324
|
+
},
|
|
37039
37325
|
"cave.admin.reindex": {
|
|
37040
37326
|
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",
|
|
37041
37327
|
messages: {
|
|
@@ -37088,6 +37374,7 @@ var zh_CN_default2 = {
|
|
|
37088
37374
|
forwardSpecialUserHandlingMode: "\u68C0\u6D4B\u5230\u7279\u6B8A\u7528\u6237 1094950020 \u65F6\u7684\u5904\u7406\u65B9\u5F0F\uFF1Aignore \u4E3A\u5FFD\u7565\uFF0Creject \u4E3A\u63D0\u9192\u5E76\u62D2\u7EDD\u5B58\u50A8\uFF0Cconfirm \u4E3A\u63D0\u9192\u3001\u5C55\u793A\u8F6C\u53D1\u5185\u5BB9\u5E76\u8981\u6C42\u786E\u8BA4\u540E\u518D\u5B58\u50A8",
|
|
37089
37375
|
alpha: "\u52A0\u6743\u968F\u673A\u62BD\u53D6\u7684\u8C03\u6574\u56E0\u5B50\uFF0C\u63A7\u5236\u62BD\u53D6\u6B21\u6570\u5BF9\u6982\u7387\u7684\u5F71\u54CD\u7A0B\u5EA6\uFF0C\u503C\u8D8A\u5927\u5F71\u54CD\u8D8A\u660E\u663E",
|
|
37090
37376
|
mediaStorage: "\u5A92\u4F53\u5B58\u50A8\u65B9\u5F0F\uFF1Alocal \u4E3A\u672C\u5730\uFF0Cs3 \u4E3A\u5BF9\u8C61\u5B58\u50A8",
|
|
37377
|
+
s3UploadFailureFallbackMode: "\u4EC5\u5728\u5A92\u4F53\u5B58\u50A8\u65B9\u5F0F\u4E3A s3 \u65F6\u751F\u6548\uFF1AS3 \u4E0A\u4F20\u5931\u8D25\u540E\u56DE\u9000\u5230\u672C\u5730\u5B58\u50A8\uFF0C\u6216\u4FDD\u7559\u539F\u59CB\u94FE\u63A5",
|
|
37091
37378
|
s3Bucket: "S3 \u5B58\u50A8\u6876\u540D\u79F0",
|
|
37092
37379
|
s3Region: "S3 \u533A\u57DF",
|
|
37093
37380
|
s3Endpoint: "S3 Endpoint\uFF0C\u53EF\u7528\u4E8E\u517C\u5BB9 S3 \u7684\u79C1\u6709\u670D\u52A1",
|
|
@@ -37133,6 +37420,7 @@ var Config = import_koishi2.Schema.intersect([
|
|
|
37133
37420
|
}).description("\u5A92\u4F53\u5904\u7406"),
|
|
37134
37421
|
import_koishi2.Schema.object({
|
|
37135
37422
|
mediaStorage: import_koishi2.Schema.union(["local", "s3"]).default("local"),
|
|
37423
|
+
s3UploadFailureFallbackMode: import_koishi2.Schema.union(["local", "original-link"]).default("local"),
|
|
37136
37424
|
s3Bucket: import_koishi2.Schema.string().default(""),
|
|
37137
37425
|
s3Region: import_koishi2.Schema.string().default(""),
|
|
37138
37426
|
s3Endpoint: import_koishi2.Schema.string().default(""),
|
|
@@ -37161,6 +37449,7 @@ var Config = import_koishi2.Schema.intersect([
|
|
|
37161
37449
|
var name = "echo-cave";
|
|
37162
37450
|
var inject = ["database"];
|
|
37163
37451
|
function toV3Record(record) {
|
|
37452
|
+
const mediaUrlFields = createEmptyCaveMediaUrlFields();
|
|
37164
37453
|
return {
|
|
37165
37454
|
id: record.id,
|
|
37166
37455
|
channelId: record.channelId,
|
|
@@ -37171,30 +37460,12 @@ function toV3Record(record) {
|
|
|
37171
37460
|
content: record.content,
|
|
37172
37461
|
relatedUsers: [...record.relatedUsers],
|
|
37173
37462
|
drawCount: record.drawCount,
|
|
37174
|
-
picDrawCount: record.picDrawCount ?? 0
|
|
37463
|
+
picDrawCount: record.picDrawCount ?? 0,
|
|
37464
|
+
...mediaUrlFields
|
|
37175
37465
|
};
|
|
37176
37466
|
}
|
|
37177
37467
|
function apply(ctx, cfg) {
|
|
37178
37468
|
ctx.i18n.define("zh-CN", zh_CN_default);
|
|
37179
|
-
ctx.model.extend(
|
|
37180
|
-
"echo_cave",
|
|
37181
|
-
{
|
|
37182
|
-
id: "unsigned",
|
|
37183
|
-
channelId: "string",
|
|
37184
|
-
createTime: "timestamp",
|
|
37185
|
-
userId: "string",
|
|
37186
|
-
originUserId: "string",
|
|
37187
|
-
type: "string",
|
|
37188
|
-
content: "text",
|
|
37189
|
-
relatedUsers: "list",
|
|
37190
|
-
drawCount: { type: "unsigned", initial: 0 },
|
|
37191
|
-
picDrawCount: { type: "unsigned", initial: 0 }
|
|
37192
|
-
},
|
|
37193
|
-
{
|
|
37194
|
-
primary: "id",
|
|
37195
|
-
autoInc: true
|
|
37196
|
-
}
|
|
37197
|
-
);
|
|
37198
37469
|
ctx.model.extend(
|
|
37199
37470
|
ACTIVE_CAVE_TABLE,
|
|
37200
37471
|
{
|
|
@@ -37208,7 +37479,11 @@ function apply(ctx, cfg) {
|
|
|
37208
37479
|
content: "text",
|
|
37209
37480
|
relatedUsers: "list",
|
|
37210
37481
|
drawCount: { type: "unsigned", initial: 0 },
|
|
37211
|
-
picDrawCount: { type: "unsigned", initial: 0 }
|
|
37482
|
+
picDrawCount: { type: "unsigned", initial: 0 },
|
|
37483
|
+
fileUrls: { type: "list", initial: [] },
|
|
37484
|
+
imageUrls: { type: "list", initial: [] },
|
|
37485
|
+
recordUrls: { type: "list", initial: [] },
|
|
37486
|
+
videoUrls: { type: "list", initial: [] }
|
|
37212
37487
|
},
|
|
37213
37488
|
{
|
|
37214
37489
|
primary: "entryId",
|
|
@@ -37262,16 +37537,6 @@ function apply(ctx, cfg) {
|
|
|
37262
37537
|
primary: "userId"
|
|
37263
37538
|
}
|
|
37264
37539
|
);
|
|
37265
|
-
ctx.model.migrate(
|
|
37266
|
-
"echo_cave",
|
|
37267
|
-
{
|
|
37268
|
-
id: "unsigned"
|
|
37269
|
-
},
|
|
37270
|
-
async (database) => {
|
|
37271
|
-
const data2 = await database.get("echo_cave", {});
|
|
37272
|
-
await database.upsert("echo_cave_v2", data2);
|
|
37273
|
-
}
|
|
37274
|
-
);
|
|
37275
37540
|
ctx.model.migrate("echo_cave_v2", {}, async (database) => {
|
|
37276
37541
|
const existing = await database.get(ACTIVE_CAVE_TABLE, {});
|
|
37277
37542
|
if (existing.length > 0) {
|
|
@@ -37311,6 +37576,9 @@ function apply(ctx, cfg) {
|
|
|
37311
37576
|
ctx.command("cave.admin.inspect-media [idRanges:text]").action(
|
|
37312
37577
|
async ({ session }, idRanges) => await inspectMediaRefsForMigration(ctx, session, cfg, idRanges)
|
|
37313
37578
|
);
|
|
37579
|
+
ctx.command("cave.admin.backfill-media-urls [idRanges:text]").action(
|
|
37580
|
+
async ({ session }, idRanges) => await backfillCaveMediaUrls(ctx, session, cfg, idRanges)
|
|
37581
|
+
);
|
|
37314
37582
|
ctx.command("cave.admin.reindex").action(async ({ session }) => await reindexCaveIds(ctx, session, cfg));
|
|
37315
37583
|
ctx.command("cave.admin.restore-reindex <backupPath:text>").action(
|
|
37316
37584
|
async ({ session }, backupPath) => await restoreReindexBackup(ctx, session, cfg, backupPath)
|
package/lib/index.d.ts
CHANGED
|
@@ -15,6 +15,10 @@ export interface EchoCave {
|
|
|
15
15
|
relatedUsers: string[];
|
|
16
16
|
drawCount: number;
|
|
17
17
|
picDrawCount: number;
|
|
18
|
+
fileUrls: string[];
|
|
19
|
+
imageUrls: string[];
|
|
20
|
+
recordUrls: string[];
|
|
21
|
+
videoUrls: string[];
|
|
18
22
|
}
|
|
19
23
|
export interface EchoCaveSendFailure {
|
|
20
24
|
id: number;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Config } from '../../config/config';
|
|
2
|
+
import { CaveMediaUrlFields } from '../../core/cave-store';
|
|
2
3
|
import { Context, Session } from 'koishi';
|
|
3
4
|
export type MediaType = 'image' | 'video' | 'file' | 'record';
|
|
4
5
|
type MaybeMediaElement = {
|
|
@@ -22,6 +23,7 @@ interface CaveMediaRefs {
|
|
|
22
23
|
refs: string[];
|
|
23
24
|
}
|
|
24
25
|
export declare function processStoredMessageMedia(ctx: Context, content: string, cfg: Config, channelId: string, progressOptions?: MediaSaveProgressOptions): Promise<string>;
|
|
26
|
+
export declare function collectStoredMessageMediaUrls(ctx: Context, content: string): Promise<CaveMediaUrlFields>;
|
|
25
27
|
export declare function inspectCaveMediaRefs(ctx: Context, shouldInclude?: (caveId: number) => boolean): Promise<CaveMediaRefs[]>;
|
|
26
28
|
export declare function saveMedia(ctx: Context, mediaElement: Record<string, unknown>, type: MediaType, cfg: Config, channelId: string, progressOptions?: MediaSaveProgressOptions): Promise<string>;
|
|
27
29
|
export declare function processMediaElement(ctx: Context, element: MaybeMediaElement, cfg: Config, channelId: string, progressOptions?: MediaSaveProgressOptions): Promise<MaybeMediaElement>;
|