mirror of
https://github.com/foxhui/WebAI2API.git
synced 2026-06-16 21:03:59 +08:00
@@ -12,6 +12,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- 再次修复豆包适配器无法点击选择模型的问题
|
- 再次修复豆包适配器无法点击选择模型的问题
|
||||||
- 修复豆包超时时间跟随配置文件
|
- 修复豆包超时时间跟随配置文件
|
||||||
|
|
||||||
|
### 🔄 Changed
|
||||||
|
- **浏览器**
|
||||||
|
- 回滚互斥锁机制,不再使用互斥锁,这是一个错误的修复
|
||||||
|
|
||||||
## [3.5.9] - 2026-03-25
|
## [3.5.9] - 2026-03-25
|
||||||
|
|
||||||
### 🐛 Fixed
|
### 🐛 Fixed
|
||||||
|
|||||||
@@ -108,8 +108,6 @@ export class PoolManager {
|
|||||||
// 建立共享关系:设置所有者引用,并添加到所有者的共享列表
|
// 建立共享关系:设置所有者引用,并添加到所有者的共享列表
|
||||||
worker._browserOwner = existing.ownerWorker;
|
worker._browserOwner = existing.ownerWorker;
|
||||||
existing.ownerWorker._sharedWorkers.push(worker);
|
existing.ownerWorker._sharedWorkers.push(worker);
|
||||||
// 共享同一把浏览器互斥锁
|
|
||||||
worker._browserMutex = existing.ownerWorker._browserMutex;
|
|
||||||
} else {
|
} else {
|
||||||
await worker.init();
|
await worker.init();
|
||||||
browserMap.set(worker.userDataDir, {
|
browserMap.set(worker.userDataDir, {
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import { logger } from '../../utils/logger.js';
|
|||||||
import { initBrowserBase, createCursor } from '../engine/launcher.js';
|
import { initBrowserBase, createCursor } from '../engine/launcher.js';
|
||||||
import { registry } from '../registry.js';
|
import { registry } from '../registry.js';
|
||||||
import { tryGotoWithCheck } from '../utils/page.js';
|
import { tryGotoWithCheck } from '../utils/page.js';
|
||||||
import { AsyncMutex } from '../../utils/asyncMutex.js';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Worker 类 - 封装单个浏览器实例
|
* Worker 类 - 封装单个浏览器实例
|
||||||
@@ -41,9 +40,6 @@ export class Worker {
|
|||||||
this._isBrowserOwner = false; // 是否是浏览器的所有者(负责重启)
|
this._isBrowserOwner = false; // 是否是浏览器的所有者(负责重启)
|
||||||
this._browserOwner = null; // 如果是共享者,指向所有者 Worker
|
this._browserOwner = null; // 如果是共享者,指向所有者 Worker
|
||||||
this._sharedWorkers = []; // 如果是所有者,保存共享该浏览器的 Worker 列表
|
this._sharedWorkers = []; // 如果是所有者,保存共享该浏览器的 Worker 列表
|
||||||
|
|
||||||
// 浏览器操作互斥锁(同一浏览器实例的 Worker 共享同一把锁)
|
|
||||||
this._browserMutex = new AsyncMutex();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -120,6 +116,7 @@ export class Worker {
|
|||||||
this.browser = sharedBrowser;
|
this.browser = sharedBrowser;
|
||||||
this.page = await sharedBrowser.newPage();
|
this.page = await sharedBrowser.newPage();
|
||||||
this.page.authState = { isHandlingAuth: false };
|
this.page.authState = { isHandlingAuth: false };
|
||||||
|
this.page._browserMutex = this._browserMutex;
|
||||||
const humanizeCursorMode = this.globalConfig?.browser?.humanizeCursor;
|
const humanizeCursorMode = this.globalConfig?.browser?.humanizeCursor;
|
||||||
this.page._humanizeCursorMode = humanizeCursorMode;
|
this.page._humanizeCursorMode = humanizeCursorMode;
|
||||||
// true 表示使用项目维护的 ghost-cursor
|
// true 表示使用项目维护的 ghost-cursor
|
||||||
@@ -482,18 +479,12 @@ export class Worker {
|
|||||||
userDataDir: this.userDataDir
|
userDataDir: this.userDataDir
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取浏览器互斥锁(防止同一浏览器实例的多个 Worker 并发操作鼠标)
|
|
||||||
const releaseLock = await this._browserMutex.acquire();
|
|
||||||
logger.debug('工作池', `[${this.name}] 已获取浏览器锁`, meta);
|
|
||||||
|
|
||||||
this.busyCount++;
|
this.busyCount++;
|
||||||
try {
|
try {
|
||||||
// 传递原始 modelId,由适配器自己解析
|
// 传递原始 modelId,由适配器自己解析
|
||||||
return await adapter.generate(subContext, prompt, paths, modelId, meta);
|
return await adapter.generate(subContext, prompt, paths, modelId, meta);
|
||||||
} finally {
|
} finally {
|
||||||
this.busyCount--;
|
this.busyCount--;
|
||||||
releaseLock();
|
|
||||||
logger.debug('工作池', `[${this.name}] 已释放浏览器锁`, meta);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -193,7 +193,8 @@ export async function scrollToElement(page, selectorOrLocator, options = {}) {
|
|||||||
* @param {object} [options.meta={}] - 日志元数据
|
* @param {object} [options.meta={}] - 日志元数据
|
||||||
* @returns {Promise<import('playwright-core').Response>} 响应对象
|
* @returns {Promise<import('playwright-core').Response>} 响应对象
|
||||||
*/
|
*/
|
||||||
export async function waitApiResponse(page, options = {}) {
|
export function waitApiResponse(page, options = {}) {
|
||||||
|
const promise = (async () => {
|
||||||
const {
|
const {
|
||||||
urlMatch,
|
urlMatch,
|
||||||
urlContains,
|
urlContains,
|
||||||
@@ -352,5 +353,13 @@ export async function waitApiResponse(page, options = {}) {
|
|||||||
} finally {
|
} finally {
|
||||||
cleanup();
|
cleanup();
|
||||||
}
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
// 关键修复:挂载一个空的 catch 处理器
|
||||||
|
// 因为适配器通常是先调用 waitApiResponse 拿到 Promise,然后执行 safeClick,最后再 await
|
||||||
|
// 如果在 safeClick 期间页面关闭/崩溃,此 Promise 会被 reject,触发 Node.js 未捕获异常崩溃
|
||||||
|
promise.catch(() => {});
|
||||||
|
|
||||||
|
return promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
/**
|
|
||||||
* @fileoverview 轻量异步互斥锁
|
|
||||||
* @description 保证同一时刻只有一个 async 任务持有锁,用于防止同一浏览器实例的多个 Worker 并发操作鼠标。
|
|
||||||
*/
|
|
||||||
|
|
||||||
export class AsyncMutex {
|
|
||||||
constructor() {
|
|
||||||
this._queue = [];
|
|
||||||
this._locked = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取锁,返回释放函数
|
|
||||||
* @returns {Promise<() => void>}
|
|
||||||
*/
|
|
||||||
acquire() {
|
|
||||||
return new Promise(resolve => {
|
|
||||||
const tryAcquire = () => {
|
|
||||||
if (!this._locked) {
|
|
||||||
this._locked = true;
|
|
||||||
resolve(() => {
|
|
||||||
this._locked = false;
|
|
||||||
if (this._queue.length > 0) {
|
|
||||||
this._queue.shift()();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this._queue.push(tryAcquire);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
tryAcquire();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user