Merge branch 'Wx-2025:main' into main

This commit is contained in:
SilenceLurker
2025-11-05 21:52:07 +08:00
committed by GitHub
39 changed files with 3982 additions and 2037 deletions

View File

@@ -300,6 +300,10 @@ export async function callCwbAPI(systemPrompt, userPromptContent, options = {})
});
console.log("【消息内容】:", messages);
// 格式化并打印完整的提示词
const fullPromptText = messages.map(msg => `[${msg.role}]\n${msg.content}`).join('\n\n');
console.log("【完整提示词】:\n", fullPromptText);
try {
let responseContent;

View File

@@ -207,7 +207,6 @@ export const cwbDefaultSettings = {
cwb_tavern_profile: '',
cwb_break_armor_prompt: cwbCompleteDefaultSettings.cwb_break_armor_prompt,
cwb_char_card_prompt: cwbCompleteDefaultSettings.cwb_char_card_prompt,
cwb_incremental_char_card_prompt: cwbCompleteDefaultSettings.cwb_incremental_char_card_prompt,
cwb_prompt_version: '1.0.2',
cwb_auto_update_threshold: 20,
cwb_auto_update_enabled: false,

View File

@@ -235,11 +235,6 @@ async function proceedWithCardUpdate($panel, messagesToUse) {
messages.push({ role: "system", content: state.currentCharCardPrompt });
}
break;
case 'cwb_incremental_char_card_prompt':
if (state.isIncrementalUpdateEnabled && state.currentIncrementalCharCardPrompt) {
messages.push({ role: "system", content: state.currentIncrementalCharCardPrompt });
}
break;
case 'oldFiles':
if (state.isIncrementalUpdateEnabled) {
let oldFilesContent = "【旧档案】\n";

View File

@@ -1,13 +1,7 @@
import { state } from './cwb_state.js';
import { logError, logDebug, showToastr, parseCustomFormat } from './cwb_utils.js';
import {
safeLorebooks,
safeCharLorebooks,
safeLorebookEntries,
safeUpdateLorebookEntries,
compatibleWriteToLorebook,
} from '../../core/tavernhelper-compatibility.js';
import { amilyHelper } from '../../core/tavern-helper/main.js';
import { loadWorldInfo, saveWorldInfo } from "/scripts/world-info.js";
const { SillyTavern } = window;
@@ -20,7 +14,7 @@ export async function getTargetWorldBook() {
return state.customWorldBook;
}
try {
const charLorebooks = await safeCharLorebooks();
const charLorebooks = await amilyHelper.getCharLorebooks();
const primaryBook = charLorebooks.primary;
if (!primaryBook) {
showToastr('error', '当前角色未设置主世界书。');
@@ -44,12 +38,12 @@ export async function deleteLorebookEntries(uids) {
const book = await getTargetWorldBook();
if (!book) throw new Error('未找到目标世界书。');
const bookData = await amilyHelper.loadWorldInfo(book);
const bookData = await loadWorldInfo(book);
if (!bookData) throw new Error(`World book "${book}" not found.`);
uids.forEach(uid => {
delete bookData.entries[uid];
});
await amilyHelper.saveWorldInfo(book, bookData, true);
await saveWorldInfo(book, bookData, true);
} catch (error) {
logError('删除世界书条目失败:', error);
showToastr('error', `删除失败: ${error.message}`);
@@ -80,7 +74,7 @@ export async function saveDescriptionToLorebook(characterName, newDescription, s
return false;
}
const entries = (await safeLorebookEntries(bookName)) || [];
const entries = await amilyHelper.getLorebookEntries(bookName);
let existing = entries.find(e =>
Array.isArray(e.keys) &&
e.keys.includes(chatIdentifier) &&
@@ -97,7 +91,7 @@ export async function saveDescriptionToLorebook(characterName, newDescription, s
};
if (existing) {
await safeUpdateLorebookEntries(bookName, [{ uid: existing.uid, ...entryData }]);
await amilyHelper.setLorebookEntries(bookName, [{ uid: existing.uid, ...entryData }]);
} else {
const cwbEntries = entries.filter(e =>
Array.isArray(e.keys) &&
@@ -180,7 +174,7 @@ export async function updateCharacterRosterLorebookEntry(processedCharacterNames
return false;
}
let entries = (await safeLorebookEntries(bookName)) || [];
let entries = await amilyHelper.getLorebookEntries(bookName);
let existingRosterEntry = entries.find(entry =>
entry.comment === rosterEntryComment ||
entry.comment === `Amily2角色总集-${chatIdentifier}-角色总览`
@@ -208,9 +202,11 @@ export async function updateCharacterRosterLorebookEntry(processedCharacterNames
}
});
}
const floorRangeKey = existingRosterEntry.keys.find(k => /^\d+-\d+$/.test(k));
if (floorRangeKey) {
[oldStartFloor] = floorRangeKey.split('-').map(Number);
if (Array.isArray(existingRosterEntry.keys)) {
const floorRangeKey = existingRosterEntry.keys.find(k => /^\d+-\d+$/.test(k));
if (floorRangeKey) {
[oldStartFloor] = floorRangeKey.split('-').map(Number);
}
}
}
@@ -243,7 +239,7 @@ export async function updateCharacterRosterLorebookEntry(processedCharacterNames
};
if (existingRosterEntry) {
await safeUpdateLorebookEntries(bookName, [
await amilyHelper.setLorebookEntries(bookName, [
{ uid: existingRosterEntry.uid, comment: rosterEntryComment, ...entryData },
]);
} else {
@@ -274,7 +270,7 @@ export async function manageAutoCardUpdateLorebookEntry() {
const bookName = await getTargetWorldBook();
if (!bookName) return;
const entries = (await safeLorebookEntries(bookName)) || [];
const entries = await amilyHelper.getLorebookEntries(bookName);
const currentChatId = state.currentChatFileIdentifier;
if (!currentChatId || currentChatId.startsWith('unknown_chat')) {
@@ -303,7 +299,7 @@ export async function manageAutoCardUpdateLorebookEntry() {
}
if (entriesToUpdate.length > 0) {
await safeUpdateLorebookEntries(bookName, entriesToUpdate);
await amilyHelper.setLorebookEntries(bookName, entriesToUpdate);
logDebug(`已为聊天: ${cleanChatId} 管理了 ${entriesToUpdate.length} 个世界书条目的状态。`);
}

View File

@@ -6,8 +6,9 @@ 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, TavernHelper } = window;
const { jQuery: $, SillyTavern } = window;
function createCharCardViewerPopupHtml(displayItems) {
const pathToLabelMap = {
@@ -184,7 +185,7 @@ function createCharCardViewerPopupHtml(displayItems) {
}
function bindCharCardViewerPopupEvents($popup) {
$popup.on('change', '.cwb-insertion-position', function () {
$popup.on('change', '.cwb-insertion-position', function() {
const $this = $(this);
const $depthContainer = $this.closest('.cwb-insertion-settings-content').find('.cwb-insertion-depth-container');
if ($this.val() === 'at_depth') {
@@ -200,7 +201,7 @@ function bindCharCardViewerPopupEvents($popup) {
showCharCardViewerPopup();
});
$popup.find('#cwb-manual-update-btn').on('click', async function () {
$popup.find('#cwb-manual-update-btn').on('click', async function() {
const $button = $(this);
$button.prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i> 更新中...');
await manualUpdateLogic();
@@ -217,7 +218,7 @@ function bindCharCardViewerPopupEvents($popup) {
$popup.find(`#cwb-char-content-${targetUid}`).addClass('active');
});
$popup.find('.cwb-cyber-tab__delete').on('click', async function (e) {
$popup.find('.cwb-cyber-tab__delete').on('click', async function(e) {
e.stopPropagation();
if (confirm('您确定要删除这个角色条目吗?此操作不可撤销。')) {
const uidToDelete = $(this).data('char-uid');
@@ -235,9 +236,9 @@ function bindCharCardViewerPopupEvents($popup) {
}
});
$popup.find('#cwb-viewer-delete-all').on('click', async function () {
$popup.find('#cwb-viewer-delete-all').on('click', async function() {
if (confirm('您确定要清除当前聊天中的所有角色卡和总览吗?此操作将删除所有相关条目,且不可撤销。')) {
const allUids = $popup.find('.cwb-cyber-tab__button').map(function () {
const allUids = $popup.find('.cwb-cyber-tab__button').map(function() {
return $(this).data('char-uid');
}).get();
if (allUids.length > 0) {
@@ -278,22 +279,11 @@ function bindCharCardViewerPopupEvents($popup) {
if ($field.data('is-array')) {
value = value.split('\n').map(l => l.trim()).filter(Boolean);
}
if (path) {
setNestedValue(collectedData, path, value);
if(path){
setNestedValue(collectedData, path, value);
}
});
let localTavernHelper = TavernHelper;
if (!localTavernHelper) {
// TavernHelper 未定义的情况下触发,但是为什么?
(localTavernHelper = window.TavernHelper);
if (localTavernHelper) {
TavernHelper = localTavernHelper;
}
}
const finalContentToSave = buildCustomFormat(collectedData);
const allEntries = await TavernHelper.getLorebookEntries(book);
const entryToUpdate = allEntries.find(e => e.uid === targetUid);
if (!entryToUpdate) throw new Error('无法在世界书中找到原始条目。');
const insertionPosition = $activePane.find('.cwb-insertion-position').val();
const insertionDepth = parseInt($activePane.find('.cwb-insertion-depth').val(), 10);
@@ -313,36 +303,27 @@ function bindCharCardViewerPopupEvents($popup) {
'at_depth': 'at_depth_as_system'
};
const finalEntryData = { ...entryToUpdate };
const finalEntryData = {
uid: targetUid,
content: finalContentToSave,
position: positionMap[insertionPosition] || 'before_character_definition',
order: isNaN(insertionOrder) ? 7001 : insertionOrder,
};
finalEntryData.content = finalContentToSave;
finalEntryData.uid = targetUid;
const newPosition = positionMap[insertionPosition];
finalEntryData.position = newPosition || 'before_character_definition';
if (insertionPosition === 'at_depth') {
finalEntryData.depth = isNaN(insertionDepth) ? 0 : insertionDepth;
} else {
finalEntryData.depth = null;
}
finalEntryData.order = isNaN(insertionOrder) ? 7001 : insertionOrder;
logDebug(`[DEBUG] 最终保存数据 UID:${targetUid}`, {
position: finalEntryData.position,
depth: finalEntryData.depth,
order: finalEntryData.order,
hasDepthField: 'depth' in finalEntryData
});
localTavernHelper = TavernHelper;
if (!localTavernHelper) {
// TavernHelper 未定义的情况下触发,但是为什么?
(localTavernHelper = window.TavernHelper);
if (localTavernHelper) {
TavernHelper = localTavernHelper;
}
}
await TavernHelper.setLorebookEntries(book, [finalEntryData]);
await amilyHelper.setLorebookEntries(book, [finalEntryData]);
showToastr('success', '角色卡已成功保存!');
} catch (error) {
logError('保存角色卡失败:', error);
@@ -368,15 +349,7 @@ export async function showCharCardViewerPopup() {
bindCharCardViewerPopupEvents($(`#${CHAR_CARD_VIEWER_POPUP_ID}`));
return;
}
let localTavernHelper = TavernHelper;
if (!localTavernHelper) {
// TavernHelper 未定义的情况下触发,但是为什么?
(localTavernHelper = window.TavernHelper);
if (localTavernHelper) {
TavernHelper = localTavernHelper;
}
}
const allEntries = await TavernHelper.getLorebookEntries(book);
const allEntries = await amilyHelper.getLorebookEntries(book);
let currentChatId = state.currentChatFileIdentifier;
if (!currentChatId || currentChatId.startsWith('unknown_chat')) {
@@ -430,47 +403,52 @@ export async function showCharCardViewerPopup() {
const characterEntries = relevantEntries
.filter(entry => !entry.keys.includes('Amily2角色总集'))
.map(entry => {
logDebug(`[DEBUG] 原始条目数据 UID:${entry.uid}`, {
position: entry.position,
depth: entry.depth,
order: entry.order,
comment: entry.comment
});
try {
logDebug(`[DEBUG] 原始条目数据 UID:${entry.uid}`, {
position: entry.position,
depth: entry.depth,
order: entry.order,
comment: entry.comment
});
const positionStringMap = {
0: 'before_char',
1: 'after_char',
2: 'before_an',
3: 'after_an',
4: 'at_depth',
'before_character_definition': 'before_char',
'after_character_definition': 'after_char',
'before_author_note': 'before_an',
'after_author_note': 'after_an',
'at_depth_as_system': 'at_depth'
};
const positionStringMap = {
0: 'before_char',
1: 'after_char',
2: 'before_an',
3: 'after_an',
4: 'at_depth',
'before_character_definition': 'before_char',
'after_character_definition': 'after_char',
'before_author_note': 'before_an',
'after_author_note': 'after_an',
'at_depth_as_system': 'at_depth'
};
const position = entry.position;
const mappedPosition = positionStringMap[position] || 'at_depth';
const finalDepth = (position === 4 || position === 'at_depth_as_system') ? (entry.depth ?? 0) : 0;
logDebug(`[DEBUG] 映射结果 UID:${entry.uid}`, {
originalPosition: position,
mappedPosition: mappedPosition,
finalDepth: finalDepth
});
const position = entry.position;
const mappedPosition = positionStringMap[position] || 'at_depth';
const finalDepth = (position === 4 || position === 'at_depth_as_system') ? (entry.depth ?? 0) : 0;
logDebug(`[DEBUG] 映射结果 UID:${entry.uid}`, {
originalPosition: position,
mappedPosition: mappedPosition,
finalDepth: finalDepth
});
return {
uid: entry.uid,
isRoster: false,
comment: entry.comment,
content: entry.content,
parsed: parseCustomFormat(entry.content),
insertionPosition: mappedPosition,
insertionDepth: finalDepth,
insertionOrder: entry.order ?? 7001,
};
return {
uid: entry.uid,
isRoster: false,
comment: entry.comment,
content: entry.content,
parsed: parseCustomFormat(entry.content),
insertionPosition: mappedPosition,
insertionDepth: finalDepth,
insertionOrder: entry.order ?? 7001,
};
} catch (e) {
logError(`解析角色条目失败 (UID: ${entry.uid}),已跳过。`, e);
return null;
}
})
.filter(c => c.parsed && Object.keys(c.parsed).length > 0);
.filter(c => c && c.parsed && Object.keys(c.parsed).length > 0);
displayItems = displayItems.concat(characterEntries);
@@ -641,39 +619,39 @@ export function updateViewerButtonVisibility() {
export function bindCwbApiEvents() {
console.log('[CWB] Binding API events');
$('#cwb-api-url').off('input').on('input', function () {
$('#cwb-api-url').off('input').on('input', function() {
const value = $(this).val();
extension_settings[extensionName].cwb_api_url = value;
saveSettingsDebounced();
});
$('#cwb-api-key').off('input').on('input', function () {
$('#cwb-api-key').off('input').on('input', function() {
const value = $(this).val();
extension_settings[extensionName].cwb_api_key = value;
saveSettingsDebounced();
});
$('#cwb-model').off('input').on('input', function () {
$('#cwb-model').off('input').on('input', function() {
const value = $(this).val();
extension_settings[extensionName].cwb_model = value;
saveSettingsDebounced();
});
$('#cwb-temperature').off('input').on('input', function () {
$('#cwb-temperature').off('input').on('input', function() {
const value = parseFloat($(this).val());
$('#cwb-temperature-value').text(value);
extension_settings[extensionName].cwb_temperature = value;
saveSettingsDebounced();
});
$('#cwb-max-tokens').off('input').on('input', function () {
$('#cwb-max-tokens').off('input').on('input', function() {
const value = parseInt($(this).val());
$('#cwb-max-tokens-value').text(value);
extension_settings[extensionName].cwb_max_tokens = value;
saveSettingsDebounced();
});
$('#cwb-test-connection').off('click').on('click', async function () {
$('#cwb-test-connection').off('click').on('click', async function() {
const $button = $(this);
$button.prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i> 测试中...');
@@ -686,7 +664,7 @@ export function bindCwbApiEvents() {
}
});
$('#cwb-fetch-models').off('click').on('click', async function () {
$('#cwb-fetch-models').off('click').on('click', async function() {
const $button = $(this);
$button.prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i> 获取中...');

View File

@@ -542,3 +542,4 @@ export const sectionTitles = {
cwb_summarizer_incremental: '角色世界书(CWB-增量)',
novel_processor: '小说处理',
};

View File

@@ -460,3 +460,80 @@
min-height: 0;
max-height: none; /* 覆盖之前写死的max-height */
}
/* 响应式设计:移动端适配 */
@media (max-width: 768px) {
#world-editor-container .world-editor-header-controls,
#world-editor-container .world-editor-toolbar-left,
#world-editor-container .world-editor-toolbar-right {
flex-direction: column;
align-items: stretch;
width: 100%;
}
#world-editor-container .world-editor-btn {
width: 100%;
margin-bottom: 5px;
}
#world-editor-container .world-editor-search-box {
width: 100%;
box-sizing: border-box;
}
#world-editor-container .world-editor-entries-header {
display: none; /* 在移动端隐藏表头 */
}
#world-editor-container .world-editor-entry-row {
grid-template-columns: 40px 1fr; /* 简化为两列:复选框和内容 */
padding: 10px;
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-entry-row {
display: grid;
grid-template-columns: auto 1fr; /* 复选框和内容区 */
gap: 10px;
padding: 10px;
}
#world-editor-container .world-editor-entry-row > div {
display: block !important; /* 确保所有单元格都可见 */
text-align: left;
padding: 5px 0;
}
#world-editor-container .world-editor-entry-row::before {
display: block;
font-weight: bold;
color: #888;
}
#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; }
#world-editor-container .world-editor-entry-keys,
#world-editor-container .world-editor-entry-content {
white-space: normal;
max-width: 100%;
}
#world-editor-container .world-editor-batch-actions {
flex-direction: column;
}
#world-editor-container .world-editor-batch-actions .world-editor-btn {
width: auto;
}
}

View File

@@ -428,37 +428,8 @@ class WorldEditor {
*/
async updateEntriesWithNativeMethod(entriesToUpdate) {
try {
const bookData = await loadWorldInfo(this.currentWorldBook);
if (!bookData || !bookData.entries) {
throw new Error("无法加载世界书数据。");
}
const uidsToUpdate = new Set(entriesToUpdate.map(e => e.uid));
const updatedUIDs = new Set();
// 更新 bookData.entries
for (const entry of entriesToUpdate) {
if (bookData.entries[entry.uid]) {
const nativeEntry = bookData.entries[entry.uid];
nativeEntry.comment = entry.comment;
nativeEntry.content = entry.content;
nativeEntry.key = entry.keys;
nativeEntry.disable = !entry.enabled;
nativeEntry.constant = entry.type === 'constant';
nativeEntry.position = this.convertPositionToNative(entry.position);
nativeEntry.depth = entry.depth;
nativeEntry.order = entry.order;
nativeEntry.exclude_recursion = entry.exclude_recursion;
nativeEntry.prevent_recursion = entry.prevent_recursion;
updatedUIDs.add(entry.uid);
}
}
if (updatedUIDs.size !== uidsToUpdate.size) {
console.warn("[世界书编辑器] 部分条目更新失败UID可能不存在。");
}
await saveWorldInfo(this.currentWorldBook, bookData, true); // true 表示静默保存
// 将所有更新逻辑统一到 amilyHelper.setLorebookEntries
await amilyHelper.setLorebookEntries(this.currentWorldBook, entriesToUpdate);
// Optimistic UI update in local state
for (const updatedEntry of entriesToUpdate) {

View File

@@ -184,12 +184,12 @@
<div class="mhb-controls-wrapper">
<div class="manual-command-block">
<label>手动熔铸范围:</label>
<label>手动总结范围:</label>
<input type="number" id="amily2_mhb_small_start_floor" class="manual-input" placeholder="起始层">
<span class="manual-command-divider">-</span>
<input type="number" id="amily2_mhb_small_end_floor" class="manual-input" placeholder="结束层">
<button id="amily2_mhb_small_manual_execute" class="menu_button primary small_button interactable">
<i class="fas fa-fire"></i> 熔铸
<i class="fas fa-fire"></i> 开始
</button>
</div>
@@ -220,12 +220,12 @@
<div class="auto-command-block" id="amily2_mhb_auto_command_block">
<button id="amily2_mhb_small_expedition_execute" class="menu_button primary small_button interactable" title="立即发动一次彻底的总结远征,将所有未归档的历史一次性清算。">
<i class="fas fa-flag-checkered"></i> 开始远征
<i class="fas fa-flag-checkered"></i> 自动批量
</button>
<div class="auto-control-pair">
<label for="amily2_mhb_small_auto_enabled" title="在您聊天时,于后台默默守护史册的完整。">自动巡录:</label>
<label for="amily2_mhb_small_auto_enabled" title="在您聊天时,于后台默默守护史册的完整。">静默总结:</label>
<label class="toggle-switch">
<input id="amily2_mhb_small_auto_enabled" type="checkbox" />
<span class="slider"></span>
@@ -234,7 +234,7 @@
<div class="auto-control-pair">
<label for="historiography_write_to_lorebook" title="将生成的总结写入世界书。">写入史册:</label>
<label for="historiography_write_to_lorebook" title="将生成的总结写入世界书。">存世界书:</label>
<label class="toggle-switch">
<input id="historiography_write_to_lorebook" type="checkbox" checked />
<span class="slider"></span>
@@ -242,7 +242,7 @@
</div>
<div class="auto-control-pair">
<label for="historiography_ingest_to_rag" title="将生成的总结存入翰林院进行向量化。">存入翰林院:</label>
<label for="historiography_ingest_to_rag" title="将生成的总结存入翰林院进行向量化。">上传向量:</label>
<label class="toggle-switch">
<input id="historiography_ingest_to_rag" type="checkbox" />
<span class="slider"></span>
@@ -250,7 +250,7 @@
</div>
<div class="auto-control-pair">
<label for="amily2_mhb_small_trigger_count" title="“自动巡录”和“开始远征”的单次作战楼层数。">远征阈值:</label>
<label for="amily2_mhb_small_trigger_count" title="“自动巡录”和“开始远征”的单次作战楼层数。">总结阈值:</label>
<input id="amily2_mhb_small_trigger_count" type="number" min="1" class="text_pole" style="width: 70px;" placeholder="30">
</div>

View File

@@ -87,6 +87,48 @@
from { text-shadow: 0 0 5px rgba(255, 107, 107, 0.5); }
to { text-shadow: 0 0 10px rgba(255, 107, 107, 0.8), 0 0 15px rgba(255, 107, 107, 0.3); }
}
.collapsible-legend {
cursor: pointer;
user-select: none;
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
.collapsible-legend:hover {
background-color: rgba(255, 255, 255, 0.05);
}
.collapse-icon {
transition: transform 0.2s ease-in-out;
}
.collapsible-content {
padding-top: 10px;
}
.disclaimer-box {
margin-top: 15px;
padding: 12px;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
background-color: rgba(0, 0, 0, 0.1);
}
.disclaimer-emo {
font-style: italic;
color: #adb6e6;
text-align: center;
margin-bottom: 10px;
font-size: 13px;
}
.disclaimer-text {
font-size: 12px;
color: #c0c0c0;
line-height: 1.6;
}
.disclaimer-text strong {
color: #ffc107;
display: block;
margin-bottom: 5px;
}
</style>
<div class="flex-container">
<div id="amily2_chat_optimiser">
@@ -123,9 +165,9 @@
<fieldset class="settings-group">
<legend><i class="fas fa-plus-circle"></i> 记忆增强</legend>
<div class="button-group" style="display: flex; justify-content: space-between; gap: 8px;">
<button id="amily2_open_additional_features" class="menu_button wide_button"><i class="fas fa-landmark-dome"></i> 内阁密室</button>
<button id="amily2_open_rag_palace" class="menu_button wide_button"><i class="fas fa-brain"></i> 翰林学院</button>
<button id="amily2_open_memorisation_forms" class="menu_button wide_button"><i class="fas fa-table"></i> 内存储司</button>
<button id="amily2_open_additional_features" class="menu_button wide_button"><i class="fas fa-landmark-dome"></i> 总结模块</button>
<button id="amily2_open_rag_palace" class="menu_button wide_button"><i class="fas fa-brain"></i> 向量模块</button>
<button id="amily2_open_memorisation_forms" class="menu_button wide_button"><i class="fas fa-table"></i> 表格模块</button>
<button id="amily2_open_character_world_book" class="menu_button wide_button"><i class="fa-solid fa-book-atlas"></i> 角色世界</button>
</div>
</fieldset>
@@ -136,6 +178,7 @@
<button id="amily2_open_plot_optimization" class="menu_button wide_button"><i class="fas fa-feather-alt"></i> 剧情优化</button>
<button id="amily2_open_world_editor" class="menu_button wide_button"><i class="fas fa-globe"></i> 世界编辑</button>
<button id="amily2_open_glossary" class="menu_button wide_button"><i class="fas fa-book"></i> 术语表单</button>
<button id="amily2_open_renderer" class="menu_button wide_button"><i class="fas fa-paint-brush"></i> 前端渲染</button>
</div>
</fieldset>
@@ -169,11 +212,18 @@
</div>
</fieldset>
<div class="disclaimer-box">
<p class="disclaimer-emo">“我也想过琴棋书画诗酒花,奈何生活只有柴米油盐酱醋茶。”</p>
<p class="disclaimer-text">
<strong>免责声明:</strong>本插件仅供个人学习与技术交流使用,严禁用于任何商业目的或非法活动。使用者需自行承担因使用本插件而产生的一切风险与法律责任,开发者对此不承担任何责任。
</p>
</div>
<hr class="header-divider">
<fieldset class="settings-group">
<legend><i class="fas fa-cogs"></i> 正文优化</legend>
<fieldset class="settings-group collapsible">
<legend class="collapsible-legend"><i class="fas fa-cogs"></i> 正文优化 <i class="fas fa-chevron-down collapse-icon"></i></legend>
<div class="collapsible-content">
<div class="control-pair-container" style="justify-content: space-around;">
<div class="amily2_settings_block">
@@ -227,10 +277,12 @@
</div>
<small class="notes">无感优化:直接替换文本,速度更快但要关流式,高楼层推荐。刷新优化:重载聊天界面,更加稳定无需关流式,低楼层推荐。</small>
</div>
</div>
</fieldset>
<fieldset class="settings-group">
<legend><i class="fas fa-network-wired"></i> API与模型配置</legend>
<fieldset class="settings-group collapsible">
<legend class="collapsible-legend"><i class="fas fa-network-wired"></i> API与模型配置 <i class="fas fa-chevron-down collapse-icon"></i></legend>
<div class="collapsible-content">
<div class="amily2_settings_block">
<label for="amily2_api_provider">API 提供商</label>
<select id="amily2_api_provider" class="text_pole">
@@ -289,11 +341,12 @@
<label for="amily2_context_messages">上下文参考数: <span id="amily2_context_messages_value"></span></label>
<input id="amily2_context_messages" type="range" min="0" max="10" step="1" />
</div>
</div>
</fieldset>
<fieldset class="settings-group">
<legend><i class="fas fa-edit"></i> 统一提示词编辑器</legend>
<fieldset class="settings-group collapsible">
<legend class="collapsible-legend"><i class="fas fa-edit"></i> 统一提示词编辑器 <i class="fas fa-chevron-down collapse-icon"></i></legend>
<div class="collapsible-content">
<div class="amily2_settings_block">
<div class="label-with-button">
@@ -314,11 +367,12 @@
<button id="amily2_unified_restore_button" class="menu_button secondary small_button interactable"><i class="fas fa-undo"></i> 恢复默认</button>
</div>
</div>
</div>
</fieldset>
<fieldset class="settings-group">
<legend><i class="fas fa-book-open"></i> 世界书档案司</legend>
<fieldset class="settings-group collapsible">
<legend class="collapsible-legend"><i class="fas fa-book-open"></i> 世界书档案司 <i class="fas fa-chevron-down collapse-icon"></i></legend>
<div class="collapsible-content">
<div class="amily2_settings_block">
<label for="amily2_wb_enabled">启用世界书</label>
<label class="toggle-switch">
@@ -366,6 +420,7 @@
</div>
</div>
</div>
</div>
</fieldset>
<fieldset class="settings-group">
<legend><i class="fas fa-gavel"></i> 总结与律法</legend>
@@ -413,11 +468,12 @@
</fieldset>
<fieldset class="settings-group">
<legend><i class="fas fa-palette"></i> 界面定制</legend>
<div class="amily2_settings_block">
<label>帝国徽记位置:</label>
<div class="radio-toggle-group">
<fieldset class="settings-group collapsible">
<legend class="collapsible-legend"><i class="fas fa-palette"></i> 界面定制 <i class="fas fa-chevron-down collapse-icon"></i></legend>
<div class="collapsible-content">
<div class="amily2_settings_block">
<label>帝国徽记位置:</label>
<div class="radio-toggle-group">
<input type="radio" id="amily2_icon_location_topbar" name="amily2_icon_location" value="topbar">
<label for="amily2_icon_location_topbar">驻扎顶栏</label>
<input type="radio" id="amily2_icon_location_extensions" name="amily2_icon_location" value="extensions">
@@ -454,8 +510,8 @@
</label>
<input type="file" id="amily2_custom_bg_image" accept="image/*" style="display: none;">
<button id="amily2_restore_bg_image" class="menu_button small_button">默认</button>
<small class="notes">选择一张图片作为背景。推荐使用小于5MB的图片。</small>
</div>
<small class="notes">选择一张图片作为背景。推荐使用小于5MB的图片。</small>
</div>
</fieldset>

View File

@@ -35,6 +35,13 @@
<span class="slider"></span>
</label>
</div>
<div class="hly-control-block" style="flex-direction: row; justify-content: space-between; align-items: center;">
<label for="hly-independent-chat-memory-toggle" title="启用后,每个聊天文件都将拥有独立的知识库。关闭后,同一角色的所有聊天将共享同一个知识库。">独立聊天记忆</label>
<label class="hly-toggle-switch">
<input type="checkbox" id="hly-independent-chat-memory-enabled" data-setting-key="retrieval.independentChatMemoryEnabled" data-type="boolean">
<span class="slider"></span>
</label>
</div>
</fieldset>
<div class="hly-edict-row">
<div class="hly-edict-item">
@@ -428,6 +435,22 @@
<small class="hly-notes">每次调用API时处理的文本数量。</small>
</div>
</fieldset>
<fieldset class="hly-settings-group">
<legend><i class="fas fa-wand-magic-sparkles"></i> 检索预处理</legend>
<div class="hly-control-block" style="flex-direction: row; justify-content: space-between; align-items: center;">
<label for="hly-query-preprocessing-enabled" title="启用后,将在向量检索前对您的提问进行净化处理,以提高准确性。">启用检索预处理</label>
<label class="hly-toggle-switch">
<input type="checkbox" id="hly-query-preprocessing-enabled" data-setting-key="queryPreprocessing.enabled" data-type="boolean">
<span class="slider"></span>
</label>
</div>
<div class="hly-button-group" style="justify-content: flex-start;">
<button id="hly-query-preprocessing-rules-btn" class="hly-action-button">配置处理规则</button>
</div>
<small class="hly-notes">此功能类似于“凝识法则”,可对您最近的几条聊天记录(即用于检索的文本)进行标签提取和内容排除,以生成更纯净、更高效的检索查询。</small>
</fieldset>
<fieldset class="hly-settings-group">
<legend><i class="fas fa-wand-magic-sparkles"></i> 圣言注入 (按来源)</legend>
<div style="text-align: center; margin-bottom: 10px;">

26
assets/renderer.css Normal file
View File

@@ -0,0 +1,26 @@
#amily2_renderer_panel {
padding: 10px;
}
.amily2-renderer-info-container {
margin-top: 20px;
padding: 15px;
background-color: rgba(45, 45, 55, 0.5);
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.emo-statement {
font-style: italic;
color: #d1c4e9;
text-align: center;
margin-bottom: 15px;
text-shadow: 0 0 5px rgba(209, 196, 233, 0.5);
}
.description-text {
font-size: 14px;
color: #dddddd;
line-height: 1.6;
}

View File

@@ -1,4 +1,3 @@
:root {
--amily2-bg-color: #2C2C2C;
--amily2-button-color: #4A4A4A;
@@ -172,6 +171,13 @@ hr.header-divider {
color: #ff9800;
}
/* === Collapsible Legend Fix === */
.collapsible-legend {
position: relative; /* Establish a stacking context */
z-index: 2; /* Ensure it's above sibling content */
cursor: pointer; /* Indicate it's clickable */
}
#amily2_chat_optimiser .color-controls-container {
flex-direction: row !important;
@@ -719,3 +725,7 @@ hr.header-divider {
#amily2_chat_optimiser .prompt-editor-area textarea {
flex: 1;
}
#amily2_test_api_connection {
margin-left: 10px;
}

View File

@@ -379,30 +379,81 @@ async function fetchSillyTavernPresetModels() {
export function getApiSettings() {
const settings = extension_settings[extensionName] || {};
const apiProvider = document.getElementById('amily2_api_provider')?.value || 'openai';
let model;
if (apiProvider === 'sillytavern_preset') {
const context = getContext();
const profileId = document.getElementById('amily2_preset_selector')?.value;
const profile = context.extensionSettings?.connectionManager?.profiles?.find(p => p.id === profileId);
model = profile?.openai_model || 'Preset Model';
} else {
model = document.getElementById('amily2_model')?.value;
}
return {
apiProvider: $("#amily2_api_provider").val() || 'openai',
apiUrl: $("#amily2_api_url").val().trim(),
apiKey: $("#amily2_api_key").val().trim(),
model: $("#amily2_model").val(),
maxTokens: extension_settings[extensionName]?.maxTokens || 4000,
temperature: extension_settings[extensionName]?.temperature || 0.7,
tavernProfile: extension_settings[extensionName]?.tavernProfile || ''
apiProvider: apiProvider,
apiUrl: document.getElementById('amily2_api_url')?.value.trim() || '',
apiKey: document.getElementById('amily2_api_key')?.value.trim() || '',
model: model,
maxTokens: settings.maxTokens || 4000,
temperature: settings.temperature || 0.7,
tavernProfile: document.getElementById('amily2_preset_selector')?.value || ''
};
}
export async function testApiConnection() {
console.log('[Amily2号-外交部] 开始API连接测试');
const $button = $("#amily2_test_api_connection");
if (!$button.length) return;
const apiProvider = $("#amily2_api_provider").val() || 'openai';
const models = await fetchModels();
const originalHtml = $button.html();
$button.prop("disabled", true).html('<i class="fas fa-spinner fa-spin"></i> 测试中');
if (models.length > 0) {
toastr.success(`${apiProvider} 提供商连接正常,找到 ${models.length} 个模型`, 'API连接正常');
return true;
} else {
toastr.error('无法获取模型列表,请检查配置', 'API连接失败');
try {
const apiSettings = getApiSettings();
if (apiSettings.apiProvider === 'sillytavern_preset') {
if (!apiSettings.tavernProfile) {
throw new Error("请先在下方选择一个SillyTavern预设");
}
} else {
if (!apiSettings.apiUrl || !apiSettings.apiKey || !apiSettings.model) {
throw new Error("API配置不完整请检查URL、Key和模型选择");
}
}
toastr.info('正在发送测试消息"你好!"...', 'API连接测试');
const userName = getContext()?.name1 || '用户';
const systemPrompt = `接下来用户会使用测试按钮测试api连接是否正常请回复ta连接是正常的称呼用户为${userName}大人”并复述群号1060183271告知ta本扩展开发者“诗与酒.”正静待ta的入驻。`;
const testMessages = [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: '你好!' }
];
const response = await callAI(testMessages, {
maxTokens: 8192,
temperature: 0.5
});
if (response && response.trim()) {
console.log('[Amily2号-外交部] 测试消息响应:', response);
toastr.success(`连接测试成功AI回复: "${response}"`, 'API连接测试成功');
return true;
} else {
throw new Error('API未返回有效响应请检查您的代理、API URL和密钥是否正确。这通常发生在网络问题或认证失败时。');
}
} catch (error) {
console.error('[Amily2号-使节团] API连接测试失败:', error);
toastr.error(`连接测试失败: ${error.message}`, 'API连接测试失败');
return false;
} finally {
$button.prop("disabled", false).html(originalHtml);
}
}
@@ -531,28 +582,30 @@ async function callOpenAICompatible(messages, options) {
}
async function callOpenAITest(messages, options) {
const body = {
chat_completion_source: 'openai',
messages: messages,
model: options.model,
reverse_proxy: options.apiUrl,
proxy_password: options.apiKey,
stream: false,
max_tokens: options.maxTokens || 30000,
temperature: options.temperature || 1,
top_p: options.top_p || 1,
custom_prompt_post_processing: 'strict',
enable_web_search: false,
frequency_penalty: 0,
group_names: [],
include_reasoning: false,
presence_penalty: 0.12,
reasoning_effort: 'medium',
request_images: false,
};
const response = await fetch('/api/backends/chat-completions/generate', {
method: 'POST',
headers: { ...getRequestHeaders(), 'Content-Type': 'application/json' },
body: JSON.stringify({
chat_completion_source: 'openai',
custom_prompt_post_processing: 'strict',
enable_web_search: false,
frequency_penalty: 0,
group_names: [],
include_reasoning: false,
max_tokens: options.maxTokens || 100000,
messages: messages,
model: options.model,
presence_penalty: 0.12,
proxy_password: options.apiKey,
reasoning_effort: 'medium',
request_images: false,
reverse_proxy: options.apiUrl,
stream: false,
temperature: options.temperature || 1,
top_p: options.top_p || 1
})
body: JSON.stringify(body)
});
if (!response.ok) {
@@ -561,6 +614,15 @@ async function callOpenAITest(messages, options) {
}
const responseData = await response.json();
if (!responseData || !responseData.choices || responseData.choices.length === 0) {
console.error('[Amily2号-OpenAI兼容(测试)] API返回了空的choices数组或错误:', responseData);
if (responseData.error) {
throw new Error(`API返回错误: ${responseData.error.message || JSON.stringify(responseData.error)}`);
}
return null;
}
return responseData?.choices?.[0]?.message?.content;
}

View File

@@ -1,6 +1,7 @@
import { extension_settings, getContext } from "/scripts/extensions.js";
import { characters, this_chid, getRequestHeaders, saveSettingsDebounced, eventSource, event_types } from "/script.js";
import { extensionName } from "../../utils/settings.js";
import { amilyHelper } from '../../core/tavern-helper/main.js';
let ChatCompletionService = undefined;
try {
@@ -184,10 +185,6 @@ async function callJqyhOpenAITest(messages, options) {
async function callJqyhSillyTavernPreset(messages, options) {
console.log('[Amily2号-JqyhST预设] 使用SillyTavern预设调用');
if (!window.TavernHelper || !window.TavernHelper.triggerSlash) {
throw new Error('TavernHelper不可用无法使用SillyTavern预设模式');
}
const context = getContext();
if (!context) {
throw new Error('无法获取SillyTavern上下文');
@@ -202,7 +199,7 @@ async function callJqyhSillyTavernPreset(messages, options) {
let responsePromise;
try {
originalProfile = await window.TavernHelper.triggerSlash('/profile');
originalProfile = await amilyHelper.triggerSlash('/profile');
console.log(`[Amily2号-JqyhST预设] 当前配置文件: ${originalProfile}`);
const targetProfile = context.extensionSettings?.connectionManager?.profiles?.find(p => p.id === profileId);
@@ -213,11 +210,11 @@ async function callJqyhSillyTavernPreset(messages, options) {
const targetProfileName = targetProfile.name;
console.log(`[Amily2号-JqyhST预设] 目标配置文件: ${targetProfileName}`);
const currentProfile = await window.TavernHelper.triggerSlash('/profile');
const currentProfile = await amilyHelper.triggerSlash('/profile');
if (currentProfile !== targetProfileName) {
console.log(`[Amily2号-JqyhST预设] 切换配置文件: ${currentProfile} -> ${targetProfileName}`);
const escapedProfileName = targetProfileName.replace(/"/g, '\\"');
await window.TavernHelper.triggerSlash(`/profile await=true "${escapedProfileName}"`);
await amilyHelper.triggerSlash(`/profile await=true "${escapedProfileName}"`);
}
if (!context.ConnectionManagerRequestService) {
@@ -233,11 +230,11 @@ async function callJqyhSillyTavernPreset(messages, options) {
} finally {
try {
const currentProfileAfterCall = await window.TavernHelper.triggerSlash('/profile');
const currentProfileAfterCall = await amilyHelper.triggerSlash('/profile');
if (originalProfile && originalProfile !== currentProfileAfterCall) {
console.log(`[Amily2号-JqyhST预设] 恢复原始配置文件: ${currentProfileAfterCall} -> ${originalProfile}`);
const escapedOriginalProfile = originalProfile.replace(/"/g, '\\"');
await window.TavernHelper.triggerSlash(`/profile await=true "${escapedOriginalProfile}"`);
await amilyHelper.triggerSlash(`/profile await=true "${escapedOriginalProfile}"`);
}
} catch (restoreError) {
console.error('[Amily2号-JqyhST预设] 恢复配置文件失败:', restoreError);

View File

@@ -1,6 +1,7 @@
import { extension_settings, getContext } from "/scripts/extensions.js";
import { characters, this_chid, getRequestHeaders, saveSettingsDebounced, eventSource, event_types } from "/script.js";
import { extensionName } from "../../utils/settings.js";
import { amilyHelper } from '../../core/tavern-helper/main.js';
let ChatCompletionService = undefined;
try {
@@ -184,10 +185,6 @@ async function callNccsOpenAITest(messages, options) {
async function callNccsSillyTavernPreset(messages, options) {
console.log('[Amily2号-NccsST预设] 使用SillyTavern预设调用');
if (!window.TavernHelper || !window.TavernHelper.triggerSlash) {
throw new Error('TavernHelper不可用无法使用SillyTavern预设模式');
}
const context = getContext();
if (!context) {
throw new Error('无法获取SillyTavern上下文');
@@ -202,7 +199,7 @@ async function callNccsSillyTavernPreset(messages, options) {
let responsePromise;
try {
originalProfile = await window.TavernHelper.triggerSlash('/profile');
originalProfile = await amilyHelper.triggerSlash('/profile');
console.log(`[Amily2号-NccsST预设] 当前配置文件: ${originalProfile}`);
const targetProfile = context.extensionSettings?.connectionManager?.profiles?.find(p => p.id === profileId);
@@ -213,11 +210,11 @@ async function callNccsSillyTavernPreset(messages, options) {
const targetProfileName = targetProfile.name;
console.log(`[Amily2号-NccsST预设] 目标配置文件: ${targetProfileName}`);
const currentProfile = await window.TavernHelper.triggerSlash('/profile');
const currentProfile = await amilyHelper.triggerSlash('/profile');
if (currentProfile !== targetProfileName) {
console.log(`[Amily2号-NccsST预设] 切换配置文件: ${currentProfile} -> ${targetProfileName}`);
const escapedProfileName = targetProfileName.replace(/"/g, '\\"');
await window.TavernHelper.triggerSlash(`/profile await=true "${escapedProfileName}"`);
await amilyHelper.triggerSlash(`/profile await=true "${escapedProfileName}"`);
}
if (!context.ConnectionManagerRequestService) {
@@ -233,11 +230,11 @@ async function callNccsSillyTavernPreset(messages, options) {
} finally {
try {
const currentProfileAfterCall = await window.TavernHelper.triggerSlash('/profile');
const currentProfileAfterCall = await amilyHelper.triggerSlash('/profile');
if (originalProfile && originalProfile !== currentProfileAfterCall) {
console.log(`[Amily2号-NccsST预设] 恢复原始配置文件: ${currentProfileAfterCall} -> ${originalProfile}`);
const escapedOriginalProfile = originalProfile.replace(/"/g, '\\"');
await window.TavernHelper.triggerSlash(`/profile await=true "${escapedOriginalProfile}"`);
await amilyHelper.triggerSlash(`/profile await=true "${escapedOriginalProfile}"`);
}
} catch (restoreError) {
console.error('[Amily2号-NccsST预设] 恢复配置文件失败:', restoreError);

View File

@@ -1,6 +1,7 @@
import { extension_settings, getContext } from "/scripts/extensions.js";
import { characters, this_chid, getRequestHeaders, saveSettingsDebounced, eventSource, event_types } from "/script.js";
import { extensionName } from "../../utils/settings.js";
import { amilyHelper } from '../../core/tavern-helper/main.js';
let ChatCompletionService = undefined;
try {
@@ -184,10 +185,6 @@ async function callNgmsOpenAITest(messages, options) {
async function callNgmsSillyTavernPreset(messages, options) {
console.log('[Amily2号-NgmsST预设] 使用SillyTavern预设调用');
if (!window.TavernHelper || !window.TavernHelper.triggerSlash) {
throw new Error('TavernHelper不可用无法使用SillyTavern预设模式');
}
const context = getContext();
if (!context) {
throw new Error('无法获取SillyTavern上下文');
@@ -202,7 +199,7 @@ async function callNgmsSillyTavernPreset(messages, options) {
let responsePromise;
try {
originalProfile = await window.TavernHelper.triggerSlash('/profile');
originalProfile = await amilyHelper.triggerSlash('/profile');
console.log(`[Amily2号-NgmsST预设] 当前配置文件: ${originalProfile}`);
const targetProfile = context.extensionSettings?.connectionManager?.profiles?.find(p => p.id === profileId);
@@ -213,11 +210,11 @@ async function callNgmsSillyTavernPreset(messages, options) {
const targetProfileName = targetProfile.name;
console.log(`[Amily2号-NgmsST预设] 目标配置文件: ${targetProfileName}`);
const currentProfile = await window.TavernHelper.triggerSlash('/profile');
const currentProfile = await amilyHelper.triggerSlash('/profile');
if (currentProfile !== targetProfileName) {
console.log(`[Amily2号-NgmsST预设] 切换配置文件: ${currentProfile} -> ${targetProfileName}`);
const escapedProfileName = targetProfileName.replace(/"/g, '\\"');
await window.TavernHelper.triggerSlash(`/profile await=true "${escapedProfileName}"`);
await amilyHelper.triggerSlash(`/profile await=true "${escapedProfileName}"`);
}
if (!context.ConnectionManagerRequestService) {
@@ -233,11 +230,11 @@ async function callNgmsSillyTavernPreset(messages, options) {
} finally {
try {
const currentProfileAfterCall = await window.TavernHelper.triggerSlash('/profile');
const currentProfileAfterCall = await amilyHelper.triggerSlash('/profile');
if (originalProfile && originalProfile !== currentProfileAfterCall) {
console.log(`[Amily2号-NgmsST预设] 恢复原始配置文件: ${currentProfileAfterCall} -> ${originalProfile}`);
const escapedOriginalProfile = originalProfile.replace(/"/g, '\\"');
await window.TavernHelper.triggerSlash(`/profile await=true "${escapedOriginalProfile}"`);
await amilyHelper.triggerSlash(`/profile await=true "${escapedOriginalProfile}"`);
}
} catch (restoreError) {
console.error('[Amily2号-NgmsST预设] 恢复配置文件失败:', restoreError);

View File

@@ -1,6 +1,7 @@
import { extension_settings, getContext } from "/scripts/extensions.js";
import { characters, this_chid, getRequestHeaders, saveSettingsDebounced, eventSource, event_types } from "/script.js";
import { extensionName } from "../../utils/settings.js";
import { amilyHelper } from '../../core/tavern-helper/main.js';
let ChatCompletionService = undefined;
try {
@@ -184,10 +185,6 @@ async function callSybdOpenAITest(messages, options) {
async function callSybdSillyTavernPreset(messages, options) {
console.log('[Amily2号-SybdST预设] 使用SillyTavern预设调用');
if (!window.TavernHelper || !window.TavernHelper.triggerSlash) {
throw new Error('TavernHelper不可用无法使用SillyTavern预设模式');
}
const context = getContext();
if (!context) {
throw new Error('无法获取SillyTavern上下文');
@@ -202,7 +199,7 @@ async function callSybdSillyTavernPreset(messages, options) {
let responsePromise;
try {
originalProfile = await window.TavernHelper.triggerSlash('/profile');
originalProfile = await amilyHelper.triggerSlash('/profile');
console.log(`[Amily2号-SybdST预设] 当前配置文件: ${originalProfile}`);
const targetProfile = context.extensionSettings?.connectionManager?.profiles?.find(p => p.id === profileId);
@@ -213,11 +210,11 @@ async function callSybdSillyTavernPreset(messages, options) {
const targetProfileName = targetProfile.name;
console.log(`[Amily2号-SybdST预设] 目标配置文件: ${targetProfileName}`);
const currentProfile = await window.TavernHelper.triggerSlash('/profile');
const currentProfile = await amilyHelper.triggerSlash('/profile');
if (currentProfile !== targetProfileName) {
console.log(`[Amily2号-SybdST预设] 切换配置文件: ${currentProfile} -> ${targetProfileName}`);
const escapedProfileName = targetProfileName.replace(/"/g, '\\"');
await window.TavernHelper.triggerSlash(`/profile await=true "${escapedProfileName}"`);
await amilyHelper.triggerSlash(`/profile await=true "${escapedProfileName}"`);
}
if (!context.ConnectionManagerRequestService) {
@@ -233,11 +230,11 @@ async function callSybdSillyTavernPreset(messages, options) {
} finally {
try {
const currentProfileAfterCall = await window.TavernHelper.triggerSlash('/profile');
const currentProfileAfterCall = await amilyHelper.triggerSlash('/profile');
if (originalProfile && originalProfile !== currentProfileAfterCall) {
console.log(`[Amily2号-SybdST预设] 恢复原始配置文件: ${currentProfileAfterCall} -> ${originalProfile}`);
const escapedOriginalProfile = originalProfile.replace(/"/g, '\\"');
await window.TavernHelper.triggerSlash(`/profile await=true "${escapedOriginalProfile}"`);
await amilyHelper.triggerSlash(`/profile await=true "${escapedOriginalProfile}"`);
}
} catch (restoreError) {
console.error('[Amily2号-SybdST预设] 恢复配置文件失败:', restoreError);

File diff suppressed because one or more lines are too long

View File

@@ -195,11 +195,8 @@ export async function executeManualSummary(startFloor, endFloor, isAuto = false)
container.find('.historiography-message-item').each(function() {
const item = $(this);
const authorType = item.data('author-type');
if ((authorType === 'user' && !includeUser) || (authorType === 'char' && !includeChar)) {
item.prop('hidden', true);
} else {
item.prop('hidden', false);
}
const shouldBeHidden = (authorType === 'user' && !includeUser) || (authorType === 'char' && !includeChar);
item.toggle(!shouldBeHidden);
});
};
@@ -207,7 +204,17 @@ export async function executeManualSummary(startFloor, endFloor, isAuto = false)
charCheckbox.on('change', updateVisibility);
},
onOk: async (dialog) => {
const textToSummarize = dialog.find('.historiography-message-item:not([hidden]) textarea')
const includeUser = dialog.find('#hist-include-user').is(':checked');
const includeChar = dialog.find('#hist-include-char').is(':checked');
const textToSummarize = dialog.find('.historiography-message-item')
.filter(function() {
const authorType = $(this).data('author-type');
if (authorType === 'user' && !includeUser) return false;
if (authorType === 'char' && !includeChar) return false;
return true;
})
.find('textarea')
.map(function() {
const floor = $(this).data('floor');
const author = $(this).closest('.historiography-message-item').find('summary').text().replace(`【第 ${floor} 楼】 `, '');

View File

@@ -1,10 +1,18 @@
import { extension_settings, getContext } from "/scripts/extensions.js";
import { characters, eventSource, event_types } from "/script.js";
import { loadWorldInfo, createNewWorldInfo, createWorldInfoEntry, saveWorldInfo, world_names } from "/scripts/world-info.js";
import { loadWorldInfo, createNewWorldInfo, createWorldInfoEntry, saveWorldInfo, world_names, updateWorldInfoList } from "/scripts/world-info.js";
import { compatibleWriteToLorebook, safeLorebooks, safeCharLorebooks, safeLorebookEntries } from "./tavernhelper-compatibility.js";
import { extensionName } from "../utils/settings.js";
document.addEventListener('amily-lorebook-created', (event) => {
if (event.detail && event.detail.bookName) {
console.log(`[Amily2-国史馆] 监听到史书《${event.detail.bookName}》变更,即刻通报工部刷新宫殿。`);
refreshWorldbookListOnly(event.detail.bookName);
}
});
export const LOREBOOK_PREFIX = "Amily2档案-";
export const DEDICATED_LOREBOOK_NAME = "Amily2号-国史馆";
export const INTRODUCTORY_TEXT =
@@ -90,34 +98,15 @@ export async function getCombinedWorldbookContent(lorebookName) {
}
}
async function refreshWorldbookListOnly(newBookName = null) {
console.log("[Amily2号-工部-v1.3] 执行“圣谕广播”式UI新...");
try {
if (newBookName) {
if (Array.isArray(world_names) && !world_names.includes(newBookName)) {
world_names.push(newBookName);
world_names.sort();
console.log(`[Amily2号-工部] 已将《${newBookName}》注入前端数据模型。`);
} else {
console.log(`[Amily2号-工部] 《${newBookName}》已存在于数据模型中,跳过注入。`);
}
export async function refreshWorldbookListOnly(newBookName = null) {
console.log("[Amily2号-工部-v2.0] 执行SillyTavern核心UI新...");
try {
await updateWorldInfoList();
console.log("[Amily2号-工部] SillyTavern核心刷新函数 (updateWorldInfoList) 调用成功。");
} catch (error) {
console.error("[Amily2号-工部] 调用核心刷新函数时出错:", error);
toastr.error("Amily2号调用核心UI刷新函数时失败。", "核心刷新失败");
}
if (
eventSource &&
typeof eventSource.emit === "function" &&
event_types.CHARACTER_PAGE_LOADED
) {
console.log(`[Amily2号-工部] 正在广播事件: ${event_types.CHARACTER_PAGE_LOADED}`);
eventSource.emit(event_types.CHARACTER_PAGE_LOADED);
console.log("[Amily2号-工部] “character_page_loaded”事件已广播UI应已响应刷新。");
} else {
console.error("[Amily2号] 致命错误: eventSource 或 event_types.CHARACTER_PAGE_LOADED 未找到。无法广播刷新事件。");
toastr.error("Amily2号无法触发UI刷新。", "核心事件系统缺失");
}
} catch (error) {
console.error("[Amily2号-工部] “圣谕广播”式刷新失败:", error);
}
}
export async function writeSummaryToLorebook(pendingData) {
@@ -281,7 +270,12 @@ export async function getPlotOptimizedWorldbookContent(context, apiSettings) {
const panel = $('#amily2_plot_optimization_panel');
let liveSettings = {};
if (panel.length > 0) {
// Check if the panel exists and its dynamic content (the entry list) has been populated.
// This helps prevent a race condition where we read from an empty, partially-rendered panel.
const isPanelReady = panel.length > 0 && panel.find('#amily2_opt_worldbook_entry_list_container input[type="checkbox"]').length > 0;
if (isPanelReady) {
// Panel is ready, so we can trust the live values from the UI.
liveSettings.worldbookEnabled = panel.find('#amily2_opt_worldbook_enabled').is(':checked');
liveSettings.worldbookSource = panel.find('input[name="amily2_opt_worldbook_source"]:checked').val() || 'character';
@@ -295,25 +289,30 @@ export async function getPlotOptimizedWorldbookContent(context, apiSettings) {
liveSettings.worldbookCharLimit = parseInt(panel.find('#amily2_opt_worldbook_char_limit').val(), 10) || 60000;
let enabledEntries = {};
panel.find('#amily2_opt_worldbook_entry_list_container input[type="checkbox"]').each(function() {
if ($(this).is(':checked')) {
const bookName = $(this).data('book');
const uid = parseInt($(this).data('uid'));
if (!enabledEntries[bookName]) {
enabledEntries[bookName] = [];
}
enabledEntries[bookName].push(uid);
panel.find('#amily2_opt_worldbook_entry_list_container input[type="checkbox"]:checked').each(function() {
const bookName = $(this).data('book');
const uid = parseInt($(this).data('uid'));
if (!enabledEntries[bookName]) {
enabledEntries[bookName] = [];
}
enabledEntries[bookName].push(uid);
});
liveSettings.enabledWorldbookEntries = enabledEntries;
} else {
console.warn('[剧情优化大师] 未找到设置面板,世界书功能将回退到使用已保存的设置。');
// Panel is not ready or doesn't exist. Fall back to the saved settings from the extension.
// This uses the correct, prefixed keys.
if (panel.length > 0) {
console.warn('[剧情优化大师] 检测到UI面板但内容未完全加载回退到使用已保存的设置。');
} else {
console.warn('[剧情优化大师] 未找到设置面板,世界书功能将使用已保存的设置。');
}
liveSettings = {
worldbookEnabled: apiSettings.worldbookEnabled,
worldbookSource: apiSettings.worldbookSource,
selectedWorldbooks: apiSettings.selectedWorldbooks,
worldbookCharLimit: apiSettings.worldbookCharLimit,
enabledWorldbookEntries: apiSettings.enabledWorldbookEntries,
worldbookEnabled: apiSettings.plotOpt_worldbook_enabled,
worldbookSource: apiSettings.plotOpt_worldbook_source || 'character', // Default to 'character'
selectedWorldbooks: apiSettings.plotOpt_worldbook_selected_worldbooks,
worldbookCharLimit: apiSettings.plotOpt_worldbook_char_limit,
enabledWorldbookEntries: apiSettings.plotOpt_worldbook_selected_entries,
};
}
@@ -355,7 +354,8 @@ export async function getPlotOptimizedWorldbookContent(context, apiSettings) {
const userEnabledEntries = allEntries.filter(entry => {
if (!entry.enabled) return false;
const bookConfig = enabledEntriesMap[entry.bookName];
return bookConfig ? bookConfig.includes(entry.uid) : false;
// 同时检查数字和字符串类型的UID以兼容从实时UI数字和已保存设置可能为字符串中读取的配置
return bookConfig ? (bookConfig.includes(entry.uid) || bookConfig.includes(String(entry.uid))) : false;
});
if (userEnabledEntries.length === 0) return '';
@@ -363,8 +363,8 @@ export async function getPlotOptimizedWorldbookContent(context, apiSettings) {
const chatHistory = context.chat.map(message => message.mes).join('\n').toLowerCase();
const getEntryKeywords = (entry) => [...new Set([...(entry.key || []), ...(entry.keys || [])])].map(k => k.toLowerCase());
const blueLightEntries = userEnabledEntries.filter(entry => entry.type === 'constant');
let pendingGreenLights = userEnabledEntries.filter(entry => entry.type !== 'constant');
const blueLightEntries = userEnabledEntries.filter(entry => entry.constant);
let pendingGreenLights = userEnabledEntries.filter(entry => !entry.constant);
const triggeredEntries = new Set([...blueLightEntries]);

File diff suppressed because one or more lines are too long

View File

@@ -10,6 +10,7 @@ export const defaultSettings = {
embeddingModel: 'text-embedding-3-small',
notify: true,
batchSize: 50,
independentChatMemoryEnabled: false,
},
advanced: {
chunkSize: 768,

File diff suppressed because one or more lines are too long

View File

@@ -234,13 +234,11 @@ export async function fillWithSecondaryApi(latestMessage, forceRun = false) {
if (currentContext.chat && currentContext.chat.length > 0) {
const lastMessage = currentContext.chat[currentContext.chat.length - 1];
if (saveStateToMessage(getMemoryState(), lastMessage)) {
saveChat();
renderTables();
updateOrInsertTableInChat();
return;
}
}
saveChatDebounced();
saveChat();
} catch (error) {
console.error(`[Amily2-副API] 发生严重错误:`, error);

View File

@@ -0,0 +1,36 @@
(function(){
if (window.frameElement) {
window.frameElement.style.height = 'auto';
}
function getGlobal() {
if (typeof self !== 'undefined') { return self; }
if (typeof window !== 'undefined') { return window; }
if (typeof global !== 'undefined') { return global; }
throw new Error('unable to locate global object');
}
const globalScope = getGlobal();
if (globalScope.generate_send_button_onclick) {
globalScope.generate_send_button_onclick_old = globalScope.generate_send_button_onclick;
globalScope.generate_send_button_onclick = function(event) {
try {
const textarea = document.getElementById('send_textarea');
if (textarea && textarea.value) {
const customEvent = new CustomEvent('xb-send-message', {
detail: {
message: textarea.value,
event: event
},
bubbles: true,
cancelable: true
});
if (!window.dispatchEvent(customEvent)) {
return;
}
}
} catch (e) {
console.error('Error in xb-send-message event dispatch:', e);
}
globalScope.generate_send_button_onclick_old(event);
};
}
})();

View File

@@ -0,0 +1,31 @@
function initializeAmilyClient() {
console.log('[Amily2-IframeClient] 正在初始化...');
document.body.addEventListener('click', function(event) {
const target = event.target.closest('[data-amily-action]');
if (target) {
const action = target.dataset.amilyAction;
const detail = { ...target.dataset };
delete detail.amilyAction;
console.log(`[Amily2-IframeClient] 触发动作: ${action}`, detail);
if (window.AmilySimpleAPI && typeof window.AmilySimpleAPI.post === 'function') {
window.AmilySimpleAPI.post(action, detail);
} else {
console.error('[Amily2-IframeClient] AmilySimpleAPI 不可用。');
}
}
});
console.log('[Amily2-IframeClient] 客户端脚本已加载并就绪。');
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializeAmilyClient);
} else {
initializeAmilyClient();
}

View File

@@ -3,15 +3,339 @@ import {
loadWorldInfo,
saveWorldInfo,
createNewWorldInfo,
createWorldInfoEntry
createWorldInfoEntry,
reloadEditor
} from "/scripts/world-info.js";
import { characters } from "/script.js";
import {
characters,
eventSource,
event_types,
chat,
reloadCurrentChat,
saveChatConditional,
name1,
name2,
addOneMessage,
messageFormatting,
substituteParamsExtended
} from "/script.js";
import { getContext } from "/scripts/extensions.js";
import { executeSlashCommandsWithOptions } from '/scripts/slash-commands.js';
class AmilyHelper {
// ==================== Chat Message 相关方法 ====================
getChatMessages(range, options = {}) {
const { role = 'all', hide_state = 'all', include_swipes = false, include_swipe = false } = options;
const includeSwipes = include_swipes || include_swipe;
if (!chat || !Array.isArray(chat)) {
throw new Error('聊天数组不可用');
}
let start, end;
const rangeStr = String(range);
if (rangeStr.match(/^(-?\d+)$/)) {
const value = Number(rangeStr);
start = end = value < 0 ? chat.length + value : value;
} else {
const match = rangeStr.match(/^(-?\d+)-(-?\d+)$/);
if (!match) {
throw new Error(`无效的消息范围: ${range}`);
}
const [, s, e] = match;
const startVal = Number(s) < 0 ? chat.length + Number(s) : Number(s);
const endVal = Number(e) < 0 ? chat.length + Number(e) : Number(e);
start = Math.min(startVal, endVal);
end = Math.max(startVal, endVal);
}
if (start < 0 || end >= chat.length || start > end) {
throw new Error(`消息范围超出界限: ${range}`);
}
const getRole = (msg) => {
if (msg.is_system) return 'system';
return msg.is_user ? 'user' : 'assistant';
};
const messages = [];
for (let i = start; i <= end; i++) {
const msg = chat[i];
if (!msg) continue;
const msgRole = getRole(msg);
if (role !== 'all' && msgRole !== role) continue;
if (hide_state !== 'all') {
if ((hide_state === 'hidden') !== msg.is_system) continue;
}
const swipe_id = msg.swipe_id ?? 0;
const swipes = msg.swipes ?? [msg.mes];
const swipes_data = msg.variables ?? [{}];
const swipes_info = msg.swipes_info ?? [msg.extra ?? {}];
if (includeSwipes) {
messages.push({
message_id: i,
name: msg.name,
role: msgRole,
is_hidden: msg.is_system,
swipe_id: swipe_id,
swipes: swipes,
swipes_data: swipes_data,
swipes_info: swipes_info
});
} else {
messages.push({
message_id: i,
name: msg.name,
role: msgRole,
is_hidden: msg.is_system,
message: msg.mes,
data: swipes_data[swipe_id],
extra: swipes_info[swipe_id]
});
}
}
return messages;
}
async setChatMessages(chat_messages, options = {}) {
const { refresh = 'affected' } = options;
if (!Array.isArray(chat_messages)) {
throw new Error('chat_messages 必须是数组');
}
for (const chatMsg of chat_messages) {
const msg = chat[chatMsg.message_id];
if (!msg) continue;
if (chatMsg.name !== undefined) msg.name = chatMsg.name;
if (chatMsg.role !== undefined) msg.is_user = chatMsg.role === 'user';
if (chatMsg.is_hidden !== undefined) msg.is_system = chatMsg.is_hidden;
if (chatMsg.message !== undefined) {
msg.mes = chatMsg.message;
if (msg.swipes) {
msg.swipes[msg.swipe_id ?? 0] = chatMsg.message;
}
}
if (chatMsg.data !== undefined) {
if (!msg.variables) {
msg.variables = Array(msg.swipes?.length ?? 1).fill({});
}
msg.variables[msg.swipe_id ?? 0] = chatMsg.data;
}
if (chatMsg.extra !== undefined) {
if (!msg.swipes_info) {
msg.swipes_info = Array(msg.swipes?.length ?? 1).fill({});
}
msg.extra = chatMsg.extra;
msg.swipes_info[msg.swipe_id ?? 0] = chatMsg.extra;
}
}
await saveChatConditional();
if (refresh === 'all') {
await reloadCurrentChat();
} else if (refresh === 'affected') {
for (const chatMsg of chat_messages) {
const $mes = $(`div.mes[mesid="${chatMsg.message_id}"]`);
if ($mes.length) {
const msg = chat[chatMsg.message_id];
$mes.find('.mes_text').empty().append(
messageFormatting(msg.mes, msg.name, msg.is_system, msg.is_user, chatMsg.message_id)
);
}
}
}
console.log(`[Amily助手] 已修改消息: ${chat_messages.map(m => m.message_id).join(', ')}`);
}
async setChatMessage(field_values, message_id, {
swipe_id = 'current',
refresh = 'display_and_render_current'
} = {}) {
field_values = typeof field_values === 'string' ? { message: field_values } : field_values;
if (typeof swipe_id !== 'number' && swipe_id !== 'current') {
throw new Error(`提供的 swipe_id 无效, 请提供 'current' 或序号, 你提供的是: ${swipe_id}`);
}
if (!['none', 'display_current', 'display_and_render_current', 'all'].includes(refresh)) {
throw new Error(
`提供的 refresh 无效, 请提供 'none', 'display_current', 'display_and_render_current' 或 'all', 你提供的是: ${refresh}`
);
}
const chat_message = chat[message_id];
if (!chat_message) {
console.warn(`[Amily助手] 未找到第 ${message_id} 楼的消息`);
return;
}
const add_swipes_if_required = () => {
if (swipe_id === 'current') {
return false;
}
if (swipe_id == 0 || (chat_message.swipes && swipe_id < chat_message.swipes.length)) {
return true;
}
if (!chat_message.swipes) {
chat_message.swipe_id = 0;
chat_message.swipes = [chat_message.mes];
chat_message.variables = [{}];
}
for (let i = chat_message.swipes.length; i <= swipe_id; ++i) {
chat_message.swipes.push('');
chat_message.variables.push({});
}
return true;
};
const swipe_id_previous_index = chat_message.swipe_id ?? 0;
const swipe_id_to_set_index = swipe_id == 'current' ? swipe_id_previous_index : swipe_id;
const swipe_id_to_use_index = refresh != 'none' ? swipe_id_to_set_index : swipe_id_previous_index;
const message = field_values.message ??
(chat_message.swipes ? chat_message.swipes[swipe_id_to_set_index] : undefined) ??
chat_message.mes;
const update_chat_message = () => {
const message_demacroed = substituteParamsExtended(message);
if (field_values.data) {
if (!chat_message.variables) {
chat_message.variables = [];
}
chat_message.variables[swipe_id_to_set_index] = field_values.data;
}
if (chat_message.swipes) {
chat_message.swipes[swipe_id_to_set_index] = message_demacroed;
chat_message.swipe_id = swipe_id_to_use_index;
}
if (swipe_id_to_use_index === swipe_id_to_set_index) {
chat_message.mes = message_demacroed;
}
};
const update_partial_html = async (should_update_swipe) => {
const mes_html = $(`div.mes[mesid="${message_id}"]`);
if (!mes_html.length) {
return;
}
if (should_update_swipe) {
mes_html.find('.swipes-counter').text(`${swipe_id_to_use_index + 1}\u200b/\u200b${chat_message.swipes.length}`);
}
if (refresh != 'none') {
mes_html
.find('.mes_text')
.empty()
.append(
messageFormatting(message, chat_message.name, chat_message.is_system, chat_message.is_user, message_id)
);
if (refresh === 'display_and_render_current') {
await eventSource.emit(
chat_message.is_user ? event_types.USER_MESSAGE_RENDERED : event_types.CHARACTER_MESSAGE_RENDERED,
message_id
);
}
}
};
const should_update_swipe = add_swipes_if_required();
update_chat_message();
await saveChatConditional();
if (refresh == 'all') {
await reloadCurrentChat();
} else {
await update_partial_html(should_update_swipe);
}
console.log(
`[Amily助手] 设置第 ${message_id} 楼消息, 选项: ${JSON.stringify({
swipe_id,
refresh,
})}, 设置前使用的消息页: ${swipe_id_previous_index}, 设置的消息页: ${swipe_id_to_set_index}, 现在使用的消息页: ${swipe_id_to_use_index}`
);
}
async createChatMessages(chat_messages, options = {}) {
const { insert_at = 'end', refresh = 'all' } = options;
let insertIndex = insert_at;
if (insert_at !== 'end') {
insertIndex = insert_at < 0 ? chat.length + insert_at : insert_at;
if (insertIndex < 0 || insertIndex > chat.length) {
throw new Error(`无效的插入位置: ${insert_at}`);
}
}
const newMessages = chat_messages.map(msg => ({
name: msg.name ?? (msg.role === 'user' ? name1 : name2),
is_user: msg.role === 'user',
is_system: msg.is_hidden ?? false,
mes: msg.message,
variables: [msg.data ?? {}]
}));
if (insertIndex === 'end') {
chat.push(...newMessages);
} else {
chat.splice(insertIndex, 0, ...newMessages);
}
await saveChatConditional();
if (refresh === 'affected' && insertIndex === 'end') {
newMessages.forEach(msg => addOneMessage(msg));
} else if (refresh === 'all') {
await reloadCurrentChat();
}
console.log(`[Amily助手] 已创建 ${chat_messages.length} 条消息`);
}
async deleteChatMessages(message_ids, options = {}) {
const { refresh = 'all' } = options;
const validIds = message_ids
.map(id => id < 0 ? chat.length + id : id)
.filter(id => id >= 0 && id < chat.length)
.sort((a, b) => b - a); // 从后往前删除
for (const id of validIds) {
chat.splice(id, 1);
}
await saveChatConditional();
if (refresh === 'all') {
await reloadCurrentChat();
}
console.log(`[Amily助手] 已删除消息: ${validIds.join(', ')}`);
}
async getLorebooks() {
return [...world_names];
}
@@ -19,8 +343,8 @@ class AmilyHelper {
async getCharLorebooks(options = { type: 'all' }) {
try {
const context = getContext();
if (!context || !context.characterId) {
console.warn('[Amily助手] 无法获取当前角色上下文');
if (!context || context.characterId === undefined) {
console.warn('[Amily助手] 无法获取当前角色上下文');
return { primary: null, additional: [] };
}
const character = characters[context.characterId];
@@ -38,14 +362,22 @@ class AmilyHelper {
if (!bookData || !bookData.entries) {
return [];
}
const positionMap = {
0: 'before_character_definition',
1: 'after_character_definition',
2: 'before_author_note',
3: 'after_author_note',
4: 'at_depth_as_system'
};
return Object.entries(bookData.entries).map(([uid, entry]) => ({
uid: parseInt(uid),
comment: entry.comment || '无标题条目',
content: entry.content || '',
key: entry.key || [],
keys: entry.key || [],
enabled: !entry.disable,
constant: entry.constant || false,
position: entry.position || 4,
position: positionMap[entry.position] || 'at_depth_as_system',
depth: entry.depth || 998,
}));
} catch (error) {
@@ -58,7 +390,7 @@ class AmilyHelper {
try {
const bookData = await loadWorldInfo(bookName);
if (!bookData) {
console.error(`[Amily助手] 更新失败:找不到世界书《${bookName}`);
console.error(`[Amily助手] 更新失败:找不到世界书《${bookName}`);
return false;
}
for (const entryUpdate of entries) {
@@ -68,12 +400,27 @@ class AmilyHelper {
if (entryUpdate.enabled !== undefined) existingEntry.disable = !entryUpdate.enabled;
if (entryUpdate.comment !== undefined) existingEntry.comment = entryUpdate.comment;
if (entryUpdate.key !== undefined) existingEntry.key = entryUpdate.key;
if (entryUpdate.keys !== undefined) existingEntry.key = entryUpdate.keys;
if (entryUpdate.constant !== undefined) existingEntry.constant = entryUpdate.constant;
if (entryUpdate.position !== undefined) existingEntry.position = entryUpdate.position;
if (entryUpdate.type === 'constant') existingEntry.constant = true;
if (entryUpdate.type === 'selective') existingEntry.constant = false;
if (entryUpdate.position !== undefined) {
const positionMap = {
'before_character_definition': 0,
'after_character_definition': 1,
'before_author_note': 2,
'after_author_note': 3,
'at_depth': 4,
'at_depth_as_system': 4
};
existingEntry.position = positionMap[entryUpdate.position] ?? 4;
}
if (entryUpdate.depth !== undefined) existingEntry.depth = entryUpdate.depth;
}
}
await saveWorldInfo(bookName, bookData, true);
reloadEditor(bookName);
eventSource.emit(event_types.WORLD_INFO_UPDATED, bookName);
return true;
} catch (error) {
console.error(`[Amily助手] 更新世界书《${bookName}》条目时出错:`, error);
@@ -85,27 +432,37 @@ class AmilyHelper {
try {
let bookData = await loadWorldInfo(bookName);
if (!bookData) {
console.warn(`[Amily助手] 世界书《${bookName}》不存在,将自动创建`);
console.warn(`[Amily助手] 世界书《${bookName}》不存在,将自动创建`);
await this.createLorebook(bookName);
bookData = await loadWorldInfo(bookName);
if (!bookData) {
throw new Error(`创建并加载世界书《${bookName}》失败`);
throw new Error(`创建并加载世界书《${bookName}》失败`);
}
}
for (const newEntryData of entries) {
const newEntry = createWorldInfoEntry(bookName, bookData);
const positionMap = {
'before_character_definition': 0,
'after_character_definition': 1,
'before_author_note': 2,
'after_author_note': 3,
'at_depth': 4,
'at_depth_as_system': 4
};
Object.assign(newEntry, {
comment: newEntryData.comment || '新条目',
content: newEntryData.content || '',
key: newEntryData.key || [],
constant: newEntryData.constant || false,
position: newEntryData.position ?? 4,
key: newEntryData.keys || newEntryData.key || [],
constant: newEntryData.type === 'constant' ? true : (newEntryData.constant || false),
position: typeof newEntryData.position === 'string' ? (positionMap[newEntryData.position] ?? 4) : (newEntryData.position ?? 4),
depth: newEntryData.depth ?? 998,
disable: !(newEntryData.enabled ?? true),
});
if (newEntryData.type === 'selective') newEntry.constant = false;
}
await saveWorldInfo(bookName, bookData, true);
reloadEditor(bookName);
return true;
} catch (error) {
console.error(`[Amily助手] 在世界书《${bookName}》中创建新条目时出错:`, error);
@@ -116,7 +473,7 @@ class AmilyHelper {
async createLorebook(bookName) {
try {
if (world_names.includes(bookName)) {
console.warn(`[Amily助手] 创建失败:世界书《${bookName}》已存在`);
console.warn(`[Amily助手] 创建失败:世界书《${bookName}》已存在`);
return false;
}
await createNewWorldInfo(bookName);
@@ -124,6 +481,7 @@ class AmilyHelper {
world_names.push(bookName);
world_names.sort();
}
document.dispatchEvent(new CustomEvent('amily-lorebook-created', { detail: { bookName } }));
return true;
} catch (error) {
console.error(`[Amily助手] 创建世界书《${bookName}》时出错:`, error);
@@ -131,6 +489,8 @@ class AmilyHelper {
}
}
// ==================== 斜杠命令相关 ====================
async triggerSlash(command) {
try {
console.log(`[Amily助手] 正在执行斜杠命令: ${command}`);
@@ -144,6 +504,116 @@ class AmilyHelper {
throw error;
}
}
// ==================== 工具方法 ====================
async loadWorldInfo(bookName) {
return await loadWorldInfo(bookName);
}
async saveWorldInfo(bookName, data, isWorldInfo) {
await saveWorldInfo(bookName, data, isWorldInfo);
}
getLastMessageId() {
return chat.length - 1;
}
}
export const amilyHelper = new AmilyHelper();
export function initializeAmilyHelper() {
if (!window.AmilyHelper) {
window.AmilyHelper = amilyHelper;
console.log('[Amily2] AmilyHelper 已成功初始化并附加到 window 对象');
}
}
// ==================== iframe 通信 API ====================
export function makeRequest(request, data) {
return new Promise((resolve, reject) => {
const uid = Date.now() + Math.random();
const callbackRequest = `${request}_callback`;
function handleMessage(event) {
const msgData = event.data || {};
if (msgData.request === callbackRequest && msgData.uid === uid) {
window.removeEventListener('message', handleMessage);
if (msgData.error) {
reject(new Error(msgData.error));
} else {
resolve(msgData.result);
}
}
}
window.addEventListener('message', handleMessage);
setTimeout(() => {
window.removeEventListener('message', handleMessage);
reject(new Error(`请求 '${request}' 超时 (30秒)`));
}, 30000);
window.parent.postMessage({
source: 'amily2-iframe-request',
request: request,
uid: uid,
data: data
}, '*');
});
}
// ==================== 主窗口 API ====================
const apiHandlers = new Map();
export function registerApiHandler(request, handler) {
if (apiHandlers.has(request)) {
console.warn(`[Amily2-IframeAPI] 覆盖请求处理器: ${request}`);
}
apiHandlers.set(request, handler);
}
export function initializeApiListener() {
window.addEventListener('message', async (event) => {
const data = event.data || {};
if (data.source !== 'amily2-iframe-request' || !data.request || data.uid === undefined) {
return;
}
const handler = apiHandlers.get(data.request);
const callbackRequest = `${data.request}_callback`;
if (!handler) {
console.error(`[Amily2-IframeAPI] 收到未知请求: ${data.request}`);
event.source.postMessage({
request: callbackRequest,
uid: data.uid,
error: `未注册请求 '${data.request}' 的处理器`
}, '*');
return;
}
try {
const result = await handler(data.data, event);
event.source.postMessage({
request: callbackRequest,
uid: data.uid,
result: result
}, '*');
} catch (error) {
console.error(`[Amily2-IframeAPI] 执行处理器 '${data.request}' 时出错:`, error);
event.source.postMessage({
request: callbackRequest,
uid: data.uid,
error: error.message || String(error)
}, '*');
}
});
console.log('[Amily2-IframeAPI] 主窗口监听器已初始化');
}

View File

@@ -0,0 +1,51 @@
import { renderAllIframes, clearAllIframes, initializeRenderer } from './renderer.js';
import { extension_settings } from "/scripts/extensions.js";
import { extensionName } from "../../utils/settings.js";
import { saveSettingsDebounced } from "/script.js";
let isRendererInitialized = false;
export function initializeRendererBindings() {
const container = $("#amily2_drawer_content").length
? $("#amily2_drawer_content")
: $("#amily2_chat_optimiser");
if (!container.length) {
console.warn("[Amily2-Renderer] Could not find the settings container.");
return;
}
container.on('change', '#render-enable-toggle', function() {
const isChecked = this.checked;
if (!extension_settings[extensionName]) {
extension_settings[extensionName] = {};
}
extension_settings[extensionName].render_enabled = isChecked;
saveSettingsDebounced();
if (isChecked && !isRendererInitialized) {
initializeRenderer();
isRendererInitialized = true;
console.log("[Amily2-Renderer] Renderer has been initialized on-demand.");
}
if (isChecked) {
renderAllIframes();
} else {
clearAllIframes();
}
});
container.on('change', '#render-depth', function() {
const depth = parseInt(this.value, 10);
if (!extension_settings[extensionName]) {
extension_settings[extensionName] = {};
}
extension_settings[extensionName].render_depth = depth;
saveSettingsDebounced();
toastr.success(`渲染深度已保存为: ${depth}`);
});
console.log("[Amily2-Renderer] Renderer UI events have been successfully bound.");
}

View File

@@ -0,0 +1,21 @@
<div class="flex-container">
<button id="amily2_renderer_back_button" class="menu_button wide_button"><i class="fas fa-arrow-left"></i> 返回主殿</button>
</div>
<div class="extension-content-item">
<div class="name">启用前端渲染</div>
<div class="description">在聊天消息中渲染HTML内容。</div>
<input id="render-enable-toggle" type="checkbox" class="slider">
</div>
<div class="extension-content-item">
<div class="name">渲染深度</div>
<div class="description">设置要渲染的最新消息的数量。0表示无限制。</div>
<input id="render-depth" type="number" class="text_pole" value="5">
</div>
<div class="amily2-renderer-info-container">
<p class="emo-statement">“想给温柔的人奏响一段温柔的小插曲。”</p>
<p class="description-text">
当开启Amily前端渲染后务必关闭酒馆助手的前端渲染借鉴了酒馆助手的渲染和交互逻辑实现了更加轻量级渲染更快降低卡顿。
<br><br>
与酒馆助手的脚本、变量等功能,完全无冲突,可并存使用。
</p>
</div>

View File

@@ -0,0 +1,606 @@
import { eventSource, event_types } from '/script.js';
import { extension_settings } from '/scripts/extensions.js';
import { extensionName } from '../../utils/settings.js';
const settings = {
sandboxMode: false,
useBlob: false,
wrapperIframe: true,
renderEnabled: true
};
const winMap = new Map();
let lastHeights = new WeakMap();
const blobUrls = new WeakMap();
const hashToBlobUrl = new Map();
const blobLRU = [];
const BLOB_CACHE_LIMIT = 32;
function generateUniqueId() {
return `amily2-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
}
function shouldRenderContentByBlock(codeBlock) {
if (!codeBlock) return false;
const content = (codeBlock.textContent || '').trim();
if (!content) return false;
return /^\s*<!doctype html/i.test(content) || /^\s*<html/i.test(content) || /<script/i.test(content);
}
function djb2(str) {
let h = 5381;
for (let i = 0; i < str.length; i++) {
h = ((h << 5) + h) ^ str.charCodeAt(i);
}
return (h >>> 0).toString(16);
}
function buildResourceHints(html) {
const urls = Array.from(new Set((html.match(/https?:\/\/[^"'()\s]+/gi) || []).map(u => { try { return new URL(u).origin } catch { return null } }).filter(Boolean)));
let hints = "";
const maxHosts = 6;
for (let i = 0; i < Math.min(urls.length, maxHosts); i++) {
const origin = urls[i];
hints += `<link rel="dns-prefetch" href="${origin}">`;
hints += `<link rel="preconnect" href="${origin}" crossorigin>`;
}
let preload = "";
const font = (html.match(/https?:\/\/[^"'()\s]+\.(?:woff2|woff|ttf|otf)/i) || [])[0];
if (font) {
const type = font.endsWith(".woff2") ? "font/woff2" : font.endsWith(".woff") ? "font/woff" : font.endsWith(".ttf") ? "font/ttf" : "font/otf";
preload += `<link rel="preload" as="font" href="${font}" type="${type}" crossorigin fetchpriority="high">`;
}
const css = (html.match(/https?:\/\/[^"'()\s]+\.css/i) || [])[0];
if (css) {
preload += `<link rel="preload" as="style" href="${css}" crossorigin fetchpriority="high">`;
}
const img = (html.match(/https?:\/\/[^"'()\s]+\.(?:png|jpg|jpeg|webp|gif|svg)/i) || [])[0];
if (img) {
preload += `<link rel="preload" as="image" href="${img}" crossorigin fetchpriority="high">`;
}
return hints + preload;
}
function iframeClientScript() {
return `
(function(){
function measureVisibleHeight(){
try{
var doc = document;
var target = doc.querySelector('.calendar-wrapper') || doc.body;
if(!target) return 0;
var minTop = Infinity, maxBottom = 0;
var addRect = function(el){
try{
var r = el.getBoundingClientRect();
if(r && r.height > 0){
if(minTop > r.top) minTop = r.top;
if(maxBottom < r.bottom) maxBottom = r.bottom;
}
}catch(e){}
};
addRect(target);
var children = target.children || [];
for(var i=0;i<children.length;i++){
var child = children[i];
if(!child) continue;
try{
var s = window.getComputedStyle(child);
if(s.display === 'none' || s.visibility === 'hidden') continue;
if(!child.offsetParent && s.position !== 'fixed') continue;
}catch(e){}
addRect(child);
}
return maxBottom > 0 ? Math.ceil(maxBottom - Math.min(minTop, 0)) : (target.scrollHeight || 0);
}catch(e){
return (document.body && document.body.scrollHeight) || 0;
}
} function post(m){ try{ parent.postMessage(m,'*') }catch(e){} }
var rafPending=false, lastH=0;
var HYSTERESIS = 2;
function send(force){
if(rafPending && !force) return;
rafPending = true;
requestAnimationFrame(function(){
rafPending = false;
var h = measureVisibleHeight();
if(force || Math.abs(h - lastH) >= HYSTERESIS){
lastH = h;
post({height:h, force:!!force});
}
});
}
try{ send(true) }catch(e){}
document.addEventListener('DOMContentLoaded', function(){ send(true) }, {once:true});
window.addEventListener('load', function(){ send(true) }, {once:true});
try{
if(document.fonts){
document.fonts.ready.then(function(){ send(true) }).catch(function(){});
if(document.fonts.addEventListener){
document.fonts.addEventListener('loadingdone', function(){ send(true) });
document.fonts.addEventListener('loadingerror', function(){ send(true) });
}
}
}catch(e){}
['transitionend','animationend'].forEach(function(evt){
document.addEventListener(evt, function(){ send(false) }, {passive:true, capture:true});
});
try{
var root = document.querySelector('.calendar-wrapper') || document.body || document.documentElement;
var ro = new ResizeObserver(function(){ send(false) });
ro.observe(root);
}catch(e){
try{
var rootMO = document.querySelector('.calendar-wrapper') || document.body || document.documentElement;
new MutationObserver(function(){ send(false) })
.observe(rootMO, {childList:true, subtree:true, attributes:true, characterData:true});
}catch(e){}
window.addEventListener('resize', function(){ send(false) }, {passive:true});
}
window.addEventListener('message', function(e){
var d = e && e.data || {};
if(d && d.type === 'probe') setTimeout(function(){ send(true) }, 10);
});
})();`;
}
function buildWrappedHtml(html) {
const origin = (typeof location !== 'undefined' && location.origin) ? location.origin : '';
const baseTag = settings && settings.useBlob ? `<base href="${origin}/">` : "";
const headHints = buildResourceHints(html);
const vhFix = `<style>html,body{height:auto!important;min-height:0!important;max-height:none!important}.profile-container,[style*="100vh"]{height:auto!important;min-height:600px!important}[style*="height:100%"]{height:auto!important;min-height:100%!important}</style>`;
const apiScript = `
<script>
window.makeRequest = function(request, data) {
return new Promise(function(resolve, reject) {
var uid = Date.now() + Math.random();
var callbackRequest = request + '_callback';
function handleMessage(event) {
var msgData = event.data || {};
if (msgData.request === callbackRequest && msgData.uid === uid) {
window.removeEventListener('message', handleMessage);
if (msgData.error) {
reject(new Error(msgData.error));
} else {
resolve(msgData.result);
}
}
}
window.addEventListener('message', handleMessage);
setTimeout(function() {
window.removeEventListener('message', handleMessage);
reject(new Error('请求 "' + request + '" 超时 (30秒)'));
}, 30000);
window.parent.postMessage({
source: 'amily2-iframe-request',
request: request,
uid: uid,
data: data
}, '*');
});
};
window.AmilyHelper = {
getChatMessages: function(range, options) {
return makeRequest('getChatMessages', { range: range, options: options });
},
setChatMessages: function(messages, options) {
return makeRequest('setChatMessages', { messages: messages, options: options });
},
setChatMessage: function(index, content) {
return makeRequest('setChatMessage', { index: index, content: content });
},
createChatMessages: function(messages, options) {
return makeRequest('createChatMessages', { messages: messages, options: options });
},
deleteChatMessages: function(ids, options) {
return makeRequest('deleteChatMessages', { ids: ids, options: options });
},
getLorebooks: function() {
return makeRequest('getLorebooks', {});
},
getCharLorebooks: function(options) {
return makeRequest('getCharLorebooks', { options: options });
},
getLorebookEntries: function(bookName) {
return makeRequest('getLorebookEntries', { bookName: bookName });
},
setLorebookEntries: function(bookName, entries) {
return makeRequest('setLorebookEntries', { bookName: bookName, entries: entries });
},
createLorebookEntries: function(bookName, entries) {
return makeRequest('createLorebookEntries', { bookName: bookName, entries: entries });
},
createLorebook: function(bookName) {
return makeRequest('createLorebook', { bookName: bookName });
},
triggerSlash: function(command) {
return makeRequest('triggerSlash', { command: command });
},
getLastMessageId: function() {
return makeRequest('getLastMessageId', {});
},
toastr: function(type, message, title) {
return makeRequest('toastr', { type: type, message: message, title: title });
}
};
if (!window.TavernHelper) {
window.TavernHelper = window.AmilyHelper;
console.log('[Amily2-Iframe] TavernHelper 别名已创建');
} else {
console.log('[Amily2-Iframe] 检测到已存在的 TavernHelper,保持原有实现');
}
window.triggerSlash = function(command) {
return makeRequest('triggerSlash', { command: command });
};
window.getChatMessages = function(range, options) {
return makeRequest('getChatMessages', { range: range, options: options });
};
window.setChatMessages = function(messages, options) {
return makeRequest('setChatMessages', { messages: messages, options: options });
};
window.setChatMessage = function(field_values, message_id, options) {
return makeRequest('setChatMessage', {
field_values: field_values,
message_id: message_id,
options: options || {}
});
};
window.switchSwipe = function(messageIndex, swipeIndex) {
return makeRequest('switchSwipe', { messageIndex: messageIndex, swipeIndex: swipeIndex });
};
window.createChatMessages = function(messages, options) {
return makeRequest('createChatMessages', { messages: messages, options: options });
};
window.deleteChatMessages = function(ids, options) {
return makeRequest('deleteChatMessages', { ids: ids, options: options });
};
window.getLorebooks = function() {
return makeRequest('getLorebooks', {});
};
window.getCharLorebooks = function(options) {
return makeRequest('getCharLorebooks', { options: options });
};
window.getLorebookEntries = function(bookName) {
return makeRequest('getLorebookEntries', { bookName: bookName });
};
window.setLorebookEntries = function(bookName, entries) {
return makeRequest('setLorebookEntries', { bookName: bookName, entries: entries });
};
window.createLorebookEntries = function(bookName, entries) {
return makeRequest('createLorebookEntries', { bookName: bookName, entries: entries });
};
window.createLorebook = function(bookName) {
return makeRequest('createLorebook', { bookName: bookName });
};
window.getLastMessageId = function() {
return makeRequest('getLastMessageId', {});
};
window.getVariables = function(options) {
return makeRequest('getVariables', { options: options });
};
window.setVariables = function(variables, options) {
return makeRequest('setVariables', { variables: variables, options: options });
};
window.deleteVariable = function(variablePath, options) {
return makeRequest('deleteVariable', { variablePath: variablePath, options: options });
};
window.getCharData = function(name) {
return makeRequest('getCharData', { name: name });
};
window.getCharAvatarPath = function(name) {
return makeRequest('getCharAvatarPath', { name: name });
};
window.getLorebookSettings = function() {
return makeRequest('getLorebookSettings', {});
};
window.setLorebookSettings = function(settings) {
return makeRequest('setLorebookSettings', { settings: settings });
};
window.getChatLorebook = function() {
return makeRequest('getChatLorebook', {});
};
window.setChatLorebook = function(lorebook) {
return makeRequest('setChatLorebook', { lorebook: lorebook });
};
window.substitudeMacros = function(text) {
return makeRequest('substitudeMacros', { text: text });
};
window.toastr = {
success: function(message, title) {
return makeRequest('toastr', { type: 'success', message: message, title: title });
},
info: function(message, title) {
return makeRequest('toastr', { type: 'info', message: message, title: title });
},
warning: function(message, title) {
return makeRequest('toastr', { type: 'warning', message: message, title: title });
},
warn: function(message, title) {
return makeRequest('toastr', { type: 'warning', message: message, title: title });
},
error: function(message, title) {
return makeRequest('toastr', { type: 'error', message: message, title: title });
}
};
console.log('[Amily2-Iframe] 完整的 API 已加载到全局作用域');
console.log('[Amily2-Iframe] 可用的全局对象: AmilyHelper, TavernHelper');
console.log('[Amily2-Iframe] 可用的全局函数: triggerSlash, getChatMessages, setChatMessage, toastr, 等');
</script>
<script type="module" src="/scripts/extensions/third-party/${extensionName}/core/tavern-helper/iframe_client.js"></script>
`;
const injectionBlock = `
${baseTag}
<script>${iframeClientScript()}</script>
${headHints}
${vhFix}
${apiScript}
`;
const isFullHtml = /<html/i.test(html) && /<\/html>/i.test(html);
if (isFullHtml) {
if (html.includes('</head>')) {
return html.replace('</head>', `${injectionBlock}</head>`);
} else if (html.includes('<body')) {
return html.replace('<body', `<head>${injectionBlock}</head><body`);
}
return `<!DOCTYPE html>${injectionBlock}${html}`;
}
return `<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="color-scheme" content="dark light">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>html,body{margin:0;padding:0;background:transparent;font-family:inherit;color:inherit}</style>
${injectionBlock}
</head>
<body>${html}</body></html>`;
}
function getOrCreateWrapper(preEl) {
let wrapper = preEl.previousElementSibling;
if (!wrapper || !wrapper.classList.contains('amily2-iframe-wrapper')) {
wrapper = document.createElement('div');
wrapper.className = 'amily2-iframe-wrapper';
wrapper.style.cssText = 'margin:0;';
preEl.parentNode.insertBefore(wrapper, preEl);
}
return wrapper;
}
function registerIframeMapping(iframe, wrapper) {
const tryMap = () => {
try {
if (iframe && iframe.contentWindow) {
winMap.set(iframe.contentWindow, { iframe, wrapper });
return true;
}
} catch (e) { }
return false;
};
if (tryMap()) return;
let tries = 0;
const t = setInterval(() => {
tries++;
if (tryMap() || tries > 20) clearInterval(t);
}, 25);
}
function handleIframeMessage(event) {
const data = event.data || {};
let rec = winMap.get(event.source);
if (!rec || !rec.iframe) {
const iframes = document.querySelectorAll('iframe.amily2-iframe');
for (const iframe of iframes) {
if (iframe.contentWindow === event.source) {
rec = { iframe, wrapper: iframe.parentElement };
winMap.set(event.source, rec);
break;
}
}
}
if (rec && rec.iframe && typeof data.height === 'number') {
const next = Math.max(0, Number(data.height) || 0);
if (next < 1) return;
const prev = lastHeights.get(rec.iframe) || 0;
if (!data.force && Math.abs(next - prev) < 1) return;
lastHeights.set(rec.iframe, next);
requestAnimationFrame(() => { rec.iframe.style.height = `${next}px`; });
}
}
function setIframeBlobHTML(iframe, fullHTML, codeHash) {
const existing = hashToBlobUrl.get(codeHash);
if (existing) {
iframe.src = existing;
blobUrls.set(iframe, existing);
return;
}
const blob = new Blob([fullHTML], { type: 'text/html' });
const url = URL.createObjectURL(blob);
iframe.src = url;
blobUrls.set(iframe, url);
hashToBlobUrl.set(codeHash, url);
blobLRU.push(codeHash);
while (blobLRU.length > BLOB_CACHE_LIMIT) {
const old = blobLRU.shift();
const u = hashToBlobUrl.get(old);
hashToBlobUrl.delete(old);
try { URL.revokeObjectURL(u) } catch (e) { }
}
}
function releaseIframeBlob(iframe) {
try {
const url = blobUrls.get(iframe);
if (url) URL.revokeObjectURL(url);
blobUrls.delete(iframe);
} catch (e) { }
}
function renderHtmlInIframe(htmlContent, container, preElement) {
try {
const originalHash = djb2(htmlContent);
const iframe = document.createElement('iframe');
iframe.id = generateUniqueId();
iframe.className = 'amily2-iframe';
iframe.style.cssText = 'width:100%;border:none;background:transparent;overflow:hidden;height:0;margin:0;padding:0;display:block;contain:layout paint style;will-change:height;min-height:50px';
iframe.setAttribute('frameborder', '0');
iframe.setAttribute('scrolling', 'no');
iframe.loading = 'eager';
if (settings.sandboxMode) {
iframe.setAttribute('sandbox', 'allow-scripts allow-modals');
} else {
iframe.setAttribute('sandbox', 'allow-scripts allow-same-origin allow-forms allow-modals allow-popups');
}
const wrapper = getOrCreateWrapper(preElement);
wrapper.querySelectorAll('.amily2-iframe').forEach(old => {
try { old.src = 'about:blank'; } catch (e) { }
releaseIframeBlob(old);
old.remove();
});
const codeHash = djb2(htmlContent);
const full = buildWrappedHtml(htmlContent);
if (settings.useBlob) {
setIframeBlobHTML(iframe, full, codeHash);
} else {
iframe.srcdoc = full;
}
wrapper.appendChild(iframe);
preElement.classList.remove('amily2-show');
preElement.style.display = 'none';
registerIframeMapping(iframe, wrapper);
try { iframe.contentWindow?.postMessage({ type: 'probe' }, '*'); } catch (e) { }
preElement.dataset.amily2Final = 'true';
preElement.dataset.amily2Hash = originalHash;
return iframe;
} catch (err) {
return null;
}
}
function processCodeBlocks(messageElement) {
if (extension_settings[extensionName].render_enabled === false) return;
try {
const codeBlocks = messageElement.querySelectorAll('pre > code');
codeBlocks.forEach(codeBlock => {
const preElement = codeBlock.parentElement;
const should = shouldRenderContentByBlock(codeBlock);
const html = codeBlock.textContent || '';
const hash = djb2(html);
const isFinal = preElement.dataset.amily2Final === 'true';
const same = preElement.dataset.amily2Hash === hash;
if (isFinal && same) return;
if (should) {
renderHtmlInIframe(html, preElement.parentNode, preElement);
} else {
preElement.classList.add('amily2-show');
preElement.removeAttribute('data-amily2-final');
preElement.removeAttribute('data-amily2-hash');
preElement.style.display = '';
}
preElement.dataset.amily2Bound = 'true';
});
} catch (err) {
console.error('[Amily2-Renderer] Error during processCodeBlocks:', err);
}
}
function processMessageById(messageId) {
const messageElement = document.querySelector(`div.mes[mesid="${messageId}"] .mes_text`);
if (!messageElement) return;
processCodeBlocks(messageElement);
}
export function initializeRenderer() {
if (window.isXiaobaixEnabled) {
console.log('[Amily2-Renderer] 检测到 LittleWhiteBox 已激活为避免冲突Amily2 渲染器已禁用。');
return;
}
const handleMessage = (data) => {
const messageId = typeof data === 'object' ? data.messageId : data;
if (messageId == null) return;
console.log('[Amily2-Renderer] 处理消息渲染:', messageId);
setTimeout(() => processMessageById(messageId), 50);
};
eventSource.on(event_types.MESSAGE_RECEIVED, handleMessage);
eventSource.on(event_types.MESSAGE_UPDATED, handleMessage);
eventSource.on(event_types.MESSAGE_SWIPED, handleMessage);
eventSource.on(event_types.MESSAGE_EDITED, handleMessage);
eventSource.on(event_types.USER_MESSAGE_RENDERED, handleMessage);
eventSource.on(event_types.CHARACTER_MESSAGE_RENDERED, handleMessage);
eventSource.on(event_types.IMPERSONATE_READY, handleMessage);
eventSource.on(event_types.CHAT_CHANGED, () => {
console.log('[Amily2-Renderer] 聊天已切换,重新渲染所有 iframe');
setTimeout(renderAllIframes, 100);
});
window.addEventListener('message', handleIframeMessage);
console.log('[Amily2-Renderer] 渲染器已初始化,监听事件: MESSAGE_RECEIVED, MESSAGE_UPDATED, MESSAGE_SWIPED, MESSAGE_EDITED, USER_MESSAGE_RENDERED, CHARACTER_MESSAGE_RENDERED, IMPERSONATE_READY');
}
export function renderAllIframes() {
const messages = document.querySelectorAll('.mes');
messages.forEach(message => {
const messageId = message.getAttribute('mesid');
if (messageId) {
processMessageById(messageId);
}
});
}
export function clearAllIframes() {
const iframes = document.querySelectorAll('.amily2-iframe');
iframes.forEach(iframe => {
const wrapper = iframe.parentElement;
if (wrapper && wrapper.classList.contains('amily2-iframe-wrapper')) {
const preElement = wrapper.nextElementSibling;
if (preElement && preElement.tagName === 'PRE') {
preElement.classList.add('amily2-show');
preElement.style.display = '';
}
wrapper.remove();
}
});
}

View File

@@ -1,9 +1,17 @@
import { amilyHelper } from './tavern-helper/main.js';
import { eventSource, event_types } from "/script.js";
import {
world_names,
loadWorldInfo,
createNewWorldInfo,
createWorldInfoEntry,
saveWorldInfo,
reloadEditor
} from "/scripts/world-info.js";
import { refreshWorldbookListOnly } from './lore.js';
// 我们现在总是“可用”的,因为我们依赖自己的实现,而不是那个屎山酒馆。
// 检查我们自己的 amilyHelper 是否存在
export function isTavernHelperAvailable() {
return true;
return typeof amilyHelper !== 'undefined' && amilyHelper !== null;
}
export async function compatibleTriggerSlash(command) {
return await amilyHelper.triggerSlash(command);
@@ -27,43 +35,96 @@ export async function safeUpdateLorebookEntries(bookName, entries) {
export async function compatibleWriteToLorebook(targetLorebookName, entryComment, contentUpdateCallback, options = {}) {
console.log('[Amily助手-写入模块] 接收到的写入选项:', options);
console.log('[兼容写入模块] 接收到的写入选项:', options);
// 优先使用 AmilyHelper
if (isTavernHelperAvailable()) {
try {
console.log('[兼容写入模块] 检测到 AmilyHelper优先使用新逻辑...');
const entries = await amilyHelper.getLorebookEntries(targetLorebookName);
const existingEntry = entries.find((e) => e.comment === entryComment && e.enabled);
if (existingEntry) {
const newContent = contentUpdateCallback(existingEntry.content);
await amilyHelper.setLorebookEntries(targetLorebookName, [{ uid: existingEntry.uid, content: newContent }]);
} else {
const newContent = contentUpdateCallback(null);
const { keys = [], isConstant = false, insertion_position, depth: insertion_depth } = options;
const positionMap = { 'before_char': 0, 'after_char': 1, 'before_an': 2, 'after_an': 3, 'at_depth': 4 };
const newEntryData = {
comment: entryComment,
content: newContent,
key: keys,
constant: isConstant,
position: positionMap[insertion_position] ?? 4,
depth: parseInt(insertion_depth) || 998,
enabled: true,
};
await amilyHelper.createLorebookEntries(targetLorebookName, [newEntryData]);
}
console.log(`[Amily助手] 成功将条目 "${entryComment}" 写入《${targetLorebookName}》。`);
// 派发被证明有效的自定义刷新事件
document.dispatchEvent(new CustomEvent('amily-lorebook-created', { detail: { bookName: targetLorebookName } }));
refreshWorldbookListOnly(); // 刷新UI
return true;
} catch (error) {
console.error(`[Amily助手] 写入失败,将尝试回退到传统逻辑。错误:`, error);
toastr.warning('Amily助手写入失败尝试使用传统方式...', '兼容模式');
}
}
// AmilyHelper 不可用或失败时的后备传统逻辑
try {
const entries = await amilyHelper.getLorebookEntries(targetLorebookName);
const existingEntry = entries.find((e) => e.comment === entryComment && e.enabled);
console.log('[兼容写入模块] AmilyHelper 不可用或失败,使用传统逻辑...');
let bookData = await loadWorldInfo(targetLorebookName);
if (!bookData) {
console.warn(`[传统逻辑] 世界书《${targetLorebookName}》不存在,将自动创建。`);
await createNewWorldInfo(targetLorebookName);
if (!world_names.includes(targetLorebookName)) {
world_names.push(targetLorebookName);
world_names.sort();
refreshWorldbookListOnly(); // 刷新UI
}
document.dispatchEvent(new CustomEvent('amily-lorebook-created', { detail: { bookName: targetLorebookName } }));
bookData = await loadWorldInfo(targetLorebookName);
if (!bookData) throw new Error(`创建并加载世界书《${targetLorebookName}》失败。`);
}
const existingEntry = Object.values(bookData.entries).find(e => e.comment === entryComment && !e.disable);
if (existingEntry) {
const newContent = contentUpdateCallback(existingEntry.content);
await amilyHelper.setLorebookEntries(targetLorebookName, [{ uid: existingEntry.uid, content: newContent }]);
existingEntry.content = contentUpdateCallback(existingEntry.content);
} else {
const newContent = contentUpdateCallback(null);
const newEntry = createWorldInfoEntry(targetLorebookName, bookData);
const { keys = [], isConstant = false, insertion_position, depth: insertion_depth } = options;
const positionMap = { 'before_char': 0, 'after_char': 1, 'before_an': 2, 'after_an': 3, 'at_depth': 4 };
const newEntryData = {
Object.assign(newEntry, {
comment: entryComment,
content: newContent,
content: contentUpdateCallback(null),
key: keys,
constant: isConstant,
position: positionMap[insertion_position] ?? 4,
depth: parseInt(insertion_depth) || 998,
enabled: true,
};
await amilyHelper.createLorebookEntries(targetLorebookName, [newEntryData]);
disable: false,
});
}
if (eventSource && typeof eventSource.emit === "function" && event_types.CHARACTER_PAGE_LOADED) {
eventSource.emit(event_types.CHARACTER_PAGE_LOADED);
}
await saveWorldInfo(targetLorebookName, bookData, true);
console.log(`[传统逻辑] 成功将条目 "${entryComment}" 写入《${targetLorebookName}》。`);
console.log(`[Amily助手] 成功将条目 "${entryComment}" 写入《${targetLorebookName}》。`);
// 刷新编辑器(如果正在查看)
reloadEditor(targetLorebookName);
// 派发被证明有效的自定义刷新事件
document.dispatchEvent(new CustomEvent('amily-lorebook-created', { detail: { bookName: targetLorebookName } }));
return true;
} catch (error) {
console.error(`[Amily助手] 写入世界书时发生严重错误:`, error);
toastr.error(`写入世界书失败: ${error.message}`, "Amily助手");
console.error(`[传统逻辑] 写入世界书时发生严重错误:`, error);
toastr.error(`写入世界书失败: ${error.message}`, "传统逻辑");
return false;
}
}

View File

@@ -659,8 +659,9 @@ export function bindGlossaryEvents() {
bindReorganizeEvents();
loadWorldBooks();
eventSource.on(event_types.CHARACTER_PAGE_LOADED, () => {
console.log('[Amily2-术语表] 检测到角色加载,重新加载世界书列表以确保同步。');
// 监听我们自己的世界书创建事件,而不是监听全局的角色加载事件,避免冲突
document.addEventListener('amily-lorebook-created', (event) => {
console.log(`[Amily2-术语表] 检测到新世界书《${event.detail.bookName}》创建,重新加载列表以确保同步。`);
loadWorldBooks();
});

194
index.js
View File

@@ -11,6 +11,7 @@ import { characters, this_chid } from '/script.js';
import { injectTableData, generateTableContent } from "./core/table-system/injector.js";
import { initialize as initializeRagProcessor } from "./core/rag-processor.js";
import { loadTables, clearHighlights, rollbackAndRefill, rollbackState, commitPendingDeletions, saveStateToMessage, getMemoryState, clearUpdatedTables } from './core/table-system/manager.js';
import { fillWithSecondaryApi } from './core/table-system/secondary-filler.js';
import { renderTables } from './ui/table-bindings.js';
import { log } from './core/table-system/logger.js';
import { eventSource, event_types, saveSettingsDebounced } from '/script.js';
@@ -25,7 +26,8 @@ import { cwbDefaultSettings } from './CharacterWorldBook/src/cwb_config.js';
import { bindGlossaryEvents } from './glossary/GT_bindings.js';
import './core/amily2-updater.js';
import { updateOrInsertTableInChat, startContinuousRendering, stopContinuousRendering } from './ui/message-table-renderer.js';
import { isTavernHelperAvailable } from './core/tavernhelper-compatibility.js';
import { initializeRenderer } from './core/tavern-helper/renderer.js';
import { initializeApiListener, registerApiHandler, amilyHelper, initializeAmilyHelper } from './core/tavern-helper/main.js';
const STYLE_SETTINGS_KEY = 'amily2_custom_styles';
const STYLE_ROOT_SELECTOR = '#amily2_memorisation_forms_panel';
@@ -226,6 +228,8 @@ function loadPluginStyles() {
loadStyleFile("amily2-glossary.css"); // 【新圣谕】为术语表披上其专属华服
loadStyleFile("table.css"); // 【第四道圣谕】为内存储司披上其专属华服
loadStyleFile("optimization.css"); // 【第五道圣谕】为剧情优化披上其专属华服
loadStyleFile("renderer.css"); // 【新圣谕】为渲染器披上其专属华服
loadStyleFile("iframe-renderer.css"); // 【新圣谕】为iframe渲染内容披上其专属华服
// 【第六道圣谕】为角色世界书披上其专属华服
const cwbStyleId = 'cwb-feature-style';
@@ -254,6 +258,59 @@ function loadPluginStyles() {
}
window.addEventListener('message', function (event) {
// 处理头像获取请求
if (event.data && event.data.type === 'getAvatars') {
// 【兼容性修复】如果 LittleWhiteBox 激活,则不处理此消息,避免冲突
if (window.isXiaobaixEnabled) {
return;
}
const userAvatar = `/characters/${getContext().userCharacter?.avatar ?? ''}`;
const charAvatar = `/characters/${getContext().characters[this_chid]?.avatar ?? ''}`;
event.source.postMessage({
source: 'amily2-host',
type: 'avatars',
urls: { user: userAvatar, char: charAvatar }
}, '*');
return;
}
// 处理来自 iframe 的交互事件
if (event.data && event.data.source === 'amily2-iframe') {
const { action, detail } = event.data;
console.log(`[Amily2-主窗口] 收到来自iframe的动作: ${action}`, detail);
switch (action) {
case 'sendMessage':
if (detail && detail.message) {
$('#send_textarea').val(detail.message).trigger('input');
$('#send_but').trigger('click');
console.log(`[Amily2-主窗口] 已发送消息: ${detail.message}`);
}
break;
case 'showToast':
if (detail && detail.message && window.toastr) {
const toastType = detail.type || 'info';
if (typeof window.toastr[toastType] === 'function') {
window.toastr[toastType](detail.message, detail.title || '通知');
}
}
break;
case 'buttonClick':
console.log(`[Amily2-主窗口] 按钮被点击:`, detail);
if (window.toastr) {
window.toastr.info(`按钮 "${detail.buttonId || '未知'}" 被点击`, 'iframe交互');
}
break;
default:
console.warn(`[Amily2-主窗口] 未知的动作类型: ${action}`);
}
}
});
window.addEventListener("error", (event) => {
const stackTrace = event.error?.stack || "";
if (stackTrace.includes("ST-Amily2-Chat-Optimisation")) {
@@ -264,12 +321,106 @@ window.addEventListener("error", (event) => {
jQuery(async () => {
console.log("[Amily2号-帝国枢密院] 开始执行开国大典...");
initializeApiListener();
registerApiHandler('getChatMessages', async (data) => {
return amilyHelper.getChatMessages(data.range, data.options);
});
registerApiHandler('setChatMessages', async (data) => {
return await amilyHelper.setChatMessages(data.messages, data.options);
});
registerApiHandler('setChatMessage', async (data) => {
const field_values = data.field_values || data.content;
const message_id = data.message_id !== undefined ? data.message_id : data.index;
const options = data.options || {};
console.log('[Amily2-API] setChatMessage 收到参数:', { field_values, message_id, options, raw_data: data });
return await amilyHelper.setChatMessage(field_values, message_id, options);
});
registerApiHandler('createChatMessages', async (data) => {
return await amilyHelper.createChatMessages(data.messages, data.options);
});
registerApiHandler('deleteChatMessages', async (data) => {
return await amilyHelper.deleteChatMessages(data.ids, data.options);
});
registerApiHandler('getLorebooks', async (data) => {
return await amilyHelper.getLorebooks();
});
registerApiHandler('getCharLorebooks', async (data) => {
return await amilyHelper.getCharLorebooks(data.options);
});
registerApiHandler('getLorebookEntries', async (data) => {
return await amilyHelper.getLorebookEntries(data.bookName);
});
registerApiHandler('setLorebookEntries', async (data) => {
return await amilyHelper.setLorebookEntries(data.bookName, data.entries);
});
registerApiHandler('createLorebookEntries', async (data) => {
return await amilyHelper.createLorebookEntries(data.bookName, data.entries);
});
registerApiHandler('createLorebook', async (data) => {
return await amilyHelper.createLorebook(data.bookName);
});
registerApiHandler('triggerSlash', async (data) => {
return await amilyHelper.triggerSlash(data.command);
});
registerApiHandler('getLastMessageId', async (data) => {
return amilyHelper.getLastMessageId();
});
registerApiHandler('toastr', async (data) => {
if (window.toastr && typeof window.toastr[data.type] === 'function') {
window.toastr[data.type](data.message, data.title);
}
return true;
});
registerApiHandler('switchSwipe', async (data) => {
const { messageIndex, swipeIndex } = data;
const messages = await amilyHelper.getChatMessages(messageIndex, { include_swipes: true });
if (messages && messages.length > 0 && messages[0].swipes) {
const content = messages[0].swipes[swipeIndex];
if (content !== undefined) {
await amilyHelper.setChatMessages([{
message_id: messageIndex,
message: content
}], { refresh: 'affected' });
const context = getContext();
if (context.chat[messageIndex]) {
context.chat[messageIndex].swipe_id = swipeIndex;
}
return { success: true, message: `已切换至开场白 ${swipeIndex}` };
}
}
throw new Error(`无法切换到开场白 ${swipeIndex}`);
});
initializeAmilyHelper();
console.log("[Amily2号-帝国枢密院] 开始执行开国大典...");
if (!extension_settings[extensionName]) {
extension_settings[extensionName] = {};
}
const combinedDefaultSettings = { ...defaultSettings, ...tableSystemDefaultSettings, ...cwbDefaultSettings, render_on_every_message: false };
const combinedDefaultSettings = { ...defaultSettings, ...tableSystemDefaultSettings, ...cwbDefaultSettings, render_on_every_message: false, render_enabled: false };
for (const key in combinedDefaultSettings) {
if (extension_settings[extensionName][key] === undefined) {
@@ -298,7 +449,6 @@ jQuery(async () => {
console.log("[Amily2号-开国大典] 步骤三:开始召唤府邸...");
createDrawer();
// 【V15.0 修复】为术语表面板添加轮询加载,确保在面板渲染后再绑定事件
function waitForGlossaryPanelAndBindEvents() {
let attempts = 0;
const maxAttempts = 50;
@@ -380,10 +530,8 @@ jQuery(async () => {
let isProcessingPlotOptimization = false;
async function onPlotGenerationAfterCommands(type, params, dryRun) {
// 【V15.2 新增】在发送消息后,清除所有表格的“已更新”高亮状态
clearUpdatedTables();
// 【V15.3 修正】提交删除的逻辑已移至 injector.js此处不再需要
console.log("[Amily2-剧情优化] Generation after commands triggered", { type, params, dryRun, isProcessing: isProcessingPlotOptimization });
@@ -493,14 +641,38 @@ jQuery(async () => {
eventSource.on(event_types.MESSAGE_RECEIVED, onMessageReceived);
eventSource.on(event_types.IMPERSONATE_READY, onMessageReceived);
eventSource.on(event_types.MESSAGE_RECEIVED, (chat_id) => handleTableUpdate(chat_id));
eventSource.on(event_types.MESSAGE_SWIPED, (chat_id) => {
eventSource.on(event_types.MESSAGE_SWIPED, async (chat_id) => {
const context = getContext();
if (context.chat.length < 2) {
log(`【监察系统】检测到消息滑动,但聊天记录不足2条,已跳过状态回退。`, 'info');
log('【监察系统】检测到消息滑动,但聊天记录不足,已跳过状态回退。', 'info');
return;
}
log(`【监察系统】检测到消息滑动 (SWIPED),开始执行状态回退...`, 'warn');
log('【监察系统】检测到消息滑动 (SWIPED),开始执行状态回退...', 'warn');
rollbackState();
const latestMessage = context.chat[chat_id] || context.chat[context.chat.length - 1];
if (latestMessage.is_user) {
log('【监察系统】滑动后最新消息是用户,跳过填表。', 'info');
renderTables();
return;
}
const settings = extension_settings[extensionName];
const fillingMode = settings.filling_mode || 'main-api';
if (fillingMode === 'main-api') {
log(`【监察系统】主填表模式回退后强制刷新消息ID: ${chat_id}`, 'info');
await handleTableUpdate(chat_id, true);
} else if (fillingMode === 'secondary-api' || fillingMode === 'optimized') {
log('【监察系统】分步/优化模式,回退后强制二次填表最新消息。', 'info');
await fillWithSecondaryApi(latestMessage, true);
} else {
log('【监察系统】未配置填表模式,跳过填表。', 'info');
}
renderTables();
log('【监察系统】滑动后填表完成UI 已刷新。', 'success');
});
eventSource.on(event_types.MESSAGE_EDITED, (mes_id) => {
handleTableUpdate(mes_id);
@@ -515,7 +687,7 @@ jQuery(async () => {
setTimeout(() => {
log("【监察系统】检测到“朝代更迭”(CHAT_CHANGED),开始重修史书并刷新宫殿...", 'info');
clearHighlights();
clearUpdatedTables(); // 【V15.2 新增】切换聊天时清除“已更新”高亮
clearUpdatedTables();
loadTables();
renderTables();
@@ -555,7 +727,6 @@ jQuery(async () => {
console.log('[Amily2-核心引擎] 开始执行统一注入 (聊天长度:', args[0]?.length || 0, ')');
try {
// 【V15.3 修正】由于 injectTableData 现在是异步的,需要 await
await injectTableData(...args);
} catch (error) {
console.error('[Amily2-内存储司] 表格注入失败:', error);
@@ -593,6 +764,8 @@ jQuery(async () => {
handleUpdateCheck();
handleMessageBoard();
initializeRenderer();
if (extension_settings[extensionName].render_on_every_message) {
startContinuousRendering();
}
@@ -627,5 +800,4 @@ jQuery(async () => {
}
}
}, checkInterval);
});

View File

@@ -1,7 +1,7 @@
{
"name": "Amily2号聊天优化助手",
"display_name": "Amily2号助手",
"version": "1.5.9",
"version": "1.6.2",
"author": "Wx-2025",
"description": "一个拥有独立UI的智能引擎正文优化、自动总结、记忆表格、rag向量、隐藏楼层、剧情推进六大功能整合。",
"minSillyTavernVersion": "1.10.0",
@@ -28,6 +28,9 @@

View File

@@ -2,7 +2,7 @@ import { extension_settings, getContext } from "/scripts/extensions.js";
import { characters, this_chid, getRequestHeaders, saveSettingsDebounced, eventSource, event_types } from "/script.js";
import { defaultSettings, extensionName, saveSettings } from "../utils/settings.js";
import { pluginAuthStatus, activatePluginAuthorization, getPasswordForDate } from "../utils/auth.js";
import { fetchModels } from "../core/api.js";
import { fetchModels, testApiConnection } from "../core/api.js";
import { getJqyhApiSettings, testJqyhApiConnection, fetchJqyhModels } from '../core/api/JqyhApi.js';
import { safeLorebooks, safeCharLorebooks, safeLorebookEntries, isTavernHelperAvailable } from "../core/tavernhelper-compatibility.js";
@@ -421,12 +421,50 @@ function bindAmily2ModalWorldBookSettings() {
}
export function bindModalEvents() {
const refreshButton = document.getElementById('amily2_refresh_models');
if (refreshButton && !document.getElementById('amily2_test_api_connection')) {
const testButton = document.createElement('button');
testButton.id = 'amily2_test_api_connection';
testButton.className = 'menu_button interactable';
testButton.innerHTML = '<i class="fas fa-plug"></i> 测试连接';
refreshButton.insertAdjacentElement('afterend', testButton);
}
initializePlotOptimizationBindings();
bindAmily2ModalWorldBookSettings();
const container = $("#amily2_drawer_content").length ? $("#amily2_drawer_content") : $("#amily2_chat_optimiser");
// Collapsible sections logic
container.find('.collapsible-legend').each(function() {
$(this).on('click', function(e) {
e.preventDefault();
e.stopPropagation();
const legend = $(this);
const content = legend.siblings('.collapsible-content');
const icon = legend.find('.collapse-icon');
const isCurrentlyVisible = content.is(':visible');
const isCollapsedAfterClick = isCurrentlyVisible;
if (isCollapsedAfterClick) {
content.hide();
icon.removeClass('fa-chevron-up').addClass('fa-chevron-down');
} else {
content.show();
icon.removeClass('fa-chevron-down').addClass('fa-chevron-up');
}
const sectionId = legend.text().trim();
if (!extension_settings[extensionName]) {
extension_settings[extensionName] = {};
}
extension_settings[extensionName][`collapsible_${sectionId}_collapsed`] = isCollapsedAfterClick;
saveSettingsDebounced();
});
});
displayDailyAuthCode();
function updateModelInputView() {
const settings = extension_settings[extensionName] || {};
@@ -497,7 +535,7 @@ export function bindModalEvents() {
.off("click.amily2.actions")
.on(
"click.amily2.actions",
"#amily2_refresh_models, #amily2_test, #amily2_fix_now",
"#amily2_refresh_models, #amily2_test_api_connection, #amily2_test, #amily2_fix_now",
async function () {
if (!pluginAuthStatus.authorized) return;
const button = $(this);
@@ -511,13 +549,16 @@ export function bindModalEvents() {
const models = await fetchModels();
if (models.length > 0) {
setAvailableModels(models);
localStorage.setItem(
"cached_models_amily2",
JSON.stringify(models),
);
localStorage.setItem(
"cached_models_amily2",
JSON.stringify(models),
);
populateModelDropdown();
}
break;
case "amily2_test_api_connection":
await testApiConnection();
break;
case "amily2_test":
await testReplyChecker();
break;
@@ -662,7 +703,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_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", 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_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 () {
if (!pluginAuthStatus.authorized) return;
const mainPanel = container.find('.plugin-features');
@@ -673,6 +714,7 @@ container
const characterWorldBookPanel = container.find('#amily2_character_world_book_panel');
const worldEditorPanel = container.find('#amily2_world_editor_panel');
const glossaryPanel = container.find('#amily2_glossary_panel');
const rendererPanel = container.find('#amily2_renderer_panel');
mainPanel.hide();
additionalPanel.hide();
@@ -682,8 +724,12 @@ container
characterWorldBookPanel.hide();
worldEditorPanel.hide();
glossaryPanel.hide();
rendererPanel.hide();
switch (this.id) {
case 'amily2_open_renderer':
rendererPanel.show();
break;
case 'amily2_open_plot_optimization':
plotOptimizationPanel.show();
break;
@@ -712,6 +758,7 @@ container
case 'amily2_back_to_main_from_cwb':
case 'amily2_back_to_main_from_world_editor':
case 'amily2_back_to_main_from_glossary':
case 'amily2_renderer_back_button':
mainPanel.show();
break;
}

View File

@@ -19,6 +19,7 @@ import { bindHistoriographyEvents } from "./historiography-bindings.js";
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";
const extensionFolderPath = `scripts/extensions/third-party/${extensionName}`;
@@ -101,6 +102,10 @@ async function initializePanel(contentPanel, errorContainer) {
const glossaryPanelHtml = `<div id="amily2_glossary_panel" style="display: none;">${glossaryContent}</div>`;
mainContainer.append(glossaryPanelHtml);
const rendererContent = await $.get(`${extensionFolderPath}/core/tavern-helper/renderer.html`);
const rendererPanelHtml = `<div id="amily2_renderer_panel" style="display: none;">${rendererContent}</div>`;
mainContainer.append(rendererPanelHtml);
// 在面板创建后,加载世界书编辑器脚本
const worldEditorScriptId = 'world-editor-script';
if (!document.getElementById(worldEditorScriptId)) {
@@ -117,6 +122,7 @@ async function initializePanel(contentPanel, errorContainer) {
await loadSettings();
bindHanlinyuanEvents();
bindTableEvents();
initializeRendererBindings();
contentPanel.data("initialized", true);
console.log("[Amily-重构] 宫殿模块已按蓝图竣工。");
applyUpdateIndicator();

File diff suppressed because one or more lines are too long

View File

@@ -148,9 +148,37 @@ export function updateUI() {
if (settings.historiographySmallTriggerThreshold !== undefined) {
$('#amily2_mhb_small_trigger_count').val(settings.historiographySmallTriggerThreshold);
}
populateModelDropdown();
updatePlotOptimizationUI();
// 同步渲染器开关状态
if (settings.render_enabled !== undefined) {
$('#render-enable-toggle').prop('checked', settings.render_enabled);
}
// 同步渲染深度设置
if (settings.render_depth !== undefined) {
$('#render-depth').val(settings.render_depth);
}
populateModelDropdown();
updatePlotOptimizationUI();
// Restore collapsible sections state
$('.collapsible').each(function() {
const section = $(this);
const legend = section.find('.collapsible-legend');
const content = section.find('.collapsible-content');
const icon = legend.find('.collapse-icon');
const sectionId = legend.text().trim();
const isCollapsed = extension_settings[extensionName][`collapsible_${sectionId}_collapsed`] ?? true;
if (isCollapsed) {
content.hide();
icon.removeClass('fa-chevron-up').addClass('fa-chevron-down');
} else {
content.show();
icon.removeClass('fa-chevron-down').addClass('fa-chevron-up');
}
});
}
}