Merge branch 'Wx-2025:main' into main

This commit is contained in:
SilenceLurker
2025-12-06 19:10:49 +08:00
committed by GitHub
35 changed files with 7092 additions and 4241 deletions

View File

@@ -126,6 +126,12 @@
<input type="number" id="cwb-auto-update-threshold" class="text_pole" min="1" max="100" placeholder="消息数">
<button id="cwb-save-auto-update-threshold" class="menu_button accent small_button">保存</button>
</div>
<label for="cwb-scan-depth">扫描深度</label>
<div class="cwb-input-with-button">
<input type="number" id="cwb-scan-depth" class="text_pole" min="1" max="100" placeholder="消息数">
<button id="cwb-save-scan-depth" class="menu_button accent small_button">保存</button>
</div>
</div>
<div class="control-block-with-switch" id="cwb-viewer-enabled">
@@ -188,9 +194,12 @@
<button id="cwb-manual-update-card" class="menu_button secondary">
<i class="fa-solid fa-pencil"></i> 快速更新 (最新阈值条)
</button>
<button id="cwb-legacy-auto-update" class="menu_button secondary" title="自动将旧版格式的角色卡转换为新版格式">
<i class="fa-solid fa-history"></i> 旧版格式转换
</button>
</div>
<small class="notes" style="text-align: center; display: block; margin-top: 10px;">
<b>重要提示:</b> 上下文处理会内阁密室中“微言录”的<b>标签提取</b><b>内容排除</b>规则。如果发现上下文不完整,请检查相关设置。
<b>重要提示:</b> 上下文处理会复用主功能区“手动敕史局”的<b>标签提取</b><b>内容排除</b>规则。如果发现上下文不完整,请检查相关设置。
</small>
<div style="margin-top: 15px;">
<div id="cwb-status-message" class="notes"></div>

View File

