Compare commits
2 Commits
604bd8343a
...
0e7213f006
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0e7213f006 | ||
|
|
14276b51b7 |
257
modules/button-collapse.js
Normal file
257
modules/button-collapse.js
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
let stylesInjected = false;
|
||||||
|
|
||||||
|
const SELECTORS = {
|
||||||
|
chat: '#chat',
|
||||||
|
messages: '.mes',
|
||||||
|
mesButtons: '.mes_block .mes_buttons',
|
||||||
|
buttons: '.memory-button, .dynamic-prompt-analysis-btn, .mes_history_preview',
|
||||||
|
collapse: '.xiaobaix-collapse-btn',
|
||||||
|
};
|
||||||
|
|
||||||
|
const XPOS_KEY = 'xiaobaix_x_btn_position';
|
||||||
|
const getXBtnPosition = () => {
|
||||||
|
try {
|
||||||
|
return (
|
||||||
|
window?.extension_settings?.LittleWhiteBox?.xBtnPosition ||
|
||||||
|
localStorage.getItem(XPOS_KEY) ||
|
||||||
|
'name-left'
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
return 'name-left';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const injectStyles = () => {
|
||||||
|
if (stylesInjected) return;
|
||||||
|
const css = `
|
||||||
|
.mes_block .mes_buttons{align-items:center}
|
||||||
|
.xiaobaix-collapse-btn{
|
||||||
|
position:relative;display:inline-flex;width:32px;height:32px;justify-content:center;align-items:center;
|
||||||
|
border-radius:50%;background:var(--SmartThemeBlurTintColor);cursor:pointer;
|
||||||
|
box-shadow:inset 0 0 15px rgba(0,0,0,.6),0 2px 8px rgba(0,0,0,.2);
|
||||||
|
transition:opacity .15s ease,transform .15s ease}
|
||||||
|
.xiaobaix-xstack{position:relative;display:inline-flex;align-items:center;justify-content:center;pointer-events:none}
|
||||||
|
.xiaobaix-xstack span{
|
||||||
|
position:absolute;font:italic 900 20px 'Arial Black',sans-serif;letter-spacing:-2px;transform:scaleX(.8);
|
||||||
|
text-shadow:0 0 10px rgba(255,255,255,.5),0 0 20px rgba(100,200,255,.3);color:#fff}
|
||||||
|
.xiaobaix-xstack span:nth-child(1){color:rgba(255,255,255,.1);transform:scaleX(.8) translateX(-8px);text-shadow:none}
|
||||||
|
.xiaobaix-xstack span:nth-child(2){color:rgba(255,255,255,.2);transform:scaleX(.8) translateX(-4px);text-shadow:none}
|
||||||
|
.xiaobaix-xstack span:nth-child(3){color:rgba(255,255,255,.4);transform:scaleX(.8) translateX(-2px);text-shadow:none}
|
||||||
|
.xiaobaix-sub-container{display:none;position:absolute;right:38px;border-radius:8px;padding:4px;gap:8px;pointer-events:auto}
|
||||||
|
.xiaobaix-collapse-btn.open .xiaobaix-sub-container{display:flex;background:var(--SmartThemeBlurTintColor)}
|
||||||
|
.xiaobaix-collapse-btn.open,.xiaobaix-collapse-btn.open ~ *{pointer-events:auto!important}
|
||||||
|
.mes_block .mes_buttons.xiaobaix-expanded{width:150px}
|
||||||
|
.xiaobaix-sub-container,.xiaobaix-sub-container *{pointer-events:auto!important}
|
||||||
|
.xiaobaix-sub-container .memory-button,.xiaobaix-sub-container .dynamic-prompt-analysis-btn,.xiaobaix-sub-container .mes_history_preview{opacity:1!important;filter:none!important}
|
||||||
|
.xiaobaix-sub-container.dir-right{left:38px;right:auto;z-index:1000;margin-top:2px}
|
||||||
|
`;
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.textContent = css;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
stylesInjected = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createCollapseButton = (dirRight) => {
|
||||||
|
injectStyles();
|
||||||
|
const btn = document.createElement('div');
|
||||||
|
btn.className = 'mes_btn xiaobaix-collapse-btn';
|
||||||
|
btn.innerHTML = `
|
||||||
|
<div class="xiaobaix-xstack"><span>X</span><span>X</span><span>X</span><span>X</span></div>
|
||||||
|
<div class="xiaobaix-sub-container${dirRight ? ' dir-right' : ''}"></div>
|
||||||
|
`;
|
||||||
|
const sub = btn.lastElementChild;
|
||||||
|
|
||||||
|
['click','pointerdown','pointerup'].forEach(t => {
|
||||||
|
sub.addEventListener(t, e => e.stopPropagation(), { passive: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
btn.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault(); e.stopPropagation();
|
||||||
|
const open = btn.classList.toggle('open');
|
||||||
|
const mesButtons = btn.closest(SELECTORS.mesButtons);
|
||||||
|
if (mesButtons) mesButtons.classList.toggle('xiaobaix-expanded', open);
|
||||||
|
});
|
||||||
|
|
||||||
|
return btn;
|
||||||
|
};
|
||||||
|
|
||||||
|
const findInsertPoint = (messageEl) => {
|
||||||
|
return messageEl.querySelector(
|
||||||
|
'.ch_name.flex-container.justifySpaceBetween .flex-container.flex1.alignitemscenter,' +
|
||||||
|
'.ch_name.flex-container.justifySpaceBetween .flex-container.flex1.alignItemsCenter'
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ensureCollapseForMessage = (messageEl, pos) => {
|
||||||
|
const mesButtons = messageEl.querySelector(SELECTORS.mesButtons);
|
||||||
|
if (!mesButtons) return null;
|
||||||
|
|
||||||
|
let collapseBtn = messageEl.querySelector(SELECTORS.collapse);
|
||||||
|
const dirRight = pos === 'edit-right';
|
||||||
|
|
||||||
|
if (!collapseBtn) collapseBtn = createCollapseButton(dirRight);
|
||||||
|
else collapseBtn.querySelector('.xiaobaix-sub-container')?.classList.toggle('dir-right', dirRight);
|
||||||
|
|
||||||
|
if (dirRight) {
|
||||||
|
const container = findInsertPoint(messageEl);
|
||||||
|
if (!container) return null;
|
||||||
|
if (collapseBtn.parentNode !== container) container.appendChild(collapseBtn);
|
||||||
|
} else {
|
||||||
|
if (mesButtons.lastElementChild !== collapseBtn) mesButtons.appendChild(collapseBtn);
|
||||||
|
}
|
||||||
|
return collapseBtn;
|
||||||
|
};
|
||||||
|
|
||||||
|
let processed = new WeakSet();
|
||||||
|
let io = null;
|
||||||
|
let mo = null;
|
||||||
|
let queue = [];
|
||||||
|
let rafScheduled = false;
|
||||||
|
|
||||||
|
const processOneMessage = (message) => {
|
||||||
|
if (!message || processed.has(message)) return;
|
||||||
|
|
||||||
|
const mesButtons = message.querySelector(SELECTORS.mesButtons);
|
||||||
|
if (!mesButtons) { processed.add(message); return; }
|
||||||
|
|
||||||
|
const pos = getXBtnPosition();
|
||||||
|
if (pos === 'edit-right' && !findInsertPoint(message)) { processed.add(message); return; }
|
||||||
|
|
||||||
|
const targetBtns = mesButtons.querySelectorAll(SELECTORS.buttons);
|
||||||
|
if (!targetBtns.length) { processed.add(message); return; }
|
||||||
|
|
||||||
|
const collapseBtn = ensureCollapseForMessage(message, pos);
|
||||||
|
if (!collapseBtn) { processed.add(message); return; }
|
||||||
|
|
||||||
|
const sub = collapseBtn.querySelector('.xiaobaix-sub-container');
|
||||||
|
const frag = document.createDocumentFragment();
|
||||||
|
targetBtns.forEach(b => frag.appendChild(b));
|
||||||
|
sub.appendChild(frag);
|
||||||
|
|
||||||
|
processed.add(message);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ensureIO = () => {
|
||||||
|
if (io) return io;
|
||||||
|
io = new IntersectionObserver((entries) => {
|
||||||
|
for (const e of entries) {
|
||||||
|
if (!e.isIntersecting) continue;
|
||||||
|
processOneMessage(e.target);
|
||||||
|
io.unobserve(e.target);
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
root: document.querySelector(SELECTORS.chat) || null,
|
||||||
|
rootMargin: '200px 0px',
|
||||||
|
threshold: 0
|
||||||
|
});
|
||||||
|
return io;
|
||||||
|
};
|
||||||
|
|
||||||
|
const observeVisibility = (nodes) => {
|
||||||
|
const obs = ensureIO();
|
||||||
|
nodes.forEach(n => { if (n && !processed.has(n)) obs.observe(n); });
|
||||||
|
};
|
||||||
|
|
||||||
|
const hookMutations = () => {
|
||||||
|
const chat = document.querySelector(SELECTORS.chat);
|
||||||
|
if (!chat) return;
|
||||||
|
|
||||||
|
if (!mo) {
|
||||||
|
mo = new MutationObserver((muts) => {
|
||||||
|
for (const m of muts) {
|
||||||
|
m.addedNodes && m.addedNodes.forEach(n => {
|
||||||
|
if (n.nodeType !== 1) return;
|
||||||
|
const el = n;
|
||||||
|
if (el.matches?.(SELECTORS.messages)) queue.push(el);
|
||||||
|
else el.querySelectorAll?.(SELECTORS.messages)?.forEach(mes => queue.push(mes));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!rafScheduled && queue.length) {
|
||||||
|
rafScheduled = true;
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
observeVisibility(queue);
|
||||||
|
queue = [];
|
||||||
|
rafScheduled = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
mo.observe(chat, { childList: true, subtree: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
const processExistingVisible = () => {
|
||||||
|
const all = document.querySelectorAll(`${SELECTORS.chat} ${SELECTORS.messages}`);
|
||||||
|
if (!all.length) return;
|
||||||
|
const unprocessed = [];
|
||||||
|
all.forEach(n => { if (!processed.has(n)) unprocessed.push(n); });
|
||||||
|
if (unprocessed.length) observeVisibility(unprocessed);
|
||||||
|
};
|
||||||
|
|
||||||
|
const initButtonCollapse = () => {
|
||||||
|
injectStyles();
|
||||||
|
hookMutations();
|
||||||
|
processExistingVisible();
|
||||||
|
if (window && window['registerModuleCleanup']) {
|
||||||
|
try { window['registerModuleCleanup']('buttonCollapse', cleanup); } catch {}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const processButtonCollapse = () => {
|
||||||
|
processExistingVisible();
|
||||||
|
};
|
||||||
|
|
||||||
|
const registerButtonToSubContainer = (messageId, buttonEl) => {
|
||||||
|
if (!buttonEl) return false;
|
||||||
|
const message = document.querySelector(`${SELECTORS.chat} ${SELECTORS.messages}[mesid="${messageId}"]`);
|
||||||
|
if (!message) return false;
|
||||||
|
|
||||||
|
processOneMessage(message);
|
||||||
|
|
||||||
|
const pos = getXBtnPosition();
|
||||||
|
const collapseBtn = message.querySelector(SELECTORS.collapse) || ensureCollapseForMessage(message, pos);
|
||||||
|
if (!collapseBtn) return false;
|
||||||
|
|
||||||
|
const sub = collapseBtn.querySelector('.xiaobaix-sub-container');
|
||||||
|
sub.appendChild(buttonEl);
|
||||||
|
buttonEl.style.pointerEvents = 'auto';
|
||||||
|
buttonEl.style.opacity = '1';
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const cleanup = () => {
|
||||||
|
io?.disconnect(); io = null;
|
||||||
|
mo?.disconnect(); mo = null;
|
||||||
|
queue = [];
|
||||||
|
rafScheduled = false;
|
||||||
|
|
||||||
|
document.querySelectorAll(SELECTORS.collapse).forEach(btn => {
|
||||||
|
const sub = btn.querySelector('.xiaobaix-sub-container');
|
||||||
|
const message = btn.closest(SELECTORS.messages) || btn.closest('.mes');
|
||||||
|
const mesButtons = message?.querySelector(SELECTORS.mesButtons) || message?.querySelector('.mes_buttons');
|
||||||
|
if (sub && mesButtons) {
|
||||||
|
mesButtons.classList.remove('xiaobaix-expanded');
|
||||||
|
const frag = document.createDocumentFragment();
|
||||||
|
while (sub.firstChild) frag.appendChild(sub.firstChild);
|
||||||
|
mesButtons.appendChild(frag);
|
||||||
|
}
|
||||||
|
btn.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
processed = new WeakSet();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
Object.assign(window, {
|
||||||
|
initButtonCollapse,
|
||||||
|
cleanupButtonCollapse: cleanup,
|
||||||
|
registerButtonToSubContainer,
|
||||||
|
processButtonCollapse,
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('xiaobaixEnabledChanged', (e) => {
|
||||||
|
const en = e && e.detail && e.detail.enabled;
|
||||||
|
if (!en) cleanup();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export { initButtonCollapse, cleanup, registerButtonToSubContainer, processButtonCollapse };
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
import { xbLog } from '../../../../core/debug-core.js';
|
import { xbLog } from '../../../../core/debug-core.js';
|
||||||
import { getVectorConfig } from '../../data/config.js';
|
import { getVectorConfig } from '../../data/config.js';
|
||||||
|
import { getApiKey } from './siliconflow.js';
|
||||||
|
|
||||||
const MODULE_ID = 'vector-llm-service';
|
const MODULE_ID = 'vector-llm-service';
|
||||||
const SILICONFLOW_API_URL = 'https://api.siliconflow.cn/v1';
|
const SILICONFLOW_API_URL = 'https://api.siliconflow.cn/v1';
|
||||||
@@ -40,8 +41,7 @@ export async function callLLM(messages, options = {}) {
|
|||||||
const mod = getStreamingModule();
|
const mod = getStreamingModule();
|
||||||
if (!mod) throw new Error('Streaming module not ready');
|
if (!mod) throw new Error('Streaming module not ready');
|
||||||
|
|
||||||
const cfg = getVectorConfig();
|
const apiKey = getApiKey() || '';
|
||||||
const apiKey = cfg?.online?.key || '';
|
|
||||||
if (!apiKey) {
|
if (!apiKey) {
|
||||||
throw new Error('L0 requires siliconflow API key');
|
throw new Error('L0 requires siliconflow API key');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,63 @@
|
|||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
// siliconflow.js - 仅保留 Embedding
|
// siliconflow.js - Embedding + 多 Key 轮询
|
||||||
|
//
|
||||||
|
// 在 API Key 输入框中用逗号、分号、竖线或换行分隔多个 Key,例如:
|
||||||
|
// sk-aaa,sk-bbb,sk-ccc
|
||||||
|
// 每次调用自动轮询到下一个 Key,并发请求会均匀分布到所有 Key 上。
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
const BASE_URL = 'https://api.siliconflow.cn';
|
const BASE_URL = 'https://api.siliconflow.cn';
|
||||||
const EMBEDDING_MODEL = 'BAAI/bge-m3';
|
const EMBEDDING_MODEL = 'BAAI/bge-m3';
|
||||||
|
|
||||||
export function getApiKey() {
|
// ★ 多 Key 轮询状态
|
||||||
|
let _keyIndex = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 localStorage 解析所有 Key(支持逗号、分号、竖线、换行分隔)
|
||||||
|
*/
|
||||||
|
function parseKeys() {
|
||||||
try {
|
try {
|
||||||
const raw = localStorage.getItem('summary_panel_config');
|
const raw = localStorage.getItem('summary_panel_config');
|
||||||
if (raw) {
|
if (raw) {
|
||||||
const parsed = JSON.parse(raw);
|
const parsed = JSON.parse(raw);
|
||||||
return parsed.vector?.online?.key || null;
|
const keyStr = parsed.vector?.online?.key || '';
|
||||||
|
return keyStr
|
||||||
|
.split(/[,;|\n]+/)
|
||||||
|
.map(k => k.trim())
|
||||||
|
.filter(k => k.length > 0);
|
||||||
}
|
}
|
||||||
} catch { }
|
} catch { }
|
||||||
return null;
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取下一个可用的 API Key(轮询)
|
||||||
|
* 每次调用返回不同的 Key,自动循环
|
||||||
|
*/
|
||||||
|
export function getApiKey() {
|
||||||
|
const keys = parseKeys();
|
||||||
|
if (!keys.length) return null;
|
||||||
|
if (keys.length === 1) return keys[0];
|
||||||
|
|
||||||
|
const idx = _keyIndex % keys.length;
|
||||||
|
const key = keys[idx];
|
||||||
|
_keyIndex = (_keyIndex + 1) % keys.length;
|
||||||
|
const masked = key.length > 10 ? key.slice(0, 6) + '***' + key.slice(-4) : '***';
|
||||||
|
console.log(`[SiliconFlow] 使用 Key ${idx + 1}/${keys.length}: ${masked}`);
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前配置的 Key 数量(供外部模块动态调整并发用)
|
||||||
|
*/
|
||||||
|
export function getKeyCount() {
|
||||||
|
return Math.max(1, parseKeys().length);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
// Embedding
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
export async function embed(texts, options = {}) {
|
export async function embed(texts, options = {}) {
|
||||||
if (!texts?.length) return [];
|
if (!texts?.length) return [];
|
||||||
|
|
||||||
|
|||||||
@@ -181,25 +181,16 @@ export async function incrementalExtractAtoms(chatId, chat, onProgress, options
|
|||||||
// ★ Phase 1: 收集所有新提取的 atoms(不向量化)
|
// ★ Phase 1: 收集所有新提取的 atoms(不向量化)
|
||||||
const allNewAtoms = [];
|
const allNewAtoms = [];
|
||||||
|
|
||||||
// ★ 30 并发批次处理
|
// ★ 限流检测:连续失败 N 次后暂停并降速
|
||||||
// 并发池处理(保持固定并发度)
|
let consecutiveFailures = 0;
|
||||||
const poolSize = Math.min(CONCURRENCY, pendingPairs.length);
|
let rateLimited = false;
|
||||||
let nextIndex = 0;
|
const RATE_LIMIT_THRESHOLD = 3; // 连续失败多少次触发限流保护
|
||||||
let started = 0;
|
const RATE_LIMIT_WAIT_MS = 60000; // 限流后等待时间(60 秒)
|
||||||
const runWorker = async (workerId) => {
|
const RETRY_INTERVAL_MS = 1000; // 降速模式下每次请求间隔(1 秒)
|
||||||
while (true) {
|
const RETRY_CONCURRENCY = 1; // ★ 降速模式下的并发数(默认1,建议不要超过5)
|
||||||
if (extractionCancelled) return;
|
|
||||||
const idx = nextIndex++;
|
|
||||||
if (idx >= pendingPairs.length) return;
|
|
||||||
|
|
||||||
const pair = pendingPairs[idx];
|
|
||||||
const stagger = started++;
|
|
||||||
if (STAGGER_DELAY > 0) {
|
|
||||||
await new Promise(r => setTimeout(r, stagger * STAGGER_DELAY));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (extractionCancelled) return;
|
|
||||||
|
|
||||||
|
// ★ 通用处理单个 pair 的逻辑(复用于正常模式和降速模式)
|
||||||
|
const processPair = async (pair, idx, workerId) => {
|
||||||
const floor = pair.aiFloor;
|
const floor = pair.aiFloor;
|
||||||
const prev = getL0FloorStatus(floor);
|
const prev = getL0FloorStatus(floor);
|
||||||
|
|
||||||
@@ -218,12 +209,14 @@ export async function incrementalExtractAtoms(chatId, chat, onProgress, options
|
|||||||
throw new Error('llm_failed');
|
throw new Error('llm_failed');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ★ 成功:重置连续失败计数
|
||||||
|
consecutiveFailures = 0;
|
||||||
|
|
||||||
if (!atoms.length) {
|
if (!atoms.length) {
|
||||||
setL0FloorStatus(floor, { status: 'empty', reason: 'llm_empty', atoms: 0 });
|
setL0FloorStatus(floor, { status: 'empty', reason: 'llm_empty', atoms: 0 });
|
||||||
} else {
|
} else {
|
||||||
atoms.forEach(a => a.chatId = chatId);
|
atoms.forEach(a => a.chatId = chatId);
|
||||||
saveStateAtoms(atoms);
|
saveStateAtoms(atoms);
|
||||||
// Phase 1: 只收集,不向量化
|
|
||||||
allNewAtoms.push(...atoms);
|
allNewAtoms.push(...atoms);
|
||||||
|
|
||||||
setL0FloorStatus(floor, { status: 'ok', atoms: atoms.length });
|
setL0FloorStatus(floor, { status: 'ok', atoms: atoms.length });
|
||||||
@@ -238,6 +231,13 @@ export async function incrementalExtractAtoms(chatId, chat, onProgress, options
|
|||||||
reason: String(e?.message || e).replace(/\s+/g, ' ').slice(0, 120),
|
reason: String(e?.message || e).replace(/\s+/g, ' ').slice(0, 120),
|
||||||
});
|
});
|
||||||
failed++;
|
failed++;
|
||||||
|
|
||||||
|
// ★ 限流检测:连续失败累加
|
||||||
|
consecutiveFailures++;
|
||||||
|
if (consecutiveFailures >= RATE_LIMIT_THRESHOLD && !rateLimited) {
|
||||||
|
rateLimited = true;
|
||||||
|
xbLog.warn(MODULE_ID, `连续失败 ${consecutiveFailures} 次,疑似触发 API 限流,将暂停所有并发`);
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
active--;
|
active--;
|
||||||
if (!extractionCancelled) {
|
if (!extractionCancelled) {
|
||||||
@@ -249,6 +249,27 @@ export async function incrementalExtractAtoms(chatId, chat, onProgress, options
|
|||||||
xbLog.info(MODULE_ID, `L0 pool progress=${completed}/${total} active=${active} peak=${peakActive} elapsedMs=${elapsed}`);
|
xbLog.info(MODULE_ID, `L0 pool progress=${completed}/${total} active=${active} peak=${peakActive} elapsedMs=${elapsed}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ★ 并发池处理(保持固定并发度)
|
||||||
|
const poolSize = Math.min(CONCURRENCY, pendingPairs.length);
|
||||||
|
let nextIndex = 0;
|
||||||
|
let started = 0;
|
||||||
|
const runWorker = async (workerId) => {
|
||||||
|
while (true) {
|
||||||
|
if (extractionCancelled || rateLimited) return;
|
||||||
|
const idx = nextIndex++;
|
||||||
|
if (idx >= pendingPairs.length) return;
|
||||||
|
|
||||||
|
const pair = pendingPairs[idx];
|
||||||
|
const stagger = started++;
|
||||||
|
if (STAGGER_DELAY > 0) {
|
||||||
|
await new Promise(r => setTimeout(r, stagger * STAGGER_DELAY));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extractionCancelled || rateLimited) return;
|
||||||
|
|
||||||
|
await processPair(pair, idx, workerId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -258,6 +279,61 @@ export async function incrementalExtractAtoms(chatId, chat, onProgress, options
|
|||||||
xbLog.info(MODULE_ID, `L0 pool done completed=${completed}/${total} failed=${failed} peakActive=${peakActive} elapsedMs=${elapsed}`);
|
xbLog.info(MODULE_ID, `L0 pool done completed=${completed}/${total} failed=${failed} peakActive=${peakActive} elapsedMs=${elapsed}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ═════════════════════════════════════════════════════════════════════
|
||||||
|
// ★ 限流恢复:重置进度,从头开始以限速模式慢慢跑
|
||||||
|
// ═════════════════════════════════════════════════════════════════════
|
||||||
|
if (rateLimited && !extractionCancelled) {
|
||||||
|
const waitSec = RATE_LIMIT_WAIT_MS / 1000;
|
||||||
|
xbLog.info(MODULE_ID, `限流保护:将重置进度并从头开始降速重来(并发=${RETRY_CONCURRENCY}, 间隔=${RETRY_INTERVAL_MS}ms)`);
|
||||||
|
onProgress?.(`疑似限流,${waitSec}s 后降速重头开始...`, completed, total);
|
||||||
|
|
||||||
|
await new Promise(r => setTimeout(r, RATE_LIMIT_WAIT_MS));
|
||||||
|
|
||||||
|
if (!extractionCancelled) {
|
||||||
|
// ★ 核心逻辑:重置计数器,让 UI 从 0 开始跑,给用户“重头开始”的反馈
|
||||||
|
rateLimited = false;
|
||||||
|
consecutiveFailures = 0;
|
||||||
|
completed = 0;
|
||||||
|
failed = 0;
|
||||||
|
|
||||||
|
let retryNextIdx = 0;
|
||||||
|
|
||||||
|
xbLog.info(MODULE_ID, `限流恢复:开始降速模式扫描 ${pendingPairs.length} 个楼层`);
|
||||||
|
|
||||||
|
const retryWorkers = Math.min(RETRY_CONCURRENCY, pendingPairs.length);
|
||||||
|
const runRetryWorker = async (wid) => {
|
||||||
|
while (true) {
|
||||||
|
if (extractionCancelled) return;
|
||||||
|
const idx = retryNextIdx++;
|
||||||
|
if (idx >= pendingPairs.length) return;
|
||||||
|
|
||||||
|
const pair = pendingPairs[idx];
|
||||||
|
const floor = pair.aiFloor;
|
||||||
|
|
||||||
|
// ★ 检查该楼层状态
|
||||||
|
const st = getL0FloorStatus(floor);
|
||||||
|
if (st?.status === 'ok' || st?.status === 'empty') {
|
||||||
|
// 刚才已经成功了,直接跳过(仅增加进度计数)
|
||||||
|
completed++;
|
||||||
|
onProgress?.(`提取: ${completed}/${total} (跳过已完成)`, completed, total);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ★ 没做过的,用 slow 模式处理
|
||||||
|
await processPair(pair, idx, `retry-${wid}`);
|
||||||
|
|
||||||
|
// 每个请求后休息,避免再次触发限流
|
||||||
|
if (idx < pendingPairs.length - 1 && RETRY_INTERVAL_MS > 0) {
|
||||||
|
await new Promise(r => setTimeout(r, RETRY_INTERVAL_MS));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await Promise.all(Array.from({ length: retryWorkers }, (_, i) => runRetryWorker(i)));
|
||||||
|
xbLog.info(MODULE_ID, `降速重头开始阶段结束`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
saveMetadataDebounced?.();
|
saveMetadataDebounced?.();
|
||||||
} catch { }
|
} catch { }
|
||||||
|
|||||||
Reference in New Issue
Block a user