diff --git a/docs/HTTP-API.md b/docs/HTTP-API.md index bc25545..fb2c636 100644 --- a/docs/HTTP-API.md +++ b/docs/HTTP-API.md @@ -466,10 +466,15 @@ curl "http://127.0.0.1:5031/api/v1/sns/timeline?limit=3&media=1&inline=1" 媒体字段说明(`media=1`): +- `media[].url/thumb`:你应该优先直接使用的字段。 +- `replace=1`(默认)时,`media[].url/thumb` 会直接被替换成可访问地址,等价于 `resolvedUrl/resolvedThumbUrl`。 +- `replace=0` 时,`media[].url/thumb` 仍保留微信原始地址;这时再结合下面的 `raw/proxy/resolved` 字段自己决定用哪一个。 - `media[].rawUrl/rawThumb`:原始朋友圈地址 - `media[].proxyUrl/proxyThumbUrl`:可直接访问的代理地址 - `media[].resolvedUrl/resolvedThumbUrl`:最终可用地址(`inline=1` 时可能是 `data:` URL) -- `replace=1` 时,`media[].url/thumb` 会被替换为可用地址 +- `media[].token/key/encIdx`:微信源数据里的访问/解密参数。通常不需要你自己处理;如果你手动调用 `/api/v1/sns/media/proxy`,把当前条目的 `url` 和 `key` 原样传回即可。 +- `media[].livePhoto`:实况图的视频部分。外层 `media[].url/thumb` 仍是封面图,`livePhoto` 内部会再提供一组自己的 `url/thumb/raw*/proxy*/resolved*` 字段。 +- `media=0` 时,不会补充 `raw*/proxy*/resolved*`,接口只返回原始 `url/thumb` 以及源字段(如 `key/token/encIdx`)。 ### 7.2 获取朋友圈发布者 diff --git a/electron/services/httpService.ts b/electron/services/httpService.ts index c757db2..29d8952 100644 --- a/electron/services/httpService.ts +++ b/electron/services/httpService.ts @@ -891,15 +891,42 @@ class HttpService { const key = this.toSnsMediaKey(url.searchParams.get('key')) const result = await snsService.downloadImage(mediaUrl, key) - if (!result.success || !result.data) { + if (!result.success) { this.sendError(res, 502, result.error || 'Failed to proxy sns media') return } - res.setHeader('Content-Type', result.contentType || 'application/octet-stream') - res.setHeader('Content-Length', result.data.length) - res.writeHead(200) - res.end(result.data) + if (result.data) { + res.setHeader('Content-Type', result.contentType || 'application/octet-stream') + res.setHeader('Content-Length', result.data.length) + res.writeHead(200) + res.end(result.data) + return + } + + if (result.cachePath && fs.existsSync(result.cachePath)) { + try { + const stat = fs.statSync(result.cachePath) + res.setHeader('Content-Type', result.contentType || 'application/octet-stream') + res.setHeader('Content-Length', stat.size) + res.writeHead(200) + + const stream = fs.createReadStream(result.cachePath) + stream.on('error', () => { + if (!res.headersSent) { + this.sendError(res, 500, 'Failed to read proxied sns media') + } else { + try { res.destroy() } catch {} + } + }) + stream.pipe(res) + return + } catch (error) { + console.error('[HttpService] Failed to stream sns media cache:', error) + } + } + + this.sendError(res, 502, result.error || 'Failed to proxy sns media') } private async handleSnsExport(url: URL, res: http.ServerResponse): Promise { diff --git a/electron/services/snsService.ts b/electron/services/snsService.ts index b23d26f..d7fe76e 100644 --- a/electron/services/snsService.ts +++ b/electron/services/snsService.ts @@ -1240,7 +1240,7 @@ class SnsService { return { success: false, error: result.error } } - async downloadImage(url: string, key?: string | number): Promise<{ success: boolean; data?: Buffer; contentType?: string; error?: string }> { + async downloadImage(url: string, key?: string | number): Promise<{ success: boolean; data?: Buffer; contentType?: string; cachePath?: string; error?: string }> { return this.fetchAndDecryptImage(url, key) }