diff --git a/CharacterWorldBook/cwb_settings.html b/CharacterWorldBook/cwb_settings.html
index 6b5b2ea..a09b28b 100644
--- a/CharacterWorldBook/cwb_settings.html
+++ b/CharacterWorldBook/cwb_settings.html
@@ -126,6 +126,12 @@
+
+ 扫描深度
+
@@ -188,9 +194,12 @@
+
diff --git a/CharacterWorldBook/src/cwb_config.js b/CharacterWorldBook/src/cwb_config.js
index 322330a..e5c6771 100644
--- a/CharacterWorldBook/src/cwb_config.js
+++ b/CharacterWorldBook/src/cwb_config.js
@@ -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,
diff --git a/CharacterWorldBook/src/cwb_core.js b/CharacterWorldBook/src/cwb_core.js
index 4d74e14..28da9df 100644
--- a/CharacterWorldBook/src/cwb_core.js
+++ b/CharacterWorldBook/src/cwb_core.js
@@ -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,7 +191,12 @@ 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 &&
Array.isArray(e.keys) &&
@@ -200,16 +206,28 @@ async function proceedWithCardUpdate($panel, messagesToUse) {
for (const entry of characterEntries) {
try {
- const parsedData = parseCustomFormat(entry.content);
- const entryCharName = parsedData?.name?.trim() || parsedData?.core_identity?.name?.trim();
- if (entryCharName) {
- existingData[entryCharName] = entry.content;
+ 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?.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('
转换中...');
+
+ 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('
旧版格式转换');
+ }
+}
+
export async function initializeCore($panel) {
const initialChatName = await getLatestChatName();
await resetScriptStateForNewChat($panel, initialChatName);
diff --git a/CharacterWorldBook/src/cwb_lorebookManager.js b/CharacterWorldBook/src/cwb_lorebookManager.js
index 1e06e8e..6dbc43c 100644
--- a/CharacterWorldBook/src/cwb_lorebookManager.js
+++ b/CharacterWorldBook/src/cwb_lorebookManager.js
@@ -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) {
diff --git a/CharacterWorldBook/src/cwb_settingsManager.js b/CharacterWorldBook/src/cwb_settingsManager.js
index 87d63ca..4564d4c 100644
--- a/CharacterWorldBook/src/cwb_settingsManager.js
+++ b/CharacterWorldBook/src/cwb_settingsManager.js
@@ -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;
@@ -226,7 +240,8 @@ export function bindSettingsEvents($settingsPanel) {
});
$panel.on('change', '#cwb-api-mode', function() {
const selectedMode = $(this).val();
-
+
+ // 自动保存API模式设置
getSettings().cwb_api_mode = selectedMode;
saveSettingsDebounced();
@@ -239,7 +254,8 @@ 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;
@@ -275,7 +294,8 @@ 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);
@@ -513,12 +536,13 @@ export function loadSettings() {
console.log('[CWB] Loading settings...');
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];
@@ -527,7 +551,8 @@ 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) {
diff --git a/CharacterWorldBook/src/cwb_uiManager.js b/CharacterWorldBook/src/cwb_uiManager.js
index 450cf7b..9447dc4 100644
--- a/CharacterWorldBook/src/cwb_uiManager.js
+++ b/CharacterWorldBook/src/cwb_uiManager.js
@@ -1,692 +1,740 @@
-import { SCRIPT_ID_PREFIX, CHAR_CARD_VIEWER_BUTTON_ID, CHAR_CARD_VIEWER_POPUP_ID, state } from './cwb_state.js';
-import { logDebug, logError, showToastr, escapeHtml, parseCustomFormat, buildCustomFormat, isCwbEnabled } from './cwb_utils.js';
-import { deleteLorebookEntries, getTargetWorldBook } from './cwb_lorebookManager.js';
-import { manualUpdateLogic } from './cwb_core.js';
-import { testCwbConnection, fetchCwbModels } from './cwb_apiService.js';
-import { extensionName } from '../../utils/settings.js';
-import { extension_settings } from '/scripts/extensions.js';
-import { saveSettingsDebounced } from '/script.js';
-import { amilyHelper } from '../../core/tavern-helper/main.js';
+ import { SCRIPT_ID_PREFIX, CHAR_CARD_VIEWER_BUTTON_ID, CHAR_CARD_VIEWER_POPUP_ID, state } from './cwb_state.js';
+ import { logDebug, logError, showToastr, escapeHtml, parseCustomFormat, buildCustomFormat, isCwbEnabled } from './cwb_utils.js';
+ import { deleteLorebookEntries, getTargetWorldBook } from './cwb_lorebookManager.js';
+ import { manualUpdateLogic } from './cwb_core.js';
+ import { testCwbConnection, fetchCwbModels } from './cwb_apiService.js';
+ import { extensionName } from '../../utils/settings.js';
+ import { extension_settings } from '/scripts/extensions.js';
+ import { saveSettingsDebounced } from '/script.js';
+ import { amilyHelper } from '../../core/tavern-helper/main.js';
-const { jQuery: $, SillyTavern } = window;
+ const { jQuery: $, SillyTavern } = window;
-function createCharCardViewerPopupHtml(displayItems) {
- const pathToLabelMap = {
- 'narrative_essence.core_traits.name': '特质名称',
- 'narrative_essence.key_relationships.name': '关系人姓名',
- };
- const keyToLabelMap = {
- 'name': '姓名',
- 'archetype': '身份原型',
- 'gender': '性别',
- 'age': '年龄',
- 'race': '种族',
- 'current_status': '当前状态',
+ 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': '关键关系',
+ 'definition': '特质定义',
+ 'evidence': '具体事例',
+ 'style_summary': '风格总结',
+ 'quotes': '代表性引言',
+ 'summary': '关系概述',
- 'first_impression': '第一印象',
- 'key_features': '显著特征',
- 'attire': '衣着风格',
- 'mannerisms': '习惯举止',
- 'voice': '声音特征',
+ // New short keys
+ 'CI': '核心认同',
+ 'PI': '物理印记',
+ 'PP': '心智侧写',
+ 'SM': '社交矩阵',
+ 'NE': '叙事精粹',
+
+ 'arch': '身份原型',
+ 'gen': '性别',
+ // age is same
+ // race is same
+ 'status': '当前状态',
- 'tags': '性格标签',
- 'description': '性格详述',
- 'motivation': '内在驱动',
- 'values': '价值观',
- 'inner_conflict': '内心挣扎',
+ 'first': '第一印象',
+ 'feat': '显著特征',
+ // attire is same
+ 'manner': '习惯举止',
+ // voice is same
- 'interaction_style': '互动风格',
- 'skills': '技能能力',
- 'reputation': '他人声望',
+ // tags is same
+ 'desc': '性格详述',
+ 'mot': '内在驱动',
+ 'val': '价值观',
+ 'conf': '内心挣扎',
- 'core_traits': '核心特质',
- 'verbal_patterns': '语言范式',
- 'key_relationships': '关键关系',
- 'definition': '特质定义',
- 'evidence': '具体事例',
- 'style_summary': '风格总结',
- 'quotes': '代表性引言',
- 'summary': '关系概述',
- };
- const getLabel = (key, path) => {
- const pathKey = path.replace(/\.\d+\./g, '.');
- if (pathToLabelMap[pathKey]) {
- return pathToLabelMap[pathKey];
- }
- return keyToLabelMap[key] || key.replace(/_/g, ' ');
- };
+ 'style': '互动风格/风格总结', // Shared by SM.style and NE.verb.style
+ 'skill': '技能能力',
+ 'rep': '他人声望',
- const renderField = (label, path, value, isTextarea = false, isArray = false) => {
- const escapedLabel = escapeHtml(label);
- const escapedValue = escapeHtml(isArray ? value.join('\n') : value || '');
+ 'trait': '核心特质',
+ 'verb': '语言范式',
+ 'rel': '关键关系',
- const isLongContent = (value && String(value).length > 50) || (Array.isArray(value) && value.length > 1);
- const rows = isArray ? Math.max(3, value.length) : (isLongContent ? 4 : 2);
-
- const inputElement = `
`;
-
- return `
- ${escapedLabel}
- ${inputElement}
-
`;
- };
-
- const renderCard = (title, data, pathPrefix) => {
- if (!data || typeof data !== 'object' || Object.keys(data).length === 0) return '';
- let cardHtml = `
${escapeHtml(title)} `;
- for (const [key, value] of Object.entries(data)) {
- const currentPath = pathPrefix ? `${pathPrefix}.${key}` : key;
- const label = getLabel(key, currentPath);
- if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
- cardHtml += renderCard(label, value, currentPath); // Recursive call for nested objects
- } else if (Array.isArray(value) && value.length > 0 && typeof value[0] === 'object') {
- cardHtml += `
${escapeHtml(label)} `;
- value.forEach((item, itemIndex) => {
- cardHtml += `
`;
- for (const [itemKey, itemValue] of Object.entries(item)) {
- const itemPath = `${currentPath}.${itemIndex}.${itemKey}`;
- cardHtml += renderField(getLabel(itemKey, itemPath), itemPath, itemValue, false, Array.isArray(itemValue));
- }
- cardHtml += `
`;
- });
- cardHtml += `
`;
- } else {
- cardHtml += renderField(label, currentPath, value, false, Array.isArray(value));
+ 'def': '特质定义',
+ 'evid': '具体事例',
+ 'quote': '代表性引言',
+ 'sum': '关系概述',
+ };
+ const getLabel = (key, path) => {
+ const pathKey = path.replace(/\.\d+\./g, '.');
+ if (pathToLabelMap[pathKey]) {
+ return pathToLabelMap[pathKey];
}
+ return keyToLabelMap[key] || key.replace(/_/g, ' ');
+ };
+
+ const renderField = (label, path, value, isTextarea = false, isArray = false) => {
+ const escapedLabel = escapeHtml(label);
+ const escapedValue = escapeHtml(isArray ? value.join('\n') : value || '');
+
+ const isLongContent = (value && String(value).length > 50) || (Array.isArray(value) && value.length > 1);
+ const rows = isArray ? Math.max(3, value.length) : (isLongContent ? 4 : 2);
+
+ const inputElement = `
`;
+
+ return `
+ ${escapedLabel}
+ ${inputElement}
+
`;
+ };
+
+ const renderCard = (title, data, pathPrefix) => {
+ if (!data || typeof data !== 'object' || Object.keys(data).length === 0) return '';
+ let cardHtml = `
${escapeHtml(title)} `;
+ for (const [key, value] of Object.entries(data)) {
+ const currentPath = pathPrefix ? `${pathPrefix}.${key}` : key;
+ const label = getLabel(key, currentPath);
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
+ cardHtml += renderCard(label, value, currentPath); // Recursive call for nested objects
+ } else if (Array.isArray(value) && value.length > 0 && typeof value[0] === 'object') {
+ cardHtml += `
${escapeHtml(label)} `;
+ value.forEach((item, itemIndex) => {
+ cardHtml += `
`;
+ for (const [itemKey, itemValue] of Object.entries(item)) {
+ const itemPath = `${currentPath}.${itemIndex}.${itemKey}`;
+ cardHtml += renderField(getLabel(itemKey, itemPath), itemPath, itemValue, false, Array.isArray(itemValue));
+ }
+ cardHtml += `
`;
+ });
+ cardHtml += `
`;
+ } else {
+ cardHtml += renderField(label, currentPath, value, false, Array.isArray(value));
+ }
+ }
+ cardHtml += `
`;
+ return cardHtml;
+ };
+
+ let html = ``;
+ return html;
}
- cardHtml += `
`;
- return cardHtml;
- };
- let html = `
`;
return html;
}
- html += `