diff --git a/apps/desktop/src/main/avatar_protocol.ts b/apps/desktop/src/main/avatar_protocol.ts index c0f3272b..07e821ef 100644 --- a/apps/desktop/src/main/avatar_protocol.ts +++ b/apps/desktop/src/main/avatar_protocol.ts @@ -57,9 +57,10 @@ export async function handleAvatarRequest(request: Request): Promise { status: 200, headers: { 'Content-Type': blob.contentType, - // Let the renderer / Chromium memory-cache it too; the on-disk cache - // is authoritative, this just avoids re-asking the protocol. - 'Cache-Control': 'public, max-age=86400', + // Let the renderer / Chromium memory-cache it too; the on-disk cache is + // authoritative (and applies the TTL), so keep this short — a long + // max-age would outlive the disk entry and pin a stale avatar. + 'Cache-Control': 'public, max-age=300', }, }); } catch { diff --git a/apps/desktop/src/main/context/app_context.ts b/apps/desktop/src/main/context/app_context.ts index a423f4c0..27335014 100644 --- a/apps/desktop/src/main/context/app_context.ts +++ b/apps/desktop/src/main/context/app_context.ts @@ -488,6 +488,12 @@ export interface AppContext { * no account is open / QQ is offline / harvest failed. */ refreshRkeysNow(): Promise; + /** + * Transcribe a SILK file on disk with the globally selected model. Account + * independent (the model is a global setting), so callers that already have a + * path — e.g. the Ptt cache browser — can skip the message-lookup dance. + */ + transcribeSilk(silkPath: string): Promise<{ ok: boolean; text?: string; error?: string }>; } let cached: AppContext | undefined; @@ -529,6 +535,9 @@ export function initAppContext(): AppContext { refreshRkeysNow(): Promise { return Promise.resolve(false); }, + transcribeSilk(): Promise<{ ok: boolean; text?: string; error?: string }> { + return Promise.resolve({ ok: false, error: '原生组件未就绪' }); + }, }; return cached; } @@ -605,7 +614,7 @@ export function initAppContext(): AppContext { keys: new Win32KeyService(platform, stubHooks), userConfig, globalConfig: new GlobalConfigService(platform, userConfig), - avatarCache: new AvatarCacheService(platform, userConfig), + avatarCache: new AvatarCacheService(userConfig), linkPreview, agentLabConfig: new AgentLabConfigService(userConfig), voiceTranscribe: new VoiceTranscribeService(platform), @@ -663,6 +672,7 @@ export function initAppContext(): AppContext { account: null, services: null, scheduler: null, + transcribeSilk, async setAccount(accountCtx: AccountContext, metadata: AccountConfigMetadata = {}): Promise { logger.info('opening account session', { event: 'open-account-start', diff --git a/apps/desktop/src/main/file_response.ts b/apps/desktop/src/main/file_response.ts index 49c71f43..87260001 100644 --- a/apps/desktop/src/main/file_response.ts +++ b/apps/desktop/src/main/file_response.ts @@ -72,13 +72,26 @@ function parseRange(header: string | null, size: number): { start: number; end: /** * Stream `path` as a `Response`, honoring the request's `Range` header. * Returns 404 when the file is missing, 206 for a partial response. + * + * `revalidate` opts the file into `Last-Modified` + `Cache-Control: no-cache`, + * so Chromium re-asks with `If-Modified-Since` and we answer 304 when the file + * hasn't changed. Use it for paths whose NAME is stable but whose CONTENT is + * rewritten in place — QQ's `nt_data/avatar` files are keyed by a uid hash, so + * changing your avatar overwrites the same path and a plain `max-age` would + * pin the old picture in the renderer's cache until it expired. */ -export async function fileResponse(path: string, request?: Request): Promise { +export async function fileResponse( + path: string, + request?: Request, + opts: { revalidate?: boolean } = {}, +): Promise { let size: number; + let mtimeMs: number; try { const st = await stat(path); if (!st.isFile()) return new Response('not a file', { status: 404 }); size = st.size; + mtimeMs = st.mtimeMs; } catch { return new Response('not found', { status: 404 }); } @@ -86,6 +99,20 @@ export async function fileResponse(path: string, request?: Request): Promise`/`