修复生成图片OSS签名缓存链路

前端将完整阿里云OSS generated 地址归一为 legacy path 后走 read-url 换签。

platform-oss 为 generated 私有对象 PostObject 和 PutObject 写入 immutable Cache-Control。

补齐 shared-contracts 与 api-server 直传票据字段映射。

更新后端、运维文档和 Hermes 团队记忆,明确不使用服务端磁盘缓存兜底。
This commit is contained in:
2026-06-07 17:19:03 +08:00
parent decded991e
commit d3a3238028
10 changed files with 193 additions and 8 deletions

View File

@@ -140,6 +140,48 @@ describe('assetReadUrlService', () => {
);
});
test('resolveAssetReadUrl exchanges generated Aliyun OSS url for signed url', async () => {
vi.spyOn(globalThis, 'fetch').mockResolvedValue(
new Response(
JSON.stringify({
ok: true,
data: {
read: {
objectKey:
'generated-puzzle-assets/puzzle-session-1/candidate-1/asset-1/image.png',
signedUrl: 'https://signed.example.com/puzzle.png',
expiresAt: '2099-01-01T00:10:00Z',
},
},
error: null,
meta: {
apiVersion: '2026-04-08',
routeVersion: '2026-04-08',
latencyMs: 1,
timestamp: '2099-01-01T00:00:00Z',
},
}),
{
status: 200,
headers: {
'Content-Type': 'application/json',
},
},
),
);
await expect(
resolveAssetReadUrl(
'https://genarrative-release.oss-cn-beijing.aliyuncs.com/generated-puzzle-assets/puzzle-session-1/candidate-1/asset-1/image.png',
),
).resolves.toBe('https://signed.example.com/puzzle.png');
expect(globalThis.fetch).toHaveBeenCalledTimes(1);
expect(String(vi.mocked(globalThis.fetch).mock.calls[0]?.[0])).toContain(
'legacyPublicPath=%2Fgenerated-puzzle-assets%2Fpuzzle-session-1%2Fcandidate-1%2Fasset-1%2Fimage.png',
);
});
test('resolveAssetReadUrl does not append cache busting query to OSS signed url', async () => {
vi.spyOn(globalThis, 'fetch').mockResolvedValue(
new Response(

View File

@@ -67,6 +67,26 @@ export function isGeneratedLegacyPath(value: string) {
return /^\/?generated-[^/?#]+\/.+/u.test(value.trim());
}
function isAliyunOssHost(hostname: string) {
return /^[^.]+\.oss-[^.]+\.aliyuncs\.com$/iu.test(hostname.trim());
}
function resolveGeneratedLegacyPathFromUrl(value: string) {
try {
const parsedUrl = new URL(
value,
globalThis.location?.origin ?? 'http://localhost',
);
if (!isAliyunOssHost(parsedUrl.hostname)) {
return '';
}
const legacyPath = decodeURIComponent(parsedUrl.pathname);
return isGeneratedLegacyPath(legacyPath) ? legacyPath : '';
} catch {
return '';
}
}
function normalizeLegacyPublicPath(value: string) {
return `/${value.trim().replace(/^\/+/u, '')}`;
}
@@ -284,6 +304,21 @@ export async function resolveAssetReadUrl(
value.startsWith('data:') ||
value.startsWith('blob:')
) {
const legacyPath = resolveGeneratedLegacyPathFromUrl(value);
if (legacyPath) {
const signedUrl = await getSignedAssetReadUrl(
{
legacyPublicPath: legacyPath,
expireSeconds: options.expireSeconds,
},
options.signal,
{
bypassCache:
options.refreshKey !== null && options.refreshKey !== undefined,
},
);
return signedUrl;
}
return appendCacheBustParam(value, options.refreshKey);
}