mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-10-06 13:54:10 +08:00
fix: align media errors and serialize bigint server IDs
This commit is contained in:
+2
-2
@@ -85,9 +85,9 @@ curl -H "$AUTH" "$BASE/chatlog?talker=技术交流群&time=2026-08-07"
|
|||||||
- `200`:请求成功;
|
- `200`:请求成功;
|
||||||
- `401`:缺少、错误或已失效的 Bearer Token;
|
- `401`:缺少、错误或已失效的 Bearer Token;
|
||||||
- `400`:参数或 JSON 请求体无效;
|
- `400`:参数或 JSON 请求体无效;
|
||||||
- `422`:媒体标识无效,或目标消息不是可读取的图片;
|
- `422`:媒体标识格式错误,或目标消息不是可读取的图片(`NOT_IMAGE`);
|
||||||
- `403`:浏览器 Origin 不在允许的 loopback 列表;
|
- `403`:浏览器 Origin 不在允许的 loopback 列表;
|
||||||
- `404`:端点、会话或群聊不存在;
|
- `404`:端点、会话或群聊不存在;媒体标识未登记、已过期、有歧义,或图片文件不存在(`NOT_FOUND`)。媒体请求遇到此状态时,先重新读取 `/chatlog` 并使用新的 `media.url`;若仍失败,再检查本地图片文件是否存在;
|
||||||
- `503`:数据库或 Agent Hub 尚未就绪;
|
- `503`:数据库或 Agent Hub 尚未就绪;
|
||||||
- `500`:服务端处理或报告渲染失败。
|
- `500`:服务端处理或报告渲染失败。
|
||||||
|
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ description: 通过 TraceMemo 本地 HTTP API 按需读取用户有权访问的
|
|||||||
|
|
||||||
- `401`:Token 缺失、错误或被轮换;请用户回 API Center 复制最新 Token。
|
- `401`:Token 缺失、错误或被轮换;请用户回 API Center 复制最新 Token。
|
||||||
- `403`:浏览器 Origin 不在 loopback 允许列表;CLI/Agent 通常不带 Origin。
|
- `403`:浏览器 Origin 不在 loopback 允许列表;CLI/Agent 通常不带 Origin。
|
||||||
- `404`:先用 `/resolve` 确认会话标识。
|
- `404`:会话查询失败时先用 `/resolve` 确认会话标识;媒体请求表示标识未登记、已过期、有歧义,或图片文件不存在(`NOT_FOUND`)。先重新读取 `/chatlog` 并使用新的 `media.url`;若仍失败,再检查本地图片文件是否存在。
|
||||||
- `422`:媒体标识无效,或消息不是可读取的图片。
|
- `422`:媒体标识格式错误,或消息不是可读取的图片(`NOT_IMAGE`)。
|
||||||
- `503`:用户还没有完成数据库连接或对应服务未就绪。
|
- `503`:用户还没有完成数据库连接或对应服务未就绪。
|
||||||
- 空结果:缩小/扩大时间范围,确认账号和会话,再检查媒体或语音是否可读。
|
- 空结果:缩小/扩大时间范围,确认账号和会话,再检查媒体或语音是否可读。
|
||||||
|
|||||||
@@ -159,6 +159,7 @@ export interface ImageMessageReference {
|
|||||||
const imageMessageReferences = new Map<string, ImageMessageReference | null>()
|
const imageMessageReferences = new Map<string, ImageMessageReference | null>()
|
||||||
let imageReferenceScope = randomUUID()
|
let imageReferenceScope = randomUUID()
|
||||||
|
|
||||||
|
/** Replace the active database and invalidate connection-scoped lookup caches. */
|
||||||
export function setChatDb(db: WechatDb | null): boolean {
|
export function setChatDb(db: WechatDb | null): boolean {
|
||||||
if (shutdownRequested) {
|
if (shutdownRequested) {
|
||||||
db?.close()
|
db?.close()
|
||||||
@@ -315,6 +316,7 @@ export async function getContactAvatars(
|
|||||||
return client.getAvatarUrlsAsync(normalized)
|
return client.getAvatarUrlsAsync(normalized)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Format source rows and register image handles without merging recall archives. */
|
||||||
function listSourceMessages(
|
function listSourceMessages(
|
||||||
userMd5: string,
|
userMd5: string,
|
||||||
startTime?: number,
|
startTime?: number,
|
||||||
@@ -565,7 +567,10 @@ function listSourceMessages(
|
|||||||
senderId,
|
senderId,
|
||||||
sessionId: username,
|
sessionId: username,
|
||||||
localId,
|
localId,
|
||||||
serverId: typeof msg.serverId === 'string' ? msg.serverId : undefined,
|
serverId:
|
||||||
|
typeof msg.serverId === 'string' || typeof msg.serverId === 'bigint'
|
||||||
|
? String(msg.serverId)
|
||||||
|
: undefined,
|
||||||
createTime,
|
createTime,
|
||||||
recoveredFromRecallJournal,
|
recoveredFromRecallJournal,
|
||||||
contentData,
|
contentData,
|
||||||
|
|||||||
@@ -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(
|
const handle = await startFixtureServer(
|
||||||
() => VALID_TOKEN,
|
() => VALID_TOKEN,
|
||||||
async () => {
|
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}` }
|
headers: { Authorization: `Bearer ${VALID_TOKEN}` }
|
||||||
})
|
})
|
||||||
expect(response.status).toBe(422)
|
expect(response.status).toBe(status)
|
||||||
await expect(response.json()).resolves.toMatchObject({ status: 422 })
|
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'])(
|
it.each(['Basic xxx', 'Bearer', 'bearer xxx', 'Bearer xxx', 'xxx'])(
|
||||||
'rejects the invalid Authorization format %s',
|
'rejects the invalid Authorization format %s',
|
||||||
async (authorization) => {
|
async (authorization) => {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
const IMAGE_A = 'a'.repeat(32)
|
const IMAGE_A = 'a'.repeat(32)
|
||||||
const IMAGE_B = 'b'.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> = {}): WechatMessage {
|
function image(md5: string, overrides: Partial<WechatMessage> = {}): WechatMessage {
|
||||||
return {
|
return {
|
||||||
mesLocalID: '56',
|
mesLocalID: '56',
|
||||||
@@ -29,6 +30,7 @@ function image(md5: string, overrides: Partial<WechatMessage> = {}): WechatMessa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Attach an in-memory fixture database without reading a real account. */
|
||||||
function connect(messages: Record<string, WechatMessage[]>): void {
|
function connect(messages: Record<string, WechatMessage[]>): void {
|
||||||
const client = { getUsernameByMd5: (md5: string) => `wxid_${md5}` }
|
const client = { getUsernameByMd5: (md5: string) => `wxid_${md5}` }
|
||||||
setChatDb({
|
setChatDb({
|
||||||
@@ -39,6 +41,7 @@ function connect(messages: Record<string, WechatMessage[]>): void {
|
|||||||
} as unknown as WechatDb)
|
} as unknown as WechatDb)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Extract the opaque handle after checking the formatted image metadata. */
|
||||||
function mediaId(message: FormattedMessage): string {
|
function mediaId(message: FormattedMessage): string {
|
||||||
expect(message.media).toMatchObject({ type: 'image', available: true })
|
expect(message.media).toMatchObject({ type: 'image', available: true })
|
||||||
return decodeURIComponent(message.media!.url.slice('/api/v1/media/'.length))
|
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', () => {
|
it('normalizes native server ids without losing integer precision', () => {
|
||||||
const message = image(IMAGE_A, { serverId: 9007199254740993123n })
|
const message = image(IMAGE_A, { serverId: 9007199254740993123n })
|
||||||
connect({ first: [message] })
|
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'
|
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', () => {
|
it('scopes recovered images and supports images identified only by dat name', () => {
|
||||||
connect({
|
connect({
|
||||||
first: [image(IMAGE_A, { _wxe_recovered: true })],
|
first: [image(IMAGE_A, { _wxe_recovered: true })],
|
||||||
|
|||||||
Reference in New Issue
Block a user