Update README and vector assets

This commit is contained in:
2026-01-29 01:18:50 +08:00
parent 3313b5efa7
commit 18ceff4c01
3 changed files with 197 additions and 131 deletions

View File

@@ -178,12 +178,11 @@ h1 span {
}
#keep-visible-count {
width: 3ch; /* 可稳定显示 3 位数字0-50 足够 */
min-width: 3ch;
max-width: 4ch;
font-variant-numeric: tabular-nums;
padding: 2px 4px;
margin: 0 2px;
width: 3.5em;
min-width: 3em;
max-width: 4em;
padding: 4px 6px;
margin: 0 4px;
background: var(--bg2);
border: 1px solid var(--bdr);
font-size: inherit;
@@ -191,6 +190,17 @@ h1 span {
color: var(--hl);
text-align: center;
border-radius: 3px;
font-variant-numeric: tabular-nums;
/* 禁用 number input 的 spinnerPC 上会挤掉数字) */
-moz-appearance: textfield;
appearance: textfield;
}
#keep-visible-count::-webkit-outer-spin-button,
#keep-visible-count::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
#keep-visible-count:focus {

View File

@@ -464,18 +464,35 @@ export async function fetchOnlineModels(config) {
/**
* 使用在线服务生成向量
*/
async function embedOnline(texts, provider, config) {
async function embedOnline(texts, provider, config, options = {}) {
const { url, key, model } = config;
const signal = options?.signal;
const providerConfig = ONLINE_PROVIDERS[provider];
const baseUrl = (providerConfig?.baseUrl || url || '').replace(/\/+$/, '');
const reqId = Math.random().toString(36).slice(2, 6);
const maxRetries = 3;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
// 永远重试:指数退避 + 上限 + 抖动
const BASE_WAIT_MS = 1200;
const MAX_WAIT_MS = 15000;
const sleepAbortable = (ms) => new Promise((resolve, reject) => {
if (signal?.aborted) return reject(new DOMException('Aborted', 'AbortError'));
const t = setTimeout(resolve, ms);
if (signal) {
signal.addEventListener('abort', () => {
clearTimeout(t);
reject(new DOMException('Aborted', 'AbortError'));
}, { once: true });
}
});
let attempt = 0;
while (true) {
attempt++;
const startTime = Date.now();
console.log(`[embed ${reqId}] send ${texts.length} items${attempt > 1 ? ` (retry ${attempt}/${maxRetries})` : ''}`);
console.log(`[embed ${reqId}] send ${texts.length} items (attempt ${attempt})`);
try {
let response;
@@ -492,6 +509,7 @@ async function embedOnline(texts, provider, config) {
texts: texts,
input_type: 'search_document',
}),
signal,
});
} else {
response = await fetch(`${baseUrl}/v1/embeddings`, {
@@ -504,40 +522,55 @@ async function embedOnline(texts, provider, config) {
model: model,
input: texts,
}),
signal,
});
}
console.log(`[embed ${reqId}] status=${response.status} time=${Date.now() - startTime}ms`);
// 需要“永远重试”的典型状态:
// - 429限流
// - 403配额/风控/未实名等(你提到的硅基未认证)
// - 5xx服务端错误
const retryableStatus = (s) => s === 429 || s === 403 || (s >= 500 && s <= 599);
if (!response.ok) {
const error = await response.text();
throw new Error(`API 返回 ${response.status}: ${error}`);
const errorText = await response.text().catch(() => '');
if (retryableStatus(response.status)) {
const exp = Math.min(MAX_WAIT_MS, BASE_WAIT_MS * Math.pow(2, Math.min(attempt, 6) - 1));
const jitter = Math.floor(Math.random() * 350);
const waitMs = exp + jitter;
console.warn(`[embed ${reqId}] retryable error ${response.status}, wait ${waitMs}ms`);
await sleepAbortable(waitMs);
continue;
}
// 非可恢复错误:直接抛出(比如 400 参数错、401 key 错等)
const err = new Error(`API 返回 ${response.status}: ${errorText}`);
err.status = response.status;
throw err;
}
const data = await response.json();
if (provider === 'cohere') {
console.log(`[embed ${reqId}] done items=${data.embeddings?.length || 0} total=${Date.now() - startTime}ms`);
return data.embeddings.map(e => Array.isArray(e) ? e : Array.from(e));
return (data.embeddings || []).map(e => Array.isArray(e) ? e : Array.from(e));
}
console.log(`[embed ${reqId}] done items=${data.data?.length || 0} total=${Date.now() - startTime}ms`);
return data.data.map(item => {
return (data.data || []).map(item => {
const embedding = item.embedding;
return Array.isArray(embedding) ? embedding : Array.from(embedding);
});
} catch (e) {
console.warn(`[embed ${reqId}] failed attempt=${attempt} time=${Date.now() - startTime}ms`, e.message);
// 取消:必须立刻退出
if (e?.name === 'AbortError') throw e;
if (attempt < maxRetries) {
const waitTime = Math.pow(2, attempt - 1) * 1000;
console.log(`[embed ${reqId}] wait ${waitTime}ms then retry`);
await new Promise(r => setTimeout(r, waitTime));
continue;
}
console.error(`[embed ${reqId}] final failure`, e);
throw e;
// 网络错误:永远重试
const exp = Math.min(MAX_WAIT_MS, BASE_WAIT_MS * Math.pow(2, Math.min(attempt, 6) - 1));
const jitter = Math.floor(Math.random() * 350);
const waitMs = exp + jitter;
console.warn(`[embed ${reqId}] network/error, wait ${waitMs}ms then retry: ${e?.message || e}`);
await sleepAbortable(waitMs);
}
}
}
@@ -553,7 +586,7 @@ async function embedOnline(texts, provider, config) {
* @param {Object} config - 配置
* @returns {Promise<number[][]>}
*/
export async function embed(texts, config) {
export async function embed(texts, config, options = {}) {
if (!texts?.length) return [];
const { engine, local, online } = config;
@@ -567,7 +600,7 @@ export async function embed(texts, config) {
if (!online?.key || !online?.model) {
throw new Error('在线服务配置不完整');
}
return await embedOnline(texts, provider, online);
return await embedOnline(texts, provider, online, options);
} else {
throw new Error(`未知的引擎类型: ${engine}`);