mirror of
https://github.com/foxhui/WebAI2API.git
synced 2026-06-16 21:03:59 +08:00
feat: 为图片生成结果下载单独提供重试机制以及相关设置 (closes #20)
This commit is contained in:
@@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
## [3.5.8] - 2026-03-22
|
||||
|
||||
### ✨ Added
|
||||
- **适配器**
|
||||
- 为图片生成结果下载单独提供重试机制以及相关设置
|
||||
- **WebUI**
|
||||
- 增加批量操作实例设置代理或者删除
|
||||
|
||||
|
||||
+4
-2
@@ -31,8 +31,10 @@ backend:
|
||||
# ========================================
|
||||
# 当适配器返回网络错误时,自动尝试其他支持相同模型的 Worker
|
||||
failover:
|
||||
enabled: true # 启用故障转移
|
||||
maxRetries: 2 # 最多重试次数 (0=无限制)
|
||||
enabled: true # 启用故障转移
|
||||
maxRetries: 2 # 最多重试次数 (0=无限制)
|
||||
imgDlRetry: false # 图片下载器重试,为图片生成结果单独提供重试机会
|
||||
imgDlRetryMaxRetries: 2 # 图片下载器重试次数
|
||||
|
||||
# ========================================
|
||||
# 生成等待时间
|
||||
|
||||
@@ -160,7 +160,10 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) {
|
||||
logger.info('适配器', '正在下载图片...', meta);
|
||||
|
||||
// 7. 使用 useContextDownload 下载图片
|
||||
const result = await useContextDownload(downloadUrl, page);
|
||||
const imgDlCfg = config?.backend?.pool?.failover || {};
|
||||
const result = await useContextDownload(downloadUrl, page, {
|
||||
retries: imgDlCfg.imgDlRetry ? (imgDlCfg.imgDlRetryMaxRetries || 2) : 0
|
||||
});
|
||||
if (result.error) {
|
||||
logger.error('适配器', result.error, meta);
|
||||
return result;
|
||||
|
||||
@@ -29,7 +29,7 @@ const TARGET_URL = 'https://www.doubao.com/chat/';
|
||||
* @returns {Promise<{image?: string, error?: string}>}
|
||||
*/
|
||||
async function generate(context, prompt, imgPaths, modelId, meta = {}) {
|
||||
const { page } = context;
|
||||
const { page, config } = context;
|
||||
|
||||
// 获取模型配置
|
||||
const modelConfig = manifest.models.find(m => m.id === modelId) || manifest.models[0];
|
||||
@@ -161,7 +161,10 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) {
|
||||
logger.info('适配器', '已获取图片链接,开始下载...', meta);
|
||||
|
||||
// 8. 下载图片
|
||||
const downloadResult = await useContextDownload(imageUrl, page);
|
||||
const imgDlCfg = config?.backend?.pool?.failover || {};
|
||||
const downloadResult = await useContextDownload(imageUrl, page, {
|
||||
retries: imgDlCfg.imgDlRetry ? (imgDlCfg.imgDlRetryMaxRetries || 2) : 0
|
||||
});
|
||||
if (downloadResult.error) {
|
||||
logger.error('适配器', downloadResult.error, meta);
|
||||
return downloadResult;
|
||||
|
||||
@@ -179,7 +179,10 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) {
|
||||
logger.info('适配器', `找到 ${imageUrls.length} 张图片,开始下载...`, meta);
|
||||
|
||||
// 使用封装的下载函数
|
||||
const result = await useContextDownload(imageUrl, page);
|
||||
const imgDlCfg = config?.backend?.pool?.failover || {};
|
||||
const result = await useContextDownload(imageUrl, page, {
|
||||
retries: imgDlCfg.imgDlRetry ? (imgDlCfg.imgDlRetryMaxRetries || 2) : 0
|
||||
});
|
||||
if (result.error) {
|
||||
logger.error('适配器', result.error, meta);
|
||||
return result;
|
||||
|
||||
@@ -231,7 +231,10 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) {
|
||||
|
||||
// 11. 下载图片并转为 base64
|
||||
logger.info('适配器', '正在下载图片...', meta);
|
||||
const downloadResult = await useContextDownload(imageUrl, page);
|
||||
const imgDlCfg = config?.backend?.pool?.failover || {};
|
||||
const downloadResult = await useContextDownload(imageUrl, page, {
|
||||
retries: imgDlCfg.imgDlRetry ? (imgDlCfg.imgDlRetryMaxRetries || 2) : 0
|
||||
});
|
||||
|
||||
if (downloadResult.error) {
|
||||
logger.error('适配器', downloadResult.error, meta);
|
||||
|
||||
@@ -160,7 +160,10 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) {
|
||||
}
|
||||
|
||||
logger.info('适配器', '已获取结果,正在下载图片...', meta);
|
||||
const result = await useContextDownload(img, page);
|
||||
const imgDlCfg = config?.backend?.pool?.failover || {};
|
||||
const result = await useContextDownload(img, page, {
|
||||
retries: imgDlCfg.imgDlRetry ? (imgDlCfg.imgDlRetryMaxRetries || 2) : 0
|
||||
});
|
||||
if (result.image) {
|
||||
logger.info('适配器', '已下载图片,任务完成', meta);
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ const INPUT_SELECTOR = 'textarea';
|
||||
* @returns {Promise<{video?: string, error?: string}>}
|
||||
*/
|
||||
async function generate(context, prompt, imgPaths, modelId, meta = {}) {
|
||||
const { page } = context;
|
||||
const { page, config } = context;
|
||||
|
||||
// 用于存储任务 ID 和视频 URL
|
||||
let taskId = null;
|
||||
@@ -199,7 +199,10 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) {
|
||||
|
||||
// 9. 下载视频并转为 base64
|
||||
logger.info('适配器', '正在下载视频...', meta);
|
||||
const downloadResult = await useContextDownload(videoUrl, page);
|
||||
const imgDlCfg = config?.backend?.pool?.failover || {};
|
||||
const downloadResult = await useContextDownload(videoUrl, page, {
|
||||
retries: imgDlCfg.imgDlRetry ? (imgDlCfg.imgDlRetryMaxRetries || 2) : 0
|
||||
});
|
||||
|
||||
if (downloadResult.error) {
|
||||
logger.error('适配器', downloadResult.error, meta);
|
||||
|
||||
@@ -362,7 +362,10 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) {
|
||||
logger.info('适配器', `已提取图片链接: ${imageUrl}`, meta);
|
||||
|
||||
// 下载图片
|
||||
const downloadResult = await useContextDownload(imageUrl, page);
|
||||
const imgDlCfg = config?.backend?.pool?.failover || {};
|
||||
const downloadResult = await useContextDownload(imageUrl, page, {
|
||||
retries: imgDlCfg.imgDlRetry ? (imgDlCfg.imgDlRetryMaxRetries || 2) : 0
|
||||
});
|
||||
if (downloadResult.error) {
|
||||
return downloadResult;
|
||||
}
|
||||
|
||||
@@ -10,25 +10,30 @@
|
||||
* @param {import('playwright-core').Page} page - Playwright 页面对象
|
||||
* @param {object} [options] - 可选配置
|
||||
* @param {number} [options.timeout=60000] - 超时时间(毫秒)
|
||||
* @param {number} [options.retries=0] - 下载失败时的重试次数
|
||||
* @returns {Promise<{ image?: string, error?: string }>} 下载结果
|
||||
*/
|
||||
export async function useContextDownload(url, page, options = {}) {
|
||||
const { timeout = 60000 } = options;
|
||||
const { timeout = 60000, retries = 0 } = options;
|
||||
|
||||
try {
|
||||
const response = await page.request.get(url, { timeout });
|
||||
for (let attempt = 0; attempt <= retries; attempt++) {
|
||||
try {
|
||||
const response = await page.request.get(url, { timeout });
|
||||
|
||||
if (!response.ok()) {
|
||||
return { error: `下载失败: HTTP ${response.status()}` };
|
||||
if (!response.ok()) {
|
||||
if (attempt < retries) continue;
|
||||
return { error: `下载失败: HTTP ${response.status()}` };
|
||||
}
|
||||
|
||||
const buffer = await response.body();
|
||||
const base64 = buffer.toString('base64');
|
||||
const contentType = response.headers()['content-type'] || 'image/png';
|
||||
const mimeType = contentType.split(';')[0].trim();
|
||||
|
||||
return { image: `data:${mimeType};base64,${base64}` };
|
||||
} catch (e) {
|
||||
if (attempt < retries) continue;
|
||||
return { error: `已获取结果,但图片下载时遇到错误: ${e.message}` };
|
||||
}
|
||||
|
||||
const buffer = await response.body();
|
||||
const base64 = buffer.toString('base64');
|
||||
const contentType = response.headers()['content-type'] || 'image/png';
|
||||
const mimeType = contentType.split(';')[0].trim();
|
||||
|
||||
return { image: `data:${mimeType};base64,${base64}` };
|
||||
} catch (e) {
|
||||
return { error: `已获取结果,但图片下载时遇到错误: ${e.message}` };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -280,6 +280,12 @@ export function loadConfig() {
|
||||
if (config.backend.pool.failover.maxRetries === undefined) {
|
||||
config.backend.pool.failover.maxRetries = 2;
|
||||
}
|
||||
if (config.backend.pool.failover.imgDlRetry === undefined) {
|
||||
config.backend.pool.failover.imgDlRetry = false;
|
||||
}
|
||||
if (config.backend.pool.failover.imgDlRetryMaxRetries === undefined) {
|
||||
config.backend.pool.failover.imgDlRetryMaxRetries = 2;
|
||||
}
|
||||
|
||||
// 校验 instances 配置
|
||||
if (!config.backend.pool.instances || !Array.isArray(config.backend.pool.instances)) {
|
||||
|
||||
@@ -284,7 +284,9 @@ export function getPoolConfig() {
|
||||
waitTimeout: pool.waitTimeout != null ? Math.round(pool.waitTimeout / 1000) : 120,
|
||||
failover: {
|
||||
enabled: failover.enabled !== false, // 默认 true
|
||||
maxRetries: failover.maxRetries ?? 2
|
||||
maxRetries: failover.maxRetries ?? 2,
|
||||
imgDlRetry: failover.imgDlRetry || false,
|
||||
imgDlRetryMaxRetries: failover.imgDlRetryMaxRetries ?? 2
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -317,6 +319,12 @@ export function savePoolConfig(data) {
|
||||
if (data.failover.maxRetries !== undefined) {
|
||||
config.backend.pool.failover.maxRetries = data.failover.maxRetries;
|
||||
}
|
||||
if (data.failover.imgDlRetry !== undefined) {
|
||||
config.backend.pool.failover.imgDlRetry = data.failover.imgDlRetry;
|
||||
}
|
||||
if (data.failover.imgDlRetryMaxRetries !== undefined) {
|
||||
config.backend.pool.failover.imgDlRetryMaxRetries = data.failover.imgDlRetryMaxRetries;
|
||||
}
|
||||
}
|
||||
|
||||
writeConfig(config);
|
||||
|
||||
@@ -250,6 +250,16 @@ export function validatePoolConfig(data) {
|
||||
errors.push('failover.maxRetries 不能为负数');
|
||||
}
|
||||
}
|
||||
if (data.failover.imgDlRetry !== undefined && typeof data.failover.imgDlRetry !== 'boolean') {
|
||||
errors.push('failover.imgDlRetry 必须是布尔值');
|
||||
}
|
||||
if (data.failover.imgDlRetryMaxRetries !== undefined) {
|
||||
if (typeof data.failover.imgDlRetryMaxRetries !== 'number' || !Number.isInteger(data.failover.imgDlRetryMaxRetries)) {
|
||||
errors.push('failover.imgDlRetryMaxRetries 必须是整数');
|
||||
} else if (data.failover.imgDlRetryMaxRetries < 1 || data.failover.imgDlRetryMaxRetries > 10) {
|
||||
errors.push('failover.imgDlRetryMaxRetries 必须在 1-10 范围内');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: errors.length === 0, errors };
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -376,6 +376,32 @@ const handleRemoveWorker = (index) => {
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-divider style="margin: 12px 0;" />
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :xs="24" :md="12">
|
||||
<div style="margin-bottom: 8px;">
|
||||
<div style="font-weight: 600; margin-bottom: 8px;">图片下载重试</div>
|
||||
<div style="font-size: 12px; color: #8c8c8c; margin-bottom: 12px;">
|
||||
启用后,图片/视频下载失败时会自动重试下载(不重新生成)
|
||||
</div>
|
||||
<a-switch v-model:checked="poolConfig.failover.imgDlRetry" />
|
||||
</div>
|
||||
</a-col>
|
||||
|
||||
<a-col :xs="24" :md="12">
|
||||
<div style="margin-bottom: 8px;">
|
||||
<div style="font-weight: 600; margin-bottom: 8px;">下载重试次数</div>
|
||||
<div style="font-size: 12px; color: #8c8c8c; margin-bottom: 12px;">
|
||||
图片下载失败时的最大重试次数,范围 1-10
|
||||
</div>
|
||||
<a-input-number v-model:value="poolConfig.failover.imgDlRetryMaxRetries" :min="1"
|
||||
:max="10" :disabled="!poolConfig.failover.imgDlRetry" style="width: 100%"
|
||||
placeholder="请输入下载重试次数" />
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
</div>
|
||||
|
||||
@@ -12,7 +12,9 @@ export const useSettingsStore = defineStore('settings', {
|
||||
waitTimeout: 120,
|
||||
failover: {
|
||||
enabled: false,
|
||||
maxRetries: 3
|
||||
maxRetries: 3,
|
||||
imgDlRetry: false,
|
||||
imgDlRetryMaxRetries: 2
|
||||
}
|
||||
},
|
||||
adapterConfig: {},
|
||||
@@ -168,7 +170,9 @@ export const useSettingsStore = defineStore('settings', {
|
||||
waitTimeout: data.waitTimeout ?? 120,
|
||||
failover: {
|
||||
enabled: data.failover?.enabled || false,
|
||||
maxRetries: data.failover?.maxRetries || 3
|
||||
maxRetries: data.failover?.maxRetries || 3,
|
||||
imgDlRetry: data.failover?.imgDlRetry || false,
|
||||
imgDlRetryMaxRetries: data.failover?.imgDlRetryMaxRetries ?? 2
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user