再次合并 master
合入 origin/master 最新后端、OSS 与认证链路调整。 保留本枝架构收口修改并合并 Hermes 决策记录。 通过 typecheck、编码检查、Spacetime schema guard 与 api-server cargo check。
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user