diff --git a/CharacterWorldBook/src/cwb_lorebookManager.js b/CharacterWorldBook/src/cwb_lorebookManager.js index 4fb699e..14958a0 100644 --- a/CharacterWorldBook/src/cwb_lorebookManager.js +++ b/CharacterWorldBook/src/cwb_lorebookManager.js @@ -317,11 +317,7 @@ export async function manageAutoCardUpdateLorebookEntry() { logError('管理世界书条目时出错:', error); } } -/** - * (重构) 通用函数,用于同步小说处理生成的世界书条目。 - * @param {string} bookName - 目标世界书名称。 - * @param {Array<{title: string, content: string}>} entries - 从API回复中解析出的条目数组。 - */ + export async function syncNovelLorebookEntries(bookName, entries) { if (!bookName || !Array.isArray(entries) || entries.length === 0) { logError('[CWB-NovelSync] 参数无效或条目为空'); @@ -337,8 +333,6 @@ export async function syncNovelLorebookEntries(bookName, entries) { const entriesToUpdate = []; const entriesToCreate = []; - - // 查找“章节内容概述”的最新部分编号 let maxPart = 0; managedEntries.forEach(entry => { const match = entry.comment.match(/章节内容概述-第(\d+)部分/); @@ -352,7 +346,6 @@ export async function syncNovelLorebookEntries(bookName, entries) { const { title, content } = entry; if (title === '章节内容概述') { - // “章节内容概述”条目总是新建 const loreData = { keys: [`小说处理`, title, `第${nextPart}部分`], content: content, @@ -362,9 +355,8 @@ export async function syncNovelLorebookEntries(bookName, entries) { position: 'before_char', }; entriesToCreate.push(loreData); - nextPart++; // 为同一批次中的下一个概述增加编号 + nextPart++; } else { - // 其他条目(世界观、时间线等)是动态更新的 const existingEntry = managedEntries.find(e => e.comment === `[Amily2小说处理] ${title}`); const loreData = { diff --git a/PresetSettings/config.js b/PresetSettings/config.js index 8c630b7..95384dd 100644 --- a/PresetSettings/config.js +++ b/PresetSettings/config.js @@ -205,31 +205,121 @@ export const defaultPrompts = { { role: 'assistant', content: '\n CustomThink: Active\n Follow the above rules and use the above.\n Based on the previous task block, the content is generated.\nRequires processing and cannot be returned in its original block.\n\nStart and end labels correctly.' }, ], novel_processor: [ - { - role: 'system', - content: `你是一位专业的小说分析师,擅长从字里行间捕捉关键信息并进行结构化整理。你的任务是根据提供的章节内容和已有的世界书条目,提取新的、或更新已有的关键信息。 + { + role: "system", + content: `## 一、 详细要求提示词 (Detailed Requirements Prompt) -输出格式要求: -1. 严格按照Markdown表格格式输出。 -2. 表格必须包含以下四列:| 关键词 | 类别 | 描述 | 关联项 | -3. “关键词”是核心识别名称,必须唯一且简洁。 -4. “类别”必须是以下之一:角色, 地点, 组织, 物品。 -5. “描述”应详细、客观地概括该条目的所有相关信息。 -6. “关联项”列出与该条目直接相关的其他关键词,用逗号分隔。 -7. 如果章节内容没有需要新增或更新的信息,则只输出 "无需更新"。` - }, - { - role: 'user', - content: `# 已有世界书条目` - }, - { - role: 'user', - content: `# 最新章节内容` - }, - { - role: 'user', - content: `请根据以上信息,分析并输出需要新增或更新的世界书条目。` - } +**核心指令**: 你是一个专业的小说分析师和世界观构建师。请仔细阅读以下提供的小说章节内容,并根据要求,以Markdown表格和Mermaid图表的形式,生成一份全面、结构化的分析报告。 + +**重要提醒**:你的所有回复,都会对除\`章节内容概述\`以外的所有条目进行动态更新,所以你需要在原有的基础上修改,你的修改会完全覆盖原有条目,请务必完整输出,以免丢失重要信息。 + +**分析维度**: + +### 1. 世界观设定 (Worldview Settings) +- **目标**: 梳理并总结故事的宏观背景。 +- **要求**: 创建一个包含以下列的Markdown表格:\`| 类别 | 详细设定 |\`。 +- **内容应涵盖**: + - **时空背景**: 故事发生的时代、世界的基本构成(例如:修真、科幻、都市)。 + - **核心种族**: 世界上存在的主要智慧种族。 + - **势力分布**: 各大国家、组织、宗门等。 + - **能量体系**: 力量的来源和等级划分(例如:魔法、斗气、灵力等级)。 + - **特殊法则**: 世界独有的物理或社会规则。 + +### 2. 章节内容概述 (Chapter Content Overview) +- **目标**: 为本次提供的每一个章节生成一个简洁的摘要。 +- **要求**: 创建一个包含以下列的Markdown表格:\`| 章节 | 内容概要 |\`。 +- **注意**: 仅总结当前批次处理的章节内容(也就是当前发送给你的小说原文),此表不会被覆盖,只会新建一个新的概述简要条目。 + +### 3. 时间线 (Timeline) +- **目标**: 梳理出故事至今为止的关键事件,并按时间顺序排列。 +- **要求**: 使用清晰的层级结构来展示事件的先后顺序和从属关系。可以参考以下格式: + \`\`\` + 【时期/阶段】 + ├─ 事件A + ├─ 事件B + │ ╰─ 子事件B1 + ╰─ 事件C + \`\`\` + +### 4. 角色关系网 (Character Relationship Network) +- **目标**: 可视化展示主要角色之间的人际关系。 +- **要求**: 使用 **Mermaid \`graph LR\`** 语法生成关系图。 +- **关系描述**: 在连接线上清晰地标注关系类型(例如:\`-->|师徒|\`, \`-->|敌对|\`, \`-->|爱慕|\`)。 + +### 5. 角色总览 (Character Overview) +- **目标**: 创建详细的角色档案,按阵营分类。 +- **要求**: 分别为“主角阵营”、“反派阵营”和“中立势力”创建三个独立的Markdown表格。 +- **表格列名 (可自定义)**: + - **主角阵营表格列名**: \`默认\` + - **反派阵营表格列名**: \`默认\` + - **中立势力表格列名**: \`默认\` +- **默认列名**: \`| 角色名 | 身份/实力 | 定位 | 性格 | 能力/底牌 | 人际关系 | 关键线索 |\` +- **内容填充**: 深入分析角色的背景、动机、能力和与其他角色的互动,填充表格内容。` + }, + { + role: "system", + content: "# 已有世界书条目\n<已有表格总结>" + }, + { + role: "system", + content: "已有表格总结>" + }, + { + role: "user", + content: `## 输出规范提示词 (Output Specification Prompt) + +**核心指令**: 你的所有输出**必须**严格遵守以下格式规范,以便程序能够正确解析。任何格式错误都将导致处理失败。 + +1. **条目分离 (Entry Separation)**: + - 每一个分析维度(如“世界观设定”、“时间线”等)都是一个独立的“条目”。 + - 每个条目必须以 \`[--START_TABLE--]\` 开始,并以 \`[--END_TABLE--]\` 结束。 + +2. **条目标题格式 (Entry Title Format)**: + - \`[--START_TABLE--]\` 标签的下一行必须是条目名称,格式为 \`[name]:条目名称\`。 + - 固定的条目名称为: \`世界观设定\`, \`章节内容概述\`, \`时间线\`, \`角色关系网\`, \`角色总览\`。 + +3. **内容包裹 (Content Wrapping)**: + - 每个条目的所有内容(无论是Markdown表格、Mermaid代码还是纯文本)**必须**被 \`[--START_TABLE--]\` 和 \`[--END_TABLE--]\` 标签完全包裹。 + - 标签本身不能包含任何多余的空格或字符。 + +4. **完整输出示例**: + + \`\`\` + [--START_TABLE--] + [name]:世界观设定 + | **类别** | **详细设定** | + |---|---| + | **时空背景** | 修真世界与凡人王朝并存... | + [--END_TABLE--] + + [--START_TABLE--] + [name]:章节内容概述 + | 章节 | 内容概要 | + |---|---| + | 第1章 | 现代人项云澈穿越... | + [--END_TABLE--] + + [--START_TABLE--] + [name]:角色关系网 + graph LR + 酒剑翁 -->|倾囊相授| 项云澈 + 周衍 -->|敌视| 项云澈 + [--END_TABLE--] +(后略…) + \`\`\` + +**最终要求**: 请将上述所有分析维度的结果,按照输出规范,一次性完整生成。 +**二次重要提醒**:你的所有回复,都会对除\`章节内容概述\`以外的所有条目进行动态更新,所以你需要在原有的基础上修改,你的修改会完全覆盖原有条目,请务必完整输出,以免丢失重要信息。 +` + }, + { + role: "system", + content: "<最新批次小说原文>" + }, + { + role: "system", + content: "最新批次小说原文>" + } ] }; @@ -363,12 +453,38 @@ export const defaultMixedOrder = { { type: 'prompt', index: 7 } ], novel_processor: [ - { type: 'prompt', index: 0 }, - { type: 'prompt', index: 1 }, - { type: 'conditional', id: 'existingLore' }, - { type: 'prompt', index: 2 }, - { type: 'conditional', id: 'chapterContent' }, - { type: 'prompt', index: 3 } + { + type: "prompt", + index: 0 + }, + { + type: "prompt", + index: 1 + }, + { + type: "conditional", + id: "existingLore" + }, + { + type: "prompt", + index: 2 + }, + { + type: "prompt", + index: 4 + }, + { + type: "conditional", + id: "chapterContent" + }, + { + type: "prompt", + index: 5 + }, + { + type: "prompt", + index: 3 + } ] }; diff --git a/PresetSettings/prese_dragdrop.js b/PresetSettings/prese_dragdrop.js index 350f045..935f62e 100644 --- a/PresetSettings/prese_dragdrop.js +++ b/PresetSettings/prese_dragdrop.js @@ -41,8 +41,8 @@ function onDragStart(e, item) { draggedSection = draggedItem.data('section'); draggedOrderIndex = draggedItem.data('order-index'); - const popup = draggedItem.closest('.popup'); - scrollContainer = popup.length ? popup.find('.popup-body') : null; + // 修复:直接查找固定的滚动容器 + scrollContainer = $('#amily2-preset-settings-popup').find('#prompt-editor-container'); const pos = getEventPosition(e); startX = pos.x; @@ -103,23 +103,31 @@ function onDragEnd(e) { function completeDrag() { if (!draggedItem || !dragPlaceholder) return; - const placeholderIndex = dragPlaceholder.index(); const sectionContainer = dragPlaceholder.closest('.mixed-list'); - - const order = state.getCurrentMixedOrder()[draggedSection]; - const draggedElement = order[draggedOrderIndex]; - - order.splice(draggedOrderIndex, 1); - - const newIndex = placeholderIndex > draggedOrderIndex ? placeholderIndex - 1 : placeholderIndex; - - order.splice(newIndex, 0, draggedElement); - dragPlaceholder.before(draggedItem); - + + const newOrder = []; sectionContainer.find('.mixed-item').each(function(index) { - $(this).attr('data-order-index', index); + const $item = $(this); + $item.attr('data-order-index', index); // 更新UI索引属性 + + const type = $item.data('type'); + if (type === 'prompt') { + newOrder.push({ + type: 'prompt', + index: parseInt($item.data('prompt-index'), 10) + }); + } else if (type === 'conditional') { + newOrder.push({ + type: 'conditional', + id: $item.data('conditional-id') + }); + } }); + + const allOrders = state.getCurrentMixedOrder(); + allOrders[draggedSection] = newOrder; + state.setCurrentMixedOrder(allOrders); toastr.info('顺序已调整,请点击保存按钮以生效。', '', { timeOut: 3000 }); } diff --git a/amily2_message_board.json b/amily2_message_board.json index 7f56837..aee2495 100644 --- a/amily2_message_board.json +++ b/amily2_message_board.json @@ -1,5 +1,5 @@ { - "message": "插件群:1060183271,v1.5.2超级更新(新功能与优化多多喔~),祝大家假期玩的开心。个人原因,降低更新频率以及无暇看帖子,有问题最好加群。" + "message": "插件群:1060183271,更新了多个版本了,现在是v1.5.7,术语表上线。个人原因,降低更新频率以及无暇看帖子,有问题最好加群。" } @@ -42,5 +42,6 @@ + diff --git a/assets/amily2-glossary.css b/assets/amily2-glossary.css index 453a6c8..f5b53ec 100644 --- a/assets/amily2-glossary.css +++ b/assets/amily2-glossary.css @@ -1,118 +1,325 @@ -/* --- 术语表 (Glossary) 专属样式 --- */ -/* 所有样式均已限定在 #amily2_glossary_panel 范围内,防止全局污染 */ -/* 标签页导航容器 */ -#amily2_glossary_panel .glossary-tabs { - display: flex; - border-bottom: 1px solid rgba(255, 255, 255, 0.2); - margin-bottom: 15px; +#amily2_glossary_panel { + --am2-gap-main: 15px; + --am2-padding-main: 10px; + + --am2-container-bg: rgba(0,0,0,0.2); + --am2-container-border: 1px solid rgba(255, 255, 255, 0.15); + --am2-container-border-radius: 12px; + --am2-container-padding: 20px; + --am2-container-shadow: inset 0 0 15px rgba(0,0,0,0.25); + + --am2-title-font-size: 1.15em; + --am2-title-font-weight: bold; + --am2-title-icon-color: #9e8aff; + --am2-title-icon-margin: 10px; + + --am2-button-bg: #4A4A4A; + --am2-button-border-color: rgba(255, 255, 255, 0.2); + --am2-button-hover-bg: rgba(255, 255, 255, 0.15); + --am2-button-hover-border-color: #fff; + --am2-button-text-color: #E0E0E0; +} + +/* --- 整体布局 --- */ +#amily2_glossary_panel { + display: flex; + flex-direction: column; + gap: var(--am2-gap-main); + padding: var(--am2-padding-main); +} + +/* --- 标签页系统 --- */ +#amily2_glossary_panel .glossary-tabs { + display: flex; + border-bottom: 1px solid var(--am2-container-border); + margin: 0 5px; } -/* 单个标签页按钮 */ #amily2_glossary_panel .glossary-tab { background: none; border: none; + border-bottom: 3px solid transparent; color: var(--text-color-secondary); - padding: 10px 15px; cursor: pointer; font-size: 1em; - transition: color 0.2s, border-bottom 0.2s; - border-bottom: 2px solid transparent; - margin-bottom: -1px; /* 让下边框与容器边框重合 */ + padding: 10px 15px; + transition: all 0.3s ease; + margin-bottom: -1px; } #amily2_glossary_panel .glossary-tab:hover { + background-color: rgba(255, 255, 255, 0.05); color: var(--text-color-light); } -/* 激活状态的标签页 */ #amily2_glossary_panel .glossary-tab.active { - color: var(--text-color-accent); - border-bottom: 2px solid var(--text-color-accent); + color: var(--am2-title-icon-color); + border-bottom-color: var(--am2-title-icon-color); + font-weight: bold; } #amily2_glossary_panel .glossary-tab i { margin-right: 8px; } -/* 标签页内容面板 */ #amily2_glossary_panel .glossary-content { - display: none; /* 默认隐藏 */ + display: none; } -/* 激活状态的内容面板 */ #amily2_glossary_panel .glossary-content.active { - display: block; /* 显示激活的面板 */ + display: block; } -/* 隐藏内容的辅助类 */ -#amily2_glossary_panel .amily2-content-hidden { - display: none !important; -} - -/* 通用设置组样式 */ +/* --- 设置组 (卡片样式) --- */ #amily2_glossary_panel .settings-group { - background-color: rgba(0, 0, 0, 0.15); - border: 1px solid rgba(255, 255, 255, 0.1); - border-radius: 10px; - padding: 15px; - margin-bottom: 15px; + background: var(--am2-container-bg); + border: var(--am2-container-border); + border-radius: var(--am2-container-border-radius); + padding: var(--am2-container-padding); + box-shadow: var(--am2-container-shadow); + display: flex; + flex-direction: column; + gap: 18px; /* 组内元素的间距 */ } #amily2_glossary_panel .settings-group .legend { - font-size: 1.1em; - font-weight: bold; - color: var(--text-color-light); - margin-bottom: 10px; - padding-bottom: 5px; - border-bottom: 1px solid rgba(255, 255, 255, 0.15); + font-size: var(--am2-title-font-size); + font-weight: var(--am2-title-font-weight); + color: var(--am2-button-text-color); + margin: 0; + padding-bottom: 15px; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); } #amily2_glossary_panel .settings-group .legend i { - margin-right: 8px; - color: var(--text-color-accent); + margin-right: var(--am2-title-icon-margin); + color: var(--am2-title-icon-color); } -/* 设置块 */ +/* --- 表单控件 --- */ +#amily2_glossary_panel .control-group, #amily2_glossary_panel .amily2_settings_block { display: flex; - justify-content: space-between; + flex-wrap: wrap; align-items: center; - padding: 10px 0; - border-bottom: 1px solid rgba(255, 255, 255, 0.08); -} -#amily2_glossary_panel .amily2_settings_block:last-child { - border-bottom: none; + gap: 10px 15px; } -/* 控制组(标签和输入框) */ -#amily2_glossary_panel .control-group { - display: flex; - align-items: center; - margin-bottom: 12px; -} - -#amily2_glossary_panel .control-group label { - flex-basis: 150px; /* 固定标签宽度 */ - flex-shrink: 0; - margin-right: 10px; +#amily2_glossary_panel .control-group label, +#amily2_glossary_panel .amily2_settings_block > label { /* > 选择器避免影响toggle-switch内的label */ + flex: 1 1 150px; + min-width: 120px; + font-weight: bold; } #amily2_glossary_panel .control-group .text_pole, -#amily2_glossary_panel .control-group .select-with-refresh { - flex-grow: 1; +#amily2_glossary_panel .control-group .select-with-refresh, +#amily2_glossary_panel .control-group input[type="range"] { + flex: 2 1 250px; } -/* 带刷新按钮的选择器 */ -#amily2_glossary_panel .select-with-refresh { - display: flex; - align-items: center; +/* --- 开关按钮 (使用专属Class,彻底隔离污染) --- */ +#amily2_glossary_panel .amily2-glossary-toggle { + position: relative; + display: inline-block; + width: 50px; + height: 28px; + min-width: 50px; + flex-shrink: 0; + vertical-align: middle; } -/* 按钮行 */ +#amily2_glossary_panel .amily2-glossary-toggle input { + opacity: 0; + width: 0; + height: 0; +} + +#amily2_glossary_panel .amily2-glossary-toggle .slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #555; + transition: .4s; + border-radius: 28px; +} + +#amily2_glossary_panel .amily2-glossary-toggle .slider:before { + position: absolute; + content: ""; + height: 20px; + width: 20px; + left: 4px; + top: 50%; + background-color: white; + border-radius: 50%; + transition: .4s; + transform: translateY(-50%); +} + +#amily2_glossary_panel .amily2-glossary-toggle input:checked + .slider { + background-color: #8a72ff; +} + +#amily2_glossary_panel .amily2-glossary-toggle input:focus + .slider { + box-shadow: 0 0 1px #8a72ff; +} + +#amily2_glossary_panel .amily2-glossary-toggle input:checked + .slider:before { + transform: translateX(22px) translateY(-50%); +} + +/* --- 按钮 (移植自 table.css) --- */ #amily2_glossary_panel .sybd-button-row { display: flex; gap: 10px; justify-content: center; - margin-top: 20px; + margin-top: 10px; +} + +#amily2_glossary_panel .menu_button { + background: var(--am2-button-bg, #4A4A4A); + border: 1px solid var(--am2-button-border-color, rgba(255, 255, 255, 0.2)) !important; + color: var(--am2-button-text-color, #E0E0E0) !important; + padding: 8px 15px; + border-radius: 8px; + cursor: pointer; + font-weight: bold; + transition: all 0.4s ease-out !important; + white-space: nowrap; /* 防止文字换行 */ + display: inline-flex; /* 确保图标和文字对齐 */ + align-items: center; + justify-content: center; + gap: 5px; +} + +#amily2_glossary_panel .menu_button:hover { + background-color: var(--am2-button-hover-bg, rgba(255, 255, 255, 0.15)); + border-color: var(--am2-button-hover-border-color, #fff) !important; + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0,0,0,0.3); +} + +#amily2_glossary_panel .menu_button.small_button { + padding: 5px 12px; + font-size: 0.9em; + border-radius: 6px; +} + +/* --- 世界书条目渲染 --- */ +#amily2_glossary_panel .world-book-entry-item { + background-color: rgba(0, 0, 0, 0.2); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 8px; + padding: 15px; +} + +#amily2_glossary_panel .entry-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 10px; + padding-bottom: 10px; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); +} + +#amily2_glossary_panel .entry-title { + font-size: 1.2em; + font-weight: bold; +} + +#amily2_glossary_panel .entry-content-display pre { + white-space: pre-wrap; + word-wrap: break-word; + background-color: rgba(0, 0, 0, 0.25); + padding: 10px; + border-radius: 5px; +} + +#amily2_glossary_panel .table-render { + width: 100%; + border-collapse: collapse; +} + +#amily2_glossary_panel .table-render th, +#amily2_glossary_panel .table-render td { + border: 1px solid rgba(255, 255, 255, 0.2); + padding: 8px; +} + +#amily2_glossary_panel .table-render th { + background-color: rgba(255, 255, 255, 0.1); +} + +/* --- 小说处理面板特定样式修复 --- */ + +/* 为需要水平布局的组应用flex */ +#amily2_glossary_panel #glossary-content-novel-process .horizontal-group { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 10px 15px; +} +#amily2_glossary_panel #glossary-content-novel-process .horizontal-group label { + flex: 0 0 150px; /* 固定标签宽度 */ +} +#amily2_glossary_panel #glossary-content-novel-process .horizontal-group .text_pole, +#amily2_glossary_panel #glossary-content-novel-process .horizontal-group select { + flex: 1 1 200px; /* 输入框占据剩余空间 */ +} + +/* 上传按钮容器 */ +#amily2_glossary_panel #glossary-content-novel-process .upload-button-container { + display: flex; + justify-content: center; + align-items: center; + padding: 10px 0; +} +#amily2_glossary_panel #glossary-content-novel-process .upload-button-container .menu_button { + cursor: pointer; /* 确保鼠标指针是手型 */ +} + + +/* 预览区域 */ +#amily2_glossary_panel #glossary-content-novel-process .preview-container label { + display: block; + margin-bottom: 8px; + font-weight: bold; +} +#amily2_glossary_panel #glossary-content-novel-process #novel-chunk-preview { + height: 150px; + width: 100%; + overflow-y: auto; + border: 1px solid rgba(255, 255, 255, 0.2); + padding: 10px; + background-color: rgba(0, 0, 0, 0.2); + border-radius: 6px; + box-sizing: border-box; + font-size: 0.9em; +} +#amily2_glossary_panel #glossary-content-novel-process #novel-chunk-preview .chunk-preview-item { + padding: 4px; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); +} +#amily2_glossary_panel #glossary-content-novel-process #novel-chunk-preview .chunk-preview-item:last-child { + border-bottom: none; +} + + +/* 开关组的对齐 */ +#amily2_glossary_panel #glossary-content-novel-process .amily2_settings_block { + justify-content: space-between; /* 让label和开关分布在两端 */ + padding: 12px; + background-color: rgba(255, 255, 255, 0.05); + border-radius: 8px; +} +#amily2_glossary_panel #glossary-content-novel-process .amily2_settings_block > label { + flex: 1 1 auto; /* 让标签占据可用空间 */ +} +#amily2_glossary_panel #glossary-content-novel-process .amily2_settings_block .amily2-glossary-toggle { + flex: 0 0 auto; /* 开关不拉伸 */ } diff --git a/assets/amily2-glossary.html b/assets/amily2-glossary.html index 8b4f4dc..fc858b9 100644 --- a/assets/amily2-glossary.html +++ b/assets/amily2-glossary.html @@ -20,7 +20,7 @@ 待开发 - 待开发 + 世界书条目 @@ -35,15 +35,7 @@ 独立的API调用系统,可与主系统并行使用,支持全兼容和SillyTavern预设两种模式。 - - 启用Sybd API系统 - - - - - - - + API调用模式: @@ -116,11 +108,19 @@ - + - 待开发 - 待开发 + 世界书条目预览 + + 此处将显示在“小说处理”标签页中选定世界书的条目。 + + + + + + + @@ -129,53 +129,51 @@ 小说文件处理流程 - - - 1. 上传小说文件 (.txt) + + + 1. 选择目标世界书: + + + + + + + 2. 上传小说文件 (.txt) - - 2. 章节识别规则 (高级, 可选): - + + 3. 每批处理字符数: + - - - ① 识别章节 - - + - - - - 3. 章节预览 (共 0 章): - - 请先上传文件并识别章节... + + + 4. 内容分块预览 (共 0 块): + + 请先上传文件... - - 4. 每批处理章节数: - - - - + + 强制新建条目 (不更新现有条目) - + - - - ② 确认并开始处理 + + + 确认并开始处理 - + 等待操作... diff --git a/assets/amily2-modal.html b/assets/amily2-modal.html index ba5db7e..42f4543 100644 --- a/assets/amily2-modal.html +++ b/assets/amily2-modal.html @@ -135,7 +135,7 @@ 剧情优化 世界编辑 - 术语表单 + 术语表单 diff --git a/core/lore.js b/core/lore.js index 3ea1c79..f5d9a19 100644 --- a/core/lore.js +++ b/core/lore.js @@ -294,7 +294,14 @@ export async function getPlotOptimizedWorldbookContent(context, apiSettings) { if (panel.length > 0) { liveSettings.worldbookEnabled = panel.find('#amily2_opt_worldbook_enabled').is(':checked'); liveSettings.worldbookSource = panel.find('input[name="amily2_opt_worldbook_source"]:checked').val() || 'character'; - liveSettings.selectedWorldbooks = panel.find('#amily2_opt_selected_worldbooks').val() || []; + + liveSettings.selectedWorldbooks = []; + if (liveSettings.worldbookSource === 'manual') { + panel.find('#amily2_opt_worldbook_checkbox_list input[type="checkbox"]:checked').each(function() { + liveSettings.selectedWorldbooks.push($(this).val()); + }); + } + liveSettings.worldbookCharLimit = parseInt(panel.find('#amily2_opt_worldbook_char_limit').val(), 10) || 60000; let enabledEntries = {}; diff --git a/glossary/GT_bindings.js b/glossary/GT_bindings.js index 025e73d..901efd2 100644 --- a/glossary/GT_bindings.js +++ b/glossary/GT_bindings.js @@ -1,10 +1,15 @@ import { extension_settings, getContext } from "/scripts/extensions.js"; -import { saveSettingsDebounced } from "/script.js"; +import { saveSettingsDebounced, eventSource, event_types } from "/script.js"; +import { world_names } from "/scripts/world-info.js"; import { extensionName } from "../utils/settings.js"; import { testSybdApiConnection, fetchSybdModels } from '../core/api/SybdApi.js'; -import { handleFileUpload, recognizeChapters, processNovel } from './index.js'; +import { handleFileUpload, processNovel } from './index.js'; import { SETTINGS_KEY as PRESET_SETTINGS_KEY } from '../PresetSettings/config.js'; +const moduleState = { + selectedWorldBook: '', +}; + function updateAndSaveSetting(key, value) { if (!extension_settings[extensionName]) { extension_settings[extensionName] = {}; @@ -49,10 +54,9 @@ function loadSettingsToUI() { } }); - const sybdToggle = document.getElementById('amily2_sybd_enabled'); const sybdContent = document.getElementById('amily2_sybd_content'); - if (sybdToggle && sybdContent) { - sybdContent.classList.toggle('amily2-content-hidden', !sybdToggle.checked); + if (sybdContent) { + sybdContent.classList.remove('amily2-content-hidden'); } const apiModeSelect = document.getElementById('amily2_sybd_api_mode'); @@ -87,9 +91,6 @@ function bindAutoSaveEvents() { updateAndSaveSetting(key, value); - if (key === 'sybdEnabled') { - document.getElementById('amily2_sybd_content').classList.toggle('amily2-content-hidden', !value); - } if (key === 'sybdApiMode') { updateConfigVisibility(value); } @@ -204,6 +205,178 @@ function bindManualActionEvents() { } } +async function renderWorldBookEntries() { + const container = document.getElementById('world-book-entries-display'); + if (!container) return; + + const selectedBook = moduleState.selectedWorldBook; + if (!selectedBook) { + container.innerHTML = '请先在“小说处理”标签页中选择一个世界书。'; + return; + } + + container.innerHTML = ' 正在加载条目...'; + + try { + const { TavernHelper } = window; + if (!TavernHelper) { + container.innerHTML = 'TavernHelper 未找到!'; + return; + } + + const allEntries = await TavernHelper.getLorebookEntries(selectedBook); + let managedEntries = allEntries.filter(e => e.comment?.startsWith('[Amily2小说处理]')); + + if (managedEntries.length === 0) { + container.innerHTML = '未找到由小说处理功能生成的条目。'; + return; + } + + container.innerHTML = ''; + + const summaryEntries = managedEntries.filter(e => e.comment.replace('[Amily2小说处理]', '').trim().startsWith('章节内容概述')); + const otherEntries = managedEntries.filter(e => !e.comment.replace('[Amily2小说处理]', '').trim().startsWith('章节内容概述')); + const sortedEntries = otherEntries.concat(summaryEntries); + + sortedEntries.forEach(entry => { + const entryElement = document.createElement('div'); + entryElement.className = 'world-book-entry-item'; + entryElement.dataset.entryId = entry.uid; + + const title = entry.comment.replace('[Amily2小说处理]', '').trim(); + + const renderContent = (content) => { + const trimmedContent = content.trim(); + if (trimmedContent.startsWith('graph') || trimmedContent.startsWith('flowchart')) { + try { + const lines = trimmedContent.split('\n').map(l => l.trim()).filter(l => l.includes('-->') || l.includes('--')); + let body = ''; + lines.forEach(line => { + if (line.startsWith('flowchart')) return; + let source = '', rel = '', target = ''; + + let match = line.match(/(.+?)\s*--\s*"(.*?)"\s*-->(.+)/); + if (match) { + [source, rel, target] = [match[1], match[2], match[3]]; + } else { + match = line.match(/(.+?)\s*-->\s*\|(.*?)\|(.+)/); + if (match) { + [source, rel, target] = [match[1], match[2], match[3]]; + } else { + match = line.match(/(.+?)\s*-->(.+)/); + if (match) { + [source, target] = [match[1], match[2]]; + rel = '(直接关联)'; + } + } + } + + if (source && target) { + body += `${source.trim()}${rel.trim()}${target.trim().replace(';','')}`; + } + }); + return `源头关系目标${body}`; + } catch { + return `${content}`; + } + } + if (trimmedContent.includes('|') && trimmedContent.includes('\n')) { + try { + const rows = trimmedContent.split('\n').filter(row => row.trim() && row.includes('|')); + let header = ''; + let body = ''; + let isHeaderRow = true; + rows.forEach(rowStr => { + if (rowStr.includes('---')) return; + const cells = rowStr.split('|').filter(c => c.trim()).map(cell => `${cell.trim()}`).join(''); + if (isHeaderRow) { + header += `${cells.replace(//g, '').replace(/<\/td>/g, '')}`; + isHeaderRow = false; + } else { + body += `${cells}`; + } + }); + return `${header}${body}`; + } catch { + return `${content}`; + } + } + return `${content}`; + }; + + entryElement.innerHTML = ` + + ${title} + + 保存 + 取消 + 编辑 + + + ${renderContent(entry.content)} + + ${entry.content} + + `; + + const editBtn = entryElement.querySelector('.edit-entry-btn'); + const saveBtn = entryElement.querySelector('.save-entry-btn'); + const cancelBtn = entryElement.querySelector('.cancel-entry-btn'); + const displayDiv = entryElement.querySelector('.entry-content-display'); + const editorDiv = entryElement.querySelector('.entry-content-editor'); + const textarea = editorDiv.querySelector('textarea'); + const originalContent = entry.content; + + editBtn.addEventListener('click', () => { + displayDiv.style.display = 'none'; + editorDiv.style.display = 'block'; + saveBtn.style.display = 'inline-block'; + cancelBtn.style.display = 'inline-block'; + editBtn.style.display = 'none'; + }); + + const hideEditor = () => { + displayDiv.style.display = 'block'; + editorDiv.style.display = 'none'; + saveBtn.style.display = 'none'; + cancelBtn.style.display = 'none'; + editBtn.style.display = 'inline-block'; + }; + + cancelBtn.addEventListener('click', () => { + textarea.value = originalContent; + hideEditor(); + }); + + saveBtn.addEventListener('click', async () => { + const newContent = textarea.value; + + displayDiv.innerHTML = renderContent(newContent); + hideEditor(); + + try { + const { TavernHelper } = window; + const entryToUpdate = { uid: entry.uid, content: newContent }; + await TavernHelper.setLorebookEntries(selectedBook, [entryToUpdate]); + toastr.success(`条目 "${title}" 已保存。`); + entry.content = newContent; + } catch (error) { + displayDiv.innerHTML = renderContent(originalContent); + console.error('保存世界书条目失败:', error); + toastr.error(`保存失败: ${error.message}`); + } + }); + + container.appendChild(entryElement); + }); + + } catch (error) { + console.error('加载世界书条目失败:', error); + container.innerHTML = `加载失败: ${error.message}`; + } +} + + function bindTabEvents() { const tabs = document.querySelectorAll('.glossary-tab'); const contents = document.querySelectorAll('.glossary-content'); @@ -223,6 +396,9 @@ function bindTabEvents() { } }); + if (tabId === 'context') { + renderWorldBookEntries(); + } }); }); } @@ -230,46 +406,190 @@ function bindTabEvents() { function bindNovelProcessEvents() { const fileInput = document.getElementById('novel-file-input'); const fileLabel = document.querySelector('label[for="novel-file-input"]'); - const recognizeBtn = document.getElementById('novel-recognize-chapters'); const processBtn = document.getElementById('novel-confirm-and-process'); + const chunkSizeInput = document.getElementById('novel-chunk-size'); + const chunkCountEl = document.getElementById('novel-chunk-count'); + const chunkPreviewEl = document.getElementById('novel-chunk-preview'); + + let fileContent = ''; + let processingState = { + chunks: [], + batchSize: 1, + forceNew: false, + selectedWorldBook: '', + currentIndex: 0, + isAborted: false, + isRunning: false, + lastStatus: 'idle', + }; + + function updateChunks() { + if (!fileContent) return; + const chunkSize = parseInt(chunkSizeInput.value, 10) || 5000; + const newChunks = []; + for (let i = 0; i < fileContent.length; i += chunkSize) { + newChunks.push({ title: `Part ${i/chunkSize + 1}`, content: fileContent.substring(i, i + chunkSize) }); + } + processingState.chunks = newChunks; + + chunkCountEl.textContent = newChunks.length; + chunkPreviewEl.innerHTML = newChunks.map((chunk, index) => + `块 ${index + 1}: ${chunk.content.substring(0, 100)}...` + ).join(''); + + resetProcessing(); + } + + function resetProcessing() { + processingState.currentIndex = 0; + processingState.isAborted = false; + processingState.isRunning = false; + processingState.lastStatus = 'idle'; + updateButtonUI(); + } + + function updateButtonUI() { + if (processingState.isRunning) { + processBtn.disabled = false; + processBtn.innerHTML = ' 请求中止'; + processBtn.classList.add('danger'); + } else { + processBtn.classList.remove('danger'); + switch (processingState.lastStatus) { + case 'paused': + processBtn.innerHTML = ' 继续处理'; + processBtn.disabled = false; + break; + case 'failed': + processBtn.innerHTML = ' 重试处理'; + processBtn.disabled = false; + break; + case 'success': + processBtn.innerHTML = ' 处理完成'; + processBtn.disabled = true; + break; + case 'idle': + default: + processBtn.innerHTML = '确认并开始处理'; + processBtn.disabled = processingState.chunks.length === 0; + break; + } + } + } + + async function startOrResumeProcessing() { + if (processingState.isRunning) return; + + processingState.isRunning = true; + processingState.isAborted = false; + updateButtonUI(); + + processingState.forceNew = document.getElementById('novel-force-new').checked; + processingState.batchSize = 1; + processingState.selectedWorldBook = moduleState.selectedWorldBook; + + try { + const result = await processNovel(processingState); + if (result === 'paused') { + processingState.lastStatus = 'paused'; + } else if (result === 'success') { + processingState.lastStatus = 'success'; + processingState.currentIndex = 0; + } + } catch (error) { + processingState.lastStatus = 'failed'; + processingState.isAborted = true; + } finally { + processingState.isRunning = false; + updateButtonUI(); + } + } if (fileLabel && fileInput) { fileLabel.addEventListener('click', (event) => { - event.preventDefault(); - fileInput.click(); + event.preventDefault(); + fileInput.click(); }); fileInput.addEventListener('change', (event) => { - handleFileUpload(event.target.files[0]); + handleFileUpload(event.target.files[0], (content) => { + fileContent = content; + updateChunks(); + }); }); } - if (recognizeBtn) { - recognizeBtn.addEventListener('click', async () => { - const originalHtml = recognizeBtn.innerHTML; - recognizeBtn.disabled = true; - recognizeBtn.innerHTML = ' 识别中...'; - - await recognizeChapters(); - - recognizeBtn.disabled = false; - recognizeBtn.innerHTML = originalHtml; - }); + if (chunkSizeInput) { + chunkSizeInput.addEventListener('input', updateChunks); } + if (processBtn) { processBtn.addEventListener('click', async () => { - const originalHtml = processBtn.innerHTML; - processBtn.disabled = true; - processBtn.innerHTML = ' 处理中...'; - - await processNovel(); - - processBtn.disabled = false; - processBtn.innerHTML = originalHtml; + if (processingState.isRunning) { + processingState.isAborted = true; + processBtn.innerHTML = ' 正在中止...'; + processBtn.disabled = true; + } else { + if (processingState.lastStatus === 'success') { + resetProcessing(); + } + if (processingState.lastStatus === 'idle' || processingState.lastStatus === 'success') { + processingState.currentIndex = 0; + } + startOrResumeProcessing(); + } }); } } +function isTavernHelperAvailable() { + return typeof window.TavernHelper !== 'undefined' && + window.TavernHelper !== null && + typeof window.TavernHelper.getLorebooks === 'function'; +} + +async function safeLorebooks() { + try { + if (isTavernHelperAvailable()) { + return await window.TavernHelper.getLorebooks(); + } + return [...world_names]; + } catch (error) { + console.error('[Amily2-兼容性] 获取世界书列表失败:', error); + return [...world_names]; + } +} + +async function loadWorldBooks() { + const select = document.getElementById('novel-world-book-select'); + if (!select) return; + + const { extension_settings } = window; + const savedBook = extension_settings[extensionName]?.selectedWorldBook; + moduleState.selectedWorldBook = savedBook || ''; + + try { + const allBooks = await safeLorebooks(); + select.innerHTML = '-- 请选择世界书 --'; + + if (allBooks && allBooks.length > 0) { + allBooks.forEach(bookName => { + const option = new Option(bookName, bookName); + select.add(option); + }); + + if (savedBook && allBooks.includes(savedBook)) { + select.value = savedBook; + } + } else { + select.innerHTML = '未找到世界书'; + } + } catch (error) { + console.error('[Amily2-术语表] 加载世界书失败:', error); + select.innerHTML = '加载失败'; + } +} + export function bindGlossaryEvents() { const panel = document.getElementById('amily2_glossary_panel'); if (!panel || panel.dataset.eventsBound) { @@ -283,6 +603,27 @@ export function bindGlossaryEvents() { bindManualActionEvents(); bindTabEvents(); bindNovelProcessEvents(); + loadWorldBooks(); + + // 监听角色加载事件,以确保 world_names 可用 + eventSource.on(event_types.CHARACTER_PAGE_LOADED, () => { + console.log('[Amily2-术语表] 检测到角色加载,重新加载世界书列表以确保同步。'); + loadWorldBooks(); + }); + + const worldBookSelect = document.getElementById('novel-world-book-select'); + if (worldBookSelect) { + worldBookSelect.addEventListener('change', () => { + const selectedValue = worldBookSelect.value; + updateAndSaveSetting('selectedWorldBook', selectedValue); + moduleState.selectedWorldBook = selectedValue; + + const contextTab = document.querySelector('.glossary-tab[data-tab="context"]'); + if (contextTab && contextTab.classList.contains('active')) { + renderWorldBookEntries(); + } + }); + } panel.dataset.eventsBound = 'true'; console.log('[Amily2-术语表] UI事件绑定完成 (最终重构版)。'); diff --git a/glossary/executor.js b/glossary/executor.js index a2e0988..0288fd9 100644 --- a/glossary/executor.js +++ b/glossary/executor.js @@ -1,13 +1,29 @@ import { callSybdAI } from '../core/api/SybdApi.js'; -import { getTargetWorldBook, syncNovelLorebookEntries } from '../CharacterWorldBook/src/cwb_lorebookManager.js'; +import { extensionName } from '../utils/settings.js'; import { getPresetPrompts, getMixedOrder } from '../PresetSettings/index.js'; import { generateRandomSeed } from '../core/api.js'; const { TavernHelper } = window; +function buildContextFromEntries(entries) { + if (!entries || entries.length === 0) { + return '当前世界书为空。'; + } + + const mappedContent = entries.map(entry => { + if (!Array.isArray(entry.keyword) || entry.keyword.length < 2) { + return null; + } + const name = entry.keyword[1]; + return `[--START_TABLE--]\n[name]:${name}\n${entry.content}\n[--END_TABLE--]`; + }).filter(Boolean).join('\n\n'); + + return mappedContent || '当前世界书为空。'; +} + function parseStructuredResponse(responseText) { const entries = []; - const entryRegex = /【(.*?)】.*?\[START_TABLE\]([\s\S]*?)\[END_TABLE\]/g; + const entryRegex = /\[--START_TABLE--\]\s*\[name\]:(.*?)\n([\s\S]*?)\[--END_TABLE--\]/g; let match; while ((match = entryRegex.exec(responseText)) !== null) { @@ -22,41 +38,46 @@ function parseStructuredResponse(responseText) { } -export async function executeNovelProcessing(recognizedChapters, batchSize, forceNew, updateStatusCallback) { +export async function executeNovelProcessing(processingState, updateStatusCallback) { + const { chunks: recognizedChapters, batchSize, forceNew, selectedWorldBook } = processingState; + if (recognizedChapters.length === 0) { updateStatusCallback('没有可处理的章节。', 'error'); - return; + throw new Error('没有可处理的章节。'); } updateStatusCallback('开始处理小说...', 'info'); try { - const bookName = await getTargetWorldBook(); - if (!bookName) throw new Error('无法确定目标世界书。'); - let existingEntriesContent = '当前世界书为空。'; - if (!forceNew) { - const allEntries = (await TavernHelper.getLorebookEntries(bookName)) || []; - const managedEntries = allEntries.filter(e => e.comment?.startsWith(`[Amily2小说处理]`)); - if (managedEntries.length > 0) { - existingEntriesContent = managedEntries.map(entry => { - return `【${entry.keyword}】\n[START_TABLE]\n${entry.content}\n[END_TABLE]`; - }).join('\n\n'); - } + const bookName = selectedWorldBook; + if (!bookName) { + throw new Error('请先在设置中选择一个目标世界书。'); } - for (let i = 0; i < recognizedChapters.length; i += batchSize) { + const allEntries = (await TavernHelper.getLorebookEntries(bookName)) || []; + const managedEntries = allEntries.filter(e => e.comment?.startsWith('[Amily2小说处理]') || e.comment?.startsWith('[Amily2-Glossary]')); + const localManagedEntries = [...managedEntries]; + + let existingEntriesContent = '当前世界书为空。'; + if (!forceNew) { + existingEntriesContent = buildContextFromEntries(localManagedEntries); + } + + for (let i = processingState.currentIndex; i < recognizedChapters.length; i += batchSize) { + if (processingState.isAborted) { + updateStatusCallback(`处理已中止。当前进度: ${i}/${recognizedChapters.length}`, 'info'); + return 'paused'; + } + processingState.currentIndex = i; + const batch = recognizedChapters.slice(i, i + batchSize); const progress = `(${i + batch.length}/${recognizedChapters.length})`; updateStatusCallback(`正在处理批次 ${Math.floor(i / batchSize) + 1}... ${progress}`, 'info'); const chapterContent = batch.map(c => `## ${c.title}\n${c.content}`).join('\n\n---\n\n'); - const order = getMixedOrder('novel_processor') || []; const presetPrompts = await getPresetPrompts('novel_processor'); - - const messages = [ - { role: 'system', content: generateRandomSeed() } - ]; + const messages = [{ role: 'system', content: generateRandomSeed() }]; let promptCounter = 0; for (const item of order) { @@ -66,40 +87,93 @@ export async function executeNovelProcessing(recognizedChapters, batchSize, forc promptCounter++; } } else if (item.type === 'conditional') { - switch (item.id) { - case 'existingLore': - messages.push({ role: 'user', content: `# 已有世界书条目\n\n${existingEntriesContent}` }); - break; - case 'chapterContent': - messages.push({ role: 'user', content: `# 最新章节内容\n\n${chapterContent}\n\n请根据以上信息,分析并输出需要新增或更新的世界书条目。` }); - break; + if (item.id === 'existingLore') { + messages.push({ role: 'user', content: `# 已有世界书条目\n\n${existingEntriesContent}` }); + } else if (item.id === 'chapterContent') { + messages.push({ role: 'user', content: `# 最新章节内容\n\n${chapterContent}\n\n请根据以上信息,分析并输出需要新增或更新的世界书条目。` }); } } } - if (messages.length <= 1) { - throw new Error('未能根据预设构建有效的API请求。'); - } + if (messages.length <= 1) throw new Error('未能根据预设构建有效的API请求。'); const response = await callSybdAI(messages); - if (!response || response.trim() === '无需更新') { + if (!response) { + throw new Error(`API调用失败,批次 ${Math.floor(i / batchSize) + 1} 未收到响应。`); + } + if (response.trim() === '无需更新') { updateStatusCallback(`批次 ${Math.floor(i / batchSize) + 1} 无需更新。`, 'info'); continue; } const structuredData = parseStructuredResponse(response); if (structuredData.length === 0) { - updateStatusCallback(`批次 ${Math.floor(i / batchSize) + 1} 未提取到有效信息。`, 'info'); - continue; + throw new Error(`未能从API响应中提取有效信息,批次 ${Math.floor(i / batchSize) + 1}。`); + } + + const entriesToUpdate = []; + const entriesToCreate = []; + const fixedNovelEntries = ['世界观设定', '时间线', '角色关系网', '角色总览']; + + let maxPart = 0; + localManagedEntries.forEach(entry => { + const match = entry.comment.match(/章节内容概述-第(\d+)部分/); + if (match && parseInt(match[1], 10) > maxPart) maxPart = parseInt(match[1], 10); + }); + let nextPart = maxPart + 1; + + for (const entry of structuredData) { + const { title, content } = entry; + let comment; + let keys; + + if (title === '章节内容概述') { + comment = `[Amily2小说处理] ${title}-第${nextPart}部分`; + keys = [`小说处理`, title, `第${nextPart}部分`]; + const newEntryData = { keys, content, comment, enabled: true, order: 100, position: 'before_char' }; + entriesToCreate.push(newEntryData); + localManagedEntries.push({ uid: -1, ...newEntryData, keyword: keys }); + nextPart++; + continue; + } + + if (fixedNovelEntries.includes(title)) { + comment = `[Amily2小说处理] ${title}`; + keys = [`小说处理`, title]; + } else { + comment = `[Amily2-Glossary] ${title}`; + keys = [`自定义条目`, title]; + } + + const existingEntry = localManagedEntries.find(e => e.comment === comment); + const loreData = { keys, content, comment, enabled: true, order: 100, position: 'before_char' }; + + if (existingEntry) { + entriesToUpdate.push({ uid: existingEntry.uid, ...loreData }); + Object.assign(existingEntry, { ...loreData, keyword: keys }); + } else { + entriesToCreate.push(loreData); + localManagedEntries.push({ uid: -1, ...loreData, keyword: keys }); + } } - await syncNovelLorebookEntries(bookName, structuredData); - existingEntriesContent = response; + if (entriesToUpdate.length > 0) { + await TavernHelper.setLorebookEntries(bookName, entriesToUpdate); + updateStatusCallback(`更新了 ${entriesToUpdate.length} 个世界书条目。`, 'info'); + } + if (entriesToCreate.length > 0) { + await TavernHelper.createLorebookEntries(bookName, entriesToCreate); + updateStatusCallback(`创建了 ${entriesToCreate.length} 个新世界书条目。`, 'success'); + } + + existingEntriesContent = buildContextFromEntries(localManagedEntries); } updateStatusCallback('小说处理完成!', 'success'); + return 'success'; } catch (error) { console.error('处理小说时发生严重错误:', error); updateStatusCallback(`处理失败: ${error.message}`, 'error'); + throw error; } } diff --git a/glossary/index.js b/glossary/index.js index bf61eb3..8255829 100644 --- a/glossary/index.js +++ b/glossary/index.js @@ -1,18 +1,7 @@ import { executeNovelProcessing } from './executor.js'; -let novelText = null; -let recognizedChaptersList = []; - -const getNovelFileInput = () => document.getElementById('novel-file-input'); -const getChapterRegexInput = () => document.getElementById('novel-chapter-regex'); -const getRecognizeBtn = () => document.getElementById('novel-recognize-chapters'); const getProcessBtn = () => document.getElementById('novel-confirm-and-process'); -const getChapterPreview = () => document.getElementById('novel-chapter-preview'); -const getChapterCount = () => document.getElementById('novel-chapter-count'); const getStatusDisplay = () => document.getElementById('novel-process-status'); -const getPresetSelect = () => document.getElementById('novel-preset-select'); -const getBatchSizeInput = () => document.getElementById('novel-batch-size'); -const getForceNewCheckbox = () => document.getElementById('novel-force-new'); export function updateStatus(message, type = 'info') { const statusDisplay = getStatusDisplay(); @@ -22,105 +11,31 @@ export function updateStatus(message, type = 'info') { } } -function resetChapterUI() { - const preview = getChapterPreview(); - const count = getChapterCount(); - const processBtn = getProcessBtn(); - if (preview) preview.innerHTML = '请先上传文件并识别章节...'; - if (count) count.textContent = '0'; - if (processBtn) processBtn.disabled = true; - recognizedChaptersList = []; -} - -export function handleFileUpload(file) { +export function handleFileUpload(file, callback) { if (!file || !file.type.startsWith('text/')) { updateStatus('请选择一个有效的 .txt 文件。', 'error'); return; } const reader = new FileReader(); reader.onload = (event) => { - novelText = event.target.result; - updateStatus(`文件 "${file.name}" 已成功加载。请点击“识别章节”。`, 'success'); - resetChapterUI(); + const content = event.target.result; + updateStatus(`文件 "${file.name}" 已成功加载。`, 'success'); + if (callback) { + callback(content); + } }; reader.onerror = () => { updateStatus(`读取文件 "${file.name}" 时发生错误。`, 'error'); - novelText = null; }; reader.readAsText(file); } -export function recognizeChapters() { - if (!novelText) { - updateStatus('请先上传一个小说文件。', 'error'); - return; - } - const regexInput = getChapterRegexInput(); - const customRegex = regexInput.value.trim(); - const defaultRegex = '(^\\s*(?:(?:第|卷)\\s*[一二三四五六七八九十百千万零〇\\d]+\\s*[章回节部篇]|Chapter\\s+\\d+|\\d+\\s*[.、]|序章|楔子|引子|序幕|尾声|终章|后记|番外)\\s*.*)'; - let finalRegex; +export async function processNovel(processingState) { try { - finalRegex = new RegExp(customRegex || defaultRegex, 'gm'); - } catch (e) { - updateStatus('无效的正则表达式。', 'error'); - return; - } - - updateStatus('正在识别章节...', 'info'); - recognizedChaptersList = []; - const matches = [...novelText.matchAll(finalRegex)]; - - if (matches.length > 0) { - for (let i = 0; i < matches.length; i++) { - const currentMatch = matches[i]; - const nextMatch = matches[i + 1]; - - const title = currentMatch[0].trim(); - const startIndex = currentMatch.index + currentMatch[0].length; - const endIndex = nextMatch ? nextMatch.index : novelText.length; - - const content = novelText.substring(startIndex, endIndex).trim(); - - if (title) { - recognizedChaptersList.push({ title, content }); - } - } - } - - const preview = getChapterPreview(); - const count = getChapterCount(); - const processBtn = getProcessBtn(); - - if (preview) { - preview.innerHTML = recognizedChaptersList.map((chap, index) => `${index + 1}. ${chap.title}`).join(''); - } - if (count) { - count.textContent = recognizedChaptersList.length; - } - - if (recognizedChaptersList.length > 0) { - processBtn.disabled = false; - updateStatus(`成功识别 ${recognizedChaptersList.length} 个章节。请预览并确认。`, 'success'); - } else { - updateStatus('未能识别出章节。请尝试调整正则表达式或检查文件内容。', 'error'); - processBtn.disabled = true; - } -} - -export async function processNovel() { - const processBtn = getProcessBtn(); - processBtn.disabled = true; - - try { - const batchSize = parseInt(getBatchSizeInput().value, 10); - const forceNew = getForceNewCheckbox().checked; - - await executeNovelProcessing(recognizedChaptersList, batchSize, forceNew, updateStatus); - + return await executeNovelProcessing(processingState, updateStatus); } catch (error) { console.error('处理小说时发生UI层错误:', error); updateStatus(`处理失败: ${error.message}`, 'error'); - } finally { - processBtn.disabled = false; + throw error; } } diff --git a/manifest.json b/manifest.json index 8a6882c..883a8d5 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "name": "Amily2号聊天优化助手", "display_name": "Amily2号助手", - "version": "1.5.6", + "version": "1.5.7", "author": "Wx-2025", "description": "一个拥有独立UI的智能引擎,正文优化、自动总结、记忆表格、rag向量、隐藏楼层、剧情推进六大功能整合。", "minSillyTavernVersion": "1.10.0", @@ -29,3 +29,4 @@ +
待开发
请先在“小说处理”标签页中选择一个世界书。
正在加载条目...
TavernHelper 未找到!
未找到由小说处理功能生成的条目。
${content}
加载失败: ${error.message}