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

179
README.md
View File

@@ -4,115 +4,138 @@
``` ```
LittleWhiteBox/ LittleWhiteBox/
├── index.js # 入口:初始化/注册所有模块 ├── .editorconfig # 编辑器格式规范
├── .eslintignore # ESLint 忽略规则
├── .eslintrc.cjs # ESLint 配置
├── .gitignore # Git 忽略规则
├── index.js # 插件入口:初始化/注册所有模块
├── jsconfig.json # JS/TS 编辑器提示
├── manifest.json # 插件清单:版本/依赖/入口 ├── manifest.json # 插件清单:版本/依赖/入口
├── package-lock.json # 依赖锁定
├── package.json # 开发依赖/脚本
├── README.md # 说明文档
├── settings.html # 主设置页:模块开关/UI ├── settings.html # 主设置页:模块开关/UI
├── style.css # 全局样式 ├── style.css # 全局样式
├── README.md # 说明文档
├── .eslintrc.cjs # ESLint 规则
├── .eslintignore # ESLint 忽略
├── .gitignore # Git 忽略
├── package.json # 开发依赖/脚本
├── package-lock.json # 依赖锁定
├── jsconfig.json # 编辑器提示
├── core/ # 核心基础设施不直接做功能UI ├── bridges/ # 外部桥接
│ ├── constants.js # 常量/路径 │ ├── call-generate-service.js # 调用生成服务桥接
│ ├── event-manager.js # 统一事件管理 │ ├── worldbook-bridge.js # 世界书桥接
│ └── wrapper-iframe.js # iframe 包装桥接
├── core/ # 核心基础设施
│ ├── constants.js # 常量/路径定义
│ ├── debug-core.js # 日志/缓存注册 │ ├── debug-core.js # 日志/缓存注册
│ ├── event-manager.js # 统一事件管理
│ ├── iframe-messaging.js # postMessage 封装
│ ├── server-storage.js # 服务器存储封装
│ ├── slash-command.js # 斜杠命令封装 │ ├── slash-command.js # 斜杠命令封装
│ ├── variable-path.js # 变量路径解析 │ ├── variable-path.js # 变量路径解析
── server-storage.js # 服务器存储(防抖/重试) ── wrapper-inline.js # iframe 内联脚本
│ ├── wrapper-inline.js # iframe 内联脚本
│ └── iframe-messaging.js # postMessage 封装与 origin 校验
├── widgets/ # 通用UI组件跨功能复用 ├── docs/ # 文档与许可
│ ├── message-toolbar.js # 消息区工具条注册/管理 │ ├── COPYRIGHT # 版权声明
── button-collapse.js # 消息区按钮收纳 ── LICENSE.md # 许可协议
│ └── NOTICE # 通知/第三方声明
├── modules/ # 功能模块每个功能自带UI ├── 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 类型声明
├── modules/ # 功能模块
│ ├── control-audio.js # 音频权限控制 │ ├── control-audio.js # 音频权限控制
│ ├── iframe-renderer.js # iframe 渲染 │ ├── iframe-renderer.js # iframe 渲染
│ ├── immersive-mode.js # 沉浸模式 │ ├── immersive-mode.js # 沉浸模式
│ ├── message-preview.js # 消息预览/拦截 │ ├── message-preview.js # 消息预览/拦截
│ ├── streaming-generation.js # 生成相关功能xbgenraw │ ├── streaming-generation.js # 生成相关功能
│ │ │ │
│ ├── debug-panel/ # 调试面板 │ ├── debug-panel/ # 调试面板
│ │ ├── debug-panel.js # 悬浮窗控制 │ │ ├── debug-panel.html # 调试面板 UI
│ │ └── debug-panel.html # UI │ │ └── debug-panel.js # 调试面板逻辑
│ │ │ │
│ ├── fourth-wall/ # 四次元壁 │ ├── fourth-wall/ # 四次元壁
│ │ ├── fourth-wall.js # 逻辑
│ │ ├── fourth-wall.html # UI │ │ ├── fourth-wall.html # UI
│ │ ├── fw-image.js # 图像交互 │ │ ├── fourth-wall.js # 主逻辑
│ │ ├── fw-image.js # 图像相关增强
│ │ ├── fw-message-enhancer.js # 消息增强 │ │ ├── fw-message-enhancer.js # 消息增强
│ │ ├── fw-prompt.js # 提示词编辑 │ │ ├── fw-prompt.js # 提示词/注入
│ │ └── fw-voice.js # 语音展示 │ │ └── fw-voice.js # 语音相关
│ │ │ │
│ ├── novel-draw/ # 画图 │ ├── novel-draw/ # 画图模块
│ │ ├── novel-draw.js # 主逻辑 │ │ ├── cloud-presets.js # 云端预设
│ │ ├── floating-panel.js # 浮动面板
│ │ ├── gallery-cache.js # 图库缓存
│ │ ├── image-live-effect.js # 图像动态效果
│ │ ├── llm-service.js # LLM 服务调用
│ │ ├── novel-draw.html # UI │ │ ├── novel-draw.html # UI
│ │ ├── llm-service.js # LLM 分析 │ │ ├── novel-draw.js # 主逻辑
│ │ ── floating-panel.js # 悬浮面板 │ │ ── TAG编写指南.md # TAG 编写指南
│ │ ├── 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/ # 定时任务
│ │ ├── scheduled-tasks.js # 调度 │ │ ├── embedded-tasks.html # 内嵌任务 UI
│ │ ├── scheduled-tasks.html # UI │ │ ├── scheduled-tasks.html # 主 UI
│ │ └── embedded-tasks.html # 嵌入UI │ │ └── scheduled-tasks.js # 逻辑
│ │
│ ├── template-editor/ # 模板编辑器
│ │ ├── template-editor.js # 逻辑
│ │ └── template-editor.html # UI
│ │ │ │
│ ├── story-outline/ # 故事大纲 │ ├── story-outline/ # 故事大纲
│ │ ├── story-outline.js # 逻辑 │ │ ├── story-outline-prompt.js # Prompt 模板
│ │ ├── story-outline.html # UI │ │ ├── story-outline.html # UI
│ │ └── story-outline-prompt.js # 提示词 │ │ └── story-outline.js # 逻辑
│ │ │ │
│ ├── story-summary/ # 剧情总结 │ ├── story-summary/ # 剧情总结 + 记忆系统
│ │ ├── story-summary.js # 逻辑 │ │ ├── story-summary-ui.js # UI 逻辑
│ │ ├── story-summary.html # UI │ │ ├── story-summary.css # 样式
│ │ ── llm-service.js # LLM 服务 │ │ ── 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/ # 变量系统 │ └── variables/ # 变量系统
│ ├── var-commands.js # 命令 │ ├── var-commands.js # 变量命令/宏/路径解析
│ ├── varevent-editor.js # 编辑器 │ ├── varevent-editor.js # 变量编辑器/注入处理
│ ├── variables-core.js # 核心 │ ├── variables-core.js # 变量系统核心
│ └── variables-panel.js # 面板 │ └── variables-panel.js # 变量面板 UI
── bridges/ # 外部服务桥接 ── widgets/ # 通用 UI 组件
├── call-generate-service.js # ST 生成服务 ├── button-collapse.js # 按钮收纳
── worldbook-bridge.js # 世界书桥接 ── message-toolbar.js # 消息工具条
│ └── wrapper-iframe.js # iframe 客户端脚本
├── libs/ # 第三方库
│ └── pixi.min.js # PixiJS
└── docs/ # 许可/声明
├── COPYRIGHT
├── LICENSE.md
└── NOTICE
node_modules/ # 本地依赖(不提交)
``` ```
## 📄 许可证 ## 📄 许可证

