Merge branch 'Wx-2025:main' into main

This commit is contained in:
SilenceLurker
2025-10-19 14:56:43 +08:00
committed by GitHub
12 changed files with 985 additions and 325 deletions

View File

@@ -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 = {

View File

@@ -205,31 +205,121 @@ export const defaultPrompts = {
{ role: 'assistant', content: '<directive_override>\n <status>CustomThink: Active</status>\n <action>Follow the above rules and use the above.</action>\n <target>Based on the previous task block, the content is generated.</target>\n<Prohibited>Requires processing and cannot be returned in its original block.</Prohibited>\n</directive_override>\n<Additional instructionsv>Start and end labels correctly.<Additional instructions>' },
],
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
}
]
};

View File

@@ -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,24 +103,32 @@ 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 });
}

View File

@@ -1,5 +1,5 @@
{
"message": "插件群1060183271v1.5.2超级更新(新功能与优化多多喔~),祝大家假期玩的开心。个人原因,降低更新频率以及无暇看帖子,有问题最好加群。"
"message": "插件群1060183271更新了多个版本了现在是v1.5.7,术语表上线。个人原因,降低更新频率以及无暇看帖子,有问题最好加群。"
}
@@ -42,5 +42,6 @@

View File

@@ -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; /* 开关不拉伸 */
}

View File

