From 53909d91e3ca570d4b565ba1abd00f027ca78d6b Mon Sep 17 00:00:00 2001 From: weiwei Wang <54494072+weivwang@users.noreply.github.com> Date: Fri, 14 Aug 2026 13:08:30 +0800 Subject: [PATCH] fix(kimi-web): cache content-hashed assets (#2865) --- .changeset/cache-web-assets.md | 5 ++ packages/kap-server/src/routes/webAssets.ts | 12 ++++- packages/kap-server/test/webAssets.test.ts | 53 +++++++++++++++++++++ 3 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 .changeset/cache-web-assets.md create mode 100644 packages/kap-server/test/webAssets.test.ts diff --git a/.changeset/cache-web-assets.md b/.changeset/cache-web-assets.md new file mode 100644 index 000000000..587caf6d1 --- /dev/null +++ b/.changeset/cache-web-assets.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Cache content-hashed Kimi Web assets across reloads while keeping the app entry point revalidated. diff --git a/packages/kap-server/src/routes/webAssets.ts b/packages/kap-server/src/routes/webAssets.ts index 7b7ad0176..e45359317 100644 --- a/packages/kap-server/src/routes/webAssets.ts +++ b/packages/kap-server/src/routes/webAssets.ts @@ -1,6 +1,6 @@ import { createReadStream } from 'node:fs'; import { stat } from 'node:fs/promises'; -import { extname, join, normalize, resolve, sep } from 'node:path'; +import { extname, join, normalize, relative, resolve, sep } from 'node:path'; import type { FastifyReply, FastifyRequest } from 'fastify'; @@ -56,10 +56,20 @@ async function serveWebAsset( return reply .type(mimeType(filePath)) + .header('Cache-Control', cacheControl(assetsDir, filePath)) .header('Content-Length', String(fileInfo.size)) .send(createReadStream(filePath)); } +function cacheControl(assetsDir: string, filePath: string): string { + const assetPath = relative(assetsDir, filePath); + const fileName = filePath.slice(filePath.lastIndexOf(sep) + 1); + if (assetPath.startsWith(`assets${sep}`) && /[-.][A-Za-z0-9_-]{8}\.[^.]+$/.test(fileName)) { + return 'public, max-age=31536000, immutable'; + } + return 'no-cache'; +} + async function resolveStaticFile( assetsDir: string, pathname: string, diff --git a/packages/kap-server/test/webAssets.test.ts b/packages/kap-server/test/webAssets.test.ts new file mode 100644 index 000000000..70dbb5995 --- /dev/null +++ b/packages/kap-server/test/webAssets.test.ts @@ -0,0 +1,53 @@ +import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +import Fastify, { type FastifyInstance } from 'fastify'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; + +import { registerWebAssetRoutes } from '../src/routes/webAssets'; + +describe('web asset cache policy', () => { + let app: FastifyInstance; + let assetsDir: string; + + beforeEach(async () => { + assetsDir = await mkdtemp(join(tmpdir(), 'kimi-web-assets-')); + await mkdir(join(assetsDir, 'assets')); + await Promise.all([ + writeFile(join(assetsDir, 'index.html'), '
Kimi
'), + writeFile(join(assetsDir, 'assets', 'index-Dy7xs5tu.js'), 'export {};'), + writeFile(join(assetsDir, 'assets', 'application-configuration.json'), '{}'), + writeFile(join(assetsDir, 'favicon.svg'), ''), + ]); + app = Fastify(); + await registerWebAssetRoutes(app, assetsDir); + }); + + afterEach(async () => { + await app.close(); + await rm(assetsDir, { recursive: true, force: true }); + }); + + it('caches content-hashed assets as immutable', async () => { + const response = await app.inject({ method: 'GET', url: '/assets/index-Dy7xs5tu.js' }); + + expect(response.statusCode).toBe(200); + expect(response.headers['cache-control']).toBe('public, max-age=31536000, immutable'); + }); + + it.each([ + '/index.html', + '/sessions/active', + '/favicon.svg', + '/assets/application-configuration.json', + ])( + 'requires revalidation for %s', + async (url) => { + const response = await app.inject({ method: 'GET', url }); + + expect(response.statusCode).toBe(200); + expect(response.headers['cache-control']).toBe('no-cache'); + }, + ); +});