mirror of
https://github.com/SilenceLurker/ST-Amily2-Chat-Optimisation.git
synced 2026-06-06 13:35:51 +00:00
Merge branch 'Wx-2025:main' into main
This commit is contained in:
@@ -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 = {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"message": "插件群:1060183271,v1.5.2超级更新(新功能与优化多多喔~),祝大家假期玩的开心。个人原因,降低更新频率以及无暇看帖子,有问题最好加群。"
|
||||
"message": "插件群:1060183271,更新了多个版本了,现在是v1.5.7,术语表上线。个人原因,降低更新频率以及无暇看帖子,有问题最好加群。"
|
||||
}
|
||||
|
||||
|
||||
@@ -42,5 +42,6 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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; /* 开关不拉伸 */
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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 = {};
|
||||
|
||||
@@ -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,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) =>
|
||||
`<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) => {
|
||||
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 = '<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事件绑定完成 (最终重构版)。');
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user