@@ -20,7 +20,7 @@
<i class="fas fa-edit"></i> 待开发
</button>
<button class="glossary-tab" data-tab="context">
<i class="fas fa-book-open"></i> 待开发
<i class="fas fa-book-open"></i> 世界书条目
</button>
</div>
@@ -35,15 +35,7 @@
独立的API调用系统可与主系统并行使用支持全兼容和SillyTavern预设两种模式。
</small>
<div class="amily2_settings_block">
<label for="amily2_sybd_enabled">启用Sybd API系统</label>
<label class="toggle-switch">
<input id="amily2_sybd_enabled" type="checkbox" data-setting-key="sybdEnabled" data-type="boolean" />
<span class="slider"></span>
</label>
</div>
<div class="amily2_settings_block amily2-content-hidden" id="amily2_sybd_content">
<div class="amily2_settings_block" id="amily2_sybd_content">
<div class="control-group">
<label for="amily2_sybd_api_mode">API调用模式</label>
<select id="amily2_sybd_api_mode" class="text_pole" data-setting-key="sybdApiMode" data-type="string">
@@ -116,11 +108,19 @@
</div>
</div>
<!-- 上下文设置面板 -->
<!-- 世界书条目面板 -->
<div id="glossary-content-context" class="glossary-content">
<div class="settings-group">
<div class="legend"><i class="fas fa-book-open"></i> 待开发</div>
<p style="text-align: center; margin-top: 20px;">待开发</p>
<div class="legend"><i class="fas fa-book-open"></i> 世界书条目预览</div>
<small class="notes" style="text-align: center; display: block; margin-bottom: 15px;">
此处将显示在“小说处理”标签页中选定世界书的条目。
</small>
<div class="amily2-glossary-toolbar" style="text-align: center; margin-bottom: 15px;">
</div>
<div id="world-book-entries-display" class="world-book-entries-container">
<!-- 条目将在这里动态生成 -->
</div>
</div>
</div>
@@ -129,53 +129,51 @@
<div class="settings-group">
<div class="legend"><i class="fas fa-file-upload"></i> 小说文件处理流程</div>
<div class="control-group" style="display: flex; flex-direction: column; align-items: center; gap: 10px;">
<label for="novel-file-input" class="menu_button secondary small_button interactable" style="cursor: pointer;">
<i class="fas fa-upload"></i> 1. 上传小说文件 (.txt)
<!-- 使用 horizontal-group 来确保标签和控件水平对齐 -->
<div class="control-group horizontal-group">
<label for="novel-world-book-select">1. 选择目标世界书:</label>
<select id="novel-world-book-select" class="text_pole"></select>
</div>
<!-- 为上传按钮使用独立的容器,不再使用 control-group -->
<div class="upload-button-container">
<label for="novel-file-input" class="menu_button secondary interactable">
<i class="fas fa-upload"></i> 2. 上传小说文件 (.txt)
</label>
<input type="file" id="novel-file-input" accept=".txt" style="display: none;">
</div>
<div class="control-group">
<label for="novel-chapter-regex">2. 章节识别规则 (高级, 可选):</label>
<input type="text" id="novel-chapter-regex" class="text_pole" placeholder="默认支持: 第1章, 1. , Chapter 1, 序章等多种格式">
<div class="control-group horizontal-group">
<label for="novel-chunk-size">3. 每批处理字符数:</label>
<input type="number" id="novel-chunk-size" class="text_pole" value="300000" min="1000" step="500" data-setting-key="novelChunkSize" data-type="integer">
</div>
<div class="sybd-button-row" style="display: flex; gap: 10px; justify-content: center; margin-top: 15px;">
<button id="novel-recognize-chapters" class="menu_button secondary small_button interactable">
<i class="fas fa-search"></i> ① 识别章节
</button>
</div>
<hr class="header-divider">
<hr class="header-divider" style="margin-top: 20px; margin-bottom: 20px;">
<div class="control-group">
<label>3. 章节预览 (共 <span id="novel-chapter-count">0</span> 章):</label>
<div id="novel-chapter-preview" style="height: 150px; overflow-y: auto; border: 1px solid #444; padding: 10px; background-color: #2e2e2e; border-radius: 5px;">
<small>请先上传文件并识别章节...</small>
<!-- 为预览区域使用独立的容器 -->
<div class="preview-container">
<label>4. 内容分块预览 (共 <span id="novel-chunk-count">0</span> 块):</label>
<div id="novel-chunk-preview">
<small>请先上传文件...</small>
</div>
</div>
<div class="control-group">
<label for="novel-batch-size">4. 每批处理章节数:</label>
<input type="number" id="novel-batch-size" class="text_pole" value="10" min="1">
</div>
<div class="amily2_settings_block" style="justify-content: center;">
<!-- 开关按钮使用 amily2_settings_block但CSS会对其进行特殊处理 -->
<div class="amily2_settings_block">
<label for="novel-force-new">强制新建条目 (不更新现有条目)</label>
<label class="toggle-switch">
<label class="amily2-glossary-toggle">
<input id="novel-force-new" type="checkbox" />
<span class="slider"></span>
</label>
</div>
<div class="sybd-button-row" style="display: flex; gap: 10px; justify-content: center; margin-top: 15px;">
<button id="novel-confirm-and-process" class="menu_button primary small_button interactable" disabled>
<i class="fas fa-play-circle"></i> 确认并开始处理
<div class="sybd-button-row">
<button id="novel-confirm-and-process" class="menu_button primary interactable" disabled>
<i class="fas fa-play-circle"></i> 确认并开始处理
</button>
</div>
<div id="novel-process-status" style="text-align: center; margin-top: 20px; font-weight: bold;">
<div id="novel-process-status" style="text-align: center; margin-top: 10px; font-weight: bold;">
等待操作...
</div>
</div>

View File

@@ -135,7 +135,7 @@
<div class="button-group" style="display: flex; justify-content: space-between; gap: 8px;">
<button id="amily2_open_plot_optimization" class="menu_button wide_button"><i class="fas fa-feather-alt"></i> 剧情优化</button>
<button id="amily2_open_world_editor" class="menu_button wide_button"><i class="fas fa-globe"></i> 世界编辑</button>
<button id="amily2_open_glossary" class="menu_button wide_button" disabled><i class="fas fa-book"></i> 术语表单</button>
<button id="amily2_open_glossary" class="menu_button wide_button"><i class="fas fa-book"></i> 术语表单</button>
</div>
</fieldset>

View File

@@ -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 = {};

View File

