mirror of
https://github.com/foxhui/WebAI2API.git
synced 2026-06-16 21:03:59 +08:00
feat: 支持自定义生成等待超时时间 (closes #21)
This commit is contained in:
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [3.5.5] - 2026-03-05
|
||||
|
||||
### ✨ Added
|
||||
- **自定义**
|
||||
- 增加自定义生成等待超时时间
|
||||
|
||||
### 🐛 Fixed
|
||||
- **适配器**
|
||||
- 修复 ChatGPT 文本适配器响应被截断的问题
|
||||
|
||||
@@ -34,6 +34,12 @@ backend:
|
||||
enabled: true # 启用故障转移
|
||||
maxRetries: 2 # 最多重试次数 (0=无限制)
|
||||
|
||||
# ========================================
|
||||
# 生成等待时间
|
||||
# ========================================
|
||||
# 程序等待生成结果返回的最大超时时间,单位毫秒
|
||||
waitTimeout: 120000
|
||||
|
||||
# ========================================
|
||||
# 浏览器实例列表
|
||||
# ========================================
|
||||
|
||||
@@ -31,7 +31,8 @@ const INPUT_SELECTOR = '.ProseMirror';
|
||||
* @returns {Promise<{image?: string, error?: string}>}
|
||||
*/
|
||||
async function generate(context, prompt, imgPaths, modelId, meta = {}) {
|
||||
const { page } = context;
|
||||
const { page, config } = context;
|
||||
const waitTimeout = config?.backend?.pool?.waitTimeout ?? 120000;
|
||||
const sendBtnLocator = page.getByRole('button', { name: 'Send prompt' });
|
||||
|
||||
try {
|
||||
@@ -94,7 +95,7 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) {
|
||||
conversationResponse = await waitApiResponse(page, {
|
||||
urlMatch: 'backend-api/f/conversation',
|
||||
method: 'POST',
|
||||
timeout: 120000, // 图片生成可能较慢
|
||||
timeout: waitTimeout, // 图片生成可能较慢
|
||||
meta
|
||||
});
|
||||
} catch (e) {
|
||||
@@ -144,7 +145,7 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) {
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}, { timeout: 120000 });
|
||||
}, { timeout: waitTimeout });
|
||||
} catch (e) {
|
||||
const pageError = normalizePageError(e, meta);
|
||||
if (pageError) return pageError;
|
||||
|
||||
@@ -80,7 +80,8 @@ async function selectModel(page, codeName, meta = {}) {
|
||||
* @returns {Promise<{text?: string, error?: string}>}
|
||||
*/
|
||||
async function generate(context, prompt, imgPaths, modelId, meta = {}) {
|
||||
const { page } = context;
|
||||
const { page, config } = context;
|
||||
const waitTimeout = config?.backend?.pool?.waitTimeout ?? 120000;
|
||||
const sendBtnLocator = page.getByRole('button', { name: 'Send prompt' });
|
||||
|
||||
try {
|
||||
@@ -223,7 +224,7 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) {
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}, { timeout: 120000 });
|
||||
}, { timeout: waitTimeout });
|
||||
} catch (e) {
|
||||
const pageError = normalizePageError(e, meta);
|
||||
if (pageError) return pageError;
|
||||
|
||||
@@ -82,7 +82,8 @@ async function configureModel(page, modelConfig, meta = {}) {
|
||||
* @returns {Promise<{text?: string, error?: string}>}
|
||||
*/
|
||||
async function generate(context, prompt, imgPaths, modelId, meta = {}) {
|
||||
const { page } = context;
|
||||
const { page, config } = context;
|
||||
const waitTimeout = config?.backend?.pool?.waitTimeout ?? 120000;
|
||||
|
||||
try {
|
||||
logger.info('适配器', '开启新会话...', meta);
|
||||
@@ -208,7 +209,7 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) {
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}, { timeout: 120000 });
|
||||
}, { timeout: waitTimeout });
|
||||
|
||||
// 5. 发送提示词
|
||||
logger.debug('适配器', '发送提示词...', meta);
|
||||
|
||||
@@ -32,7 +32,8 @@ const TARGET_URL = 'https://gemini.google.com/app?hl=en';
|
||||
* @returns {Promise<{image?: string, error?: string}>}
|
||||
*/
|
||||
async function generate(context, prompt, imgPaths, modelId, meta = {}) {
|
||||
const { page } = context;
|
||||
const { page, config } = context;
|
||||
const waitTimeout = config?.backend?.pool?.waitTimeout ?? 120000;
|
||||
const inputLocator = page.getByRole('textbox');
|
||||
const sendBtnLocator = page.getByRole('button', { name: 'Send message' });
|
||||
|
||||
@@ -100,7 +101,7 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) {
|
||||
const streamApiResponsePromise = waitApiResponse(page, {
|
||||
urlMatch: 'assistant.lamda.BardFrontendService/StreamGenerate',
|
||||
method: 'POST',
|
||||
timeout: 120000,
|
||||
timeout: waitTimeout,
|
||||
meta
|
||||
});
|
||||
|
||||
@@ -138,7 +139,7 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) {
|
||||
urlMatch: 'contribution.usercontent.google.com/download',
|
||||
urlContains: 'filename=video.mp4',
|
||||
method: 'GET',
|
||||
timeout: 180000, // 视频生成可能更慢
|
||||
timeout: waitTimeout,
|
||||
meta
|
||||
});
|
||||
} catch (e) {
|
||||
|
||||
@@ -98,6 +98,7 @@ async function handleAccountChooser(page) {
|
||||
*/
|
||||
async function generate(context, prompt, imgPaths, modelId, meta = {}) {
|
||||
const { page, config } = context;
|
||||
const waitTimeout = config?.backend?.pool?.waitTimeout ?? 120000;
|
||||
|
||||
try {
|
||||
// 支持新路径 adapter.gemini_biz.entryUrl,向下兼容旧路径 geminiBiz.entryUrl
|
||||
@@ -180,7 +181,7 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) {
|
||||
const apiResponsePromise = waitApiResponse(page, {
|
||||
urlMatch: 'global/widgetStreamAssist',
|
||||
method: 'POST',
|
||||
timeout: 120000,
|
||||
timeout: waitTimeout,
|
||||
errorText: ['modelArmorViolation'],
|
||||
meta
|
||||
});
|
||||
@@ -217,7 +218,7 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) {
|
||||
const imageResponsePromise = waitApiResponse(page, {
|
||||
urlMatch: 'download/v1alpha/projects',
|
||||
method: 'GET',
|
||||
timeout: 120000,
|
||||
timeout: waitTimeout,
|
||||
errorText: ['is unable to reply as the prompt'],
|
||||
meta
|
||||
});
|
||||
|
||||
@@ -97,6 +97,7 @@ async function handleAccountChooser(page) {
|
||||
*/
|
||||
async function generate(context, prompt, imgPaths, modelId, meta = {}) {
|
||||
const { page, config } = context;
|
||||
const waitTimeout = config?.backend?.pool?.waitTimeout ?? 120000;
|
||||
|
||||
try {
|
||||
// 支持新路径 adapter.gemini_biz.entryUrl,向下兼容旧路径 geminiBiz.entryUrl
|
||||
@@ -190,7 +191,7 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) {
|
||||
const apiResponsePromise = waitApiResponse(page, {
|
||||
urlMatch: 'global/widgetStreamAssist',
|
||||
method: 'POST',
|
||||
timeout: 120000,
|
||||
timeout: waitTimeout,
|
||||
errorText: ['modelArmorViolation'],
|
||||
meta
|
||||
});
|
||||
|
||||
@@ -30,7 +30,8 @@ const TARGET_URL = 'https://gemini.google.com/app?hl=en';
|
||||
* @returns {Promise<{text?: string, error?: string}>}
|
||||
*/
|
||||
async function generate(context, prompt, imgPaths, modelId, meta = {}) {
|
||||
const { page } = context;
|
||||
const { page, config } = context;
|
||||
const waitTimeout = config?.backend?.pool?.waitTimeout ?? 120000;
|
||||
const inputLocator = page.getByRole('textbox');
|
||||
const sendBtnLocator = page.getByRole('button', { name: 'Send message' });
|
||||
|
||||
@@ -151,7 +152,7 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) {
|
||||
const apiResponsePromise = waitApiResponse(page, {
|
||||
urlMatch: 'assistant.lamda.BardFrontendService/StreamGenerate',
|
||||
method: 'POST',
|
||||
timeout: 120000,
|
||||
timeout: waitTimeout,
|
||||
meta
|
||||
});
|
||||
|
||||
|
||||
@@ -48,7 +48,8 @@ async function detectImageAspect(imgPath) {
|
||||
* @returns {Promise<{image?: string, error?: string}>}
|
||||
*/
|
||||
async function generate(context, prompt, imgPaths, modelId, meta = {}) {
|
||||
const { page } = context;
|
||||
const { page, config } = context;
|
||||
const waitTimeout = config?.backend?.pool?.waitTimeout ?? 120000;
|
||||
|
||||
// 获取模型配置
|
||||
const modelConfig = manifest.models.find(m => m.id === modelId) || manifest.models[0];
|
||||
@@ -197,7 +198,7 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) {
|
||||
const apiResponsePromise = waitApiResponse(page, {
|
||||
urlMatch: 'flowMedia:batchGenerateImages',
|
||||
method: 'POST',
|
||||
timeout: 120000,
|
||||
timeout: waitTimeout,
|
||||
meta
|
||||
});
|
||||
|
||||
|
||||
@@ -52,6 +52,7 @@ function extractImage(text) {
|
||||
*/
|
||||
async function generate(context, prompt, imgPaths, modelId, meta = {}) {
|
||||
const { page, config } = context;
|
||||
const waitTimeout = config?.backend?.pool?.waitTimeout ?? 120000;
|
||||
const textareaSelector = 'textarea';
|
||||
|
||||
try {
|
||||
@@ -117,7 +118,7 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) {
|
||||
const responsePromise = waitApiResponse(page, {
|
||||
urlMatch: '/nextjs-api/stream',
|
||||
method: 'POST',
|
||||
timeout: 120000,
|
||||
timeout: waitTimeout,
|
||||
meta
|
||||
});
|
||||
|
||||
|
||||
@@ -31,7 +31,8 @@ const TARGET_URL_SEARCH = 'https://lmarena.ai/zh/c/new?mode=direct&chat-modality
|
||||
* @returns {Promise<{image?: string, text?: string, error?: string}>} 生成结果
|
||||
*/
|
||||
async function generate(context, prompt, imgPaths, modelId, meta = {}) {
|
||||
const { page } = context;
|
||||
const { page, config } = context;
|
||||
const waitTimeout = config?.backend?.pool?.waitTimeout ?? 120000;
|
||||
const textareaSelector = 'textarea';
|
||||
|
||||
// Worker 已验证,直接解析模型配置
|
||||
@@ -101,7 +102,7 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) {
|
||||
const responsePromise = waitApiResponse(page, {
|
||||
urlMatch: '/nextjs-api/stream',
|
||||
method: 'POST',
|
||||
timeout: 120000,
|
||||
timeout: waitTimeout,
|
||||
meta
|
||||
});
|
||||
|
||||
|
||||
@@ -31,7 +31,8 @@ const TARGET_URL = 'https://nanobananafree.ai/';
|
||||
* @returns {Promise<{image?: string, text?: string, error?: string}>} 生成结果
|
||||
*/
|
||||
async function generate(context, prompt, imgPaths, modelId, meta = {}) {
|
||||
const { page } = context;
|
||||
const { page, config } = context;
|
||||
const waitTimeout = config?.backend?.pool?.waitTimeout ?? 120000;
|
||||
const textareaSelector = 'textarea';
|
||||
|
||||
try {
|
||||
@@ -63,7 +64,7 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) {
|
||||
const responsePromise = waitApiResponse(page, {
|
||||
urlMatch: 'v1/generateContent',
|
||||
method: 'POST',
|
||||
timeout: 120000,
|
||||
timeout: waitTimeout,
|
||||
meta
|
||||
});
|
||||
|
||||
|
||||
@@ -116,6 +116,7 @@ async function handleDiscordAuth(page) {
|
||||
*/
|
||||
async function generate(context, prompt, imgPaths, modelId, meta = {}) {
|
||||
const { page, config } = context;
|
||||
const waitTimeout = config?.backend?.pool?.waitTimeout ?? 120000;
|
||||
|
||||
try {
|
||||
// 开启新对话 - 先等待可能正在进行的登录处理完成
|
||||
@@ -276,7 +277,7 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) {
|
||||
completionsResponse = await waitApiResponse(page, {
|
||||
urlMatch: 'chat/completions',
|
||||
method: 'POST',
|
||||
timeout: 120000,
|
||||
timeout: waitTimeout,
|
||||
errorText: ['Model is unable to process your request', 'Rate limit reached'],
|
||||
meta
|
||||
});
|
||||
@@ -312,7 +313,7 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) {
|
||||
completedResponse = await waitApiResponse(page, {
|
||||
urlMatch: 'chat/completed',
|
||||
method: 'POST',
|
||||
timeout: 120000,
|
||||
timeout: waitTimeout,
|
||||
errorText: ['Model is unable to process your request', 'Rate limit reached'],
|
||||
meta
|
||||
});
|
||||
|
||||
@@ -133,6 +133,7 @@ function extractTextContent(content) {
|
||||
*/
|
||||
async function generate(context, prompt, imgPaths, modelId, meta = {}) {
|
||||
const { page, config } = context;
|
||||
const waitTimeout = config?.backend?.pool?.waitTimeout ?? 120000;
|
||||
|
||||
try {
|
||||
// 开启新对话 - 先等待可能正在进行的登录处理完成
|
||||
@@ -245,7 +246,7 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) {
|
||||
completionsResponse = await waitApiResponse(page, {
|
||||
urlMatch: 'chat/completions',
|
||||
method: 'POST',
|
||||
timeout: 120000,
|
||||
timeout: waitTimeout,
|
||||
errorText: ['Model is unable to process your request', 'Rate limit reached'],
|
||||
meta
|
||||
});
|
||||
@@ -281,7 +282,7 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) {
|
||||
completedResponse = await waitApiResponse(page, {
|
||||
urlMatch: 'chat/completed',
|
||||
method: 'POST',
|
||||
timeout: 120000,
|
||||
timeout: waitTimeout,
|
||||
errorText: ['Model is unable to process your request', 'Rate limit reached'],
|
||||
meta
|
||||
});
|
||||
|
||||
@@ -30,7 +30,8 @@ const SEND_BUTTON_SELECTOR = '.input-actions-send button';
|
||||
* @returns {Promise<{text?: string, error?: string}>} 生成结果
|
||||
*/
|
||||
async function generate(context, prompt, imgPaths, modelId, meta = {}) {
|
||||
const { page } = context;
|
||||
const { page, config } = context;
|
||||
const waitTimeout = config?.backend?.pool?.waitTimeout ?? 120000;
|
||||
|
||||
try {
|
||||
const targetUrl = 'https://zenmux.ai/settings/chat';
|
||||
@@ -137,7 +138,7 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) {
|
||||
const apiResponsePromise = waitApiResponse(page, {
|
||||
urlMatch: 'v1/chat/completions',
|
||||
method: 'POST',
|
||||
timeout: 120000,
|
||||
timeout: waitTimeout,
|
||||
meta
|
||||
});
|
||||
|
||||
|
||||
@@ -281,6 +281,7 @@ export function getPoolConfig() {
|
||||
|
||||
return {
|
||||
strategy: pool.strategy || 'least_busy',
|
||||
waitTimeout: pool.waitTimeout != null ? Math.round(pool.waitTimeout / 1000) : 120,
|
||||
failover: {
|
||||
enabled: failover.enabled !== false, // 默认 true
|
||||
maxRetries: failover.maxRetries ?? 2
|
||||
@@ -302,6 +303,12 @@ export function savePoolConfig(data) {
|
||||
config.backend.pool.strategy = data.strategy;
|
||||
}
|
||||
|
||||
if (data.waitTimeout !== undefined) {
|
||||
// 前端传入秒,写入 YAML 为毫秒
|
||||
const ms = Number(data.waitTimeout) * 1000;
|
||||
if (ms > 0) config.backend.pool.waitTimeout = ms;
|
||||
}
|
||||
|
||||
if (data.failover) {
|
||||
if (!config.backend.pool.failover) config.backend.pool.failover = {};
|
||||
if (data.failover.enabled !== undefined) {
|
||||
|
||||
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
@@ -261,29 +261,47 @@ const handleRemoveWorker = (index) => {
|
||||
]" />
|
||||
</div>
|
||||
|
||||
<!-- 故障转移 -->
|
||||
<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.enabled" />
|
||||
</div>
|
||||
</a-col>
|
||||
<!-- 生成等待超时 -->
|
||||
<div style="margin-bottom: 24px;">
|
||||
<div style="font-weight: 600; margin-bottom: 8px;">生成等待超时</div>
|
||||
<div style="font-size: 12px; color: #8c8c8c; margin-bottom: 12px;">
|
||||
等待 AI 生成结果的最长时间,单位:秒(默认 120 秒)
|
||||
</div>
|
||||
<a-input-number v-model:value="poolConfig.waitTimeout" :min="30" :max="3600" :step="30"
|
||||
style="width: 100%" placeholder="请输入超时秒数">
|
||||
<template #addonAfter>秒</template>
|
||||
</a-input-number>
|
||||
</div>
|
||||
|
||||
<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.maxRetries" :min="1" :max="10"
|
||||
:disabled="!poolConfig.failover.enabled" style="width: 100%" placeholder="请输入重试次数" />
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<!-- 故障转移(折叠面板) -->
|
||||
<div style="margin-bottom: 24px;">
|
||||
<a-collapse>
|
||||
<a-collapse-panel key="failover" header="故障转移">
|
||||
<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.enabled" />
|
||||
</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.maxRetries" :min="1" :max="10"
|
||||
:disabled="!poolConfig.failover.enabled" style="width: 100%" placeholder="请输入重试次数" />
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
</div>
|
||||
|
||||
<!-- 保存按钮 -->
|
||||
<div style="display: flex; justify-content: flex-end; margin-top: 24px;">
|
||||
|
||||
@@ -9,6 +9,7 @@ export const useSettingsStore = defineStore('settings', {
|
||||
workerConfig: [],
|
||||
poolConfig: {
|
||||
strategy: 'least_busy',
|
||||
waitTimeout: 120,
|
||||
failover: {
|
||||
enabled: false,
|
||||
maxRetries: 3
|
||||
@@ -164,6 +165,7 @@ export const useSettingsStore = defineStore('settings', {
|
||||
// 合并以确保结构存在
|
||||
this.poolConfig = {
|
||||
strategy: data.strategy || 'least_busy',
|
||||
waitTimeout: data.waitTimeout ?? 120,
|
||||
failover: {
|
||||
enabled: data.failover?.enabled || false,
|
||||
maxRetries: data.failover?.maxRetries || 3
|
||||
|
||||
Reference in New Issue
Block a user