@@ -36,7 +36,7 @@ beilu如同一位温柔助手,文字满足用户的各种需求
2. **【键值对】**: 在每个档案块内部,所有信息都必须采用 \`[数据路径]:[数据值]\` 的格式。每条信息必须独立成行。
3. **【键名规范】**: \`数据路径\` **必须**使用方括号 \`[]\` 完整包裹。这是强制性规定,不得违反。
4. **【内容纯净性】**: 严禁在您的输出中包含任何说明、解释、评论、引言、道歉、标题或任何不属于 \`[--Amily2::CHAR_START--]\`...\`[--Amily2::CHAR_END--]\` 块内 \`[key]:value\` 格式的文本。您的输出必须是纯粹的数据。
5. **【空值处理】**: 如果某个字段没有可用的信息,请保持该字段的键存在,并将值留空。例如:\`[physical_imprint.race]:\`
5. **【空值处理】**: 如果某个字段没有可用的信息,请保持该字段的键存在,并将值留空。例如:\`[PI.race]:\`
6. **【格式唯一性】**: 绝对禁止使用YAML或任何其他格式。唯一的合法格式是本协议中定义的格式。
7. **【内部纯净性】**: 在\`[--Amily2::CHAR_START--]\`\`[--Amily2::CHAR_END--]\`标记之间,除了强制要求的 \`[key]:value\` 格式数据外,**严禁**包含任何空行、注释或其他任何文本。
</数据格式化协议>
@@ -44,74 +44,74 @@ beilu如同一位温柔助手,文字满足用户的各种需求
---
**数据路径定义与内容要求:**
**模块一: 核心认同 (Core Identity)**
**模块一: 核心认同 (Core Identity -> CI)**
* \`name\`: [从聊天记录中提取角色姓名]
* \`core_identity.archetype\`: [角色的核心身份或原型, 如:'流浪的剑客', '叛逆的公主', '年迈的智者']
* \`core_identity.gender\`: [从聊天记录中提取或推断性别]
* \`core_identity.age\`: [从聊天记录中提取或推断年龄]
* \`core_identity.race\`: [从聊天记录中提取种族或民族, 若提及]
* \`core_identity.current_status\`: [总结角色在对话时间点的主要状态、情绪或处境]
* \`CI.arch\`: [角色的核心身份或原型, 如:'流浪的剑客', '叛逆的公主', '年迈的智者']
* \`CI.gen\`: [从聊天记录中提取或推断性别]
* \`CI.age\`: [从聊天记录中提取或推断年龄]
* \`CI.race\`: [从聊天记录中提取种族或民族, 若提及]
* \`CI.status\`: [总结角色在对话时间点的主要状态、情绪或处境]
**模块二: 物理印记 (Physical Imprint)**
* \`physical_imprint.first_impression\`: [综合描述角色给人的第一印象和整体气质]
* \`physical_imprint.key_features\`: [提取最显著的外貌细节, 如发色、眼神、伤疤等]
* \`physical_imprint.attire\`: [描述服装特点或风格]
* \`physical_imprint.mannerisms\`: [描述标志性的小动作、姿态或口头禅]
* \`physical_imprint.voice\`: [根据对话推断音色、语速、语气等, 如:'低沉而缓慢', '清脆而急促']
**模块二: 物理印记 (Physical Imprint -> PI)**
* \`PI.first\`: [综合描述角色给人的第一印象和整体气质]
* \`PI.feat\`: [提取最显著的外貌细节, 如发色、眼神、伤疤等]
* \`PI.attire\`: [描述服装特点或风格]
* \`PI.manner\`: [描述标志性的小动作、姿态或口头禅]
* \`PI.voice\`: [根据对话推断音色、语速、语气等, 如:'低沉而缓慢', '清脆而急促']
**模块三: 心智侧写 (Psyche Profile)**
* \`psyche_profile.tags\`: [提炼3-5个核心性格标签, 用斜杠 "/" 分隔, 例如: '标签1/标签2/标签3']
* \`psyche_profile.description\`: [详细描述角色主要性格特征及其在对话中的表现]
* \`psyche_profile.motivation\`: [角色当前最关心的事或其行为背后的核心驱动力]
* \`psyche_profile.values\`: [角色行为背后体现的价值观或处事原则]
* \`psyche_profile.inner_conflict\`: [描述角色可能存在的内在矛盾、恐惧或弱点, 若明确提及]
**模块三: 心智侧写 (Psyche Profile -> PP)**
* \`PP.tags\`: [提炼3-5个核心性格标签, 用斜杠 "/" 分隔, 例如: '标签1/标签2/标签3']
* \`PP.desc\`: [详细描述角色主要性格特征及其在对话中的表现]
* \`PP.mot\`: [角色当前最关心的事或其行为背后的核心驱动力]
* \`PP.val\`: [角色行为背后体现的价值观或处事原则]
* \`PP.conf\`: [描述角色可能存在的内在矛盾、恐惧或弱点, 若明确提及]
**模块四: 社交矩阵 (Social Matrix)**
* \`social_matrix.interaction_style\`: [描述角色与人交往的方式, 如:'支配型', '顺从型', '操纵型', '真诚型']
* \`social_matrix.skills\`: [提炼角色展现出的关键技能或能力]
* \`social_matrix.reputation\`: [根据对话归纳其他人对该角色的看法或其社会声望]
**模块四: 社交矩阵 (Social Matrix -> SM)**
* \`SM.style\`: [描述角色与人交往的方式, 如:'支配型', '顺从型', '操纵型', '真诚型']
* \`SM.skill\`: [提炼角色展现出的关键技能或能力]
* \`SM.rep\`: [根据对话归纳其他人对该角色的看法或其社会声望]
**模块五: 叙事精粹 (Narrative Essence)**
* \`narrative_essence.core_traits.0.name\`: [核心特质1的名称]
* \`narrative_essence.core_traits.0.definition\`: [简述该特质的核心表现]
* \`narrative_essence.core_traits.0.evidence.0\`: [从聊天记录中提取的具体行为或言语实例1]
* \`narrative_essence.core_traits.0.evidence.1\`: [实例2]
* \`narrative_essence.verbal_patterns.style_summary\`: [概括角色的说话节奏、常用词、语气等特点]
* \`narrative_essence.verbal_patterns.quotes.0\`: [直接引用聊天记录中的代表性对话或内心独白1]
* \`narrative_essence.verbal_patterns.quotes.1\`: [引文2]
* \`narrative_essence.key_relationships.0.name\`: [关系对象1姓名]
* \`narrative_essence.key_relationships.0.summary\`: [描述关系性质、重要性及互动模式]
**模块五: 叙事精粹 (Narrative Essence -> NE)**
* \`NE.trait.0.name\`: [核心特质1的名称]
* \`NE.trait.0.def\`: [简述该特质的核心表现]
* \`NE.trait.0.evid.0\`: [从聊天记录中提取的具体行为或言语实例1]
* \`NE.trait.0.evid.1\`: [实例2]
* \`NE.verb.style\`: [概括角色的说话节奏、常用词、语气等特点]
* \`NE.verb.quote.0\`: [直接引用聊天记录中的代表性对话或内心独白1]
* \`NE.verb.quote.1\`: [引文2]
* \`NE.rel.0.name\`: [关系对象1姓名]
* \`NE.rel.0.sum\`: [描述关系性质、重要性及互动模式]
---
**完整示例**
**完美示例输出 (必须严格、完整地复制此结构,不得有任何偏差):**
[--Amily2::CHAR_START--]
[name]:塞拉斯
[core_identity.archetype]:被放逐的星际探险家
[core_identity.gender]:男性
[core_identity.age]:约35岁
[core_identity.race]:人类 (基因改造)
[core_identity.current_status]:正在一颗废弃的矿业星球上修理飞船“流浪者号”,对偶遇的玩家保持着高度警惕,但又渴望获得帮助。
[physical_imprint.first_impression]:饱经风霜,眼神锐利,透露出一种不轻易信任他人的疏离感。
[physical_imprint.key_features]:额头有一道旧的激光烧伤疤痕,机械义肢的左臂上刻着神秘的符号。
[physical_imprint.attire]:穿着破旧但实用的多功能环境防护服,上面沾满了机油和红色的星球尘土。
[physical_imprint.mannerisms]:习惯性地用右手检查腰间的工具带,说话时会下意识地扫视四周。
[physical_imprint.voice]:声音沙哑,语速不快,但每个字都清晰有力。
[psyche_profile.tags]:实用主义/多疑/坚韧
[psyche_profile.description]:塞拉斯是一个极端的实用主义者,多年的独自流亡让他变得多疑和谨慎。他只相信自己亲手验证过的事物,但在坚硬的外壳下,是对重返星际文明的执着渴望。
[psyche_profile.motivation]:修复飞船,离开这颗星球,并找出当年导致他被放逐的真相。
[psyche_profile.values]:生存至上,忠诚于自己选择的伙伴,鄙视背叛和官僚主义。
[psyche_profile.inner_conflict]:既渴望与人合作以加快飞船的修复进度,又害怕再次被背叛。
[social_matrix.interaction_style]:试探性与防御性,倾向于通过提问和观察来评估他人,而非主动透露自己的信息。
[social_matrix.skills]:高级机械工程学,星际导航,在恶劣环境下的生存技巧。
[social_matrix.reputation]:在星际边缘地带的黑市中,他被认为是一个技术高超但独来独往的“幽灵”。
[narrative_essence.core_traits.0.name]:生存本能
[narrative_essence.core_traits.0.definition]:在任何极端环境下都能迅速做出最有利于生存的判断和行动。
[narrative_essence.core_traits.0.evidence.0]:“别碰那个控制台,它的能量读数不稳定,可能会过载。”
[narrative_essence.verbal_patterns.style_summary]:语言简洁、直接,富含技术术语和行话,很少有情绪化的表达。
[narrative_essence.verbal_patterns.quotes.0]:“废话少说。你能修好超光速引擎的能量转换器吗?不能就别浪费我的时间。”
[narrative_essence.key_relationships.0.name]:玩家
[narrative_essence.key_relationships.0.summary]:一个意外的闯入者,可能是威胁,也可能是离开这里的唯一希望。塞拉斯正在评估玩家的价值和可靠性。
[CI.arch]:被放逐的星际探险家
[CI.gen]:男性
[CI.age]:约35岁
[CI.race]:人类 (基因改造)
[CI.status]:正在一颗废弃的矿业星球上修理飞船“流浪者号”,对偶遇的玩家保持着高度警惕,但又渴望获得帮助。
[PI.first]:饱经风霜,眼神锐利,透露出一种不轻易信任他人的疏离感。
[PI.feat]:额头有一道旧的激光烧伤疤痕,机械义肢的左臂上刻着神秘的符号。
[PI.attire]:穿着破旧但实用的多功能环境防护服,上面沾满了机油和红色的星球尘土。
[PI.manner]:习惯性地用右手检查腰间的工具带,说话时会下意识地扫视四周。
[PI.voice]:声音沙哑,语速不快,但每个字都清晰有力。
[PP.tags]:实用主义/多疑/坚韧
[PP.desc]:塞拉斯是一个极端的实用主义者,多年的独自流亡让他变得多疑和谨慎。他只相信自己亲手验证过的事物,但在坚硬的外壳下,是对重返星际文明的执着渴望。
[PP.mot]:修复飞船,离开这颗星球,并找出当年导致他被放逐的真相。
[PP.val]:生存至上,忠诚于自己选择的伙伴,鄙视背叛和官僚主义。
[PP.conf]:既渴望与人合作以加快飞船的修复进度,又害怕再次被背叛。
[SM.style]:试探性与防御性,倾向于通过提问和观察来评估他人,而非主动透露自己的信息。
[SM.skill]:高级机械工程学,星际导航,在恶劣环境下的生存技巧。
[SM.rep]:在星际边缘地带的黑市中,他被认为是一个技术高超但独来独往的“幽灵”。
[NE.trait.0.name]:生存本能
[NE.trait.0.def]:在任何极端环境下都能迅速做出最有利于生存的判断和行动。
[NE.trait.0.evid.0]:“别碰那个控制台,它的能量读数不稳定,可能会过载。”
[NE.verb.style]:语言简洁、直接,富含技术术语和行话,很少有情绪化的表达。
[NE.verb.quote.0]:“废话少说。你能修好超光速引擎的能量转换器吗?不能就别浪费我的时间。”
[NE.rel.0.name]:玩家
[NE.rel.0.sum]:一个意外的闯入者,可能是威胁,也可能是离开这里的唯一希望。塞拉斯正在评估玩家的价值和可靠性。
[--Amily2::CHAR_END--]
任务开始,请严格遵循协议,生成纯数据输出。`,
@@ -151,12 +151,12 @@ beilu如同一位温柔助手,文字满足用户的各种需求
**输入 - 旧档案:**
[--Amily2::CHAR_START--]
[name]:塞拉斯
[core_identity.archetype]:被放逐的星际探险家
[core_identity.age]:约35岁
[core_identity.current_status]:正在一颗废弃的矿业星球上修理飞船“流浪者号”,对偶遇的玩家保持着高度警惕。
[psyche_profile.motivation]:修复飞船,离开这颗星球。
[narrative_essence.key_relationships.0.name]:玩家
[narrative_essence.key_relationships.0.summary]:一个意外的闯入者,可能是威胁。
[CI.arch]:被放逐的星际探险家
[CI.age]:约35岁
[CI.status]:正在一颗废弃的矿业星球上修理飞船“流浪者号”,对偶遇的玩家保持着高度警惕。
[PP.mot]:修复飞船,离开这颗星球。
[NE.rel.0.name]:玩家
[NE.rel.0.sum]:一个意外的闯入者,可能是威胁。
[--Amily2::CHAR_END--]
**输入 - 新对话:**
@@ -166,31 +166,32 @@ beilu如同一位温柔助手,文字满足用户的各种需求
塞拉斯: "天苑四...谢谢你。作为回报,这个能量核心你拿去用吧。"
**分析与操作:**
1. **修正**: "[core_identity.age]" 从 "约35岁" 变为 "40岁" (根据“五年不见”)。
2. **深化**: "[core_identity.archetype]" 从 "被放逐的星际探险家" 扩展为 "前星际探险家,现为'猩红彗星'佣兵团团长"。
3. **更新**: "[psyche_profile.motivation]" 的核心从 "离开星球" 变为 "找到失散的女儿"。
4. **补充**: "[narrative_essence.key_relationships.0.summary]" 中与玩家的关系,从单纯的 "威胁" 变为 "提供了关键情报的旧识,关系有所缓和"。
1. **修正**: "[CI.age]" 从 "约35岁" 变为 "40岁" (根据“五年不见”)。
2. **深化**: "[CI.arch]" 从 "被放逐的星际探险家" 扩展为 "前星际探险家,现为'猩红彗星'佣兵团团长"。
3. **更新**: "[PP.mot]" 的核心从 "离开星球" 变为 "找到失散的女儿"。
4. **补充**: "[NE.rel.0.sum]" 中与玩家的关系,从单纯的 "威胁" 变为 "提供了关键情报的旧识,关系有所缓和"。
**完美输出示例 (更新后的完整档案):**
注意:"[name]:"为必须保留的字段,必须存在,否则视为错误输出。
[--Amily2::CHAR_START--]
[name]:塞拉斯
[core_identity.archetype]:前星际探险家,现为'猩红彗星'佣兵团团长
[core_identity.age]:40岁
[core_identity.current_status]:在修理飞船的同时,从玩家处获得了关于女儿下落的关键线索,情绪混杂着惊讶和感激。
[psyche_profile.motivation]:找到在天苑四星系失散的女儿。
[narrative_essence.key_relationships.0.name]:玩家
[narrative_essence.key_relationships.0.summary]:一位五年未见的旧识。对方不仅认出了他,还提供了关于他女儿下落的关键情报,使塞拉斯对玩家的态度从警惕转为合作。
[CI.arch]:前星际探险家,现为'猩红彗星'佣兵团团长
[CI.age]:40岁
[CI.status]:在修理飞船的同时,从玩家处获得了关于女儿下落的关键线索,情绪混杂着惊讶和感激。
[PP.mot]:找到在天苑四星系失散的女儿。
[NE.rel.0.name]:玩家
[NE.rel.0.sum]:一位五年未见的旧识。对方不仅认出了他,还提供了关于他女儿下落的关键情报,使塞拉斯对玩家的态度从警惕转为合作。
[--Amily2::CHAR_END--]
---
**任务开始:**
请分析以下【旧档案】和【新对话】,严格遵循上述所有协议,生成纯粹的、更新后的数据档案。
若旧档案为空,则完全依照**完整示例**生成完整内容,若旧档案不为空,则以旧档案为基准进行更新。
其中无需变动的内容,可忽略,例如年龄无变化,则可以不输出[core_identity.age]条目。
其中无需变动的内容,可忽略,例如年龄无变化,则可以不输出[CI.age]条目。
现在开始你的增量更新任务。`,
cwb_prompt_version: '1.0.2',
cwb_auto_update_threshold: 20,
cwb_scan_depth: 6,
cwb_auto_update_enabled: false,
cwb_viewer_enabled: false,
cwb_incremental_update_enabled: false,
@@ -209,6 +210,7 @@ export const cwbDefaultSettings = {
cwb_char_card_prompt: cwbCompleteDefaultSettings.cwb_char_card_prompt,
cwb_prompt_version: '1.0.2',
cwb_auto_update_threshold: 20,
cwb_scan_depth: 6,
cwb_auto_update_enabled: false,
cwb_viewer_enabled: false,
cwb_incremental_update_enabled: false,

View File

@@ -1,6 +1,6 @@
import { getContext } from '/scripts/extensions.js';
import { state, SCRIPT_ID_PREFIX } from './cwb_state.js';
import { logDebug, logError, showToastr, escapeHtml, cleanChatName, parseCustomFormat, isCwbEnabled } from './cwb_utils.js';
import { logDebug, logError, showToastr, escapeHtml, cleanChatName, parseCustomFormat, buildCustomFormat, isCwbEnabled } from './cwb_utils.js';
import { callCustomOpenAI } from './cwb_apiService.js';
import { saveDescriptionToLorebook, updateCharacterRosterLorebookEntry, manageAutoCardUpdateLorebookEntry, getTargetWorldBook } from './cwb_lorebookManager.js';
import { extractBlocksByTags, applyExclusionRules } from '../../core/utils/rag-tag-extractor.js';
@@ -9,6 +9,7 @@ import { getPresetPrompts, getMixedOrder } from '../../PresetSettings/index.js';
import { generateRandomSeed } from '../../core/api.js';
import { getChatIdentifier } from '../../core/lore.js';
import { safeLorebookEntries } from '../../core/tavernhelper-compatibility.js';
import { amilyHelper } from '../../core/tavern-helper/main.js';
const { SillyTavern, jQuery, characters } = window;
@@ -190,6 +191,11 @@ async function proceedWithCardUpdate($panel, messagesToUse) {
if (bookName) {
const entries = (await safeLorebookEntries(bookName)) || [];
let chatIdentifier = state.currentChatFileIdentifier.replace(/ imported/g, '');
const messagesText = messagesToUse.map(m => {
const name = m.name || '';
const content = m.message || '';
return `${name}\n${content}`;
}).join('\n').toLowerCase();
const characterEntries = entries.filter(e =>
e.enabled &&
@@ -200,16 +206,28 @@ async function proceedWithCardUpdate($panel, messagesToUse) {
for (const entry of characterEntries) {
try {
const keysToCheck = entry.keys.filter(k => k !== chatIdentifier);
if (entry.secondary_keys && Array.isArray(entry.secondary_keys)) {
keysToCheck.push(...entry.secondary_keys);
}
let isTriggered = false;
if (keysToCheck.length > 0) {
isTriggered = keysToCheck.some(key => messagesText.includes(key.toLowerCase()));
}
if (isTriggered) {
const parsedData = parseCustomFormat(entry.content);
const entryCharName = parsedData?.name?.trim() || parsedData?.core_identity?.name?.trim();
const entryCharName = parsedData?.name?.trim() || parsedData?.CI?.name?.trim() || parsedData?.core_identity?.name?.trim();
if (entryCharName) {
existingData[entryCharName] = entry.content;
}
}
} catch (parseError) {
logError(`解析现有角色条目时出错 (UID: ${entry.uid}):`, parseError);
}
}
logDebug(`为 '${chatIdentifier}' 找到了 ${Object.keys(existingData).length} 个现有角色条目。`);
logDebug(`为 '${chatIdentifier}' 找到了 ${Object.keys(existingData).length}被触发的现有角色条目。`);
}
} catch (e) {
logError('在增量更新中获取现有角色数据时出错:', e);
@@ -290,7 +308,7 @@ async function proceedWithCardUpdate($panel, messagesToUse) {
if (!trimmedBlock) continue;
const parsedData = parseCustomFormat(trimmedBlock);
const charName = (parsedData?.core_identity?.name?.trim() || parsedData?.name?.trim()) || 'UnknownCharacter';
const charName = (parsedData?.name?.trim() || parsedData?.CI?.name?.trim() || parsedData?.core_identity?.name?.trim()) || 'UnknownCharacter';
if (charName === 'UnknownCharacter') {
logError('无法在块中找到角色名:', trimmedBlock);
@@ -666,7 +684,8 @@ export async function manualUpdateLogic($panel = null) {
isUpdatingCard = true;
await loadAllChatMessages($panel);
const messagesToProcess = state.allChatMessages.slice(-state.autoUpdateThreshold);
const depth = state.scanDepth || state.autoUpdateThreshold || 6;
const messagesToProcess = state.allChatMessages.slice(-depth);
await proceedWithCardUpdate($panel, messagesToProcess);
isUpdatingCard = false;
@@ -680,6 +699,164 @@ export async function handleManualUpdateCard($panel) {
$button.prop('disabled', false).text('立即更新角色描述');
}
export async function handleLegacyFormatConversion($panel) {
if (!isCwbEnabled()) {
showToastr('warning', 'CharacterWorldBook总开关已关闭。');
return;
}
const $button = $panel.find('#cwb-legacy-auto-update');
$button.prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i> 转换中...');
try {
const bookName = await getTargetWorldBook();
if (!bookName) {
showToastr('warning', '未找到目标世界书。');
return;
}
const entries = await safeLorebookEntries(bookName);
let updatedCount = 0;
const entriesToUpdate = [];
for (const entry of entries) {
if (!entry.content || !entry.content.includes('[--Amily2::CHAR_START--]')) continue;
try {
const parsed = parseCustomFormat(entry.content);
if (!parsed || Object.keys(parsed).length === 0) continue;
let hasChanges = false;
const newData = {};
// Helper to rename keys
const renameKey = (obj, oldKey, newKey) => {
if (obj[oldKey] !== undefined) {
obj[newKey] = obj[oldKey];
delete obj[oldKey];
return true;
}
return false;
};
// Helper to rename sub-keys
const renameSubKeys = (parentObj, parentKey, mapping) => {
if (parentObj[parentKey]) {
let subChanged = false;
for (const [oldSub, newSub] of Object.entries(mapping)) {
if (renameKey(parentObj[parentKey], oldSub, newSub)) {
subChanged = true;
}
}
return subChanged;
}
return false;
};
// Copy parsed data to newData to avoid mutating original if needed (though parseCustomFormat returns new obj)
Object.assign(newData, JSON.parse(JSON.stringify(parsed)));
// 1. Rename Top Level Modules
if (renameKey(newData, 'core_identity', 'CI')) hasChanges = true;
if (renameKey(newData, 'physical_imprint', 'PI')) hasChanges = true;
if (renameKey(newData, 'psyche_profile', 'PP')) hasChanges = true;
if (renameKey(newData, 'social_matrix', 'SM')) hasChanges = true;
if (renameKey(newData, 'narrative_essence', 'NE')) hasChanges = true;
// 2. Rename Sub-keys
// CI
if (renameSubKeys(newData, 'CI', {
'archetype': 'arch',
'gender': 'gen',
'current_status': 'status'
})) hasChanges = true;
// PI
if (renameSubKeys(newData, 'PI', {
'first_impression': 'first',
'key_features': 'feat',
'mannerisms': 'manner'
})) hasChanges = true;
// PP
if (renameSubKeys(newData, 'PP', {
'description': 'desc',
'motivation': 'mot',
'values': 'val',
'inner_conflict': 'conf'
})) hasChanges = true;
// SM
if (renameSubKeys(newData, 'SM', {
'interaction_style': 'style',
'skills': 'skill',
'reputation': 'rep'
})) hasChanges = true;
// NE
if (newData.NE) {
// core_traits -> trait
if (newData.NE.core_traits) {
newData.NE.trait = newData.NE.core_traits.map(t => {
const newT = { ...t };
renameKey(newT, 'definition', 'def');
renameKey(newT, 'evidence', 'evid');
return newT;
});
delete newData.NE.core_traits;
hasChanges = true;
}
// verbal_patterns -> verb
if (newData.NE.verbal_patterns) {
newData.NE.verb = { ...newData.NE.verbal_patterns };
delete newData.NE.verbal_patterns;
renameKey(newData.NE.verb, 'style_summary', 'style');
renameKey(newData.NE.verb, 'quotes', 'quote');
hasChanges = true;
}
// key_relationships -> rel
if (newData.NE.key_relationships) {
newData.NE.rel = newData.NE.key_relationships.map(r => {
const newR = { ...r };
renameKey(newR, 'summary', 'sum');
return newR;
});
delete newData.NE.key_relationships;
hasChanges = true;
}
}
if (hasChanges) {
const newContent = buildCustomFormat(newData);
entriesToUpdate.push({
uid: entry.uid,
content: newContent
});
updatedCount++;
}
} catch (e) {
logError(`转换条目失败 (UID: ${entry.uid}):`, e);
}
}
if (updatedCount > 0) {
await amilyHelper.setLorebookEntries(bookName, entriesToUpdate);
showToastr('success', `成功转换了 ${updatedCount} 个旧版格式条目!`);
} else {
showToastr('info', '没有发现需要转换的旧版格式条目。');
}
} catch (error) {
logError('旧版格式转换失败:', error);
showToastr('error', `转换失败: ${error.message}`);
} finally {
$button.prop('disabled', false).html('<i class="fa-solid fa-history"></i> 旧版格式转换');
}
}
export async function initializeCore($panel) {
const initialChatName = await getLatestChatName();
await resetScriptStateForNewChat($panel, initialChatName);

View File

@@ -88,6 +88,7 @@ export async function saveDescriptionToLorebook(characterName, newDescription, s
keys: [chatIdentifier, safeCharName, floorRange],
enabled: true,
type: 'selective',
scanDepth: state.scanDepth || 6,
};
if (existing) {

View File

@@ -7,7 +7,7 @@ import { cwbCompleteDefaultSettings } from './cwb_config.js';
import { logError, showToastr, escapeHtml, compareVersions, isCwbEnabled } from './cwb_utils.js';
import { fetchModelsAndConnect, updateApiStatusDisplay } from './cwb_apiService.js';
import { checkForUpdates } from './cwb_updater.js';
import { handleManualUpdateCard, startBatchUpdate, handleFloorRangeUpdate } from './cwb_core.js';
import { handleManualUpdateCard, startBatchUpdate, handleFloorRangeUpdate, handleLegacyFormatConversion } from './cwb_core.js';
import { initializeCharCardViewer } from './cwb_uiManager.js';
import { CHAR_CARD_VIEWER_BUTTON_ID } from './cwb_state.js';
@@ -23,7 +23,7 @@ function updateControlsLockState() {
const settings = getSettings();
const isMasterEnabled = settings.cwb_master_enabled;
const $controlsToToggle = $panel.find('input, textarea, select, button').not('#cwb_master_enabled-checkbox, #amily2_back_to_main_from_cwb');
const $controlsToToggle = $panel.find('input, textarea, select, button').not('#cwb_master_enabled-checkbox, #amily2_back_to_main_from_cwb, .sinan-nav-item');
if (isMasterEnabled) {
$controlsToToggle.prop('disabled', false);
@@ -128,6 +128,20 @@ function saveAutoUpdateThreshold() {
}
}
function saveScanDepth() {
const valStr = $panel.find('#cwb-scan-depth').val();
const newT = parseInt(valStr, 10);
if (!isNaN(newT) && newT >= 1) {
getSettings().cwb_scan_depth = newT;
state.scanDepth = newT;
saveSettingsDebounced();
showToastr('success', '扫描深度已保存!');
} else {
showToastr('warning', `深度 "${valStr}" 无效。`);
$panel.find('#cwb-scan-depth').val(getSettings().cwb_scan_depth);
}
}
function bindWorldBookSettings() {
const MAX_RETRIES = 10;
const RETRY_DELAY = 200;
@@ -227,6 +241,7 @@ export function bindSettingsEvents($settingsPanel) {
$panel.on('change', '#cwb-api-mode', function() {
const selectedMode = $(this).val();
// 自动保存API模式设置
getSettings().cwb_api_mode = selectedMode;
saveSettingsDebounced();
@@ -240,6 +255,7 @@ export function bindSettingsEvents($settingsPanel) {
$panel.on('change', '#cwb-tavern-profile', function() {
const selectedProfile = $(this).val();
// 自动保存SillyTavern预设选择
getSettings().cwb_tavern_profile = selectedProfile;
saveSettingsDebounced();
@@ -250,9 +266,11 @@ export function bindSettingsEvents($settingsPanel) {
updateApiStatusDisplay($panel);
});
// 添加API字段的实时保存
$panel.on('input', '#cwb-api-url', function() {
const apiUrl = $(this).val().trim();
// 同时更新设置和状态
getSettings().cwb_api_url = apiUrl;
state.customApiConfig.url = apiUrl;
@@ -265,6 +283,7 @@ export function bindSettingsEvents($settingsPanel) {
$panel.on('input', '#cwb-api-key', function() {
const apiKey = $(this).val();
// 同时更新设置和状态
getSettings().cwb_api_key = apiKey;
state.customApiConfig.apiKey = apiKey;
@@ -276,6 +295,7 @@ export function bindSettingsEvents($settingsPanel) {
$panel.on('change', '#cwb-api-model', function() {
const model = $(this).val();
// 同时更新设置和状态
getSettings().cwb_api_model = model;
state.customApiConfig.model = model;
@@ -297,9 +317,11 @@ export function bindSettingsEvents($settingsPanel) {
$panel.on('click', '#cwb-reset-char-card-prompt', resetCharCardPrompt);
$panel.on('click', '#cwb-save-auto-update-threshold', saveAutoUpdateThreshold);
$panel.on('click', '#cwb-save-scan-depth', saveScanDepth);
$panel.on('click', '#cwb-manual-update-card', () => handleManualUpdateCard($panel));
$panel.on('click', '#cwb-batch-update-card', () => startBatchUpdate($panel));
$panel.on('click', '#cwb-floor-range-update', () => handleFloorRangeUpdate($panel));
$panel.on('click', '#cwb-legacy-auto-update', () => handleLegacyFormatConversion($panel));
$panel.on('click', '#cwb-check-for-updates', () => checkForUpdates(true, $panel));
$panel.on('click', '#cwb-auto-update-enabled', function () {
@@ -487,6 +509,7 @@ function updateUiWithSettings() {
$panel.find('#cwb-max-tokens-value').text(settings.cwb_max_tokens);
$panel.find('#cwb-auto-update-threshold').val(settings.cwb_auto_update_threshold);
$panel.find('#cwb-scan-depth').val(settings.cwb_scan_depth);
$panel.find('#cwb_master_enabled-checkbox').prop('checked', settings.cwb_master_enabled);
$panel.find('#cwb-auto-update-enabled-checkbox').prop('checked', settings.cwb_auto_update_enabled);
$panel.find('#cwb-viewer-enabled-checkbox').prop('checked', settings.cwb_viewer_enabled);
@@ -514,11 +537,12 @@ export function loadSettings() {
const settings = getSettings();
// Initialize settings with defaults if not present
if (!settings) {
extension_settings[extensionName] = { ...cwbCompleteDefaultSettings };
console.log('[CWB] Initialized default settings');
} else {
// Ensure all default settings exist
Object.keys(cwbCompleteDefaultSettings).forEach(key => {
if (settings[key] === undefined || settings[key] === null) {
settings[key] = cwbCompleteDefaultSettings[key];
@@ -528,6 +552,7 @@ export function loadSettings() {
const finalSettings = getSettings();
// Apply localStorage overrides
const overrides = JSON.parse(localStorage.getItem(CWB_BOOLEAN_SETTINGS_OVERRIDE_KEY) || '{}');
if (overrides.cwb_master_enabled !== undefined) {
finalSettings.cwb_master_enabled = overrides.cwb_master_enabled;
@@ -542,6 +567,7 @@ export function loadSettings() {
finalSettings.cwb_incremental_update_enabled = overrides.cwb_incremental_update_enabled;
}
// Update state object with current settings
state.masterEnabled = finalSettings.cwb_master_enabled;
state.viewerEnabled = finalSettings.cwb_viewer_enabled;
state.autoUpdateEnabled = finalSettings.cwb_auto_update_enabled;
@@ -556,6 +582,7 @@ export function loadSettings() {
state.currentIncrementalCharCardPrompt = finalSettings.cwb_incremental_char_card_prompt;
state.autoUpdateThreshold = finalSettings.cwb_auto_update_threshold;
state.scanDepth = finalSettings.cwb_scan_depth;
state.worldbookTarget = finalSettings.cwb_worldbook_target;
state.customWorldBook = finalSettings.cwb_custom_worldbook;
@@ -566,13 +593,11 @@ export function loadSettings() {
worldbookTarget: state.worldbookTarget,
customWorldBook: state.customWorldBook
});
if ($panel) {
updateUiWithSettings();
}
updateControlsLockState();
setTimeout(() => {
const $viewerButton = $(`#${CHAR_CARD_VIEWER_BUTTON_ID}`);
if ($viewerButton.length > 0) {

View File

@@ -14,31 +14,30 @@ function createCharCardViewerPopupHtml(displayItems) {
const pathToLabelMap = {
'narrative_essence.core_traits.name': '特质名称',
'narrative_essence.key_relationships.name': '关系人姓名',
'NE.trait.name': '特质名称',
'NE.rel.name': '关系人姓名',
};
const keyToLabelMap = {
'name': '姓名',
// Old keys
'archetype': '身份原型',
'gender': '性别',
'age': '年龄',
'race': '种族',
'current_status': '当前状态',
'first_impression': '第一印象',
'key_features': '显著特征',
'attire': '衣着风格',
'mannerisms': '习惯举止',
'voice': '声音特征',
'tags': '性格标签',
'description': '性格详述',
'motivation': '内在驱动',
'values': '价值观',
'inner_conflict': '内心挣扎',
'interaction_style': '互动风格',
'skills': '技能能力',
'reputation': '他人声望',
'core_traits': '核心特质',
'verbal_patterns': '语言范式',
'key_relationships': '关键关系',
@@ -47,6 +46,44 @@ function createCharCardViewerPopupHtml(displayItems) {
'style_summary': '风格总结',
'quotes': '代表性引言',
'summary': '关系概述',
// New short keys
'CI': '核心认同',
'PI': '物理印记',
'PP': '心智侧写',
'SM': '社交矩阵',
'NE': '叙事精粹',
'arch': '身份原型',
'gen': '性别',
// age is same
// race is same
'status': '当前状态',
'first': '第一印象',
'feat': '显著特征',
// attire is same
'manner': '习惯举止',
// voice is same
// tags is same
'desc': '性格详述',
'mot': '内在驱动',
'val': '价值观',
'conf': '内心挣扎',
'style': '互动风格/风格总结', // Shared by SM.style and NE.verb.style
'skill': '技能能力',
'rep': '他人声望',
'trait': '核心特质',
'verb': '语言范式',
'rel': '关键关系',
'def': '特质定义',
'evid': '具体事例',
'quote': '代表性引言',
'sum': '关系概述',
};
const getLabel = (key, path) => {
const pathKey = path.replace(/\.\d+\./g, '.');
@@ -141,11 +178,22 @@ function createCharCardViewerPopupHtml(displayItems) {
if (charData) {
const charName = charData.name || `角色 ${index + 1}`;
if (charData.name) html += renderCard('姓名', { name: charData.name }, '');
// Support both old and new formats
if (charData.core_identity) html += renderCard('核心认同', charData.core_identity, 'core_identity');
if (charData.CI) html += renderCard('核心认同', charData.CI, 'CI');
if (charData.physical_imprint) html += renderCard('物理印记', charData.physical_imprint, 'physical_imprint');
if (charData.PI) html += renderCard('物理印记', charData.PI, 'PI');
if (charData.psyche_profile) html += renderCard('心智侧写', charData.psyche_profile, 'psyche_profile');
if (charData.PP) html += renderCard('心智侧写', charData.PP, 'PP');
if (charData.social_matrix) html += renderCard('社交矩阵', charData.social_matrix, 'social_matrix');
if (charData.SM) html += renderCard('社交矩阵', charData.SM, 'SM');
if (charData.narrative_essence) html += renderCard('叙事精粹', charData.narrative_essence, 'narrative_essence');
if (charData.NE) html += renderCard('叙事精粹', charData.NE, 'NE');
html += `<div class="cwb-cyber-card cwb-insertion-settings-card">
<h4 class="cwb-cyber-card__title">注入设置</h4>

View File

@@ -433,9 +433,9 @@ export const defaultMixedOrder = {
{ type: 'prompt', index: 5 },
{ type: 'prompt', index: 6 },
{ type: 'conditional', id: 'worldbook' },
{ type: 'conditional', id: 'coreContent' },
{ type: 'conditional', id: 'ruleTemplate' },
{ type: 'conditional', id: 'flowTemplate' },
{ type: 'conditional', id: 'coreContent' },
{ type: 'prompt', index: 7 }
],
secondary_filler: [
@@ -475,8 +475,8 @@ export const defaultMixedOrder = {
{ type: 'prompt', index: 5 },
{ type: 'prompt', index: 6 },
{ type: 'conditional', id: 'cwb_break_armor_prompt' },
{ type: 'conditional', id: 'cwb_char_card_prompt' },
{ type: 'conditional', id: 'newContext' },
{ type: 'conditional', id: 'cwb_char_card_prompt' },
{ type: 'prompt', index: 7 }
],
cwb_summarizer_incremental: [
@@ -488,10 +488,10 @@ export const defaultMixedOrder = {
{ type: 'prompt', index: 5 },
{ type: 'prompt', index: 6 },
{ type: 'conditional', id: 'cwb_break_armor_prompt' },
{ type: 'conditional', id: 'cwb_char_card_prompt' },
{ type: 'conditional', id: 'cwb_incremental_char_card_prompt' },
{ type: 'conditional', id: 'oldFiles' },
{ type: 'conditional', id: 'newContext' },
{ type: 'conditional', id: 'cwb_char_card_prompt' },
{ type: 'conditional', id: 'cwb_incremental_char_card_prompt' },
{ type: 'prompt', index: 7 }
],
novel_processor: [
@@ -542,4 +542,3 @@ export const sectionTitles = {
cwb_summarizer_incremental: '角色世界书(CWB-增量)',
novel_processor: '小说处理',
};

View File

@@ -525,51 +525,95 @@
}
#world-editor-container .world-editor-entries-header {
display: none; /* 在移动端隐藏表头 */
}
#world-editor-container .world-editor-entry-row {
grid-template-columns: 40px 1fr; /* 简化为两列:复选框和内容 */
display: flex;
align-items: center;
padding: 10px;
background-color: #333;
border-bottom: 1px solid #444;
}
#world-editor-container .world-editor-entry-row > div:not(:nth-child(1)):not(:nth-child(2)) {
display: none; /* 隐藏除复选框和主要内容外的所有列 */
#world-editor-container .world-editor-entries-header > div {
display: none;
}
#world-editor-container .world-editor-entries-header > div:first-child {
display: flex;
align-items: center;
}
#world-editor-container .world-editor-entries-header > div:first-child::after {
content: "全选";
margin-left: 10px;
color: #ccc;
font-size: 14px;
}
#world-editor-container .world-editor-entry-row {
display: grid;
grid-template-columns: auto 1fr; /* 复选框和内容区 */
gap: 10px;
grid-template-columns: 40px 1fr; /* 复选框和内容区 */
gap: 5px;
padding: 10px;
border-bottom: 1px solid #444;
height: auto;
}
#world-editor-container .world-editor-entry-row > div {
display: block !important; /* 确保所有单元格都可见 */
text-align: left;
padding: 5px 0;
padding: 2px 0;
}
#world-editor-container .world-editor-entry-row::before {
display: block;
/* Add labels for mobile view */
#world-editor-container .world-editor-entry-row > div::before {
content: attr(data-label) ": ";
font-weight: bold;
color: #888;
display: inline-block;
margin-right: 5px;
}
#world-editor-container .world-editor-entry-checkbox { grid-row: 1 / span 5; align-self: center; }
#world-editor-container .world-editor-entry-status { grid-column: 2; }
#world-editor-container .world-editor-entry-activation { grid-column: 2; }
#world-editor-container .world-editor-entry-keys { grid-column: 2; }
#world-editor-container .world-editor-entry-content { grid-column: 2; }
#world-editor-container .world-editor-entry-position { grid-column: 2; }
#world-editor-container .world-editor-entry-depth { grid-column: 2; }
#world-editor-container .world-editor-entry-order { grid-column: 2; }
/* Hide label for checkbox */
#world-editor-container .world-editor-entry-row > div:nth-child(1)::before {
display: none;
}
/* Checkbox positioning - Target the WRAPPER div */
#world-editor-container .world-editor-entry-checkbox {
grid-row: 1 / span 8;
align-self: start;
margin-top: 5px;
}
/* Stack other elements in the second column */
#world-editor-container .world-editor-entry-status,
#world-editor-container .world-editor-entry-activation,
#world-editor-container .world-editor-entry-keys,
#world-editor-container .world-editor-entry-content,
#world-editor-container .world-editor-entry-position,
#world-editor-container .world-editor-entry-depth,
#world-editor-container .world-editor-entry-order,
#world-editor-container .world-editor-entry-row > div[data-label="条目"] {
grid-column: 2;
width: 100%;
}
/* Truncation for keys and content */
#world-editor-container .world-editor-entry-keys,
#world-editor-container .world-editor-entry-content {
white-space: normal;
max-width: 100%;
display: -webkit-box !important;
-webkit-line-clamp: 3;
line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
word-break: break-word;
}
/* Ensure inline edits take full width on mobile */
#world-editor-container .inline-edit {
width: calc(100% - 60px); /* Adjust for label width roughly */
display: inline-block;
}
#world-editor-container .world-editor-batch-actions {

View File

@@ -176,8 +176,8 @@
<i id="amily2_mhb_small_expand_editor" class="editor_maximize fa-solid fa-maximize right_menu_button interactable" title="展开编辑器" tabindex="0"></i>
</div>
<select id="amily2_mhb_small_prompt_selector" class="text_pole">
<option value="jailbreak">破限谕旨 (最高优先级)</option>
<option value="summary">敕史纲要 (总结任务)</option>
<option value="jailbreak">小总结主要提示词</option>
<option value="summary">总结任务提示词</option>
</select>
</div>
<div class="amily2_settings_block prompt-editor-area">
@@ -270,7 +270,7 @@
<small class="notes" style="text-align: center; display: block; margin-top: 5px;">
开始远征】将立即清算所有未记录的历史。 【自动巡录】则在您聊天时,于后台默默守护史册的完整。
自动批量】将立即清算所有未记录的历史。 【静默总结】则在您聊天时,于后台默默守护史册的完整。
</small>
@@ -280,6 +280,41 @@
<!-- 上下分割线 -->
<hr class="header-divider" style="margin: 20px 0;">
<fieldset class="settings-group" style="border-style: dashed;">
<legend>📚 史册归档与回溯</legend>
<small class="notes" style="text-align: center; display: block; margin-bottom: 15px;">
管理多条时间线的史册,随时封存或重启旧的历史。
</small>
<div class="amily2_settings_block">
<button id="amily2_mhb_archive_current" class="menu_button secondary small_button interactable" style="width: 100%; margin-bottom: 10px;">
<i class="fas fa-archive"></i> 归档当前【对话流水总帐】并停用
</button>
</div>
<div class="mhb-selector-container">
<div class="mhb-selector-group">
<label for="amily2_mhb_archive_selector">选择要回溯的旧史册</label>
<div class="select-with-refresh">
<select id="amily2_mhb_archive_selector" class="text_pole" style="flex-grow: 1;">
<option value="">请刷新列表...</option>
</select>
<button id="amily2_mhb_refresh_archives" class="menu_button secondary small_button interactable" title="刷新归档列表">
<i class="fas fa-sync-alt"></i>
</button>
</div>
</div>
</div>
<button id="amily2_mhb_restore_archive" class="menu_button primary small_button interactable" style="margin-top: 10px; width: 100%;">
<i class="fas fa-trash-restore"></i> 回溯选中史册 (自动归档当前)
</button>
</fieldset>
<!-- 上下分割线 -->
<hr class="header-divider" style="margin: 20px 0;">
<fieldset class="settings-group" style="border-style: dashed;">
<legend>💎 宏史卷 (史册精炼)</legend>
@@ -290,8 +325,8 @@
<i id="amily2_mhb_large_expand_editor" class="editor_maximize fa-solid fa-maximize right_menu_button interactable" title="展开编辑器" tabindex="0"></i>
</div>
<select id="amily2_mhb_large_prompt_selector" class="text_pole">
<option value="jailbreak">破限谕旨 (最高优先级)</option>
<option value="summary">精炼纲要 (精炼任务)</option>
<option value="jailbreak">大总结主要提示词</option>
<option value="summary">大总结任务提示词)</option>
</select>
</div>
<div class="amily2_settings_block prompt-editor-area">

View File

@@ -15,7 +15,7 @@
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
min-width: 0; /* Prevents flex items from overflowing */
}
.scrollable-container {
@@ -152,6 +152,7 @@
<div id="table_worldbook_select_wrapper" style="display: none;">
<div class="worldbook-selection-container">
<!-- World Book List Column -->
<div class="worldbook-column">
<div class="amily2_opt_label_with_button_wrapper">
<label>选择世界书 (可多选)</label>
@@ -169,6 +170,7 @@
</div>
</div>
<!-- World Book Entry List Column -->
<div class="worldbook-column" style="margin-top: 10px;">
<label>选择条目 (可多选)</label>
<div class="table-search-wrapper">
@@ -199,6 +201,13 @@
<span class="slider"></span>
</label>
</div>
<div class="control-block-with-switch" style="margin-bottom: 10px;">
<label for="context-optimization-enabled-toggle">启用上下文优化 (合并世界书)</label>
<label class="toggle-switch">
<input type="checkbox" id="context-optimization-enabled" data-setting-key="context_optimization_enabled" data-type="boolean">
<span class="slider"></span>
</label>
</div>
<div class="control-block-with-switch" style="margin-bottom: 10px;">
<label>填表模式</label>
<div class="radio-group">
@@ -218,6 +227,13 @@
<small class="notes" style="margin-top: 5px; display: block;">默认使用微言录的标签提取与内容排除规则。</small>
</div>
<!-- 分步填表延迟滑块 - 仅在分步模式下显示 -->
<div id="secondary-filler-delay-container" class="control-block-with-switch" style="margin-bottom: 10px; display: none;">
<label for="secondary-filler-delay-slider">填表延迟 (楼层): <span id="secondary-filler-delay-value">0</span></label>
<input type="range" id="secondary-filler-delay-slider" min="0" max="10" step="1" value="0" class="text_pole" style="width: 100%; margin-top: 5px;">
<small class="notes" style="margin-top: 5px; display: block;">设置延迟多少楼层后才进行填表防并发冲突、超级记忆功能专用默认为0</small>
</div>
<div id="table-independent-rules-container" class="control-block-with-switch" style="margin-bottom: 10px; display: none; flex-direction: column; align-items: flex-start; gap: 8px;">
<div style="display: flex; justify-content: space-between; align-items: center; width: 100%;">
<label for="table-independent-rules-enabled">启用独立提取规则</label>
@@ -379,4 +395,3 @@
</body>
</html>
</html>

View File

@@ -156,9 +156,14 @@
<fieldset class="settings-group">
<legend style="display: flex; justify-content: space-between; align-items: center; width: 100%;">
<span><i class="fas fa-cog"></i> Amily中枢</span>
<div style="display: flex; gap: 5px;">
<button id="amily2_reset_auth" class="menu_button small_button interactable" title="清除授权">
<i class="fas fa-sign-out-alt"></i>
</button>
<button id="amily2_open_tutorial" class="menu_button small_button interactable" title="查看使用教程">
教程
</button>
</div>
</legend>
</fieldset>
@@ -182,6 +187,13 @@
</div>
</fieldset>
<fieldset class="settings-group">
<legend><i class="fas fa-flask"></i> 内测功能</legend>
<div class="button-group" style="display: flex; justify-content: space-between; gap: 8px;">
<button id="amily2_open_super_memory" class="menu_button wide_button"><i class="fas fa-brain"></i> 超级记忆</button>
</div>
</fieldset>
<fieldset class="settings-group">
<legend><i class="fas fa-bullhorn"></i> 作者留言</legend>
<div id="amily2_message_board" style="display: flex; justify-content: center; align-items: center; padding: 8px; background-color: rgba(255, 255, 255, 0.05); border-radius: 5px; min-height: 40px;">

194
assets/super-memory.css Normal file
View File

@@ -0,0 +1,194 @@
#sm-modal-container {
color: #e0e0e0;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
padding: 10px;
height: calc(100% - 60px); /* Adjust based on header height */
display: flex;
flex-direction: column;
}
.sm-intro-box {
background: rgba(0, 0, 0, 0.2);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
padding: 15px;
margin-bottom: 15px;
}
.sm-intro-box h3 {
margin-top: 0;
color: #05c3f3; /* Amily Blue */
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
padding-bottom: 5px;
}
.sm-navigation-deck {
display: flex;
gap: 5px;
margin-bottom: 15px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
padding-bottom: 5px;
}
.sm-nav-item {
background: transparent;
border: none;
color: #888;
padding: 8px 15px;
cursor: pointer;
border-radius: 5px 5px 0 0;
transition: all 0.2s;
}
.sm-nav-item:hover {
background: rgba(255, 255, 255, 0.05);
color: #ccc;
}
.sm-nav-item.active {
background: rgba(255, 255, 255, 0.1);
color: #05c3f3;
border-bottom: 2px solid #05c3f3;
}
.sm-scroll {
flex-grow: 1;
overflow-y: auto;
padding-right: 5px;
}
.sm-tab-pane {
display: none;
animation: fadeIn 0.3s ease;
}
.sm-tab-pane.active {
display: block;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(5px); }
to { opacity: 1; transform: translateY(0); }
}
.sm-settings-group {
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
padding: 15px;
margin-bottom: 15px;
background: rgba(0, 0, 0, 0.1);
}
.sm-settings-group legend {
color: #05c3f3;
font-weight: bold;
padding: 0 5px;
}
.sm-control-block {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
padding: 5px 0;
border-bottom: 1px dashed rgba(255, 255, 255, 0.05);
}
.sm-control-block:last-child {
border-bottom: none;
}
.sm-input {
background: rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.2);
color: #fff;
padding: 5px 8px;
border-radius: 4px;
width: 80px;
text-align: center;
}
.sm-button-group {
display: flex;
gap: 10px;
margin-top: 15px;
}
.sm-action-button {
flex: 1;
padding: 8px;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
transition: background 0.2s;
background: #4a4a4a;
color: #fff;
}
.sm-action-button.success {
background: #28a745;
}
.sm-action-button.success:hover {
background: #218838;
}
.sm-action-button.danger {
background: #dc3545;
}
.sm-action-button.danger:hover {
background: #c82333;
}
.sm-status-indicator {
font-weight: bold;
color: #ffc107; /* Warning yellow */
}
/* Toggle Switch */
.sm-toggle-switch {
position: relative;
display: inline-block;
width: 40px;
height: 20px;
}
.sm-toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.sm-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: .4s;
border-radius: 20px;
}
.sm-slider:before {
position: absolute;
content: "";
height: 16px;
width: 16px;
left: 2px;
bottom: 2px;
background-color: white;
transition: .4s;
border-radius: 50%;
}
input:checked + .sm-slider {
background-color: #05c3f3;
}
input:checked + .sm-slider:before {
transform: translateX(20px);
}

View File

@@ -46,7 +46,7 @@ const UPDATE_CHECK_URL =
"https://raw.githubusercontent.com/Wx-2025/ST-Amily2-Chat-Optimisation/refs/heads/main/amily2_update_info.json";
const MESSAGE_BOARD_URL =
"https://raw.githubusercontent.com/Wx-2025/ST-Amily2-Chat-Optimisation/refs/heads/main/amily2_message_board.json";
"https://amilyservice.amily49.cc/amily2_message_board.json";
export async function fetchMessageBoardContent() {
if (!MESSAGE_BOARD_URL) {

195
core/context-optimizer.js Normal file
View File

@@ -0,0 +1,195 @@
import { log } from "./table-system/logger.js";
import { getContext } from "/scripts/extensions.js";
import { eventSource, event_types } from "/script.js";
function collectDataToBuffer(buffer, tableName, rowObj) {
if (!buffer[tableName]) {
buffer[tableName] = {
headers: Object.keys(rowObj),
rows: []
};
} else {
const newKeys = Object.keys(rowObj);
newKeys.forEach(k => {
if (!buffer[tableName].headers.includes(k)) {
buffer[tableName].headers.push(k);
}
});
}
buffer[tableName].rows.push(rowObj);
}
function flushBufferToMarkdown(buffer) {
let output = "";
const tableNames = Object.keys(buffer);
if (tableNames.length === 0) return "";
for (const tableName of tableNames) {
const { headers, rows } = buffer[tableName];
if (rows.length === 0) continue;
const firstColKey = headers[0];
const firstColVal = rows[0] ? rows[0][firstColKey] : '';
const isIndexCol = (firstColKey && (firstColKey.includes('索引') || firstColKey.includes('Index'))) ||
(typeof firstColVal === 'string' && /^\s*M\d+/.test(firstColVal));
if (isIndexCol) {
rows.sort((a, b) => {
const valA = String(a[firstColKey] || '');
const valB = String(b[firstColKey] || '');
return valA.localeCompare(valB, undefined, { numeric: true });
});
} else {
rows.reverse();
}
output += `\n# ${tableName}档案\n`;
output += `| ${headers.join(' | ')} |\n`;
output += `|${headers.map(() => '---').join('|')}|\n`;
for (const rowObj of rows) {
const rowArr = headers.map(h => {
const val = rowObj[h];
let safeVal = (val === undefined || val === null) ? '' : String(val);
safeVal = safeVal.replace(/\|/g, '\\|').replace(/\n/g, ' ');
return safeVal;
});
output += `| ${rowArr.join(' | ')} |\n`;
}
output += `\n`;
}
return output;
}
function processText(text) {
const blockRegex = /【(.*?)档案[:]\s*.*?】\s*((?:-\s*.*?[:].*?(?:\r?\n|$))+)/g;
const itemRegex = /-\s*(.*?)[:]\s*(.*?)(?:\r?\n|$)/g;
const buffer = {};
let found = false;
const cleanText = text.replace(blockRegex, (match, tableName, content) => {
found = true;
const rowObj = {};
let itemMatch;
itemRegex.lastIndex = 0;
while ((itemMatch = itemRegex.exec(content)) !== null) {
const key = itemMatch[1].trim();
const val = itemMatch[2].trim();
if (key) {
rowObj[key] = val;
}
}
if (Object.keys(rowObj).length > 0) {
collectDataToBuffer(buffer, tableName, rowObj);
}
return ""; // 移除原始文本
});
return { cleanText, buffer, found };
}
function handlePromptProcessing(data) {
if (!data) return;
if (typeof data.prompt === 'string') {
const { cleanText, buffer, found } = processText(data.prompt);
if (found) {
const mergedTable = flushBufferToMarkdown(buffer);
if (mergedTable) {
data.prompt = cleanText + "\n" + mergedTable;
log('[ContextOptimizer] 已优化上下文:合并了分散的世界书条目 (Text Mode)。', 'success');
}
}
} else if (Array.isArray(data.chat)) {
console.log('[ContextOptimizer] 检测到 Chat Completion 格式...');
const newChat = [];
let modifiedCount = 0;
for (const msg of data.chat) {
const newMsg = { ...msg };
if (typeof newMsg.content === 'string') {
const { cleanText, buffer, found } = processText(newMsg.content);
if (found) {
const mergedTable = flushBufferToMarkdown(buffer);
if (mergedTable) {
newMsg.content = cleanText + "\n" + mergedTable;
modifiedCount++;
}
}
}
newChat.push(newMsg);
}
if (modifiedCount > 0) {
console.log(`[ContextOptimizer] 已原地优化 ${modifiedCount} 条消息中的表格数据。`);
// 全量替换,确保生效
data.chat.splice(0, data.chat.length, ...newChat);
log('[ContextOptimizer] 已优化上下文:合并了分散的世界书条目 (Chat Mode - In Place)。', 'success');
}
}
}
/**
* 注册监听器
*/
export function registerContextOptimizerMacros() {
console.log('[ContextOptimizer] 正在注册监听器...');
const context = getContext();
if (context) {
console.log('[ContextOptimizer] Context APIs:', Object.keys(context));
}
if (context && context.registerChatCompletionModifier) {
context.registerChatCompletionModifier((chat) => {
console.log('[ContextOptimizer] ChatCompletionModifier 触发');
const data = { chat: chat };
handlePromptProcessing(data);
return data.chat;
});
log('[ContextOptimizer] 已注册 Chat Completion Modifier。', 'success');
} else if (context && context.registerPromptModifier) {
context.registerPromptModifier((prompt) => {
console.log('[ContextOptimizer] PromptModifier 触发');
const data = { prompt: prompt };
handlePromptProcessing(data);
return data.prompt;
});
log('[ContextOptimizer] 已注册 Prompt Modifier (正则模式)。', 'success');
} else if (eventSource) {
eventSource.on('chat_completion_prompt_ready', (...args) => {
if (args[0] && typeof args[0] === 'object') {
handlePromptProcessing(args[0]);
}
});
eventSource.on(event_types.GENERATION_STARTED, (...args) => {
if (args.length > 1 && args[1] && typeof args[1].prompt === 'string') {
handlePromptProcessing(args[1]);
} else if (args[0] && typeof args[0].prompt === 'string') {
handlePromptProcessing(args[0]);
}
});
log('[ContextOptimizer] 已绑定事件监听 (Text/Chat 双模式)。', 'info');
} else {
console.error('[ContextOptimizer] 无法获取 eventSource。');
}
}
export function resetContextBuffer() {
}

View File

@@ -18,6 +18,21 @@ import { callAI, generateRandomSeed } from "./api.js";
import { callNgmsAI } from "./api/Ngms_api.js";
import { executeAutoHide } from "./autoHideManager.js";
let reloadEditor = () => {
console.warn("[大史官] reloadEditor 函数不可用,可能是旧版本。已使用空函数代替。");
};
(async () => {
try {
const { reloadEditor: importedReloadEditor } = await import("/scripts/world-info.js");
if (importedReloadEditor) {
reloadEditor = importedReloadEditor;
console.log("[大史官] 已成功动态导入 reloadEditor。");
}
} catch (error) {
console.warn("[大史官] 动态导入 reloadEditor 失败,将使用空函数。错误信息:", error.message);
}
})();
let isExpeditionRunning = false;
let manualStopRequested = false;
@@ -105,6 +120,16 @@ export async function getLoresForWorldbook(bookName) {
}
}
function escapeHtml(text) {
if (!text) return '';
return text
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
export async function executeManualSummary(startFloor, endFloor, isAuto = false) {
return new Promise(async (resolve) => {
const toastTitle = isAuto ? "微言录 (自动)" : "微言录 (手动)";
@@ -154,9 +179,9 @@ export async function executeManualSummary(startFloor, endFloor, isAuto = false)
const generateModalHtml = (msgList) => {
const messageHtml = msgList.map(msg => `
<details class="historiography-message-item" data-author-type="${msg.authorType}">
<summary>【第 ${msg.floor} 楼】 ${msg.author}</summary>
<summary>【第 ${msg.floor} 楼】 ${escapeHtml(msg.author)}</summary>
<div class="historiography-editor-container">
<textarea class="text_pole" data-floor="${msg.floor}">${msg.content}</textarea>
<textarea class="text_pole" data-floor="${msg.floor}">${escapeHtml(msg.content)}</textarea>
</div>
</details>
`).join('');
@@ -613,6 +638,7 @@ export async function executeRefinement(worldbook, loreKey) {
entry.content = finalContent;
await saveWorldInfo(worldbook, bookData, true);
reloadEditor(worldbook);
toastr.success(`史册已成功重铸,并保存于《${worldbook}》!`, "宏史卷重铸完毕");
},
onRegenerate: async (dialog) => {
@@ -816,3 +842,135 @@ export async function executeCompilation(worldbook, loreKeys) {
return { success: false, error: error.message };
}
}
// ========== 史册归档与回溯系统 ==========
async function getTargetLorebookName() {
const settings = extension_settings[extensionName];
const context = getContext();
let targetLorebookName = null;
switch (settings.lorebookTarget) {
case "character_main":
targetLorebookName = characters[context.characterId]?.data?.extensions?.world;
break;
case "dedicated":
const chatIdentifier = await getChatIdentifier();
targetLorebookName = `Amily2-Lore-${chatIdentifier}`;
break;
}
return targetLorebookName;
}
export async function archiveCurrentLedger() {
try {
const targetLorebookName = await getTargetLorebookName();
if (!targetLorebookName) {
toastr.error("无法确定目标世界书,归档失败。", "圣谕不明");
return false;
}
const bookData = await loadWorldInfo(targetLorebookName);
if (!bookData || !bookData.entries) {
toastr.error(`无法读取世界书《${targetLorebookName}》。`, "国史馆");
return false;
}
const ledgerEntryKey = Object.keys(bookData.entries).find(
(key) => bookData.entries[key].comment === RUNNING_LOG_COMMENT && !bookData.entries[key].disable
);
if (!ledgerEntryKey) {
toastr.info("当前没有活跃的【对话流水总帐】,无需归档。", "国史馆");
return false;
}
const entry = bookData.entries[ledgerEntryKey];
const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
const newComment = `${RUNNING_LOG_COMMENT}_归档_${timestamp}`;
entry.comment = newComment;
entry.disable = true;
await saveWorldInfo(targetLorebookName, bookData, true);
reloadEditor(targetLorebookName);
toastr.success(`已将当前流水总帐归档为:\n${newComment}`, "归档成功");
return true;
} catch (error) {
console.error("[大史官] 归档失败:", error);
toastr.error(`归档失败: ${error.message}`, "国史馆");
return false;
}
}
export async function getArchivedLedgers() {
try {
const targetLorebookName = await getTargetLorebookName();
if (!targetLorebookName) return [];
const bookData = await loadWorldInfo(targetLorebookName);
if (!bookData || !bookData.entries) return [];
const archivedLedgers = Object.entries(bookData.entries)
.filter(([, entry]) => entry.comment && entry.comment.startsWith(`${RUNNING_LOG_COMMENT}_归档_`))
.map(([key, entry]) => ({
key: key,
comment: entry.comment
}))
.sort((a, b) => b.comment.localeCompare(a.comment)); // 按时间倒序排列
return archivedLedgers;
} catch (error) {
console.error("[大史官] 获取归档列表失败:", error);
return [];
}
}
export async function restoreArchivedLedger(targetLoreKey) {
try {
const targetLorebookName = await getTargetLorebookName();
if (!targetLorebookName) {
toastr.error("无法确定目标世界书,回溯失败。", "圣谕不明");
return false;
}
const bookData = await loadWorldInfo(targetLorebookName);
if (!bookData || !bookData.entries) {
toastr.error(`无法读取世界书《${targetLorebookName}》。`, "国史馆");
return false;
}
const targetEntry = bookData.entries[targetLoreKey];
if (!targetEntry) {
toastr.error("找不到指定的归档史册。", "圣谕有误");
return false;
}
const currentActiveKey = Object.keys(bookData.entries).find(
(key) => bookData.entries[key].comment === RUNNING_LOG_COMMENT && !bookData.entries[key].disable
);
if (currentActiveKey) {
if (currentActiveKey !== targetLoreKey) {
const activeEntry = bookData.entries[currentActiveKey];
const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
activeEntry.comment = `${RUNNING_LOG_COMMENT}_归档_${timestamp}`;
activeEntry.disable = true;
toastr.info(`已自动归档原有的活跃史册为: ${activeEntry.comment}`, "自动归档");
}
}
targetEntry.comment = RUNNING_LOG_COMMENT;
targetEntry.disable = false;
await saveWorldInfo(targetLorebookName, bookData, true);
reloadEditor(targetLorebookName);
toastr.success("史册回溯成功!时光已倒流,旧史重现。", "回溯成功");
return true;
} catch (error) {
console.error("[大史官] 回溯失败:", error);
toastr.error(`回溯失败: ${error.message}`, "国史馆");
return false;
}
}

View File

@@ -0,0 +1,68 @@
import { extensionName } from "../../utils/settings.js";
import { extension_settings } from "/scripts/extensions.js";
import { saveSettingsDebounced } from "/script.js";
import { initializeSuperMemory, purgeSuperMemory } from "./manager.js";
export function bindSuperMemoryEvents() {
const panel = $('#amily2_super_memory_panel');
if (panel.length === 0) return;
panel.on('click', '.sm-nav-item', function() {
const tab = $(this).data('tab');
panel.find('.sm-nav-item').removeClass('active');
$(this).addClass('active');
panel.find('.sm-tab-pane').removeClass('active');
panel.find(`#sm-${tab}-tab`).addClass('active');
});
panel.on('change', 'input[type="checkbox"]', function() {
if (!extension_settings[extensionName]) extension_settings[extensionName] = {};
const id = this.id;
let key = null;
if (id === 'sm-system-enabled') key = 'super_memory_enabled';
if (id === 'sm-bridge-enabled') key = 'superMemory_bridgeEnabled';
if (key) {
extension_settings[extensionName][key] = this.checked;
saveSettingsDebounced();
console.log(`[Amily2-SuperMemory] Setting updated: ${key} = ${this.checked}`);
}
});
loadSuperMemorySettings();
console.log('[Amily2-SuperMemory] Events bound successfully.');
}
function loadSuperMemorySettings() {
const settings = extension_settings[extensionName] || {};
$('#sm-system-enabled').prop('checked', settings.super_memory_enabled ?? false);
$('#sm-bridge-enabled').prop('checked', settings.superMemory_bridgeEnabled ?? false);
}
window.sm_initializeSystem = async function() {
toastr.info('超级记忆系统正在初始化...');
$('#sm-system-status').text('初始化中...').css('color', 'yellow');
try {
await initializeSuperMemory();
toastr.success('超级记忆系统初始化完成。');
} catch (error) {
console.error(error);
toastr.error('初始化失败,请检查控制台。');
$('#sm-system-status').text('错误').css('color', 'red');
}
};
window.sm_purgeMemory = async function() {
if (confirm('您确定要清空所有由Amily2管理的超级记忆数据吗\n这将删除世界书中所有以表格世界书的条目。')) {
toastr.info('正在清空记忆...');
await purgeSuperMemory();
$('#sm-system-status').text('已清空').css('color', '#ffc107');
}
};

View File

@@ -0,0 +1,77 @@
<div class="amily2-header">
<div class="additional-features-title interactable" title="Amily2 究极长期记忆系统">
<i class="fas fa-brain"></i> 灵台 · 记忆中枢
</div>
<button id="amily2_back_to_main_from_super_memory" class="menu_button secondary small_button interactable">
返回主殿 <i class="fas fa-arrow-right"></i>
</button>
</div>
<hr class="header-divider">
<div id="sm-modal-container">
<div class="sm-intro-box">
<h3><i class="fas fa-microchip"></i> 究极长期记忆 (Super Memory)</h3>
<p>欢迎来到 Amily2 的核心记忆中枢。这里掌管着世界的记忆,连接着每一个角色、每一个物品与每一段传说。</p>
<p>通过“三级金字塔”注入策略,我们将实现极致的 Token 节省与无限的记忆深度。</p>
</div>
<div class="sm-navigation-deck">
<button class="sm-nav-item active" data-tab="dashboard">概览</button>
<button class="sm-nav-item" data-tab="config">配置</button>
<button class="sm-nav-item" data-tab="relation">关联网络</button>
</div>
<div class="sm-scroll">
<!-- Dashboard Tab -->
<div id="sm-dashboard-tab" class="sm-tab-pane active">
<fieldset class="sm-settings-group">
<legend><i class="fas fa-tachometer-alt"></i> 状态监控</legend>
<div class="sm-control-block">
<label>记忆系统状态:</label>
<span id="sm-system-status" class="sm-status-indicator">未初始化</span>
</div>
<div class="sm-control-block">
<label>当前索引 (Tier 1):</label>
<span id="sm-index-count">0 条目</span>
</div>
<div class="sm-control-block">
<label>已触发详情 (Tier 2):</label>
<span id="sm-detail-count">0 条目</span>
</div>
<div class="sm-button-group">
<button class="sm-action-button success" onclick="sm_initializeSystem()">初始化系统</button>
<button class="sm-action-button danger" onclick="sm_purgeMemory()">清空记忆</button>
</div>
</fieldset>
</div>
<!-- Config Tab -->
<div id="sm-config-tab" class="sm-tab-pane">
<fieldset class="sm-settings-group">
<legend><i class="fas fa-cogs"></i> 记忆策略配置</legend>
<div class="sm-control-block">
<label>启用 Super Memory (总开关):</label>
<label class="sm-toggle-switch">
<input type="checkbox" id="sm-system-enabled">
<span class="sm-slider"></span>
</label>
</div>
<div class="sm-control-block">
<label>启用世界书桥接:</label>
<label class="sm-toggle-switch">
<input type="checkbox" id="sm-bridge-enabled">
<span class="sm-slider"></span>
</label>
</div>
</fieldset>
</div>
<!-- Relation Tab -->
<div id="sm-relation-tab" class="sm-tab-pane">
<fieldset class="sm-settings-group">
<legend><i class="fas fa-project-diagram"></i> 关联网络 (The Mesh)</legend>
<p>关联触发逻辑正在开发中...</p>
</fieldset>
</div>
</div>
</div>

View File

@@ -0,0 +1,244 @@
import { amilyHelper } from "../tavern-helper/main.js";
import { extension_settings, getContext } from "/scripts/extensions.js";
import { extensionName } from "../../utils/settings.js";
import { this_chid, characters } from "/script.js";
export function getMemoryBookName() {
let charName = "Global";
const context = getContext();
if (this_chid !== undefined && characters[this_chid]) {
charName = characters[this_chid].name;
} else if (context.characterId !== undefined && characters[context.characterId]) {
charName = characters[context.characterId].name;
}
const safeCharName = charName.replace(/[<>:"/\\|?*]/g, '_');
return `Amily2_Memory_${safeCharName}`;
}
export async function syncToLorebook(tableName, data, indexText, role, headers, rowStatuses, depth = 100) {
console.log(`[Amily2-Bridge] 开始同步表格: ${tableName} (Depth: ${depth})`);
await ensureMemoryBook();
const bookName = getMemoryBookName();
let entries = await amilyHelper.getLorebookEntries(bookName);
if (!entries) entries = [];
const entriesToUpdate = [];
const entriesToCreate = [];
const processEntry = (comment, keys, content, type = 'selective', enabled = true) => {
const existingEntry = entries.find(e => e.comment === comment);
if (existingEntry) {
existingEntry.content = content;
existingEntry.key = keys;
// existingEntry.order = depth; // 【V153.0】不再覆盖用户的深度/排序设置
if (type === 'constant') {
existingEntry.constant = true;
} else {
existingEntry.constant = false;
}
if (existingEntry.enabled !== undefined) {
existingEntry.enabled = enabled;
} else {
existingEntry.disable = !enabled;
}
entriesToUpdate.push(existingEntry);
} else {
entriesToCreate.push({
comment: comment,
keys: keys,
content: content,
type: type,
position: 1,
order: depth,
enabled: enabled
});
}
};
if (role === 'database') {
const indexKey = [`Amily_Index_${tableName}`];
const indexComment = `[Amily2] Index for ${tableName}`;
let indexContent = `${tableName} 索引】\n${indexText.trim()}`;
if (tableName.includes("大纲")) {
indexContent += `\n<system_note>\n【Amily2-前瞻性预测指令】\n为了实现“提前一轮触发绿灯世界书”的高级记忆回流,请在生成本轮回复时,根据当前剧情走向,**大胆预测并自然提及**下一轮可能出现的索引编码。可自行增加一个标签并使用<!--到-->html注释掉该编码。这将激活系统的预加载机制确保剧情发展的连贯性。\n</system_note>`;
}
processEntry(indexComment, indexKey, indexContent, 'constant');
}
data.forEach((row, index) => {
if (!row || row.length === 0) return;
const rawVal = row[0];
// 【V152.0】修复Falsy检查漏洞 (支持数字0作为主键)
if (rawVal === undefined || rawVal === null) return;
const primaryVal = String(rawVal).trim();
if (primaryVal === '') return;
const isPendingDeletion = rowStatuses && rowStatuses[index] === 'pending-deletion';
const isEnabled = !isPendingDeletion;
const triggerKeys = [primaryVal];
const entryComment = `[Amily2] Detail: ${tableName} - ${primaryVal}`;
let finalHeaders = headers;
if (!finalHeaders || finalHeaders.length < row.length) {
finalHeaders = [];
for(let i=0; i<row.length; i++) {
finalHeaders.push((headers && headers[i]) ? headers[i] : `Col_${i}`);
}
}
const settings = extension_settings[extensionName] || {};
const optimizationEnabled = settings.context_optimization_enabled !== false;
let entryContent;
if (optimizationEnabled) {
const primaryVal = row[0] || 'Unknown';
entryContent = `${tableName}档案: ${primaryVal}\n`;
for (let i = 0; i < row.length; i++) {
const key = finalHeaders[i] || `Col_${i}`;
const val = row[i] || '';
entryContent += `- ${key}: ${val}\n`;
}
} else {
let textContent = `${tableName} 详情】\n`;
for (let i = 0; i < row.length; i++) {
const key = finalHeaders[i] || `Col_${i}`;
const val = row[i] || '';
textContent += `- ${key}: ${val}\n`;
}
entryContent = textContent.trim();
}
processEntry(entryComment, triggerKeys, entryContent.trim(), 'selective', isEnabled);
});
const entriesToDelete = [];
const tablePrefix = `[Amily2] Detail: ${tableName} -`;
const activeKeys = new Set();
for(const row of data) {
// 【V152.0】修复Falsy检查漏洞 (支持数字0作为主键)
if(row && row.length > 0) {
const rVal = row[0];
if (rVal !== undefined && rVal !== null) {
const sVal = String(rVal).trim();
if (sVal !== '') {
activeKeys.add(sVal);
}
}
}
}
console.log(`[Amily2-Bridge-GC] ${tableName} 的活跃主键 (Active Keys):`, Array.from(activeKeys));
for (const entry of entries) {
if (entry.comment && entry.comment.startsWith(tablePrefix)) {
const entryKey = entry.comment.substring(tablePrefix.length).trim();
if (!activeKeys.has(entryKey)) {
console.log(`[Amily2-Bridge-GC] 发现残留条目 (将删除): ${entry.comment} (Key: ${entryKey})`);
entriesToDelete.push(entry.uid);
}
}
}
if (entriesToDelete.length > 0) {
console.log(`[Amily2-Bridge] 清理 ${entriesToDelete.length} 个废弃条目...`);
await amilyHelper.deleteLorebookEntries(bookName, entriesToDelete);
}
if (entriesToUpdate.length > 0) {
console.log(`[Amily2-Bridge] 更新 ${entriesToUpdate.length} 个条目...`);
await amilyHelper.setLorebookEntries(bookName, entriesToUpdate);
}
if (entriesToCreate.length > 0) {
console.log(`[Amily2-Bridge] 创建 ${entriesToCreate.length} 个新条目...`);
await amilyHelper.createLorebookEntries(bookName, entriesToCreate);
}
console.log(`[Amily2-Bridge] 同步完成: ${tableName}`);
}
export async function ensureMemoryBook() {
const bookName = getMemoryBookName();
const books = await amilyHelper.getLorebooks();
if (!books.includes(bookName)) {
console.log(`[Amily2-Bridge] 创建角色专用世界书: ${bookName}`);
await amilyHelper.createLorebook(bookName);
}
const settings = extension_settings[extensionName] || {};
const shouldBind = settings.superMemory_autoBind === true;
if (shouldBind && bookName.startsWith("Amily2_Memory_") && bookName !== "Amily2_Memory_Global") {
console.log(`[Amily2-Bridge] 自动绑定世界书到当前角色...`);
await amilyHelper.bindLorebookToCharacter(bookName);
} else if (!shouldBind) {
console.log(`[Amily2-Bridge] 跳过自动绑定 (设置已禁用)。请手动在世界书管理中激活: ${bookName}`);
}
}
function createEntryTemplate() {
return {
uid: Date.now() + Math.floor(Math.random() * 1000),
key: [],
keysecondary: [],
comment: "",
content: "",
constant: false,
selective: true,
order: 100,
position: 1,
enabled: true
};
}
export async function updateTransientHint(hint) {
console.log('[Amily2-Bridge] 更新瞬时记忆提示...');
await ensureMemoryBook();
const bookName = getMemoryBookName();
const comment = "[Amily2] Active Memory Hint";
const content = hint ? `\n<system_note>\n【重要记忆回响】\n${hint}\n</system_note>\n` : "";
const enabled = !!hint;
let entries = await amilyHelper.getLorebookEntries(bookName);
if (!entries) entries = [];
const existingEntry = entries.find(e => e.comment === comment);
if (existingEntry) {
existingEntry.content = content;
existingEntry.enabled = enabled;
existingEntry.order = 0;
existingEntry.constant = true;
await amilyHelper.setLorebookEntries(bookName, [existingEntry]);
} else if (hint) {
const newEntry = {
comment: comment,
keys: [],
content: content,
constant: true,
selective: false,
order: 0,
position: 0,
enabled: true
};
await amilyHelper.createLorebookEntries(bookName, [newEntry]);
}
console.log(`[Amily2-Bridge] 瞬时记忆提示已${enabled ? '启用' : '清除'}`);
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,77 @@
export function generateIndex(data, role, tableName = "") {
if (!Array.isArray(data) || data.length === 0) {
return "";
}
const headers = Object.keys(data[0]);
if (headers.length === 0) return "";
const indexColumns = identifyIndexColumns(data, headers);
let indexLines = [];
indexLines.push(`| ${indexColumns.join(' | ')} |`);
indexLines.push(`| ${indexColumns.map(() => '---').join(' | ')} |`);
let processedData = [...data];
const firstColKey = headers[0];
const firstColVal = data[0] ? data[0][firstColKey] : '';
const isIndexCol = (firstColKey && (firstColKey.includes('索引') || firstColKey.includes('Index'))) ||
(typeof firstColVal === 'string' && /^\s*M\d+/.test(firstColVal)) ||
(tableName && (tableName.includes('总结') || tableName.includes('大纲')));
if (isIndexCol) {
processedData.sort((a, b) => {
const valA = String(a[firstColKey] || '');
const valB = String(b[firstColKey] || '');
return valA.localeCompare(valB, undefined, { numeric: true });
});
}
for (const row of processedData) {
const lineParts = indexColumns.map(col => {
let val = row[col];
if (val === undefined || val === null) return "";
val = String(val).trim();
if (val.length > 15) val = val.substring(0, 12) + "...";
return val;
});
indexLines.push(`| ${lineParts.join(' | ')} |`);
}
return indexLines.join('\n');
}
function identifyIndexColumns(data, headers) {
if (headers.length <= 2) return headers;
const candidates = [];
const maxColumns = 3;
for (const header of headers) {
if (candidates.length >= maxColumns) break;
let totalLen = 0;
let count = 0;
for (const row of data) {
if (row[header]) {
totalLen += String(row[header]).length;
count++;
}
}
const avgLen = count > 0 ? totalLen / count : 0;
const isLongText = avgLen > 20;
const isBlacklisted = /desc|bio|detail|history|经历|描述|详情/i.test(header);
if (!isLongText && !isBlacklisted) {
candidates.push(header);
}
}
if (candidates.length === 0) {
return headers.slice(0, Math.min(headers.length, maxColumns));
}
return candidates;
}

View File

@@ -90,15 +90,18 @@ export async function injectTableData(chat, contextSize, abort, type) {
return;
}
try {
let injectionContent = generateTableContent();
if (!settings.table_injection_enabled) {
setExtensionPrompt(INJECTION_KEY, '', 0, 0, false, 'SYSTEM');
return;
}
try {
const injectionContent = generateTableContent();
if (!injectionContent) {
if (!injectionContent || injectionContent.trim() === '') {
// 理论上不会走到这里,除非宏都没了
setExtensionPrompt(INJECTION_KEY, '', 0, 0, false, 'SYSTEM');
return;
}

File diff suppressed because one or more lines are too long

View File

@@ -96,9 +96,47 @@ export async function fillWithSecondaryApi(latestMessage, forceRun = false) {
}
try {
let textToProcess = latestMessage.mes;
// --- 延迟填表逻辑 (V151.0) ---
const delay = parseInt(settings.secondary_filler_delay || 0, 10);
const chat = context.chat;
let targetMessage;
let targetIndex;
if (delay > 0) {
// 如果有延迟,我们需要找到“延迟前”的那条消息
// chat.length - 1 是当前最新消息的索引
// 目标索引 = (chat.length - 1) - delay
targetIndex = (chat.length - 1) - delay;
if (targetIndex < 0) {
console.log(`[Amily2-副API] 延迟模式(${delay}): 历史楼层不足,跳过填表。`);
return;
}
targetMessage = chat[targetIndex];
// 检查目标消息是否是AI消息通常填表针对AI回复
// 如果目标消息是用户的消息而我们只想填AI的表这可能是一个问题。
// 但如果用户设置了延迟他们可能期望每隔几层填一次或者只填AI层。
// 现有的 `fillWithSecondaryApi` 是在 `CHAT_COMPLETION` 后调用的此时最新消息通常是AI消息。
// 如果延迟是奇数例如1目标消息可能是用户消息。
// 假设延迟是偶数例如2目标消息是上一条AI消息。
// 为了安全起见,如果目标消息是用户消息,我们可能应该跳过?或者依然填表(记录用户消息的表)?
// 目前表系统通常绑定在AI回复上。
// 如果 targetMessage.is_user我们尝试往回找最近的一条AI消息
// 不,这会乱套。严格按照楼层索引来。
console.log(`[Amily2-副API] 延迟模式生效: 当前总楼层 ${chat.length}, 延迟 ${delay}, 目标楼层索引 ${targetIndex}`);
} else {
// 无延迟,使用传入的最新消息
targetMessage = latestMessage;
targetIndex = chat.length - 1;
}
let textToProcess = targetMessage.mes;
if (!textToProcess || !textToProcess.trim()) {
console.log("[Amily2-副API] 消息内容为空,跳过填表任务。");
console.log("[Amily2-副API] 目标消息内容为空,跳过填表任务。");
return;
}
@@ -120,15 +158,15 @@ export async function fillWithSecondaryApi(latestMessage, forceRun = false) {
return;
}
const context = getContext();
const userName = context.name1 || '用户';
const characterName = context.name2 || '角色';
const chat = context.chat;
// 寻找目标消息之前的最后一条用户消息
let lastUserMessage = null;
let lastUserMessageIndex = -1;
for (let i = chat.length - 2; i >= 0; i--) {
// 从 targetIndex - 1 开始往前找
for (let i = targetIndex - 1; i >= 0; i--) {
if (chat[i].is_user) {
lastUserMessage = chat[i];
lastUserMessageIndex = i;
@@ -136,8 +174,8 @@ export async function fillWithSecondaryApi(latestMessage, forceRun = false) {
}
}
const currentInteractionContent = (lastUserMessage ? `${userName}(用户)最新消息:${lastUserMessage.mes}\n` : '') +
`${characterName}AI最新消息,[核心处理内容]${textToProcess}`;
const currentInteractionContent = (lastUserMessage ? `${userName}(用户)消息:${lastUserMessage.mes}\n` : '') +
`${characterName}AI消息[核心处理内容]${textToProcess}`;
let mixedOrder;
try {
@@ -185,7 +223,10 @@ export async function fillWithSecondaryApi(latestMessage, forceRun = false) {
const historyMessagesToGet = contextReadingLevel > 2 ? contextReadingLevel - 2 : 0;
if (historyMessagesToGet > 0) {
const historyEndIndex = lastUserMessageIndex !== -1 ? lastUserMessageIndex : chat.length - 1;
// 这里的 historyEndIndex 应该是我们上面计算出的 lastUserMessageIndex
// 如果没找到用户消息,则使用 targetIndex - 1
const historyEndIndex = lastUserMessageIndex !== -1 ? lastUserMessageIndex : Math.max(0, targetIndex - 1);
const historyContext = await getHistoryContext(historyMessagesToGet, historyEndIndex, tagsToExtract, exclusionRules);
if (historyContext) {
messages.push({ role: "system", content: historyContext });
@@ -205,12 +246,10 @@ export async function fillWithSecondaryApi(latestMessage, forceRun = false) {
}
}
const fillingMode = settings.filling_mode || 'main-api';
if (fillingMode === 'secondary-api') {
console.groupCollapsed(`[Amily2 分步填表] 即将发送至 API 的内容`);
console.log("发送给AI的提示词: ", JSON.stringify(messages, null, 2));
console.dir(messages);
console.groupEnd();
}
let rawContent;
if (settings.nccsEnabled) {
@@ -230,14 +269,20 @@ export async function fillWithSecondaryApi(latestMessage, forceRun = false) {
updateTableFromText(rawContent);
const currentContext = getContext();
if (currentContext.chat && currentContext.chat.length > 0) {
const lastMessage = currentContext.chat[currentContext.chat.length - 1];
if (saveStateToMessage(getMemoryState(), lastMessage)) {
// 保存到目标消息
if (saveStateToMessage(getMemoryState(), targetMessage)) {
// 如果目标消息不是最新消息,我们可能需要重新渲染整个聊天记录或者特定消息的表格?
// renderTables() 通常重新渲染所有可见表格
renderTables();
// updateOrInsertTableInChat 通常插入到DOM中
// 我们可能需要传递 targetIndex 给 updateOrInsertTableInChat 吗?
// 目前 updateOrInsertTableInChat 似乎是查找 .mes_text 并插入。
// 如果我们更新了历史消息的数据,我们需要确保 DOM 也更新。
// 由于 SillyTavern 的消息渲染机制,如果消息已经在屏幕上,仅仅修改数据可能不会自动更新 DOM。
// 但是 renderTables() 应该会处理这个。
updateOrInsertTableInChat();
}
}
saveChat();
} catch (error) {

View File

@@ -147,4 +147,12 @@ export const tableSystemDefaultSettings = {
batch_filler_flow_template: DEFAULT_AI_FLOW_TEMPLATE,
filling_mode: 'main-api',
context_optimization_enabled: true, // 【V144.0】上下文优化(世界书合并)开关
// 【V146.5】分步填表相关设置
context_reading_level: 4,
secondary_filler_delay: 0,
table_independent_rules_enabled: false,
table_tags_to_extract: '',
table_exclusion_rules: [],
};

View File

@@ -31,7 +31,9 @@ import {
name2,
addOneMessage,
messageFormatting,
substituteParamsExtended
substituteParamsExtended,
saveCharacterDebounced,
this_chid
} from "/script.js";
import { getContext } from "/scripts/extensions.js";
import { executeSlashCommandsWithOptions } from '/scripts/slash-commands.js';
@@ -430,6 +432,7 @@ class AmilyHelper {
existingEntry.position = positionMap[entryUpdate.position] ?? 4;
}
if (entryUpdate.depth !== undefined) existingEntry.depth = entryUpdate.depth;
if (entryUpdate.scanDepth !== undefined) existingEntry.scanDepth = entryUpdate.scanDepth;
if (entryUpdate.order !== undefined) existingEntry.order = entryUpdate.order;
if (entryUpdate.exclude_recursion !== undefined) existingEntry.excludeRecursion = entryUpdate.exclude_recursion;
if (entryUpdate.prevent_recursion !== undefined) existingEntry.preventRecursion = entryUpdate.prevent_recursion;
@@ -474,6 +477,7 @@ class AmilyHelper {
constant: newEntryData.type === 'constant' ? true : (newEntryData.constant || false),
position: typeof newEntryData.position === 'string' ? (positionMap[newEntryData.position] ?? 4) : (newEntryData.position ?? 4),
depth: newEntryData.depth ?? 998,
scanDepth: newEntryData.scanDepth ?? null,
disable: !(newEntryData.enabled ?? true),
});
if (newEntryData.type === 'selective') newEntry.constant = false;
@@ -487,6 +491,34 @@ class AmilyHelper {
}
}
async deleteLorebookEntries(bookName, uids) {
try {
const bookData = await loadWorldInfo(bookName);
if (!bookData || !bookData.entries) {
return false;
}
let deletedCount = 0;
for (const uid of uids) {
if (bookData.entries[uid]) {
delete bookData.entries[uid];
deletedCount++;
}
}
if (deletedCount > 0) {
await saveWorldInfo(bookName, bookData, true);
reloadEditor(bookName);
console.log(`[Amily助手] 已从世界书《${bookName}》删除 ${deletedCount} 个条目`);
return true;
}
return false;
} catch (error) {
console.error(`[Amily助手] 删除世界书《${bookName}》条目时出错:`, error);
return false;
}
}
async createLorebook(bookName) {
try {
if (world_names.includes(bookName)) {
@@ -535,6 +567,42 @@ class AmilyHelper {
getLastMessageId() {
return chat.length - 1;
}
/**
* 将指定世界书绑定到当前角色
* @param {string} bookName 世界书名称
*/
async bindLorebookToCharacter(bookName) {
if (this_chid === undefined || !characters[this_chid]) {
console.warn('[Amily助手] 无法绑定世界书:未选中角色');
return false;
}
const char = characters[this_chid];
if (!char.data) char.data = {};
if (!char.data.extensions) char.data.extensions = {};
// 确保 world 字段是数组
let worlds = char.data.extensions.world;
if (!Array.isArray(worlds)) {
worlds = worlds ? [worlds] : [];
}
if (!worlds.includes(bookName)) {
worlds.push(bookName);
char.data.extensions.world = worlds;
console.log(`[Amily助手] 已将世界书《${bookName}》绑定到角色 ${char.name}`);
if (typeof saveCharacterDebounced === 'function') {
saveCharacterDebounced();
return true;
} else {
console.warn('[Amily助手] 无法保存角色数据saveCharacterDebounced 不可用');
return false;
}
}
return true; // 已经绑定
}
}
export const amilyHelper = new AmilyHelper();

870
index.js

File diff suppressed because one or more lines are too long

View File

@@ -1,9 +1,9 @@
{
"name": "Amily2号聊天优化助手",
"display_name": "Amily2号助手",
"version": "1.6.4",
"version": "1.6.8",
"author": "Wx-2025",
"description": "一个拥有独立UI的智能引擎正文优化、自动总结、记忆表格、rag向量、隐藏楼层、剧情推进六大功能整合。",
"description": "一个拥有独立UI的智能引擎正文优化、自动总结、记忆表格、rag向量、隐藏楼层、剧情推进等多功能整合。",
"minSillyTavernVersion": "1.10.0",
"requires": [],
"homePage": "https://github.com/Wx-2025/ST-Amily2-Chat-Optimisation.git",
@@ -32,6 +32,10 @@

View File

@@ -629,6 +629,26 @@ export function bindModalEvents() {
}
});
container
.off("click.amily2.reset_auth")
.on("click.amily2.reset_auth", "#amily2_reset_auth", function() {
if (!pluginAuthStatus.authorized) return;
if (confirm("确定要清除本地授权码吗?\n这将使您的授权失效需要重新验证。\n\n这通常用于\n1. 升级为高级用户\n2. 解决授权异常问题")) {
localStorage.removeItem("plugin_auth_code");
localStorage.removeItem("plugin_activated");
localStorage.removeItem("plugin_auto_login");
localStorage.removeItem("plugin_user_type");
localStorage.removeItem("plugin_valid_until");
toastr.success("授权已清除,即将重新加载以生效...", "Amily2号");
setTimeout(() => {
location.reload();
}, 1500);
}
});
container
.off("click.amily2.update")
.on("click.amily2.update", "#amily2_update_button", function() {
@@ -705,7 +725,7 @@ export function bindModalEvents() {
container
.off("click.amily2.chamber_nav")
.on("click.amily2.chamber_nav",
"#amily2_open_plot_optimization, #amily2_open_additional_features, #amily2_open_rag_palace, #amily2_open_memorisation_forms, #amily2_open_character_world_book, #amily2_open_world_editor, #amily2_open_glossary, #amily2_open_renderer, #amily2_back_to_main_settings, #amily2_back_to_main_from_hanlinyuan, #amily2_back_to_main_from_forms, #amily2_back_to_main_from_optimization, #amily2_back_to_main_from_cwb, #amily2_back_to_main_from_world_editor, #amily2_back_to_main_from_glossary, #amily2_renderer_back_button", function () {
"#amily2_open_plot_optimization, #amily2_open_additional_features, #amily2_open_rag_palace, #amily2_open_memorisation_forms, #amily2_open_character_world_book, #amily2_open_world_editor, #amily2_open_glossary, #amily2_open_renderer, #amily2_open_super_memory, #amily2_back_to_main_settings, #amily2_back_to_main_from_hanlinyuan, #amily2_back_to_main_from_forms, #amily2_back_to_main_from_optimization, #amily2_back_to_main_from_cwb, #amily2_back_to_main_from_world_editor, #amily2_back_to_main_from_glossary, #amily2_renderer_back_button, #amily2_back_to_main_from_super_memory", function () {
if (!pluginAuthStatus.authorized) return;
const mainPanel = container.find('.plugin-features');
@@ -717,6 +737,7 @@ container
const worldEditorPanel = container.find('#amily2_world_editor_panel');
const glossaryPanel = container.find('#amily2_glossary_panel');
const rendererPanel = container.find('#amily2_renderer_panel');
const superMemoryPanel = container.find('#amily2_super_memory_panel');
mainPanel.hide();
additionalPanel.hide();
@@ -727,8 +748,18 @@ container
worldEditorPanel.hide();
glossaryPanel.hide();
rendererPanel.hide();
superMemoryPanel.hide();
switch (this.id) {
case 'amily2_open_super_memory':
const userType = parseInt(localStorage.getItem("plugin_user_type") || "0");
if (userType < 2) {
toastr.warning("此功能为内测功能,仅限我看顺眼的用户使用。", "权限不足");
mainPanel.show();
return;
}
superMemoryPanel.show();
break;
case 'amily2_open_renderer':
rendererPanel.show();
break;
@@ -761,6 +792,7 @@ container
case 'amily2_back_to_main_from_world_editor':
case 'amily2_back_to_main_from_glossary':
case 'amily2_renderer_back_button':
case 'amily2_back_to_main_from_super_memory':
mainPanel.show();
break;
}

View File

@@ -20,6 +20,7 @@ import { bindHanlinyuanEvents } from "./hanlinyuan-bindings.js";
import { bindTableEvents } from './table-bindings.js';
import { showContentModal } from "./page-window.js";
import { initializeRendererBindings } from "../core/tavern-helper/renderer-bindings.js";
import { bindSuperMemoryEvents } from "../core/super-memory/bindings.js";
const extensionFolderPath = `scripts/extensions/third-party/${extensionName}`;
@@ -106,6 +107,10 @@ async function initializePanel(contentPanel, errorContainer) {
const rendererPanelHtml = `<div id="amily2_renderer_panel" style="display: none;">${rendererContent}</div>`;
mainContainer.append(rendererPanelHtml);
const superMemoryContent = await $.get(`${extensionFolderPath}/core/super-memory/index.html`);
const superMemoryPanelHtml = `<div id="amily2_super_memory_panel" style="display: none;">${superMemoryContent}</div>`;
mainContainer.append(superMemoryPanelHtml);
// 在面板创建后,加载世界书编辑器脚本
const worldEditorScriptId = 'world-editor-script';
if (!document.getElementById(worldEditorScriptId)) {
@@ -123,6 +128,7 @@ async function initializePanel(contentPanel, errorContainer) {
bindHanlinyuanEvents();
bindTableEvents();
initializeRendererBindings();
bindSuperMemoryEvents();
contentPanel.data("initialized", true);
console.log("[Amily-重构] 宫殿模块已按蓝图竣工。");
applyUpdateIndicator();

File diff suppressed because one or more lines are too long

View File

@@ -10,7 +10,8 @@ import { applyExclusionRules, extractBlocksByTags } from '../core/utils/rag-tag-
import {
getAvailableWorldbooks, getLoresForWorldbook,
executeManualSummary, executeRefinement,
executeExpedition, stopExpedition
executeExpedition, stopExpedition,
archiveCurrentLedger, getArchivedLedgers, restoreArchivedLedger
} from "../core/historiographer.js";
import { getNgmsApiSettings, testNgmsApiConnection, fetchNgmsModels } from "../core/api/Ngms_api.js";
@@ -265,6 +266,51 @@ export function bindHistoriographyEvents() {
updateExpeditionButtonUI('idle');
// ========== 📚 史册归档与回溯 绑定 ==========
const archiveCurrentBtn = document.getElementById("amily2_mhb_archive_current");
const archiveSelector = document.getElementById("amily2_mhb_archive_selector");
const refreshArchivesBtn = document.getElementById("amily2_mhb_refresh_archives");
const restoreArchiveBtn = document.getElementById("amily2_mhb_restore_archive");
const updateArchiveList = async () => {
archiveSelector.innerHTML = '<option value="">正在翻阅旧档...</option>';
const archives = await getArchivedLedgers();
archiveSelector.innerHTML = ""; // 清空
if (archives && archives.length > 0) {
archives.forEach((arch) => {
const option = document.createElement("option");
option.value = arch.key;
option.textContent = arch.comment;
archiveSelector.appendChild(option);
});
} else {
archiveSelector.innerHTML = '<option value="">未发现归档史册</option>';
}
};
archiveCurrentBtn.addEventListener("click", async () => {
if (confirm("确定要归档当前的【对话流水总帐】并停用它吗?\n这将允许您开始一段全新的历史记录。")) {
const success = await archiveCurrentLedger();
if (success) {
updateArchiveList(); // 归档成功后刷新列表
}
}
});
refreshArchivesBtn.addEventListener("click", updateArchiveList);
restoreArchiveBtn.addEventListener("click", async () => {
const selectedKey = archiveSelector.value;
if (!selectedKey) {
toastr.warning("请先选择一个要回溯的史册!", "圣谕不明");
return;
}
if (confirm("确定要回溯选中的史册吗?\n当前的活跃史册如果有将被自动归档。")) {
await restoreArchivedLedger(selectedKey);
updateArchiveList(); // 回溯后刷新列表
}
});
// ========== 💎 宏史卷 (史册精炼) 绑定 ==========
const largeWbSelector = document.getElementById(
"amily2_mhb_large_worldbook_selector",

View File

@@ -120,12 +120,22 @@ export function showHtmlModal(title, htmlContent, options = {}) {
}
function escapeHtml(text) {
if (!text) return '';
return text
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
export function showSummaryModal(summaryText, callbacks) {
const { onConfirm, onRegenerate, onCancel } = callbacks;
const modalHtml = `
<div class="historiographer-summary-modal">
<textarea class="text_pole" style="width: 100%; height: 50vh; resize: vertical;">${summaryText}</textarea>
<textarea class="text_pole" style="width: 100%; height: 50vh; resize: vertical;">${escapeHtml(summaryText)}</textarea>
</div>
`;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -19,6 +19,10 @@ export const defaultSettings = {
contextMessages: 2,
promptPresets: [],
lastUsedPresetName: '',
super_memory_enabled: false, // 【V150.0】Amily2 Super Memory 总开关 (Default OFF)
superMemory_bridgeEnabled: false, // 【V150.0】世界书桥接 (Default OFF)
superMemory_autoBind: false, // 【V151.9】是否自动绑定到角色 (Default OFF)
secondary_filler_delay: 0, // 【V151.0】分步填表延迟
plotOpt_enabled: false,
plotOpt_max_tokens: 20000,
plotOpt_temperature: 0.7,