This commit is contained in:
2026-05-10 22:20:54 +08:00
parent d6219f1a0c
commit 192accd796
92 changed files with 7045 additions and 1559 deletions

View File

@@ -1,7 +1,9 @@
import { parseApiErrorMessage } from '../../packages/shared/src/http';
import {
ApiClientError,
BACKGROUND_AUTH_REQUEST_OPTIONS,
type ApiRequestOptions,
fetchWithApiAuth,
requestJson,
} from './apiClient';
@@ -23,6 +25,11 @@ type AssetReadUrlResolveOptions = {
refreshKey?: string | number | null;
};
type AssetReadBytesOptions = {
signal?: AbortSignal;
expireSeconds?: number;
};
export type AssetReadUrlResponse = {
read?: {
objectKey?: string;
@@ -44,6 +51,7 @@ type CachedReadUrlFailureEntry = {
};
const ASSET_READ_URL_API_PATH = '/api/assets/read-url';
const ASSET_READ_BYTES_API_PATH = '/api/assets/read-bytes';
const DEFAULT_CACHE_SAFETY_WINDOW_MS = 30 * 1000;
const DEFAULT_FAILURE_CACHE_WINDOW_MS = 60 * 1000;
const ASSET_READ_URL_BACKGROUND_OPTIONS =
@@ -72,6 +80,27 @@ function buildCacheKey(request: AssetReadUrlRequest) {
return '';
}
function buildAssetReadSearchParams(request: AssetReadUrlRequest) {
const searchParams = new URLSearchParams();
if (request.objectKey?.trim()) {
searchParams.set('objectKey', request.objectKey.trim().replace(/^\/+/u, ''));
}
if (request.legacyPublicPath?.trim()) {
searchParams.set(
'legacyPublicPath',
normalizeLegacyPublicPath(request.legacyPublicPath),
);
}
if (
typeof request.expireSeconds === 'number' &&
Number.isFinite(request.expireSeconds) &&
request.expireSeconds > 0
) {
searchParams.set('expireSeconds', String(Math.floor(request.expireSeconds)));
}
return searchParams;
}
function resolveSignedReadPayload(response: AssetReadUrlResponse) {
const read = response.read ?? response;
const signedUrl = typeof read.signedUrl === 'string' ? read.signedUrl.trim() : '';
@@ -146,23 +175,7 @@ export async function getSignedAssetReadUrl(
}
const requestPromise = (async () => {
const searchParams = new URLSearchParams();
if (request.objectKey?.trim()) {
searchParams.set('objectKey', request.objectKey.trim().replace(/^\/+/u, ''));
}
if (request.legacyPublicPath?.trim()) {
searchParams.set(
'legacyPublicPath',
normalizeLegacyPublicPath(request.legacyPublicPath),
);
}
if (
typeof request.expireSeconds === 'number' &&
Number.isFinite(request.expireSeconds) &&
request.expireSeconds > 0
) {
searchParams.set('expireSeconds', String(Math.floor(request.expireSeconds)));
}
const searchParams = buildAssetReadSearchParams(request);
try {
const response = await requestJson<AssetReadUrlResponse>(
@@ -289,6 +302,49 @@ export async function resolveAssetReadUrl(
return appendCacheBustParam(value, options.refreshKey);
}
export async function readAssetBytes(
source: string | null | undefined,
options: AssetReadBytesOptions = {},
) {
const value = source?.trim() ?? '';
if (!value) {
throw new Error('资源路径不能为空');
}
if (!isGeneratedLegacyPath(value)) {
const response = await fetch(value, { signal: options.signal });
if (!response.ok) {
throw new Error('读取资源内容失败');
}
return response;
}
// 中文注释:这里要拿图片字节转 Data URL不能直接 fetch OSS 签名 URL否则浏览器会受 bucket CORS 限制。
const searchParams = buildAssetReadSearchParams({
legacyPublicPath: value,
expireSeconds: options.expireSeconds,
});
const response = await fetchWithApiAuth(
`${ASSET_READ_BYTES_API_PATH}?${searchParams.toString()}`,
{
method: 'GET',
signal: options.signal,
},
{
...ASSET_READ_URL_BACKGROUND_OPTIONS,
omitEnvelopeHeader: true,
},
);
if (!response.ok) {
const message = await response
.text()
.then((text) => parseApiErrorMessage(text, '读取资源内容失败'))
.catch(() => '');
throw new Error(message || '读取资源内容失败');
}
return response;
}
export function clearSignedAssetReadUrlCache() {
signedReadUrlCache.clear();
signedReadUrlFailureCache.clear();