File size: 9,359 Bytes
d2ec5e7 e097ca3 d2ec5e7 2d57059 d2ec5e7 2d57059 d2ec5e7 2d57059 d2ec5e7 2d57059 d2ec5e7 d67e1e8 124fc9e d67e1e8 d2ec5e7 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 | import logger from "@/lib/logger.ts";
import { STATUS_CODE_MAP, POLLING_CONFIG } from "@/api/consts/common.ts";
import { handlePollingTimeout, handleGenerationFailure } from "@/lib/error-handler.ts";
/**
* 轮询状态接口
*/
export interface PollingStatus {
status: number;
failCode?: string;
itemCount: number;
finishTime?: number;
historyId?: string;
}
/**
* 轮询配置接口
*/
export interface PollingOptions {
maxPollCount?: number;
pollInterval?: number;
stableRounds?: number;
timeoutSeconds?: number;
expectedItemCount?: number;
type?: 'image' | 'video';
}
/**
* 轮询结果接口
*/
export interface PollingResult {
status: number;
failCode?: string;
itemCount: number;
elapsedTime: number;
pollCount: number;
exitReason: string;
}
/**
* 智能轮询器
* 根据状态码智能调整轮询间隔,优化性能
*/
export class SmartPoller {
private pollCount = 0;
private startTime = Date.now();
private lastItemCount = 0;
private stableItemCountRounds = 0;
private options: Required<PollingOptions>;
constructor(options: PollingOptions = {}) {
this.options = {
maxPollCount: options.maxPollCount ?? POLLING_CONFIG.MAX_POLL_COUNT,
pollInterval: options.pollInterval ?? POLLING_CONFIG.POLL_INTERVAL,
stableRounds: options.stableRounds ?? POLLING_CONFIG.STABLE_ROUNDS,
timeoutSeconds: options.timeoutSeconds ?? POLLING_CONFIG.TIMEOUT_SECONDS,
expectedItemCount: options.expectedItemCount ?? 4,
type: options.type ?? 'image'
};
}
/**
* 获取状态名称
*/
private getStatusName(status: number): string {
return STATUS_CODE_MAP[status] || `UNKNOWN(${status})`;
}
/**
* 根据状态码计算智能轮询间隔
*/
private getSmartInterval(status: number, itemCount: number): number {
const baseInterval = this.options.pollInterval;
// 根据状态码调整间隔
switch (status) {
case 20: // PROCESSING - 处理中,使用标准间隔
return baseInterval;
case 42: // POST_PROCESSING - 后处理中,稍微增加间隔
return baseInterval * 1.2;
case 45: // FINALIZING - 最终处理中,可能需要更多时间
return baseInterval * 1.5;
case 50: // COMPLETED - 已完成,快速检查
return baseInterval * 0.5;
case 10: // SUCCESS - 成功,立即返回
return 0;
case 30: // FAILED - 失败,立即返回
return 0;
default: // 未知状态,使用标准间隔
return baseInterval;
}
}
/**
* 检查是否应该退出轮询
*/
private shouldExitPolling(pollingStatus: PollingStatus): { shouldExit: boolean; reason: string } {
const { status, itemCount } = pollingStatus;
const elapsedTime = Math.round((Date.now() - this.startTime) / 1000);
// 更新图片数量稳定性检测
if (itemCount === this.lastItemCount) {
this.stableItemCountRounds++;
} else {
this.stableItemCountRounds = 0;
this.lastItemCount = itemCount;
}
// 1. 任务成功完成
if (status === 10 || status === 50) {
return { shouldExit: true, reason: '任务成功完成' };
}
// 2. 任务失败
if (status === 30) {
return { shouldExit: true, reason: '任务失败' };
}
// 3. 已获得期望数量的结果(但必须状态已完成)
if (itemCount >= this.options.expectedItemCount && (status === 10 || status === 50)) {
return { shouldExit: true, reason: `已获得完整结果集(${itemCount}/${this.options.expectedItemCount})` };
}
// 4. 图片数量已稳定
if (this.stableItemCountRounds >= this.options.stableRounds && itemCount > 0) {
return { shouldExit: true, reason: `结果数量稳定(${this.stableItemCountRounds}轮)` };
}
// 5. 轮询次数超限
if (this.pollCount >= this.options.maxPollCount) {
return { shouldExit: true, reason: '轮询次数超限' };
}
// 6. 时间超限但有结果
if (elapsedTime >= this.options.timeoutSeconds && itemCount > 0) {
return { shouldExit: true, reason: '时间超限但已有结果' };
}
return { shouldExit: false, reason: '' };
}
/**
* 执行单次轮询检查
*/
async poll<T>(
pollFunction: () => Promise<{ status: PollingStatus; data: T }>,
historyId?: string
): Promise<{ result: PollingResult; data: T }> {
logger.info(`开始智能轮询: historyId=${historyId || 'N/A'}, 最大轮询次数=${this.options.maxPollCount}, 期望结果数=${this.options.expectedItemCount}`);
let lastData: T;
let lastStatus: PollingStatus = { status: 20, itemCount: 0 };
while (true) {
this.pollCount++;
const elapsedTime = Math.round((Date.now() - this.startTime) / 1000);
try {
// 执行轮询函数
const { status, data } = await pollFunction();
lastStatus = status;
lastData = data;
// 详细日志
logger.info(`轮询 ${this.pollCount}/${this.options.maxPollCount}: status=${status.status}(${this.getStatusName(status.status)}), failCode=${status.failCode || 'none'}, items=${status.itemCount}, elapsed=${elapsedTime}s, finish_time=${status.finishTime || 0}, stable=${this.stableItemCountRounds}/${this.options.stableRounds}`);
// 如果有结果生成,记录详细信息
if (status.itemCount > 0) {
logger.info(`检测到${this.options.type === 'image' ? '图片' : '视频'}生成: 数量=${status.itemCount}, 状态=${this.getStatusName(status.status)}`);
}
// 检查是否应该退出
const { shouldExit, reason } = this.shouldExitPolling(status);
if (shouldExit) {
logger.info(`退出轮询: ${reason}, 最终${this.options.type === 'image' ? '图片' : '视频'}数量=${status.itemCount}`);
// 处理失败情况 (如果有部分结果,handleGenerationFailure 会返回 false 而不抛出异常)
if (status.status === 30) {
handleGenerationFailure(status.status, status.failCode, historyId, this.options.type, status.itemCount);
}
// 处理超时情况
if (reason === '轮询次数超限' || reason === '时间超限但已有结果') {
handlePollingTimeout(
this.pollCount,
this.options.maxPollCount,
elapsedTime,
status.status,
status.itemCount,
historyId
);
}
break;
}
// 未知状态码警告
if (![20, 42, 45, 10, 30, 50].includes(status.status)) {
logger.warn(`检测到未知状态码 ${status.status}(${this.getStatusName(status.status)}),继续轮询等待生成...`);
}
// 进度日志(每30秒输出一次)
if (this.pollCount % 30 === 0) {
logger.info(`${this.options.type === 'image' ? '图像' : '视频'}生成进度: 第 ${this.pollCount} 次轮询,状态: ${this.getStatusName(status.status)},已等待 ${elapsedTime} 秒...`);
}
// 计算下次轮询间隔
const nextInterval = this.getSmartInterval(status.status, status.itemCount);
if (nextInterval > 0) {
await new Promise(resolve => setTimeout(resolve, nextInterval));
}
} catch (error) {
// 判断是否为可重试的网络错误
const retryableErrorCodes = [
'ECONNABORTED', 'ETIMEDOUT', 'ECONNRESET', 'ENOTFOUND',
'ECONNREFUSED', 'EAI_AGAIN', 'EPIPE', 'ENETUNREACH', 'EHOSTUNREACH'
];
const isRetryableError = retryableErrorCodes.includes(error.code) ||
error.message?.includes('timeout') ||
error.message?.includes('network') ||
error.message?.includes('ECONNRESET') ||
error.message?.includes('socket hang up') ||
error.message?.includes('Proxy connection');
// 网络错误时进行轮询级别的重试,而不是直接中断整个流程
if (isRetryableError && this.pollCount < this.options.maxPollCount) {
logger.warn(`轮询过程中发生网络错误 (${error.code || error.message}),等待后继续轮询...`);
await new Promise(resolve => setTimeout(resolve, this.options.pollInterval));
continue;
}
// 不可重试的错误或已达到最大轮询次数,抛出异常
logger.error(`轮询过程中发生不可恢复的错误: ${error.message}`);
throw error;
}
}
const finalElapsedTime = Math.round((Date.now() - this.startTime) / 1000);
const result: PollingResult = {
status: lastStatus.status,
failCode: lastStatus.failCode,
itemCount: lastStatus.itemCount,
elapsedTime: finalElapsedTime,
pollCount: this.pollCount,
exitReason: this.shouldExitPolling(lastStatus).reason
};
logger.info(`${this.options.type === 'image' ? '图像' : '视频'}生成完成: 成功生成 ${lastStatus.itemCount} 个结果,总耗时 ${finalElapsedTime} 秒,最终状态: ${this.getStatusName(lastStatus.status)}`);
return { result, data: lastData! };
}
}
|