fix(onebot): keep a known media URL when a re-index carries none

Quoted images reach the media indexer with only their hash, so the stored
record was overwritten with an empty url and get_image handed callers an
empty address plus a synthetic <md5>.png name that no download can satisfy.

Reuse the URL already stored for the same object when the incoming record
has none, and let get_image sign a record whose address lives on url
instead of imageUrl.
This commit is contained in:
AmeiMiao
2026-11-09 15:19:24 +08:00
parent 744f25fd9b
commit e056952d1a
3 changed files with 102 additions and 6 deletions
+59 -1
View File
@@ -235,7 +235,7 @@ export class MediaStore {
throw new Error(`cannot store ${type} metadata without an identifier`);
}
const data = serializeMediaInfo(info);
const data = serializeMediaInfo(await this.mergeStoredUrls(type, primary, info));
await this.db.exec('BEGIN IMMEDIATE');
try {
@@ -323,6 +323,64 @@ export class MediaStore {
),
);
}
/** Re-indexing an object without a download address — a quoted or forwarded
* image often carries only its hash — must not erase an address already
* proven for the same object. `get_image` hands `url` straight to its
* caller, so a dropped address turns a known picture into an unusable
* record that the caller retries until it gives up.
*/
private async mergeStoredUrls(type: string, primary: string, info: object): Promise<object> {
const next: Record<string, unknown> = { ...(info as Record<string, unknown>) };
const missing = (['url', 'imageUrl'] as const).filter(
(field) => field in next && (typeof next[field] !== 'string' || !next[field]),
);
if (missing.length === 0) {
return info;
}
const stored = await this.readStoredUrls(type, primary);
for (const field of missing) {
if (stored[field]) {
next[field] = stored[field];
}
}
return next;
}
/** Direct row read: `remember` already runs inside the serial queue. */
private async readStoredUrls(
type: string,
primary: string,
): Promise<{ url: string; imageUrl: string }> {
const empty = { url: '', imageUrl: '' };
const row = (await this.findEntry.get(type, type, primary, primary)) as
| { data: string }
| undefined;
if (!row) {
return empty;
}
try {
const parsed: unknown = JSON.parse(row.data);
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
return empty;
}
const stored = parsed as Record<string, unknown>;
return {
url: typeof stored.url === 'string' ? stored.url : '',
imageUrl: typeof stored.imageUrl === 'string' ? stored.imageUrl : '',
};
} catch {
return empty;
}
}
}
export function serializeMediaInfo(info: object): string {
+8 -5
View File
@@ -148,14 +148,17 @@ async function resolveImageUrl(
cached: CachedImage | null,
resolver?: ImageUrlResolver | null,
): Promise<string | null> {
if (!cached?.imageUrl || !resolver) {
// get_image returns `url` ahead of `imageUrl`, so a record indexed from
// segment data can carry its address only on `url`. That address still
// needs signing when its rkey has expired; ignoring it here handed the
// caller back an unrefreshed URL.
const source = (cached?.imageUrl || cached?.url || '').trim();
if (!cached || !source || !resolver) {
return null;
}
return resolver(
{ type: 'image', imageUrl: cached.imageUrl, subType: cached.subType },
cached.isGroup,
);
return resolver({ type: 'image', imageUrl: source, subType: cached.subType }, cached.isGroup);
}
/**
+35
View File
@@ -119,6 +119,41 @@ describe('MediaStore basic semantics', () => {
await store.close();
});
it('keeps a stored download URL when a later index carries none', async () => {
const store = open('img-url-retain');
await store.rememberImage({
file: 'abc.png',
url: 'https://example.com/abc.png?rkey=live',
fileSize: 4242,
fileName: 'abc.png',
subType: 0,
summary: '',
imageUrl: 'https://example.com/abc.png?rkey=live',
isGroup: true,
sessionId: GROUP_ID,
});
// A quoted copy reaches the store with only the hash and no address.
await store.rememberImage({
file: 'abc.png',
url: '',
fileSize: 4242,
fileName: 'abc.png',
subType: 0,
summary: '',
imageUrl: '',
isGroup: true,
sessionId: GROUP_ID,
});
const cached = await store.findImage('abc.png');
expect(cached?.url).toBe('https://example.com/abc.png?rkey=live');
expect(cached?.imageUrl).toBe('https://example.com/abc.png?rkey=live');
await store.close();
});
it('indexes records by file, fileName, fileId, and url', async () => {
const store = open('rec-keys');
await store.rememberRecord({