feat: 升级至 v0.3.16 — 工单修复 51 项 + 审查修复 30 项(安全加固/适配器/工具系统/数据层/前端)
This commit is contained in:
@@ -73,6 +73,9 @@ export abstract class BaseAdapter implements IMetonaProviderAdapter {
|
||||
*
|
||||
* @param timeoutMs 超时时间(毫秒)
|
||||
* @returns 合并后的 AbortSignal
|
||||
* @deprecated 审查修复 M20: 使用 fetchWithTimeout 替代。
|
||||
* getFetchSignal 内部 AbortSignal.timeout() 创建的 timer 在请求成功完成后仍会存活到超时,
|
||||
* 高频调用下 timer 句柄累积;fetchWithTimeout 用 setTimeout + clearTimeout 已解决此问题。
|
||||
*/
|
||||
protected getFetchSignal(timeoutMs: number): AbortSignal {
|
||||
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
||||
@@ -92,6 +95,46 @@ export abstract class BaseAdapter implements IMetonaProviderAdapter {
|
||||
return AbortSignal.any([timeoutSignal, this.externalAbortSignal]);
|
||||
}
|
||||
|
||||
/**
|
||||
* #24 修复: 封装 fetch + 超时控制,在 finally 中 clearTimeout,避免 timer 泄漏
|
||||
*
|
||||
* getFetchSignal 使用 AbortSignal.timeout() 内部创建的 timer 在请求成功完成后
|
||||
* 仍会存活到超时,高频调用下 timer 句柄累积。本方法使用 setTimeout + clearTimeout
|
||||
* 确保 fetch 完成(无论成功/失败/abort)后立即清理 timer。
|
||||
*
|
||||
* @param url 请求 URL
|
||||
* @param init fetch init(不含 signal,由本方法内部管理)
|
||||
* @param timeoutMs 超时时间(毫秒)
|
||||
*/
|
||||
protected async fetchWithTimeout(url: string, init: RequestInit, timeoutMs: number): Promise<Response> {
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
||||
|
||||
// 审查修复 M20: 保存 listener 引用,finally 中 removeEventListener 清理,避免 listener 泄漏
|
||||
const onExternalAbort = () => controller.abort();
|
||||
|
||||
try {
|
||||
// 合并外部 abort signal(来自 Engine 的 abortController)
|
||||
if (this.externalAbortSignal) {
|
||||
if (this.externalAbortSignal.aborted) {
|
||||
controller.abort();
|
||||
} else {
|
||||
this.externalAbortSignal.addEventListener('abort', onExternalAbort, { once: true });
|
||||
}
|
||||
}
|
||||
|
||||
return await fetch(url, { ...init, signal: controller.signal });
|
||||
} finally {
|
||||
// #24 修复: 关键 — 无论请求成功、失败还是 abort,都清理 timer
|
||||
clearTimeout(timer);
|
||||
// 审查修复 M20: 清理 externalAbortSignal 上注册的 listener
|
||||
// (即使 { once: true },请求正常完成时 listener 仍挂在 signal 上直到 abort 或 GC,需显式移除)
|
||||
if (this.externalAbortSignal) {
|
||||
this.externalAbortSignal.removeEventListener('abort', onExternalAbort);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async healthCheck(): Promise<boolean> {
|
||||
try {
|
||||
await this.listModels();
|
||||
@@ -150,7 +193,12 @@ export abstract class BaseAdapter implements IMetonaProviderAdapter {
|
||||
};
|
||||
}
|
||||
|
||||
if (msg.includes('401') || msg.includes('unauthorized')) {
|
||||
// #23 修复: 优先基于 HTTP status code 判断 401/429,避免字符串 includes 误匹配 URL 端口等数字
|
||||
// throwHttpError 已将 response.status 挂到 error.status,优先读取此字段
|
||||
const httpStatus = (error as Error & { status?: number }).status;
|
||||
|
||||
// #23 修复: 401 认证失败 — 优先用 status code,'unauthorized' 是单词不会误匹配
|
||||
if (httpStatus === 401 || msg.includes('unauthorized')) {
|
||||
return {
|
||||
code: MetonaErrorCode.AUTH_INVALID,
|
||||
message: 'API key 无效或已过期',
|
||||
@@ -159,7 +207,8 @@ export abstract class BaseAdapter implements IMetonaProviderAdapter {
|
||||
};
|
||||
}
|
||||
|
||||
if (msg.includes('429') || msg.includes('rate limit')) {
|
||||
// #23 修复: 429 限流 — 优先用 status code,'rate limit' 是单词不会误匹配
|
||||
if (httpStatus === 429 || msg.includes('rate limit')) {
|
||||
return {
|
||||
code: MetonaErrorCode.RATE_LIMITED,
|
||||
message: '请求过于频繁,请稍后重试',
|
||||
|
||||
Reference in New Issue
Block a user