From 18ceff4c01f7ff097d398b8e875ffda06dd30ec4 Mon Sep 17 00:00:00 2001 From: bielie Date: Thu, 29 Jan 2026 01:18:50 +0800 Subject: [PATCH] Update README and vector assets --- README.md | 227 +++++++++++++---------- modules/story-summary/story-summary.css | 22 ++- modules/story-summary/vector/embedder.js | 79 +++++--- 3 files changed, 197 insertions(+), 131 deletions(-) diff --git a/README.md b/README.md index a3e0008..42674f3 100644 --- a/README.md +++ b/README.md @@ -4,115 +4,138 @@ ``` LittleWhiteBox/ -├── index.js # 入口:初始化/注册所有模块 -├── manifest.json # 插件清单:版本/依赖/入口 -├── settings.html # 主设置页:模块开关/UI -├── style.css # 全局样式 -├── README.md # 说明文档 -├── .eslintrc.cjs # ESLint 规则 -├── .eslintignore # ESLint 忽略 -├── .gitignore # Git 忽略 -├── package.json # 开发依赖/脚本 -├── package-lock.json # 依赖锁定 -├── jsconfig.json # 编辑器提示 +├── .editorconfig # 编辑器格式规范 +├── .eslintignore # ESLint 忽略规则 +├── .eslintrc.cjs # ESLint 配置 +├── .gitignore # Git 忽略规则 +├── index.js # 插件入口:初始化/注册所有模块 +├── jsconfig.json # JS/TS 编辑器提示 +├── manifest.json # 插件清单:版本/依赖/入口 +├── package-lock.json # 依赖锁定 +├── package.json # 开发依赖/脚本 +├── README.md # 说明文档 +├── settings.html # 主设置页:模块开关/UI +├── style.css # 全局样式 │ -├── core/ # 核心基础设施(不直接做功能UI) -│ ├── constants.js # 常量/路径 -│ ├── event-manager.js # 统一事件管理 -│ ├── debug-core.js # 日志/缓存注册 -│ ├── slash-command.js # 斜杠命令封装 -│ ├── variable-path.js # 变量路径解析 -│ ├── server-storage.js # 服务器存储(防抖/重试) -│ ├── wrapper-inline.js # iframe 内联脚本 -│ └── iframe-messaging.js # postMessage 封装与 origin 校验 +├── bridges/ # 外部桥接 +│ ├── call-generate-service.js # 调用生成服务桥接 +│ ├── worldbook-bridge.js # 世界书桥接 +│ └── wrapper-iframe.js # iframe 包装桥接 │ -├── widgets/ # 通用UI组件(跨功能复用) -│ ├── message-toolbar.js # 消息区工具条注册/管理 -│ └── button-collapse.js # 消息区按钮收纳 +├── core/ # 核心基础设施 +│ ├── constants.js # 常量/路径定义 +│ ├── debug-core.js # 日志/缓存注册 +│ ├── event-manager.js # 统一事件管理 +│ ├── iframe-messaging.js # postMessage 封装 +│ ├── server-storage.js # 服务器存储封装 +│ ├── slash-command.js # 斜杠命令封装 +│ ├── variable-path.js # 变量路径解析 +│ └── wrapper-inline.js # iframe 内联脚本 │ -├── modules/ # 功能模块(每个功能自带UI) -│ ├── control-audio.js # 音频权限控制 -│ ├── iframe-renderer.js # iframe 渲染 -│ ├── immersive-mode.js # 沉浸模式 -│ ├── message-preview.js # 消息预览/拦截 -│ ├── streaming-generation.js # 生成相关功能(xbgenraw) -│ │ -│ ├── debug-panel/ # 调试面板 -│ │ ├── debug-panel.js # 悬浮窗控制 -│ │ └── debug-panel.html # UI -│ │ -│ ├── fourth-wall/ # 四次元壁 -│ │ ├── fourth-wall.js # 逻辑 -│ │ ├── fourth-wall.html # UI -│ │ ├── fw-image.js # 图像交互 -│ │ ├── fw-message-enhancer.js # 消息增强 -│ │ ├── fw-prompt.js # 提示词编辑 -│ │ └── fw-voice.js # 语音展示 -│ │ -│ ├── novel-draw/ # 画图 -│ │ ├── novel-draw.js # 主逻辑 -│ │ ├── novel-draw.html # UI -│ │ ├── llm-service.js # LLM 分析 -│ │ ├── floating-panel.js # 悬浮面板 -│ │ ├── gallery-cache.js # 缓存 -│ │ ├── image-live-effect.js # Live 动效 -│ │ ├── cloud-presets.js # 云预设 -│ │ └── TAG编写指南.md # 文档 -│ │ -│ ├── tts/ # TTS -│ │ ├── tts.js # 主逻辑 -│ │ ├── tts-auth-provider.js # 鉴权 -│ │ ├── tts-free-provider.js # 试用 -│ │ ├── tts-api.js # API -│ │ ├── tts-text.js # 文本处理 -│ │ ├── tts-player.js # 播放器 -│ │ ├── tts-panel.js # 气泡UI -│ │ ├── tts-cache.js # 缓存 -│ │ ├── tts-overlay.html # 设置UI -│ │ ├── tts-voices.js # 音色数据 -│ │ ├── 开通管理.png # 说明图 -│ │ ├── 获取ID和KEY.png # 说明图 -│ │ └── 声音复刻.png # 说明图 -│ │ -│ ├── scheduled-tasks/ # 定时任务 -│ │ ├── scheduled-tasks.js # 调度 -│ │ ├── scheduled-tasks.html # UI -│ │ └── embedded-tasks.html # 嵌入UI -│ │ -│ ├── template-editor/ # 模板编辑器 -│ │ ├── template-editor.js # 逻辑 -│ │ └── template-editor.html # UI -│ │ -│ ├── story-outline/ # 故事大纲 -│ │ ├── story-outline.js # 逻辑 -│ │ ├── story-outline.html # UI -│ │ └── story-outline-prompt.js # 提示词 -│ │ -│ ├── story-summary/ # 剧情总结 -│ │ ├── story-summary.js # 逻辑 -│ │ ├── story-summary.html # UI -│ │ └── llm-service.js # LLM 服务 -│ │ -│ └── variables/ # 变量系统 -│ ├── var-commands.js # 命令 -│ ├── varevent-editor.js # 编辑器 -│ ├── variables-core.js # 核心 -│ └── variables-panel.js # 面板 +├── docs/ # 文档与许可 +│ ├── COPYRIGHT # 版权声明 +│ ├── LICENSE.md # 许可协议 +│ └── NOTICE # 通知/第三方声明 │ -├── bridges/ # 外部服务桥接 -│ ├── call-generate-service.js # ST 生成服务 -│ ├── worldbook-bridge.js # 世界书桥接 -│ └── wrapper-iframe.js # iframe 客户端脚本 +├── libs/ # 第三方库 +│ ├── dexie.mjs # IndexedDB 封装库 +│ ├── js-yaml.mjs # YAML 解析/序列化(ESM) +│ ├── minisearch.mjs # 轻量搜索库 +│ ├── pixi.min.js # PixiJS 渲染库 +│ └── jieba-wasm/ +│ ├── jieba_rs_wasm.js # 结巴分词 WASM JS 包装 +│ ├── jieba_rs_wasm_bg.wasm # 结巴分词 WASM 二进制 +│ └── jieba_rs_wasm_bg.wasm.d.ts # WASM 类型声明 │ -├── libs/ # 第三方库 -│ └── pixi.min.js # PixiJS +├── modules/ # 功能模块 +│ ├── control-audio.js # 音频权限控制 +│ ├── iframe-renderer.js # iframe 渲染 +│ ├── immersive-mode.js # 沉浸模式 +│ ├── message-preview.js # 消息预览/拦截 +│ ├── streaming-generation.js # 生成相关功能 +│ │ +│ ├── debug-panel/ # 调试面板 +│ │ ├── debug-panel.html # 调试面板 UI +│ │ └── debug-panel.js # 调试面板逻辑 +│ │ +│ ├── fourth-wall/ # 四次元壁 +│ │ ├── fourth-wall.html # UI +│ │ ├── fourth-wall.js # 主逻辑 +│ │ ├── fw-image.js # 图像相关增强 +│ │ ├── fw-message-enhancer.js # 消息增强 +│ │ ├── fw-prompt.js # 提示词/注入 +│ │ └── fw-voice.js # 语音相关 +│ │ +│ ├── novel-draw/ # 画图模块 +│ │ ├── cloud-presets.js # 云端预设 +│ │ ├── floating-panel.js # 浮动面板 +│ │ ├── gallery-cache.js # 图库缓存 +│ │ ├── image-live-effect.js # 图像动态效果 +│ │ ├── llm-service.js # LLM 服务调用 +│ │ ├── novel-draw.html # UI +│ │ ├── novel-draw.js # 主逻辑 +│ │ └── TAG编写指南.md # TAG 编写指南 +│ │ +│ ├── scheduled-tasks/ # 定时任务 +│ │ ├── embedded-tasks.html # 内嵌任务 UI +│ │ ├── scheduled-tasks.html # 主 UI +│ │ └── scheduled-tasks.js # 逻辑 +│ │ +│ ├── story-outline/ # 故事大纲 +│ │ ├── story-outline-prompt.js # Prompt 模板 +│ │ ├── story-outline.html # UI +│ │ └── story-outline.js # 逻辑 +│ │ +│ ├── story-summary/ # 剧情总结 + 记忆系统 +│ │ ├── story-summary-ui.js # UI 逻辑 +│ │ ├── story-summary.css # 样式 +│ │ ├── story-summary.html # UI(含向量设置) +│ │ ├── story-summary.js # 主入口:事件/UI/iframe 通讯 +│ │ ├── data/ +│ │ │ ├── config.js # 配置管理 +│ │ │ ├── db.js # 向量存储:L1/L2 Vectors (Dexie/IndexedDB) +│ │ │ └── store.js # 核心存储:L2事件 + L3世界状态 +│ │ ├── generate/ +│ │ │ ├── generator.js # 调度器:调用 LLM -> 解析 -> 清洗 -> 合并 +│ │ │ ├── llm.js # LLM API 与 Prompt 定义 +│ │ │ └── prompt.js # 注入层:格式化 + 预算装配 +│ │ └── vector/ +│ │ ├── chunk-builder.js # L1 切分与构建 +│ │ ├── chunk-store.js # 向量 CRUD 操作 +│ │ ├── embedder.js # 向量化服务 (Local/Online) +│ │ ├── embedder.worker.js # 本地模型 Worker +│ │ ├── entity.js # 召回实体/辅助结构 +│ │ └── recall.js # 召回引擎:加权Query + 实体加分 + MMR去重 +│ │ +│ ├── template-editor/ # 模板编辑器 +│ │ ├── template-editor.html # UI +│ │ └── template-editor.js # 逻辑 +│ │ +│ ├── tts/ # TTS +│ │ ├── tts-api.js # API 适配 +│ │ ├── tts-auth-provider.js # 鉴权提供者 +│ │ ├── tts-cache.js # 缓存 +│ │ ├── tts-free-provider.js # 免费提供者 +│ │ ├── tts-overlay.html # Overlay UI +│ │ ├── tts-panel.js # 面板逻辑 +│ │ ├── tts-player.js # 播放器 +│ │ ├── tts-text.js # 文本处理 +│ │ ├── tts-voices.js # 语音配置 +│ │ ├── tts.js # 主入口 +│ │ ├── 声音复刻.png # 说明图 +│ │ ├── 开通管理.png # 说明图 +│ │ └── 获取ID和KEY.png # 说明图 +│ │ +│ └── variables/ # 变量系统 +│ ├── var-commands.js # 变量命令/宏/路径解析 +│ ├── varevent-editor.js # 变量编辑器/注入处理 +│ ├── variables-core.js # 变量系统核心 +│ └── variables-panel.js # 变量面板 UI │ -└── docs/ # 许可/声明 - ├── COPYRIGHT - ├── LICENSE.md - └── NOTICE +└── widgets/ # 通用 UI 组件 + ├── button-collapse.js # 按钮收纳 + └── message-toolbar.js # 消息工具条 -node_modules/ # 本地依赖(不提交) ``` ## 📄 许可证 diff --git a/modules/story-summary/story-summary.css b/modules/story-summary/story-summary.css index c8b3676..6dff2a5 100644 --- a/modules/story-summary/story-summary.css +++ b/modules/story-summary/story-summary.css @@ -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 的 spinner(PC 上会挤掉数字) */ + -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 { diff --git a/modules/story-summary/vector/embedder.js b/modules/story-summary/vector/embedder.js index bf04a1b..5f7b40f 100644 --- a/modules/story-summary/vector/embedder.js +++ b/modules/story-summary/vector/embedder.js @@ -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} */ -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}`);