View File

@@ -178,12 +178,11 @@ h1 span {
} }
#keep-visible-count { #keep-visible-count {
width: 3ch; /* 可稳定显示 3 位数字0-50 足够 */ width: 3.5em;
min-width: 3ch; min-width: 3em;
max-width: 4ch; max-width: 4em;
font-variant-numeric: tabular-nums; padding: 4px 6px;
padding: 2px 4px; margin: 0 4px;
margin: 0 2px;
background: var(--bg2); background: var(--bg2);
border: 1px solid var(--bdr); border: 1px solid var(--bdr);
font-size: inherit; font-size: inherit;
@@ -191,6 +190,17 @@ h1 span {
color: var(--hl); color: var(--hl);
text-align: center; text-align: center;
border-radius: 3px; 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 { #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 { url, key, model } = config;
const signal = options?.signal;
const providerConfig = ONLINE_PROVIDERS[provider]; const providerConfig = ONLINE_PROVIDERS[provider];
const baseUrl = (providerConfig?.baseUrl || url || '').replace(/\/+$/, ''); const baseUrl = (providerConfig?.baseUrl || url || '').replace(/\/+$/, '');
const reqId = Math.random().toString(36).slice(2, 6); 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(); 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 { try {
let response; let response;
@@ -492,6 +509,7 @@ async function embedOnline(texts, provider, config) {
texts: texts, texts: texts,
input_type: 'search_document', input_type: 'search_document',
}), }),
signal,
}); });
} else { } else {
response = await fetch(`${baseUrl}/v1/embeddings`, { response = await fetch(`${baseUrl}/v1/embeddings`, {
@@ -504,40 +522,55 @@ async function embedOnline(texts, provider, config) {
model: model, model: model,
input: texts, input: texts,
}), }),
signal,
}); });
} }
console.log(`[embed ${reqId}] status=${response.status} time=${Date.now() - startTime}ms`); 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) { if (!response.ok) {
const error = await response.text(); const errorText = await response.text().catch(() => '');
throw new Error(`API 返回 ${response.status}: ${error}`);
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(); const data = await response.json();
if (provider === 'cohere') { 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));
} }
return (data.data || []).map(item => {
console.log(`[embed ${reqId}] done items=${data.data?.length || 0} total=${Date.now() - startTime}ms`);
return data.data.map(item => {
const embedding = item.embedding; const embedding = item.embedding;
return Array.isArray(embedding) ? embedding : Array.from(embedding); return Array.isArray(embedding) ? embedding : Array.from(embedding);
}); });
} catch (e) { } 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; const exp = Math.min(MAX_WAIT_MS, BASE_WAIT_MS * Math.pow(2, Math.min(attempt, 6) - 1));
console.log(`[embed ${reqId}] wait ${waitTime}ms then retry`); const jitter = Math.floor(Math.random() * 350);
await new Promise(r => setTimeout(r, waitTime)); const waitMs = exp + jitter;
continue; console.warn(`[embed ${reqId}] network/error, wait ${waitMs}ms then retry: ${e?.message || e}`);
} await sleepAbortable(waitMs);
console.error(`[embed ${reqId}] final failure`, e);
throw e;
} }
} }
} }
@@ -553,7 +586,7 @@ async function embedOnline(texts, provider, config) {
* @param {Object} config - 配置 * @param {Object} config - 配置
* @returns {Promise<number[][]>} * @returns {Promise<number[][]>}
*/ */
export async function embed(texts, config) { export async function embed(texts, config, options = {}) {
if (!texts?.length) return []; if (!texts?.length) return [];
const { engine, local, online } = config; const { engine, local, online } = config;
@@ -567,7 +600,7 @@ export async function embed(texts, config) {
if (!online?.key || !online?.model) { if (!online?.key || !online?.model) {
throw new Error('在线服务配置不完整'); throw new Error('在线服务配置不完整');
} }
return await embedOnline(texts, provider, online); return await embedOnline(texts, provider, online, options);
} else { } else {
throw new Error(`未知的引擎类型: ${engine}`); throw new Error(`未知的引擎类型: ${engine}`);