mirror of
https://github.com/SilenceLurker/ST-Amily2-Chat-Optimisation.git
synced 2026-06-06 10:05:50 +00:00
Merge branch 'Wx-2025:main' into main
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -1,44 +1,43 @@
|
||||
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) {
|
||||
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>
|
||||
@@ -182,9 +230,9 @@ function createCharCardViewerPopupHtml(displayItems) {
|
||||
});
|
||||
html += `</div></div></div>`;
|
||||
return html;
|
||||
}
|
||||
}
|
||||
|
||||
function bindCharCardViewerPopupEvents($popup) {
|
||||
function bindCharCardViewerPopupEvents($popup) {
|
||||
$popup.on('change', '.cwb-insertion-position', function() {
|
||||
const $this = $(this);
|
||||
const $depthContainer = $this.closest('.cwb-insertion-settings-content').find('.cwb-insertion-depth-container');
|
||||
@@ -332,13 +380,13 @@ function bindCharCardViewerPopupEvents($popup) {
|
||||
$button.prop('disabled', false).text(`保存修改`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function closeCharCardViewerPopup() {
|
||||
function closeCharCardViewerPopup() {
|
||||
$(`#${CHAR_CARD_VIEWER_POPUP_ID}`).remove();
|
||||
}
|
||||
}
|
||||
|
||||
export async function showCharCardViewerPopup() {
|
||||
export async function showCharCardViewerPopup() {
|
||||
if (!isCwbEnabled()) return;
|
||||
closeCharCardViewerPopup();
|
||||
try {
|
||||
@@ -460,17 +508,17 @@ export async function showCharCardViewerPopup() {
|
||||
logError('无法显示角色卡查看器:', error);
|
||||
showToastr('error', '加载角色卡数据时出错。');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function toggleCharCardViewerPopup() {
|
||||
function toggleCharCardViewerPopup() {
|
||||
if ($(`#${CHAR_CARD_VIEWER_POPUP_ID}`).length > 0) {
|
||||
closeCharCardViewerPopup();
|
||||
} else {
|
||||
showCharCardViewerPopup();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function keepButtonInBounds($element, savePosition = false) {
|
||||
function keepButtonInBounds($element, savePosition = false) {
|
||||
if (!$element || !$element.length) return;
|
||||
const windowWidth = $(window).width();
|
||||
const windowHeight = $(window).height();
|
||||
@@ -483,9 +531,9 @@ function keepButtonInBounds($element, savePosition = false) {
|
||||
if (savePosition) {
|
||||
localStorage.setItem(state.STORAGE_KEY_VIEWER_BUTTON_POS, JSON.stringify({ top: $element.css('top'), left: $element.css('left') }));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function makeButtonDraggable($button) {
|
||||
function makeButtonDraggable($button) {
|
||||
let isDragging = false, wasDragged = false, offset = { x: 0, y: 0 }, startPos = { x: 0, y: 0 };
|
||||
const DRAG_THRESHOLD = 5; // 5 pixels threshold
|
||||
|
||||
@@ -550,9 +598,9 @@ function makeButtonDraggable($button) {
|
||||
}
|
||||
toggleCharCardViewerPopup();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function initializeCharCardViewer() {
|
||||
export function initializeCharCardViewer() {
|
||||
const $existingButton = $(`#${CHAR_CARD_VIEWER_BUTTON_ID}`);
|
||||
|
||||
if ($existingButton.length > 0) {
|
||||
@@ -590,9 +638,9 @@ export function initializeCharCardViewer() {
|
||||
clearTimeout(resizeTimeout);
|
||||
resizeTimeout = setTimeout(() => keepButtonInBounds($(`#${CHAR_CARD_VIEWER_BUTTON_ID}`), true), 150);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function updateViewerButtonVisibility() {
|
||||
export function updateViewerButtonVisibility() {
|
||||
const $button = $(`#${CHAR_CARD_VIEWER_BUTTON_ID}`);
|
||||
const shouldShow = isCwbEnabled() && state.viewerEnabled;
|
||||
|
||||
@@ -614,9 +662,9 @@ export function updateViewerButtonVisibility() {
|
||||
viewerEnabled: state.viewerEnabled,
|
||||
shouldShow: shouldShow
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function bindCwbApiEvents() {
|
||||
export function bindCwbApiEvents() {
|
||||
console.log('[CWB] Binding API events');
|
||||
|
||||
$('#cwb-api-url').off('input').on('input', function() {
|
||||
@@ -689,4 +737,4 @@ export function bindCwbApiEvents() {
|
||||
$button.prop('disabled', false).html('<i class="fas fa-download"></i> 获取模型');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: '小说处理',
|
||||
};
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
194
assets/super-memory.css
Normal 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);
|
||||
}
|
||||
@@ -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
195
core/context-optimizer.js
Normal 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() {
|
||||
}
|
||||
@@ -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, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
68
core/super-memory/bindings.js
Normal file
68
core/super-memory/bindings.js
Normal 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');
|
||||
}
|
||||
};
|
||||
77
core/super-memory/index.html
Normal file
77
core/super-memory/index.html
Normal 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>
|
||||
244
core/super-memory/lorebook-bridge.js
Normal file
244
core/super-memory/lorebook-bridge.js
Normal 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 ? '启用' : '清除'}。`);
|
||||
}
|
||||
1
core/super-memory/manager.js
Normal file
1
core/super-memory/manager.js
Normal file
File diff suppressed because one or more lines are too long
77
core/super-memory/smart-indexer.js
Normal file
77
core/super-memory/smart-indexer.js
Normal 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;
|
||||
}
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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: [],
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -120,12 +120,22 @@ export function showHtmlModal(title, htmlContent, options = {}) {
|
||||
}
|
||||
|
||||
|
||||
function escapeHtml(text) {
|
||||
if (!text) return '';
|
||||
return text
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
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>
|
||||
`;
|
||||
|
||||
|
||||
2114
ui/table-bindings.js
2114
ui/table-bindings.js
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user