@@ -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 = '<p style="text-align:center;">请先在“小说处理”标签页中选择一个世界书。</p>';
return;
}
container.innerHTML = '<p style="text-align:center;"><i class="fas fa-spinner fa-spin"></i> 正在加载条目...</p>';
try {
const { TavernHelper } = window;
if (!TavernHelper) {
container.innerHTML = '<p style="text-align:center; color: #ff8a8a;">TavernHelper 未找到!</p>';
return;
}
const allEntries = await TavernHelper.getLorebookEntries(selectedBook);
let managedEntries = allEntries.filter(e => e.comment?.startsWith('[Amily2小说处理]'));
if (managedEntries.length === 0) {
container.innerHTML = '<p style="text-align:center;">未找到由小说处理功能生成的条目。</p>';
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 = '<i>(直接关联)</i>';
}
}
}
if (source && target) {
body += `<tr><td>${source.trim()}</td><td>${rel.trim()}</td><td>${target.trim().replace(';','')}</td></tr>`;
}
});
return `<table class="table-render"><thead><tr><th>源头</th><th>关系</th><th>目标</th></tr></thead><tbody>${body}</tbody></table>`;
} catch {
return `<pre>${content}</pre>`;
}
}
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 => `<td>${cell.trim()}</td>`).join('');
if (isHeaderRow) {
header += `<tr>${cells.replace(/<td>/g, '<th>').replace(/<\/td>/g, '</th>')}</tr>`;
isHeaderRow = false;
} else {
body += `<tr>${cells}</tr>`;
}
});
return `<table class="table-render"><thead>${header}</thead><tbody>${body}</tbody></table>`;
} catch {
return `<pre>${content}</pre>`;
}
}
return `<pre>${content}</pre>`;
};
entryElement.innerHTML = `
<div class="entry-header">
<strong class="entry-title">${title}</strong>
<div class="entry-actions">
<button class="menu_button primary small_button save-entry-btn" style="display: none;"><i class="fas fa-save"></i> 保存</button>
<button class="menu_button danger small_button cancel-entry-btn" style="display: none;"><i class="fas fa-times"></i> 取消</button>
<button class="menu_button secondary small_button edit-entry-btn"><i class="fas fa-edit"></i> 编辑</button>
</div>
</div>
<div class="entry-content-display">${renderContent(entry.content)}</div>
<div class="entry-content-editor" style="display: none;">
<textarea class="text_pole" style="width: 98%; min-height: 150px;">${entry.content}</textarea>
</div>
`;
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 = `<p style="text-align:center; color: #ff8a8a;">加载失败: ${error.message}</p>`;
}
}
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,8 +406,104 @@ 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) =>
`<div class="chunk-preview-item"><b>块 ${index + 1}:</b> ${chunk.content.substring(0, 100)}...</div>`
).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 = '<i class="fas fa-stop-circle"></i> 请求中止';
processBtn.classList.add('danger');
} else {
processBtn.classList.remove('danger');
switch (processingState.lastStatus) {
case 'paused':
processBtn.innerHTML = '<i class="fas fa-play"></i> 继续处理';
processBtn.disabled = false;
break;
case 'failed':
processBtn.innerHTML = '<i class="fas fa-redo"></i> 重试处理';
processBtn.disabled = false;
break;
case 'success':
processBtn.innerHTML = '<i class="fas fa-check"></i> 处理完成';
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) => {
@@ -239,37 +511,85 @@ function bindNovelProcessEvents() {
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 = '<i class="fas fa-spinner fa-spin"></i> 识别中...';
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 = '<i class="fas fa-spinner fa-spin"></i> 处理中...';
await processNovel();
processBtn.disabled = false;
processBtn.innerHTML = originalHtml;
if (processingState.isRunning) {
processingState.isAborted = true;
processBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 正在中止...';
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 = '<option value="">-- 请选择世界书 --</option>';
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 = '<option value="">未找到世界书</option>';
}
} catch (error) {
console.error('[Amily2-术语表] 加载世界书失败:', error);
select.innerHTML = '<option value="">加载失败</option>';
}
}
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事件绑定完成 (最终重构版)。');

View File

@@ -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}`);
}
await syncNovelLorebookEntries(bookName, structuredData);
existingEntriesContent = response;
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 });
}
}
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;
}
}

View File

@@ -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 = '<small>请先上传文件并识别章节...</small>';
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) => `<div>${index + 1}. ${chap.title}</div>`).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;
}
}

View File

@@ -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 @@