From 353df30ff87dfea018c7c2fe760f92a53b8addd3 Mon Sep 17 00:00:00 2001 From: yimizilu <35183838+yimizilu@users.noreply.github.com> Date: Tue, 15 Sep 2026 08:28:19 +0800 Subject: [PATCH] fix: align media errors and serialize bigint server IDs --- docs/agent/api.md | 4 ++-- docs/skill/tracememo-reader/SKILL.md | 4 ++-- src/main/services/chat-service.ts | 7 +++++- tests/integration/local-api-auth.test.ts | 28 +++++++++++++++++++----- tests/unit/chat-service-media.test.ts | 22 +++++++++++++++++-- 5 files changed, 53 insertions(+), 12 deletions(-) diff --git a/docs/agent/api.md b/docs/agent/api.md index 6e53076..d8aba45 100644 --- a/docs/agent/api.md +++ b/docs/agent/api.md @@ -85,9 +85,9 @@ curl -H "$AUTH" "$BASE/chatlog?talker=技术交流群&time=2026-08-07" - `200`:请求成功; - `401`:缺少、错误或已失效的 Bearer Token; - `400`:参数或 JSON 请求体无效; -- `422`:媒体标识无效,或目标消息不是可读取的图片; +- `422`:媒体标识格式错误,或目标消息不是可读取的图片(`NOT_IMAGE`); - `403`:浏览器 Origin 不在允许的 loopback 列表; -- `404`:端点、会话或群聊不存在; +- `404`:端点、会话或群聊不存在;媒体标识未登记、已过期、有歧义,或图片文件不存在(`NOT_FOUND`)。媒体请求遇到此状态时,先重新读取 `/chatlog` 并使用新的 `media.url`;若仍失败,再检查本地图片文件是否存在; - `503`:数据库或 Agent Hub 尚未就绪; - `500`:服务端处理或报告渲染失败。 diff --git a/docs/skill/tracememo-reader/SKILL.md b/docs/skill/tracememo-reader/SKILL.md index 6cfab72..0127e67 100644 --- a/docs/skill/tracememo-reader/SKILL.md +++ b/docs/skill/tracememo-reader/SKILL.md @@ -133,7 +133,7 @@ description: 通过 TraceMemo 本地 HTTP API 按需读取用户有权访问的 - `401`:Token 缺失、错误或被轮换;请用户回 API Center 复制最新 Token。 - `403`:浏览器 Origin 不在 loopback 允许列表;CLI/Agent 通常不带 Origin。 -- `404`:先用 `/resolve` 确认会话标识。 -- `422`:媒体标识无效,或消息不是可读取的图片。 +- `404`:会话查询失败时先用 `/resolve` 确认会话标识;媒体请求表示标识未登记、已过期、有歧义,或图片文件不存在(`NOT_FOUND`)。先重新读取 `/chatlog` 并使用新的 `media.url`;若仍失败,再检查本地图片文件是否存在。 +- `422`:媒体标识格式错误,或消息不是可读取的图片(`NOT_IMAGE`)。 - `503`:用户还没有完成数据库连接或对应服务未就绪。 - 空结果:缩小/扩大时间范围,确认账号和会话,再检查媒体或语音是否可读。 diff --git a/src/main/services/chat-service.ts b/src/main/services/chat-service.ts index 6969639..72e97cb 100644 --- a/src/main/services/chat-service.ts +++ b/src/main/services/chat-service.ts @@ -159,6 +159,7 @@ export interface ImageMessageReference { const imageMessageReferences = new Map() let imageReferenceScope = randomUUID() +/** Replace the active database and invalidate connection-scoped lookup caches. */ export function setChatDb(db: WechatDb | null): boolean { if (shutdownRequested) { db?.close() @@ -315,6 +316,7 @@ export async function getContactAvatars( return client.getAvatarUrlsAsync(normalized) } +/** Format source rows and register image handles without merging recall archives. */ function listSourceMessages( userMd5: string, startTime?: number, @@ -565,7 +567,10 @@ function listSourceMessages( senderId, sessionId: username, localId, - serverId: typeof msg.serverId === 'string' ? msg.serverId : undefined, + serverId: + typeof msg.serverId === 'string' || typeof msg.serverId === 'bigint' + ? String(msg.serverId) + : undefined, createTime, recoveredFromRecallJournal, contentData, diff --git a/tests/integration/local-api-auth.test.ts b/tests/integration/local-api-auth.test.ts index 44a6d78..675981d 100644 --- a/tests/integration/local-api-auth.test.ts +++ b/tests/integration/local-api-auth.test.ts @@ -213,20 +213,38 @@ describe('Local API authentication', () => { } }) - it('maps media lookup failures to stable API statuses', async () => { + it.each([ + ['NOT_FOUND', '未找到图片消息', 404], + ['NOT_FOUND', '图片文件不存在', 404], + ['NOT_IMAGE', '消息不是可读取的图片消息', 422] + ] as const)('maps %s (%s) to HTTP %s', async (code, message, status) => { const handle = await startFixtureServer( () => VALID_TOKEN, async () => { - throw new HttpMediaError('NOT_IMAGE', '消息不是可读取的图片消息') + throw new HttpMediaError(code, message) } ) - const response = await fetch(`${baseUrl(handle)}/api/v1/media/message-1`, { + const response = await fetch(`${baseUrl(handle)}/api/v1/media/image%3Aunresolved`, { headers: { Authorization: `Bearer ${VALID_TOKEN}` } }) - expect(response.status).toBe(422) - await expect(response.json()).resolves.toMatchObject({ status: 422 }) + expect(response.status).toBe(status) + await expect(response.json()).resolves.toMatchObject({ status, error: message }) }) + it.each(['%ZZ', 'image%2Finvalid', 'image%5Cinvalid'])( + 'rejects malformed media identifiers (%s) before lookup', + async (identifier) => { + const provider = vi.fn(async () => ({ buffer: Buffer.from('image'), mimeType: 'image/png' })) + const handle = await startFixtureServer(() => VALID_TOKEN, provider) + const response = await fetch(`${baseUrl(handle)}/api/v1/media/${identifier}`, { + headers: { Authorization: `Bearer ${VALID_TOKEN}` } + }) + expect(response.status).toBe(422) + await expect(response.json()).resolves.toMatchObject({ status: 422 }) + expect(provider).not.toHaveBeenCalled() + } + ) + it.each(['Basic xxx', 'Bearer', 'bearer xxx', 'Bearer xxx', 'xxx'])( 'rejects the invalid Authorization format %s', async (authorization) => { diff --git a/tests/unit/chat-service-media.test.ts b/tests/unit/chat-service-media.test.ts index 65776f3..3b5b2e9 100644 --- a/tests/unit/chat-service-media.test.ts +++ b/tests/unit/chat-service-media.test.ts @@ -17,6 +17,7 @@ import { const IMAGE_A = 'a'.repeat(32) const IMAGE_B = 'b'.repeat(32) +/** Create a synthetic image row with an intentionally reusable local message ID. */ function image(md5: string, overrides: Partial = {}): WechatMessage { return { mesLocalID: '56', @@ -29,6 +30,7 @@ function image(md5: string, overrides: Partial = {}): WechatMessa } } +/** Attach an in-memory fixture database without reading a real account. */ function connect(messages: Record): void { const client = { getUsernameByMd5: (md5: string) => `wxid_${md5}` } setChatDb({ @@ -39,6 +41,7 @@ function connect(messages: Record): void { } as unknown as WechatDb) } +/** Extract the opaque handle after checking the formatted image metadata. */ function mediaId(message: FormattedMessage): string { expect(message.media).toMatchObject({ type: 'image', available: true }) return decodeURIComponent(message.media!.url.slice('/api/v1/media/'.length)) @@ -102,11 +105,26 @@ describe('chat service image media handles', () => { it('normalizes native server ids without losing integer precision', () => { const message = image(IMAGE_A, { serverId: 9007199254740993123n }) connect({ first: [message] }) - const firstId = mediaId(listMessages('first')[0]) + const first = listMessages('first')[0] + const firstId = mediaId(first) + expect(first.serverId).toBe('9007199254740993123') + expect(JSON.parse(JSON.stringify(first)).serverId).toBe('9007199254740993123') message.serverId = '9007199254740993123' - expect(mediaId(listMessages('first')[0])).toBe(firstId) + const reread = listMessages('first')[0] + expect(reread.serverId).toBe('9007199254740993123') + expect(mediaId(reread)).toBe(firstId) }) + it.each([9007199254740992, null, undefined, false])( + 'omits unsupported server ID values (%s)', + (serverId) => { + connect({ first: [image(IMAGE_A, { serverId })] }) + const message = listMessages('first')[0] + expect(message.serverId).toBeUndefined() + expect(JSON.parse(JSON.stringify(message))).not.toHaveProperty('serverId') + } + ) + it('scopes recovered images and supports images identified only by dat name', () => { connect({ first: [image(IMAGE_A, { _wxe_recovered: true })],