koishi-plugin-media-luna 0.0.43 → 0.0.45

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.
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
- // 缓存服务 - 本地文件缓存(使用数据库存储元数据)
2
+ // 统一存储服务 - 支持本地/S3/WebDAV 多后端
3
+ // 根据配置的 backend 自动选择存储方式
3
4
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
5
  if (k2 === undefined) k2 = k;
5
6
  var desc = Object.getOwnPropertyDescriptor(m, k);
@@ -39,10 +40,12 @@ const fs = __importStar(require("fs"));
39
40
  const path = __importStar(require("path"));
40
41
  const crypto = __importStar(require("crypto"));
41
42
  const core_1 = require("../../core");
43
+ const s3_1 = require("./utils/s3");
44
+ const webdav_1 = require("./utils/webdav");
45
+ const mime_1 = require("./utils/mime");
42
46
  /**
43
- * 本地缓存服务
44
- * 提供文件缓存和 HTTP 访问支持
45
- * 使用数据库存储元数据,支持 sourceHash 和 contentHash 两级去重
47
+ * 统一存储服务
48
+ * 根据配置的 backend 自动选择存储方式(local/s3/webdav)
46
49
  */
47
50
  class CacheService {
48
51
  logger;
@@ -51,7 +54,7 @@ class CacheService {
51
54
  publicPath;
52
55
  publicBaseUrl;
53
56
  config;
54
- /** 基础URL(从 selfUrl 获取),用于生成访问链接 */
57
+ /** 基础URL(从 selfUrl 获取),用于生成本地访问链接 */
55
58
  baseUrl = '';
56
59
  /** 内存缓存(加速查询) */
57
60
  memoryCache = new Map();
@@ -61,7 +64,7 @@ class CacheService {
61
64
  this.ctx = ctx;
62
65
  this.logger = (0, core_1.createPluginLogger)(ctx.logger('media-luna'), 'cache');
63
66
  this.config = config;
64
- // 使用配置的目录路径,或默认值
67
+ // 本地缓存目录(即使使用 S3/WebDAV,也可能需要临时存储)
65
68
  this.cacheRoot = path.join(ctx.baseDir, config.cacheDir || 'data/media-luna/cache');
66
69
  this.publicPath = config.publicPath || '/media-luna/cache';
67
70
  this.publicBaseUrl = config.publicBaseUrl?.replace(/\/$/, '') || null;
@@ -70,7 +73,7 @@ class CacheService {
70
73
  this.initialize().catch(e => {
71
74
  this.logger.error('Failed to initialize cache service: %s', e);
72
75
  });
73
- this.logger.info('Cache service initialized at: %s', this.cacheRoot);
76
+ this.logger.info('Cache service initialized, backend: %s', config.backend || 'local');
74
77
  }
75
78
  /** 异步初始化 */
76
79
  async initialize() {
@@ -79,8 +82,10 @@ class CacheService {
79
82
  try {
80
83
  // 从数据库加载到内存缓存
81
84
  await this.loadFromDatabase();
82
- // 清理过期缓存
83
- await this.cleanupExpired();
85
+ // 清理过期缓存(仅本地模式)
86
+ if (this.config.backend === 'local' || !this.config.backend) {
87
+ await this.cleanupExpired();
88
+ }
84
89
  this.initialized = true;
85
90
  }
86
91
  catch (e) {
@@ -92,7 +97,7 @@ class CacheService {
92
97
  try {
93
98
  const records = await this.ctx.database.get('medialuna_asset_cache', {});
94
99
  for (const record of records) {
95
- const localPath = path.join(this.cacheRoot, `${record.contentHash}${this.getExtensionFromMime(record.mimeType)}`);
100
+ const localPath = path.join(this.cacheRoot, record.cachedKey);
96
101
  this.memoryCache.set(record.contentHash, {
97
102
  id: record.contentHash,
98
103
  filename: path.basename(record.cachedKey),
@@ -101,7 +106,8 @@ class CacheService {
101
106
  createdAt: record.createdAt,
102
107
  accessedAt: record.lastAccessedAt,
103
108
  localPath,
104
- url: record.cachedUrl
109
+ url: record.cachedUrl,
110
+ backend: record.backend
105
111
  });
106
112
  }
107
113
  this.logger.debug('Loaded %d cache entries from database', records.length);
@@ -113,6 +119,7 @@ class CacheService {
113
119
  /** 更新配置 */
114
120
  updateConfig(config) {
115
121
  this.config = { ...this.config, ...config };
122
+ this.logger.info('Cache config updated, backend: %s', this.config.backend || 'local');
116
123
  }
117
124
  /** 设置基础URL */
118
125
  setBaseUrl(url) {
@@ -125,57 +132,95 @@ class CacheService {
125
132
  }
126
133
  /** 检查缓存是否启用 */
127
134
  isEnabled() {
128
- return this.config.enabled;
135
+ return this.config.enabled && this.config.backend !== 'none';
136
+ }
137
+ /** 获取当前后端类型 */
138
+ getBackend() {
139
+ return this.config.backend || 'local';
129
140
  }
130
- /** 获取完整配置(供中间件使用) */
141
+ /** 获取完整配置 */
131
142
  getConfig() {
132
143
  return this.config;
133
144
  }
145
+ /** 转换为 S3 配置 */
146
+ toS3Config() {
147
+ return {
148
+ endpoint: this.config.s3Endpoint,
149
+ region: this.config.s3Region,
150
+ accessKeyId: this.config.s3AccessKeyId,
151
+ secretAccessKey: this.config.s3SecretAccessKey,
152
+ bucket: this.config.s3Bucket,
153
+ publicBaseUrl: this.config.s3PublicBaseUrl,
154
+ forcePathStyle: this.config.s3ForcePathStyle,
155
+ acl: this.config.s3Acl
156
+ };
157
+ }
158
+ /** 转换为 WebDAV 配置 */
159
+ toWebDavConfig() {
160
+ return {
161
+ endpoint: this.config.webdavEndpoint,
162
+ username: this.config.webdavUsername,
163
+ password: this.config.webdavPassword,
164
+ basePath: this.config.webdavBasePath,
165
+ publicBaseUrl: this.config.webdavPublicBaseUrl
166
+ };
167
+ }
134
168
  /**
135
- * 缓存文件到本地
136
- * @param data 文件数据
137
- * @param mime MIME 类型
138
- * @param filename 文件名
139
- * @param sourceUrl 可选的源 URL(用于 sourceHash 去重)
169
+ * 缓存文件
170
+ * 根据配置的后端自动选择存储方式
140
171
  */
141
172
  async cache(data, mime, filename, sourceUrl) {
142
173
  if (!this.config.enabled) {
143
174
  throw new Error('Cache is disabled');
144
175
  }
145
176
  const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data);
146
- const sizeMB = buffer.length / (1024 * 1024);
147
- if (sizeMB > this.config.maxFileSize) {
148
- throw new Error(`File too large: ${sizeMB.toFixed(2)}MB > ${this.config.maxFileSize}MB`);
177
+ const backend = this.config.backend || 'local';
178
+ // 检查文件大小(仅本地模式限制)
179
+ if (backend === 'local') {
180
+ const sizeMB = buffer.length / (1024 * 1024);
181
+ if (sizeMB > this.config.maxFileSize) {
182
+ throw new Error(`File too large: ${sizeMB.toFixed(2)}MB > ${this.config.maxFileSize}MB`);
183
+ }
149
184
  }
150
185
  // 计算内容哈希
151
186
  const contentHash = this.generateContentHash(buffer);
152
187
  // 检查是否已有相同内容的缓存
153
188
  const existingByContent = await this.findByContentHash(contentHash);
154
189
  if (existingByContent) {
155
- // 更新访问时间
156
190
  await this.updateAccessTime(existingByContent.contentHash);
157
191
  this.logger.debug('Cache hit by content hash: %s', contentHash);
158
192
  return this.dbRecordToCachedFile(existingByContent);
159
193
  }
160
- // 确保有足够空间
161
- await this.ensureCacheSpace(buffer.length);
162
- // 写入文件
194
+ // 根据后端存储文件
163
195
  const ext = this.getExtension(mime, filename);
164
- const localPath = path.join(this.cacheRoot, `${contentHash}${ext}`);
165
- fs.writeFileSync(localPath, buffer);
196
+ const storageKey = `${contentHash}${ext}`;
197
+ let cachedUrl;
198
+ switch (backend) {
199
+ case 'local':
200
+ cachedUrl = await this.storeLocal(buffer, storageKey, contentHash, ext);
201
+ break;
202
+ case 's3':
203
+ cachedUrl = await this.storeS3(buffer, storageKey, mime);
204
+ break;
205
+ case 'webdav':
206
+ cachedUrl = await this.storeWebDav(buffer, storageKey, mime);
207
+ break;
208
+ case 'none':
209
+ throw new Error('Storage backend is set to none');
210
+ default:
211
+ throw new Error(`Unknown storage backend: ${backend}`);
212
+ }
166
213
  // 计算源哈希(如果有源 URL)
167
214
  const sourceHash = sourceUrl ? this.generateSourceHash(sourceUrl) : contentHash;
168
- // 生成访问 URL
169
- const cachedUrl = this.buildUrl(contentHash, ext);
170
215
  // 保存到数据库
171
216
  const now = new Date();
172
217
  await this.ctx.database.create('medialuna_asset_cache', {
173
218
  sourceUrl: sourceUrl || '',
174
219
  sourceHash,
175
220
  contentHash,
176
- backend: 'local',
221
+ backend,
177
222
  cachedUrl,
178
- cachedKey: `${contentHash}${ext}`,
223
+ cachedKey: storageKey,
179
224
  mimeType: mime,
180
225
  fileSize: buffer.length,
181
226
  createdAt: now,
@@ -188,24 +233,51 @@ class CacheService {
188
233
  size: buffer.length,
189
234
  createdAt: now,
190
235
  accessedAt: now,
191
- localPath,
192
- url: cachedUrl
236
+ localPath: path.join(this.cacheRoot, storageKey),
237
+ url: cachedUrl,
238
+ backend
193
239
  };
194
240
  // 更新内存缓存
195
241
  this.memoryCache.set(contentHash, cached);
196
- this.logger.debug('Cached file: %s (%s, %sMB)', contentHash, cached.filename, sizeMB.toFixed(2));
242
+ this.logger.debug('Cached file to %s: %s', backend, contentHash);
197
243
  return cached;
198
244
  }
245
+ /** 存储到本地 */
246
+ async storeLocal(buffer, storageKey, contentHash, ext) {
247
+ // 确保有足够空间
248
+ await this.ensureCacheSpace(buffer.length);
249
+ const localPath = path.join(this.cacheRoot, storageKey);
250
+ fs.writeFileSync(localPath, buffer);
251
+ return this.buildLocalUrl(contentHash, ext);
252
+ }
253
+ /** 存储到 S3 */
254
+ async storeS3(buffer, storageKey, mime) {
255
+ const s3Config = this.toS3Config();
256
+ if (!s3Config.bucket)
257
+ throw new Error('S3 缺少 bucket 配置');
258
+ if (!s3Config.accessKeyId || !s3Config.secretAccessKey)
259
+ throw new Error('S3 需提供访问凭证');
260
+ const result = await (0, s3_1.uploadToS3)(buffer, storageKey, mime, s3Config);
261
+ return result.url;
262
+ }
263
+ /** 存储到 WebDAV */
264
+ async storeWebDav(buffer, storageKey, mime) {
265
+ const webdavConfig = this.toWebDavConfig();
266
+ if (!webdavConfig.endpoint)
267
+ throw new Error('WebDAV 缺少端点配置');
268
+ if (!webdavConfig.username || !webdavConfig.password)
269
+ throw new Error('WebDAV 需提供用户名和密码');
270
+ const result = await (0, webdav_1.uploadToWebDav)(buffer, storageKey, mime, webdavConfig);
271
+ return result.url;
272
+ }
199
273
  /**
200
274
  * 从 URL 下载并缓存
201
- * 支持 sourceHash 去重:相同 URL 不会重复下载
202
275
  */
203
276
  async cacheFromUrl(url) {
204
277
  // 先检查是否已有相同源 URL 的缓存
205
278
  const sourceHash = this.generateSourceHash(url);
206
279
  const existingBySource = await this.findBySourceHash(sourceHash);
207
280
  if (existingBySource) {
208
- // 更新访问时间
209
281
  await this.updateAccessTime(existingBySource.contentHash);
210
282
  this.logger.debug('Cache hit by source hash: %s -> %s', url, sourceHash);
211
283
  return this.dbRecordToCachedFile(existingBySource);
@@ -216,7 +288,6 @@ class CacheService {
216
288
  });
217
289
  const mime = this.guessMimeFromUrl(url);
218
290
  const filename = url.split('/').pop()?.split('?')[0] || 'downloaded';
219
- // 缓存时传入源 URL
220
291
  return this.cache(response, mime, filename, url);
221
292
  }
222
293
  /** 获取缓存文件的访问URL */
@@ -224,39 +295,40 @@ class CacheService {
224
295
  const cached = this.memoryCache.get(id);
225
296
  if (!cached)
226
297
  return null;
227
- if (cached.url)
228
- return cached.url;
229
- const ext = path.extname(cached.localPath);
230
- return this.buildUrl(id, ext);
298
+ return cached.url || null;
231
299
  }
232
300
  /** 获取缓存文件信息 */
233
301
  async get(id) {
234
- // 先查内存缓存
235
302
  let cached = this.memoryCache.get(id);
236
303
  if (!cached) {
237
- // 查数据库
238
304
  const record = await this.findByContentHash(id);
239
305
  if (!record)
240
306
  return null;
241
307
  cached = this.dbRecordToCachedFile(record);
242
308
  this.memoryCache.set(id, cached);
243
309
  }
244
- // 检查文件是否存在
245
- if (!fs.existsSync(cached.localPath)) {
246
- await this.deleteFromDatabase(id);
247
- this.memoryCache.delete(id);
248
- return null;
310
+ // 如果是本地存储,检查文件是否存在
311
+ if (cached.backend === 'local' || !cached.backend) {
312
+ if (!fs.existsSync(cached.localPath)) {
313
+ await this.deleteFromDatabase(id);
314
+ this.memoryCache.delete(id);
315
+ return null;
316
+ }
249
317
  }
250
- // 更新访问时间
251
318
  await this.updateAccessTime(id);
252
319
  cached.accessedAt = new Date();
253
320
  return cached;
254
321
  }
255
- /** 读取缓存文件内容 */
322
+ /** 读取缓存文件内容(仅本地存储可用) */
256
323
  async read(id) {
257
324
  const cached = await this.get(id);
258
325
  if (!cached)
259
326
  return null;
327
+ // 只有本地存储可以直接读取
328
+ if (cached.backend !== 'local' && cached.backend) {
329
+ this.logger.warn('Cannot read non-local cache: %s (backend: %s)', id, cached.backend);
330
+ return null;
331
+ }
260
332
  try {
261
333
  return fs.readFileSync(cached.localPath);
262
334
  }
@@ -265,7 +337,7 @@ class CacheService {
265
337
  return null;
266
338
  }
267
339
  }
268
- /** 读取为 Data URL */
340
+ /** 读取为 Data URL(仅本地存储可用) */
269
341
  async readAsDataUrl(id) {
270
342
  const cached = await this.get(id);
271
343
  if (!cached)
@@ -278,14 +350,17 @@ class CacheService {
278
350
  /** 删除缓存文件 */
279
351
  async delete(id) {
280
352
  const cached = this.memoryCache.get(id);
281
- try {
282
- // 删除本地文件
283
- if (cached && fs.existsSync(cached.localPath)) {
284
- fs.unlinkSync(cached.localPath);
353
+ if (!cached) {
354
+ const record = await this.findByContentHash(id);
355
+ if (record) {
356
+ await this.deleteFromBackend(record.cachedKey, record.backend);
357
+ await this.deleteFromDatabase(id);
285
358
  }
286
- // 从数据库删除
359
+ return true;
360
+ }
361
+ try {
362
+ await this.deleteFromBackend(cached.filename, cached.backend || 'local');
287
363
  await this.deleteFromDatabase(id);
288
- // 从内存缓存删除
289
364
  this.memoryCache.delete(id);
290
365
  return true;
291
366
  }
@@ -294,6 +369,46 @@ class CacheService {
294
369
  return false;
295
370
  }
296
371
  }
372
+ /** 从后端删除文件 */
373
+ async deleteFromBackend(key, backend) {
374
+ switch (backend) {
375
+ case 'local':
376
+ const localPath = path.join(this.cacheRoot, key);
377
+ if (fs.existsSync(localPath)) {
378
+ fs.unlinkSync(localPath);
379
+ }
380
+ break;
381
+ case 's3':
382
+ try {
383
+ await (0, s3_1.deleteFromS3)(key, this.toS3Config());
384
+ }
385
+ catch (e) {
386
+ this.logger.warn('Failed to delete from S3: %s', e);
387
+ }
388
+ break;
389
+ case 'webdav':
390
+ try {
391
+ await this.deleteFromWebDav(key);
392
+ }
393
+ catch (e) {
394
+ this.logger.warn('Failed to delete from WebDAV: %s', e);
395
+ }
396
+ break;
397
+ }
398
+ }
399
+ /** 从 WebDAV 删除 */
400
+ async deleteFromWebDav(key) {
401
+ const config = this.toWebDavConfig();
402
+ const remotePath = config.basePath
403
+ ? `${config.basePath.replace(/\/+$/, '')}/${key}`
404
+ : key;
405
+ const url = `${config.endpoint.replace(/\/+$/, '')}/${remotePath.split('/').map(encodeURIComponent).join('/')}`;
406
+ const auth = Buffer.from(`${config.username}:${config.password}`, 'utf8').toString('base64');
407
+ await fetch(url, {
408
+ method: 'DELETE',
409
+ headers: { Authorization: `Basic ${auth}` }
410
+ });
411
+ }
297
412
  /** 获取统计信息 */
298
413
  async getStats() {
299
414
  const records = await this.ctx.database.get('medialuna_asset_cache', {});
@@ -312,43 +427,61 @@ class CacheService {
312
427
  totalSizeMB: totalSize / (1024 * 1024),
313
428
  maxSizeMB: this.config.maxCacheSize,
314
429
  oldestAccess: oldest,
315
- newestAccess: newest
430
+ newestAccess: newest,
431
+ backend: this.config.backend || 'local'
316
432
  };
317
433
  }
318
434
  /** 清空所有缓存 */
319
435
  async clearAll() {
320
- // 获取所有缓存记录
321
436
  const records = await this.ctx.database.get('medialuna_asset_cache', {});
322
- // 删除所有本地文件
323
437
  for (const record of records) {
324
- const localPath = path.join(this.cacheRoot, record.cachedKey);
325
- try {
326
- if (fs.existsSync(localPath)) {
327
- fs.unlinkSync(localPath);
328
- }
329
- }
330
- catch (e) {
331
- this.logger.warn('Failed to delete file: %s', localPath);
332
- }
438
+ await this.deleteFromBackend(record.cachedKey, record.backend);
333
439
  }
334
- // 清空数据库
335
440
  await this.ctx.database.remove('medialuna_asset_cache', {});
336
- // 清空内存缓存
337
441
  this.memoryCache.clear();
338
442
  this.logger.info('All cache cleared');
339
443
  }
444
+ /**
445
+ * 测试存储连接
446
+ */
447
+ async testConnection() {
448
+ const backend = this.config.backend || 'local';
449
+ if (backend === 'none') {
450
+ return { success: true, message: '存储后端设置为"不使用",无需测试' };
451
+ }
452
+ const testContent = `Media Luna Storage Test - ${new Date().toISOString()}`;
453
+ const testBuffer = Buffer.from(testContent, 'utf-8');
454
+ const testFilename = `_test-${Date.now()}.txt`;
455
+ const testMime = 'text/plain';
456
+ const startTime = Date.now();
457
+ try {
458
+ const cached = await this.cache(testBuffer, testMime, testFilename);
459
+ const duration = Date.now() - startTime;
460
+ // 清理测试文件
461
+ await this.delete(cached.id);
462
+ return {
463
+ success: true,
464
+ message: `存储连接测试成功(耗时 ${duration}ms)`,
465
+ url: cached.url,
466
+ duration
467
+ };
468
+ }
469
+ catch (error) {
470
+ return {
471
+ success: false,
472
+ message: error instanceof Error ? error.message : 'Unknown error'
473
+ };
474
+ }
475
+ }
340
476
  // ========== 数据库操作方法 ==========
341
- /** 通过 sourceHash 查找缓存 */
342
477
  async findBySourceHash(sourceHash) {
343
478
  const records = await this.ctx.database.get('medialuna_asset_cache', { sourceHash });
344
479
  return records[0] || null;
345
480
  }
346
- /** 通过 contentHash 查找缓存 */
347
481
  async findByContentHash(contentHash) {
348
482
  const records = await this.ctx.database.get('medialuna_asset_cache', { contentHash });
349
483
  return records[0] || null;
350
484
  }
351
- /** 更新访问时间 */
352
485
  async updateAccessTime(contentHash) {
353
486
  try {
354
487
  await this.ctx.database.set('medialuna_asset_cache', { contentHash }, {
@@ -359,11 +492,9 @@ class CacheService {
359
492
  this.logger.warn('Failed to update access time: %s', e);
360
493
  }
361
494
  }
362
- /** 从数据库删除 */
363
495
  async deleteFromDatabase(contentHash) {
364
496
  await this.ctx.database.remove('medialuna_asset_cache', { contentHash });
365
497
  }
366
- /** 将数据库记录转换为 CachedFile */
367
498
  dbRecordToCachedFile(record) {
368
499
  const localPath = path.join(this.cacheRoot, record.cachedKey);
369
500
  return {
@@ -374,7 +505,8 @@ class CacheService {
374
505
  createdAt: record.createdAt,
375
506
  accessedAt: record.lastAccessedAt,
376
507
  localPath,
377
- url: record.cachedUrl
508
+ url: record.cachedUrl,
509
+ backend: record.backend
378
510
  };
379
511
  }
380
512
  // ========== 私有工具方法 ==========
@@ -383,12 +515,10 @@ class CacheService {
383
515
  fs.mkdirSync(dir, { recursive: true });
384
516
  }
385
517
  }
386
- /** 生成内容哈希(基于文件内容) */
387
518
  generateContentHash(data) {
388
519
  const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data);
389
520
  return crypto.createHash('sha256').update(buffer).digest('hex').slice(0, 16);
390
521
  }
391
- /** 生成源哈希(基于 URL) */
392
522
  generateSourceHash(url) {
393
523
  return crypto.createHash('sha256').update(url).digest('hex').slice(0, 16);
394
524
  }
@@ -398,22 +528,7 @@ class CacheService {
398
528
  if (ext)
399
529
  return ext;
400
530
  }
401
- return this.getExtensionFromMime(mime);
402
- }
403
- getExtensionFromMime(mime) {
404
- const mimeMap = {
405
- 'image/jpeg': '.jpg',
406
- 'image/png': '.png',
407
- 'image/gif': '.gif',
408
- 'image/webp': '.webp',
409
- 'image/svg+xml': '.svg',
410
- 'audio/mpeg': '.mp3',
411
- 'audio/wav': '.wav',
412
- 'audio/ogg': '.ogg',
413
- 'video/mp4': '.mp4',
414
- 'video/webm': '.webm'
415
- };
416
- return mimeMap[mime] || '.bin';
531
+ return (0, mime_1.getExtensionFromMime)(mime);
417
532
  }
418
533
  guessMimeFromUrl(url) {
419
534
  const ext = url.split('.').pop()?.toLowerCase()?.split('?')[0];
@@ -432,49 +547,38 @@ class CacheService {
432
547
  };
433
548
  return mimeMap[ext || ''] || 'application/octet-stream';
434
549
  }
435
- /** 构建访问 URL */
436
- buildUrl(id, ext) {
437
- // 优先使用配置的 publicBaseUrl
550
+ /** 构建本地访问 URL */
551
+ buildLocalUrl(id, ext) {
438
552
  if (this.publicBaseUrl) {
439
553
  return `${this.publicBaseUrl}/${id}${ext}`;
440
554
  }
441
- // 否则使用 selfUrl + publicPath
442
555
  if (!this.baseUrl) {
443
556
  try {
444
- // 尝试从 server 服务获取 selfUrl
445
557
  const server = this.ctx.get?.('server');
446
558
  const selfUrl = server?.config?.selfUrl;
447
559
  if (selfUrl) {
448
560
  this.baseUrl = selfUrl.replace(/\/$/, '');
449
- this.logger.debug('Base URL dynamically set to: %s', this.baseUrl);
450
561
  }
451
562
  else {
452
- // 如果没有配置 selfUrl,尝试使用 localhost + 端口
453
563
  const port = server?.config?.port || 5140;
454
564
  this.baseUrl = `http://127.0.0.1:${port}`;
455
- this.logger.debug('Base URL set to localhost: %s', this.baseUrl);
456
565
  }
457
566
  }
458
567
  catch {
459
- // 忽略错误,使用默认值
460
568
  this.baseUrl = 'http://127.0.0.1:5140';
461
569
  }
462
570
  }
463
571
  if (!this.baseUrl) {
464
- // 返回相对路径作为降级
465
- this.logger.debug('Base URL not set, returning relative path for cache: %s', id);
466
572
  return `${this.publicPath}/${id}${ext}`;
467
573
  }
468
574
  return `${this.baseUrl}${this.publicPath}/${id}${ext}`;
469
575
  }
470
576
  async ensureCacheSpace(needed) {
471
577
  const maxBytes = this.config.maxCacheSize * 1024 * 1024;
472
- // 从数据库获取当前缓存大小
473
- const records = await this.ctx.database.get('medialuna_asset_cache', {});
578
+ const records = await this.ctx.database.get('medialuna_asset_cache', { backend: 'local' });
474
579
  let currentSize = records.reduce((sum, r) => sum + r.fileSize, 0);
475
580
  if (currentSize + needed <= maxBytes)
476
581
  return;
477
- // 按访问时间排序,删除最旧的
478
582
  const sorted = [...records].sort((a, b) => a.lastAccessedAt.getTime() - b.lastAccessedAt.getTime());
479
583
  for (const record of sorted) {
480
584
  if (currentSize + needed <= maxBytes)
@@ -489,8 +593,7 @@ class CacheService {
489
593
  const now = new Date();
490
594
  const expireMs = this.config.expireDays * 24 * 60 * 60 * 1000;
491
595
  const expireDate = new Date(now.getTime() - expireMs);
492
- // 查询过期的缓存
493
- const records = await this.ctx.database.get('medialuna_asset_cache', {});
596
+ const records = await this.ctx.database.get('medialuna_asset_cache', { backend: 'local' });
494
597
  const toDelete = records.filter(r => r.lastAccessedAt < expireDate);
495
598
  for (const record of toDelete) {
496
599
  await this.delete(record.contentHash);