feat(douyu): 精英宝典支持多批次并发与多二维码并行扫码

- useWebSocketLogs 改为多连接(Map),新增 connectBatch/closeBatch/closeAll,旧 connect/close 向后兼容
- DouyuTasksPage runningBatchId 单值改为 Set 集合,移除 batchBusy 全局锁,右键菜单按账号维度禁用
- 二维码弹窗支持多账号标签切换并行扫码,恢复三弹窗互斥
- connectBatch 已有连接时不清日志,修复 onerror 不更新 connected
- 修复自动弹出循环覆盖 active 标签的 bug
This commit is contained in:
yml2213
2026-08-01 11:37:53 +08:00
parent dbf78b825c
commit 8c6c0bcfde
2 changed files with 162 additions and 86 deletions
+60 -35
View File
@@ -13,6 +13,7 @@ interface ConnectOptions {
}
const MAX_LOGS = 1000;
const DEFAULT_KEY = '__default__';
function toWebSocketUrl(pathOrUrl: string): string {
if (pathOrUrl.startsWith('ws://') || pathOrUrl.startsWith('wss://')) {
@@ -26,43 +27,59 @@ function toWebSocketUrl(pathOrUrl: string): string {
export function useWebSocketLogs() {
const [logs, setLogs] = useState<RealtimeLog[]>([]);
const [connected, setConnected] = useState(false);
const wsRef = useRef<WebSocket | null>(null);
const callbacksRef = useRef<ConnectOptions>({});
const suppressCloseRef = useRef(false);
const wsMapRef = useRef<Map<string, WebSocket>>(new Map());
const callbacksMapRef = useRef<Map<string, ConnectOptions>>(new Map());
const suppressCloseRef = useRef<Set<string>>(new Set());
const syncConnected = useCallback(() => {
setConnected(wsMapRef.current.size > 0);
}, []);
const clearLogs = useCallback(() => {
setLogs([]);
}, []);
const close = useCallback((notify = false) => {
if (!wsRef.current) {
setConnected(false);
return;
}
suppressCloseRef.current = !notify;
wsRef.current.close();
wsRef.current = null;
setConnected(false);
}, []);
const closeBatch = useCallback((key: string, notify = false) => {
const ws = wsMapRef.current.get(key);
if (!ws) return;
if (!notify) suppressCloseRef.current.add(key);
ws.close();
wsMapRef.current.delete(key);
syncConnected();
}, [syncConnected]);
const connect = useCallback((pathOrUrl: string, options: ConnectOptions = {}) => {
close(false);
suppressCloseRef.current = false;
callbacksRef.current = options;
if (options.clear ?? true) {
// 向后兼容:单连接场景关闭默认通道。
const close = useCallback((notify = false) => {
closeBatch(DEFAULT_KEY, notify);
}, [closeBatch]);
const closeAll = useCallback((notify = false) => {
Array.from(wsMapRef.current.keys()).forEach((key) => closeBatch(key, notify));
}, [closeBatch]);
const connectBatch = useCallback((key: string, pathOrUrl: string, options: ConnectOptions = {}) => {
// 关闭同 key 旧连接,避免重复
if (wsMapRef.current.has(key)) {
closeBatch(key, false);
}
suppressCloseRef.current.delete(key);
callbacksMapRef.current.set(key, options);
// 已有其他批次连接时不清空日志,避免冲掉正在进行的批次日志
const shouldClear = (options.clear ?? true) && wsMapRef.current.size === 0;
if (shouldClear) {
setLogs([]);
}
const ws = new WebSocket(toWebSocketUrl(pathOrUrl));
wsRef.current = ws;
setConnected(true);
wsMapRef.current.set(key, ws);
syncConnected();
ws.onmessage = (event) => {
try {
const msg = JSON.parse(event.data) as RealtimeLog;
if (msg.level === 'heartbeat') return;
if (msg.level === 'result') {
callbacksRef.current.onResult?.();
callbacksMapRef.current.get(key)?.onResult?.();
return;
}
setLogs((prev) => [...prev, msg].slice(-MAX_LOGS));
@@ -72,27 +89,32 @@ export function useWebSocketLogs() {
};
ws.onclose = () => {
const isCurrent = wsRef.current === ws;
const shouldNotify = isCurrent && !suppressCloseRef.current;
if (isCurrent) {
wsRef.current = null;
setConnected(false);
suppressCloseRef.current = false;
}
if (shouldNotify) {
callbacksRef.current.onClose?.();
// 只处理自己这个实例的关闭,避免覆盖重连后的新连接
if (wsMapRef.current.get(key) !== ws) return;
wsMapRef.current.delete(key);
syncConnected();
const cb = callbacksMapRef.current.get(key);
if (!suppressCloseRef.current.has(key)) {
cb?.onClose?.();
}
callbacksMapRef.current.delete(key);
suppressCloseRef.current.delete(key);
};
ws.onerror = () => {
setConnected(false);
callbacksRef.current.onError?.();
if (wsMapRef.current.get(key) === ws) setConnected(false);
callbacksMapRef.current.get(key)?.onError?.();
};
}, [close]);
}, [closeBatch, syncConnected]);
// 向后兼容:connect(pathOrUrl, options) 等价于 connectBatch(DEFAULT_KEY, ...)
const connect = useCallback((pathOrUrl: string, options: ConnectOptions = {}) => {
connectBatch(DEFAULT_KEY, pathOrUrl, options);
}, [connectBatch]);
useEffect(() => () => {
close(false);
}, [close]);
closeAll(false);
}, [closeAll]);
return {
logs,
@@ -100,5 +122,8 @@ export function useWebSocketLogs() {
clearLogs,
connect,
close,
connectBatch,
closeBatch,
closeAll,
};
}