mirror of
https://github.com/SilenceLurker/ST-Amily2-Chat-Optimisation.git
synced 2026-06-06 08:55:50 +00:00
Merge branch 'Wx-2025:main' into main
This commit is contained in:
@@ -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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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} 个世界书条目的状态。`);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
@@ -145,7 +146,7 @@ function createCharCardViewerPopupHtml(displayItems) {
|
||||
if (charData.psyche_profile) html += renderCard('心智侧写', charData.psyche_profile, 'psyche_profile');
|
||||
if (charData.social_matrix) html += renderCard('社交矩阵', charData.social_matrix, 'social_matrix');
|
||||
if (charData.narrative_essence) html += renderCard('叙事精粹', charData.narrative_essence, 'narrative_essence');
|
||||
|
||||
|
||||
html += `<div class="cwb-cyber-card cwb-insertion-settings-card">
|
||||
<h4 class="cwb-cyber-card__title">注入设置</h4>
|
||||
<div class="cwb-cyber-card__content cwb-insertion-settings-content">
|
||||
@@ -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);
|
||||
@@ -307,42 +297,33 @@ function bindCharCardViewerPopupEvents($popup) {
|
||||
|
||||
const positionMap = {
|
||||
'before_char': 'before_character_definition',
|
||||
'after_char': 'after_character_definition',
|
||||
'after_char': 'after_character_definition',
|
||||
'before_an': 'before_author_note',
|
||||
'after_an': 'after_author_note',
|
||||
'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);
|
||||
@@ -358,7 +339,7 @@ function closeCharCardViewerPopup() {
|
||||
}
|
||||
|
||||
export async function showCharCardViewerPopup() {
|
||||
if (!isCwbEnabled()) return;
|
||||
if (!isCwbEnabled()) return;
|
||||
closeCharCardViewerPopup();
|
||||
try {
|
||||
const book = await getTargetWorldBook();
|
||||
@@ -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')) {
|
||||
@@ -385,7 +358,7 @@ export async function showCharCardViewerPopup() {
|
||||
bindCharCardViewerPopupEvents($(`#${CHAR_CARD_VIEWER_POPUP_ID}`));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const cleanChatId = currentChatId.replace(/ imported/g, '');
|
||||
let displayItems = [];
|
||||
|
||||
@@ -402,76 +375,81 @@ export async function showCharCardViewerPopup() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return false;
|
||||
});
|
||||
} else {
|
||||
relevantEntries = allEntries.filter(entry =>
|
||||
relevantEntries = allEntries.filter(entry =>
|
||||
entry.enabled &&
|
||||
Array.isArray(entry.keys) &&
|
||||
entry.keys.includes(cleanChatId)
|
||||
);
|
||||
}
|
||||
|
||||
const rosterEntries = relevantEntries.filter(entry =>
|
||||
const rosterEntries = relevantEntries.filter(entry =>
|
||||
entry.keys.includes('Amily2角色总集') && entry.keys.includes('角色总览')
|
||||
);
|
||||
|
||||
rosterEntries.forEach((entry, index) => {
|
||||
displayItems.push({
|
||||
uid: entry.uid,
|
||||
isRoster: true,
|
||||
comment: entry.comment,
|
||||
displayItems.push({
|
||||
uid: entry.uid,
|
||||
isRoster: true,
|
||||
comment: entry.comment,
|
||||
content: entry.content,
|
||||
rosterIndex: index
|
||||
rosterIndex: index
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
const popupHtml = createCharCardViewerPopupHtml(displayItems);
|
||||
@@ -576,7 +554,7 @@ function makeButtonDraggable($button) {
|
||||
|
||||
export function initializeCharCardViewer() {
|
||||
const $existingButton = $(`#${CHAR_CARD_VIEWER_BUTTON_ID}`);
|
||||
|
||||
|
||||
if ($existingButton.length > 0) {
|
||||
console.log('[CWB] Char card viewer button already exists');
|
||||
setTimeout(() => {
|
||||
@@ -586,12 +564,12 @@ export function initializeCharCardViewer() {
|
||||
}, 100);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const buttonHtml = `<div id="${CHAR_CARD_VIEWER_BUTTON_ID}" title="查看角色世界书" class="fa-solid fa-book-open"></div>`;
|
||||
$('body').append(buttonHtml);
|
||||
const $viewerButton = $(`#${CHAR_CARD_VIEWER_BUTTON_ID}`);
|
||||
makeButtonDraggable($viewerButton);
|
||||
|
||||
|
||||
const savedPosition = JSON.parse(localStorage.getItem(state.STORAGE_KEY_VIEWER_BUTTON_POS) || 'null');
|
||||
if (savedPosition) {
|
||||
$viewerButton.css({ top: savedPosition.top, left: savedPosition.left });
|
||||
@@ -604,9 +582,9 @@ export function initializeCharCardViewer() {
|
||||
$viewerButton.toggle(shouldShow);
|
||||
console.log(`[CWB] New button created with visibility: ${shouldShow}`);
|
||||
}, 100);
|
||||
|
||||
|
||||
console.log('[CWB] Char card viewer button initialized');
|
||||
|
||||
|
||||
let resizeTimeout;
|
||||
$(window).on('resize.cwbViewer', function () {
|
||||
clearTimeout(resizeTimeout);
|
||||
@@ -617,9 +595,9 @@ export function initializeCharCardViewer() {
|
||||
export function updateViewerButtonVisibility() {
|
||||
const $button = $(`#${CHAR_CARD_VIEWER_BUTTON_ID}`);
|
||||
const shouldShow = isCwbEnabled() && state.viewerEnabled;
|
||||
|
||||
|
||||
console.log(`[CWB] Updating viewer button visibility: ${shouldShow} (master: ${isCwbEnabled()}, viewer: ${state.viewerEnabled})`);
|
||||
|
||||
|
||||
if ($button.length > 0) {
|
||||
$button.toggle(shouldShow);
|
||||
console.log(`[CWB] Viewer button visibility set to: ${shouldShow}`);
|
||||
@@ -630,7 +608,7 @@ export function updateViewerButtonVisibility() {
|
||||
initializeCharCardViewer();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
|
||||
logDebug('悬浮窗按钮显示状态更新:', {
|
||||
masterEnabled: isCwbEnabled(),
|
||||
viewerEnabled: state.viewerEnabled,
|
||||
@@ -640,43 +618,43 @@ 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> 测试中...');
|
||||
|
||||
|
||||
try {
|
||||
await testCwbConnection();
|
||||
} catch (error) {
|
||||
@@ -686,15 +664,15 @@ 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> 获取中...');
|
||||
|
||||
|
||||
try {
|
||||
const models = await fetchCwbModels();
|
||||
const $modelSelect = $('#cwb-model');
|
||||
$modelSelect.empty();
|
||||
|
||||
|
||||
if (models && models.length > 0) {
|
||||
models.forEach(model => {
|
||||
$modelSelect.append(new Option(model.name, model.id));
|
||||
|
||||
@@ -542,3 +542,4 @@ export const sectionTitles = {
|
||||
cwb_summarizer_incremental: '角色世界书(CWB-增量)',
|
||||
novel_processor: '小说处理',
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
26
assets/renderer.css
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
132
core/api.js
132
core/api.js
@@ -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 apiProvider = $("#amily2_api_provider").val() || 'openai';
|
||||
const models = await fetchModels();
|
||||
|
||||
if (models.length > 0) {
|
||||
toastr.success(`${apiProvider} 提供商连接正常,找到 ${models.length} 个模型`, 'API连接正常');
|
||||
return true;
|
||||
} else {
|
||||
toastr.error('无法获取模型列表,请检查配置', 'API连接失败');
|
||||
const $button = $("#amily2_test_api_connection");
|
||||
if (!$button.length) return;
|
||||
|
||||
const originalHtml = $button.html();
|
||||
$button.prop("disabled", true).html('<i class="fas fa-spinner fa-spin"></i> 测试中');
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,386 +1,383 @@
|
||||
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";
|
||||
|
||||
let ChatCompletionService = undefined;
|
||||
try {
|
||||
const module = await import('/scripts/custom-request.js');
|
||||
ChatCompletionService = module.ChatCompletionService;
|
||||
console.log('[Amily2号-Jqyh外交部] 已成功召唤"皇家信使"(ChatCompletionService)。');
|
||||
} catch (e) {
|
||||
console.warn("[Amily2号-Jqyh外交部] 未能召唤“皇家信使”,部分高级功能(如Claw代理)将受限。请考虑更新SillyTavern版本。", e);
|
||||
}
|
||||
|
||||
function normalizeApiResponse(responseData) {
|
||||
let data = responseData;
|
||||
if (typeof data === 'string') {
|
||||
try {
|
||||
data = JSON.parse(data);
|
||||
} catch (e) {
|
||||
console.error(`[${extensionName}] Jqyh API响应JSON解析失败:`, e);
|
||||
return { error: { message: 'Invalid JSON response' } };
|
||||
}
|
||||
}
|
||||
if (data && typeof data.data === 'object' && data.data !== null && !Array.isArray(data.data)) {
|
||||
if (Object.hasOwn(data.data, 'data')) {
|
||||
data = data.data;
|
||||
}
|
||||
}
|
||||
if (data && data.choices && data.choices[0]) {
|
||||
return { content: data.choices[0].message?.content?.trim() };
|
||||
}
|
||||
if (data && data.content) {
|
||||
return { content: data.content.trim() };
|
||||
}
|
||||
if (data && data.data) {
|
||||
return { data: data.data };
|
||||
}
|
||||
if (data && data.error) {
|
||||
return { error: data.error };
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
export function getJqyhApiSettings() {
|
||||
return {
|
||||
apiMode: extension_settings[extensionName]?.jqyhApiMode || 'openai_test',
|
||||
apiUrl: extension_settings[extensionName]?.jqyhApiUrl?.trim() || '',
|
||||
apiKey: extension_settings[extensionName]?.jqyhApiKey?.trim() || '',
|
||||
model: extension_settings[extensionName]?.jqyhModel || '',
|
||||
maxTokens: extension_settings[extensionName]?.jqyhMaxTokens || 4000,
|
||||
temperature: extension_settings[extensionName]?.jqyhTemperature || 0.7,
|
||||
tavernProfile: extension_settings[extensionName]?.jqyhTavernProfile || ''
|
||||
};
|
||||
}
|
||||
|
||||
export async function callJqyhAI(messages, options = {}) {
|
||||
if (window.AMILY2_SYSTEM_PARALYZED === true) {
|
||||
console.error("[Amily2-Jqyh制裁] 系统完整性已受损,所有外交活动被无限期中止。");
|
||||
return null;
|
||||
}
|
||||
|
||||
const apiSettings = getJqyhApiSettings();
|
||||
|
||||
const finalOptions = {
|
||||
maxTokens: apiSettings.maxTokens,
|
||||
temperature: apiSettings.temperature,
|
||||
model: apiSettings.model,
|
||||
apiUrl: apiSettings.apiUrl,
|
||||
apiKey: apiSettings.apiKey,
|
||||
apiMode: apiSettings.apiMode,
|
||||
tavernProfile: apiSettings.tavernProfile,
|
||||
...options
|
||||
};
|
||||
|
||||
if (finalOptions.apiMode !== 'sillytavern_preset') {
|
||||
if (!finalOptions.apiUrl || !finalOptions.model || !finalOptions.apiKey) {
|
||||
console.warn("[Amily2-Jqyh外交部] API配置不完整,无法调用AI");
|
||||
toastr.error("API配置不完整,请检查URL、Key和模型配置。", "Jqyh-外交部");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
console.groupCollapsed(`[Amily2号-Jqyh统一API调用] ${new Date().toLocaleTimeString()}`);
|
||||
console.log("【请求参数】:", {
|
||||
mode: finalOptions.apiMode,
|
||||
model: finalOptions.model,
|
||||
maxTokens: finalOptions.maxTokens,
|
||||
temperature: finalOptions.temperature,
|
||||
messagesCount: messages.length
|
||||
});
|
||||
console.log("【消息内容】:", messages);
|
||||
console.groupEnd();
|
||||
|
||||
try {
|
||||
let responseContent;
|
||||
|
||||
switch (finalOptions.apiMode) {
|
||||
case 'openai_test':
|
||||
responseContent = await callJqyhOpenAITest(messages, finalOptions);
|
||||
break;
|
||||
case 'sillytavern_preset':
|
||||
responseContent = await callJqyhSillyTavernPreset(messages, finalOptions);
|
||||
break;
|
||||
default:
|
||||
console.error(`[Amily2-Jqyh外交部] 未支持的API模式: ${finalOptions.apiMode}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!responseContent) {
|
||||
console.warn('[Amily2-Jqyh外交部] 未能获取AI响应内容');
|
||||
return null;
|
||||
}
|
||||
|
||||
console.groupCollapsed("[Amily2号-Jqyh AI回复]");
|
||||
console.log(responseContent);
|
||||
console.groupEnd();
|
||||
|
||||
return responseContent;
|
||||
|
||||
} catch (error) {
|
||||
console.error(`[Amily2-Jqyh外交部] API调用发生错误:`, error);
|
||||
|
||||
if (error.message.includes('400')) {
|
||||
toastr.error(`API请求格式错误 (400): 请检查消息格式和模型配置`, "Jqyh API调用失败");
|
||||
} else if (error.message.includes('401')) {
|
||||
toastr.error(`API认证失败 (401): 请检查API Key配置`, "Jqyh API调用失败");
|
||||
} else if (error.message.includes('403')) {
|
||||
toastr.error(`API访问被拒绝 (403): 请检查权限设置`, "Jqyh API调用失败");
|
||||
} else if (error.message.includes('429')) {
|
||||
toastr.error(`API调用频率超限 (429): 请稍后重试`, "Jqyh API调用失败");
|
||||
} else if (error.message.includes('500')) {
|
||||
toastr.error(`API服务器错误 (500): 请稍后重试`, "Jqyh API调用失败");
|
||||
} else {
|
||||
toastr.error(`API调用失败: ${error.message}`, "Jqyh API调用失败");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function callJqyhOpenAITest(messages, options) {
|
||||
const isGoogleApi = options.apiUrl.includes('googleapis.com');
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
if (!isGoogleApi) {
|
||||
Object.assign(body, {
|
||||
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(body)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Jqyh全兼容API请求失败: ${response.status} - ${errorText}`);
|
||||
}
|
||||
|
||||
const responseData = await response.json();
|
||||
return responseData?.choices?.[0]?.message?.content;
|
||||
}
|
||||
|
||||
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上下文');
|
||||
}
|
||||
|
||||
const profileId = options.tavernProfile;
|
||||
if (!profileId) {
|
||||
throw new Error('未配置SillyTavern预设ID');
|
||||
}
|
||||
|
||||
let originalProfile = '';
|
||||
let responsePromise;
|
||||
|
||||
try {
|
||||
originalProfile = await window.TavernHelper.triggerSlash('/profile');
|
||||
console.log(`[Amily2号-JqyhST预设] 当前配置文件: ${originalProfile}`);
|
||||
|
||||
const targetProfile = context.extensionSettings?.connectionManager?.profiles?.find(p => p.id === profileId);
|
||||
if (!targetProfile) {
|
||||
throw new Error(`未找到配置文件ID: ${profileId}`);
|
||||
}
|
||||
|
||||
const targetProfileName = targetProfile.name;
|
||||
console.log(`[Amily2号-JqyhST预设] 目标配置文件: ${targetProfileName}`);
|
||||
|
||||
const currentProfile = await window.TavernHelper.triggerSlash('/profile');
|
||||
if (currentProfile !== targetProfileName) {
|
||||
console.log(`[Amily2号-JqyhST预设] 切换配置文件: ${currentProfile} -> ${targetProfileName}`);
|
||||
const escapedProfileName = targetProfileName.replace(/"/g, '\\"');
|
||||
await window.TavernHelper.triggerSlash(`/profile await=true "${escapedProfileName}"`);
|
||||
}
|
||||
|
||||
if (!context.ConnectionManagerRequestService) {
|
||||
throw new Error('ConnectionManagerRequestService不可用');
|
||||
}
|
||||
|
||||
console.log(`[Amily2号-JqyhST预设] 通过配置文件 ${targetProfileName} 发送请求`);
|
||||
responsePromise = context.ConnectionManagerRequestService.sendRequest(
|
||||
targetProfile.id,
|
||||
messages,
|
||||
options.maxTokens || 4000
|
||||
);
|
||||
|
||||
} finally {
|
||||
try {
|
||||
const currentProfileAfterCall = await window.TavernHelper.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}"`);
|
||||
}
|
||||
} catch (restoreError) {
|
||||
console.error('[Amily2号-JqyhST预设] 恢复配置文件失败:', restoreError);
|
||||
}
|
||||
}
|
||||
|
||||
const result = await responsePromise;
|
||||
|
||||
if (!result) {
|
||||
throw new Error('未收到API响应');
|
||||
}
|
||||
|
||||
const normalizedResult = normalizeApiResponse(result);
|
||||
if (normalizedResult.error) {
|
||||
throw new Error(normalizedResult.error.message || 'SillyTavern预设API调用失败');
|
||||
}
|
||||
|
||||
return normalizedResult.content;
|
||||
}
|
||||
|
||||
export async function fetchJqyhModels() {
|
||||
console.log('[Amily2号-Jqyh外交部] 开始获取模型列表');
|
||||
|
||||
const apiSettings = getJqyhApiSettings();
|
||||
|
||||
try {
|
||||
if (apiSettings.apiMode === 'sillytavern_preset') {
|
||||
const context = getContext();
|
||||
if (!context?.extensionSettings?.connectionManager?.profiles) {
|
||||
throw new Error('无法获取SillyTavern配置文件列表');
|
||||
}
|
||||
|
||||
const targetProfile = context.extensionSettings.connectionManager.profiles.find(p => p.id === apiSettings.tavernProfile);
|
||||
if (!targetProfile) {
|
||||
throw new Error(`未找到配置文件ID: ${apiSettings.tavernProfile}`);
|
||||
}
|
||||
|
||||
const models = [];
|
||||
if (targetProfile.openai_model) {
|
||||
models.push({ id: targetProfile.openai_model, name: targetProfile.openai_model });
|
||||
}
|
||||
|
||||
if (models.length === 0) {
|
||||
throw new Error('当前预设未配置模型');
|
||||
}
|
||||
|
||||
console.log('[Amily2号-Jqyh外交部] SillyTavern预设模式获取到模型:', models);
|
||||
return models;
|
||||
|
||||
} else {
|
||||
if (!apiSettings.apiUrl || !apiSettings.apiKey) {
|
||||
throw new Error('API URL或Key未配置');
|
||||
}
|
||||
|
||||
const response = await fetch('/api/backends/chat-completions/status', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
...getRequestHeaders(),
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
reverse_proxy: apiSettings.apiUrl,
|
||||
proxy_password: apiSettings.apiKey,
|
||||
chat_completion_source: 'openai'
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const rawData = await response.json();
|
||||
const models = Array.isArray(rawData) ? rawData : (rawData.data || rawData.models || []);
|
||||
|
||||
if (!Array.isArray(models)) {
|
||||
const errorMessage = result.error?.message || 'API未返回有效的模型列表数组';
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const formattedModels = models
|
||||
.map(m => {
|
||||
const modelIdRaw = m.name || m.id || m.model || m;
|
||||
const modelName = String(modelIdRaw).replace(/^models\//, '');
|
||||
return {
|
||||
id: modelName,
|
||||
name: modelName
|
||||
};
|
||||
})
|
||||
.filter(m => m.id)
|
||||
.sort((a, b) => String(a.name).localeCompare(String(b.name)));
|
||||
|
||||
console.log('[Amily2号-Jqyh外交部] 全兼容模式获取到模型:', formattedModels);
|
||||
return formattedModels;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Amily2号-Jqyh外交部] 获取模型列表失败:', error);
|
||||
toastr.error(`获取模型列表失败: ${error.message}`, 'Jqyh API');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function testJqyhApiConnection() {
|
||||
console.log('[Amily2号-Jqyh外交部] 开始API连接测试');
|
||||
|
||||
const apiSettings = getJqyhApiSettings();
|
||||
|
||||
if (apiSettings.apiMode === 'sillytavern_preset') {
|
||||
if (!apiSettings.tavernProfile) {
|
||||
toastr.error('未配置SillyTavern预设ID', 'Jqyh API连接测试失败');
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (!apiSettings.apiUrl || !apiSettings.apiKey || !apiSettings.model) {
|
||||
toastr.error('API配置不完整,请检查URL、Key和模型', 'Jqyh API连接测试失败');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
toastr.info('正在发送测试消息"你好!"...', 'Jqyh API连接测试');
|
||||
|
||||
const userName = window.SillyTavern.getContext?.()?.name1 || '用户';
|
||||
const systemPrompt = `接下来用户会使用测试按钮测试api连接是否正常,请回复ta连接是正常的,称呼用户为:“${userName}大人”,并复述群号1060183271,告知ta,本扩展开发者:“诗与酒.”正静待ta的入驻。`;
|
||||
|
||||
const testMessages = [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
{ role: 'user', content: '你好!' }
|
||||
];
|
||||
|
||||
const response = await callJqyhAI(testMessages);
|
||||
|
||||
if (response && response.trim()) {
|
||||
console.log('[Amily2号-Jqyh外交部] 测试消息响应:', response);
|
||||
const formattedResponse = response.replace(/\*\*(.*?)\*\*/g, '<b>$1</b>');
|
||||
toastr.success(`连接测试成功!AI回复: "${formattedResponse}"`, 'Jqyh API连接测试成功', { "escapeHtml": false });
|
||||
return true;
|
||||
} else {
|
||||
throw new Error('API未返回有效响应');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Amily2号-Jqyh外交部] 连接测试失败:', error);
|
||||
toastr.error(`连接测试失败: ${error.message}`, 'Jqyh API连接测试失败');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
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 {
|
||||
const module = await import('/scripts/custom-request.js');
|
||||
ChatCompletionService = module.ChatCompletionService;
|
||||
console.log('[Amily2号-Jqyh外交部] 已成功召唤"皇家信使"(ChatCompletionService)。');
|
||||
} catch (e) {
|
||||
console.warn("[Amily2号-Jqyh外交部] 未能召唤“皇家信使”,部分高级功能(如Claw代理)将受限。请考虑更新SillyTavern版本。", e);
|
||||
}
|
||||
|
||||
function normalizeApiResponse(responseData) {
|
||||
let data = responseData;
|
||||
if (typeof data === 'string') {
|
||||
try {
|
||||
data = JSON.parse(data);
|
||||
} catch (e) {
|
||||
console.error(`[${extensionName}] Jqyh API响应JSON解析失败:`, e);
|
||||
return { error: { message: 'Invalid JSON response' } };
|
||||
}
|
||||
}
|
||||
if (data && typeof data.data === 'object' && data.data !== null && !Array.isArray(data.data)) {
|
||||
if (Object.hasOwn(data.data, 'data')) {
|
||||
data = data.data;
|
||||
}
|
||||
}
|
||||
if (data && data.choices && data.choices[0]) {
|
||||
return { content: data.choices[0].message?.content?.trim() };
|
||||
}
|
||||
if (data && data.content) {
|
||||
return { content: data.content.trim() };
|
||||
}
|
||||
if (data && data.data) {
|
||||
return { data: data.data };
|
||||
}
|
||||
if (data && data.error) {
|
||||
return { error: data.error };
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
export function getJqyhApiSettings() {
|
||||
return {
|
||||
apiMode: extension_settings[extensionName]?.jqyhApiMode || 'openai_test',
|
||||
apiUrl: extension_settings[extensionName]?.jqyhApiUrl?.trim() || '',
|
||||
apiKey: extension_settings[extensionName]?.jqyhApiKey?.trim() || '',
|
||||
model: extension_settings[extensionName]?.jqyhModel || '',
|
||||
maxTokens: extension_settings[extensionName]?.jqyhMaxTokens || 4000,
|
||||
temperature: extension_settings[extensionName]?.jqyhTemperature || 0.7,
|
||||
tavernProfile: extension_settings[extensionName]?.jqyhTavernProfile || ''
|
||||
};
|
||||
}
|
||||
|
||||
export async function callJqyhAI(messages, options = {}) {
|
||||
if (window.AMILY2_SYSTEM_PARALYZED === true) {
|
||||
console.error("[Amily2-Jqyh制裁] 系统完整性已受损,所有外交活动被无限期中止。");
|
||||
return null;
|
||||
}
|
||||
|
||||
const apiSettings = getJqyhApiSettings();
|
||||
|
||||
const finalOptions = {
|
||||
maxTokens: apiSettings.maxTokens,
|
||||
temperature: apiSettings.temperature,
|
||||
model: apiSettings.model,
|
||||
apiUrl: apiSettings.apiUrl,
|
||||
apiKey: apiSettings.apiKey,
|
||||
apiMode: apiSettings.apiMode,
|
||||
tavernProfile: apiSettings.tavernProfile,
|
||||
...options
|
||||
};
|
||||
|
||||
if (finalOptions.apiMode !== 'sillytavern_preset') {
|
||||
if (!finalOptions.apiUrl || !finalOptions.model || !finalOptions.apiKey) {
|
||||
console.warn("[Amily2-Jqyh外交部] API配置不完整,无法调用AI");
|
||||
toastr.error("API配置不完整,请检查URL、Key和模型配置。", "Jqyh-外交部");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
console.groupCollapsed(`[Amily2号-Jqyh统一API调用] ${new Date().toLocaleTimeString()}`);
|
||||
console.log("【请求参数】:", {
|
||||
mode: finalOptions.apiMode,
|
||||
model: finalOptions.model,
|
||||
maxTokens: finalOptions.maxTokens,
|
||||
temperature: finalOptions.temperature,
|
||||
messagesCount: messages.length
|
||||
});
|
||||
console.log("【消息内容】:", messages);
|
||||
console.groupEnd();
|
||||
|
||||
try {
|
||||
let responseContent;
|
||||
|
||||
switch (finalOptions.apiMode) {
|
||||
case 'openai_test':
|
||||
responseContent = await callJqyhOpenAITest(messages, finalOptions);
|
||||
break;
|
||||
case 'sillytavern_preset':
|
||||
responseContent = await callJqyhSillyTavernPreset(messages, finalOptions);
|
||||
break;
|
||||
default:
|
||||
console.error(`[Amily2-Jqyh外交部] 未支持的API模式: ${finalOptions.apiMode}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!responseContent) {
|
||||
console.warn('[Amily2-Jqyh外交部] 未能获取AI响应内容');
|
||||
return null;
|
||||
}
|
||||
|
||||
console.groupCollapsed("[Amily2号-Jqyh AI回复]");
|
||||
console.log(responseContent);
|
||||
console.groupEnd();
|
||||
|
||||
return responseContent;
|
||||
|
||||
} catch (error) {
|
||||
console.error(`[Amily2-Jqyh外交部] API调用发生错误:`, error);
|
||||
|
||||
if (error.message.includes('400')) {
|
||||
toastr.error(`API请求格式错误 (400): 请检查消息格式和模型配置`, "Jqyh API调用失败");
|
||||
} else if (error.message.includes('401')) {
|
||||
toastr.error(`API认证失败 (401): 请检查API Key配置`, "Jqyh API调用失败");
|
||||
} else if (error.message.includes('403')) {
|
||||
toastr.error(`API访问被拒绝 (403): 请检查权限设置`, "Jqyh API调用失败");
|
||||
} else if (error.message.includes('429')) {
|
||||
toastr.error(`API调用频率超限 (429): 请稍后重试`, "Jqyh API调用失败");
|
||||
} else if (error.message.includes('500')) {
|
||||
toastr.error(`API服务器错误 (500): 请稍后重试`, "Jqyh API调用失败");
|
||||
} else {
|
||||
toastr.error(`API调用失败: ${error.message}`, "Jqyh API调用失败");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function callJqyhOpenAITest(messages, options) {
|
||||
const isGoogleApi = options.apiUrl.includes('googleapis.com');
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
if (!isGoogleApi) {
|
||||
Object.assign(body, {
|
||||
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(body)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Jqyh全兼容API请求失败: ${response.status} - ${errorText}`);
|
||||
}
|
||||
|
||||
const responseData = await response.json();
|
||||
return responseData?.choices?.[0]?.message?.content;
|
||||
}
|
||||
|
||||
async function callJqyhSillyTavernPreset(messages, options) {
|
||||
console.log('[Amily2号-JqyhST预设] 使用SillyTavern预设调用');
|
||||
|
||||
const context = getContext();
|
||||
if (!context) {
|
||||
throw new Error('无法获取SillyTavern上下文');
|
||||
}
|
||||
|
||||
const profileId = options.tavernProfile;
|
||||
if (!profileId) {
|
||||
throw new Error('未配置SillyTavern预设ID');
|
||||
}
|
||||
|
||||
let originalProfile = '';
|
||||
let responsePromise;
|
||||
|
||||
try {
|
||||
originalProfile = await amilyHelper.triggerSlash('/profile');
|
||||
console.log(`[Amily2号-JqyhST预设] 当前配置文件: ${originalProfile}`);
|
||||
|
||||
const targetProfile = context.extensionSettings?.connectionManager?.profiles?.find(p => p.id === profileId);
|
||||
if (!targetProfile) {
|
||||
throw new Error(`未找到配置文件ID: ${profileId}`);
|
||||
}
|
||||
|
||||
const targetProfileName = targetProfile.name;
|
||||
console.log(`[Amily2号-JqyhST预设] 目标配置文件: ${targetProfileName}`);
|
||||
|
||||
const currentProfile = await amilyHelper.triggerSlash('/profile');
|
||||
if (currentProfile !== targetProfileName) {
|
||||
console.log(`[Amily2号-JqyhST预设] 切换配置文件: ${currentProfile} -> ${targetProfileName}`);
|
||||
const escapedProfileName = targetProfileName.replace(/"/g, '\\"');
|
||||
await amilyHelper.triggerSlash(`/profile await=true "${escapedProfileName}"`);
|
||||
}
|
||||
|
||||
if (!context.ConnectionManagerRequestService) {
|
||||
throw new Error('ConnectionManagerRequestService不可用');
|
||||
}
|
||||
|
||||
console.log(`[Amily2号-JqyhST预设] 通过配置文件 ${targetProfileName} 发送请求`);
|
||||
responsePromise = context.ConnectionManagerRequestService.sendRequest(
|
||||
targetProfile.id,
|
||||
messages,
|
||||
options.maxTokens || 4000
|
||||
);
|
||||
|
||||
} finally {
|
||||
try {
|
||||
const currentProfileAfterCall = await amilyHelper.triggerSlash('/profile');
|
||||
if (originalProfile && originalProfile !== currentProfileAfterCall) {
|
||||
console.log(`[Amily2号-JqyhST预设] 恢复原始配置文件: ${currentProfileAfterCall} -> ${originalProfile}`);
|
||||
const escapedOriginalProfile = originalProfile.replace(/"/g, '\\"');
|
||||
await amilyHelper.triggerSlash(`/profile await=true "${escapedOriginalProfile}"`);
|
||||
}
|
||||
} catch (restoreError) {
|
||||
console.error('[Amily2号-JqyhST预设] 恢复配置文件失败:', restoreError);
|
||||
}
|
||||
}
|
||||
|
||||
const result = await responsePromise;
|
||||
|
||||
if (!result) {
|
||||
throw new Error('未收到API响应');
|
||||
}
|
||||
|
||||
const normalizedResult = normalizeApiResponse(result);
|
||||
if (normalizedResult.error) {
|
||||
throw new Error(normalizedResult.error.message || 'SillyTavern预设API调用失败');
|
||||
}
|
||||
|
||||
return normalizedResult.content;
|
||||
}
|
||||
|
||||
export async function fetchJqyhModels() {
|
||||
console.log('[Amily2号-Jqyh外交部] 开始获取模型列表');
|
||||
|
||||
const apiSettings = getJqyhApiSettings();
|
||||
|
||||
try {
|
||||
if (apiSettings.apiMode === 'sillytavern_preset') {
|
||||
const context = getContext();
|
||||
if (!context?.extensionSettings?.connectionManager?.profiles) {
|
||||
throw new Error('无法获取SillyTavern配置文件列表');
|
||||
}
|
||||
|
||||
const targetProfile = context.extensionSettings.connectionManager.profiles.find(p => p.id === apiSettings.tavernProfile);
|
||||
if (!targetProfile) {
|
||||
throw new Error(`未找到配置文件ID: ${apiSettings.tavernProfile}`);
|
||||
}
|
||||
|
||||
const models = [];
|
||||
if (targetProfile.openai_model) {
|
||||
models.push({ id: targetProfile.openai_model, name: targetProfile.openai_model });
|
||||
}
|
||||
|
||||
if (models.length === 0) {
|
||||
throw new Error('当前预设未配置模型');
|
||||
}
|
||||
|
||||
console.log('[Amily2号-Jqyh外交部] SillyTavern预设模式获取到模型:', models);
|
||||
return models;
|
||||
|
||||
} else {
|
||||
if (!apiSettings.apiUrl || !apiSettings.apiKey) {
|
||||
throw new Error('API URL或Key未配置');
|
||||
}
|
||||
|
||||
const response = await fetch('/api/backends/chat-completions/status', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
...getRequestHeaders(),
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
reverse_proxy: apiSettings.apiUrl,
|
||||
proxy_password: apiSettings.apiKey,
|
||||
chat_completion_source: 'openai'
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const rawData = await response.json();
|
||||
const models = Array.isArray(rawData) ? rawData : (rawData.data || rawData.models || []);
|
||||
|
||||
if (!Array.isArray(models)) {
|
||||
const errorMessage = result.error?.message || 'API未返回有效的模型列表数组';
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const formattedModels = models
|
||||
.map(m => {
|
||||
const modelIdRaw = m.name || m.id || m.model || m;
|
||||
const modelName = String(modelIdRaw).replace(/^models\//, '');
|
||||
return {
|
||||
id: modelName,
|
||||
name: modelName
|
||||
};
|
||||
})
|
||||
.filter(m => m.id)
|
||||
.sort((a, b) => String(a.name).localeCompare(String(b.name)));
|
||||
|
||||
console.log('[Amily2号-Jqyh外交部] 全兼容模式获取到模型:', formattedModels);
|
||||
return formattedModels;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Amily2号-Jqyh外交部] 获取模型列表失败:', error);
|
||||
toastr.error(`获取模型列表失败: ${error.message}`, 'Jqyh API');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function testJqyhApiConnection() {
|
||||
console.log('[Amily2号-Jqyh外交部] 开始API连接测试');
|
||||
|
||||
const apiSettings = getJqyhApiSettings();
|
||||
|
||||
if (apiSettings.apiMode === 'sillytavern_preset') {
|
||||
if (!apiSettings.tavernProfile) {
|
||||
toastr.error('未配置SillyTavern预设ID', 'Jqyh API连接测试失败');
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (!apiSettings.apiUrl || !apiSettings.apiKey || !apiSettings.model) {
|
||||
toastr.error('API配置不完整,请检查URL、Key和模型', 'Jqyh API连接测试失败');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
toastr.info('正在发送测试消息"你好!"...', 'Jqyh API连接测试');
|
||||
|
||||
const userName = window.SillyTavern.getContext?.()?.name1 || '用户';
|
||||
const systemPrompt = `接下来用户会使用测试按钮测试api连接是否正常,请回复ta连接是正常的,称呼用户为:“${userName}大人”,并复述群号1060183271,告知ta,本扩展开发者:“诗与酒.”正静待ta的入驻。`;
|
||||
|
||||
const testMessages = [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
{ role: 'user', content: '你好!' }
|
||||
];
|
||||
|
||||
const response = await callJqyhAI(testMessages);
|
||||
|
||||
if (response && response.trim()) {
|
||||
console.log('[Amily2号-Jqyh外交部] 测试消息响应:', response);
|
||||
const formattedResponse = response.replace(/\*\*(.*?)\*\*/g, '<b>$1</b>');
|
||||
toastr.success(`连接测试成功!AI回复: "${formattedResponse}"`, 'Jqyh API连接测试成功', { "escapeHtml": false });
|
||||
return true;
|
||||
} else {
|
||||
throw new Error('API未返回有效响应');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Amily2号-Jqyh外交部] 连接测试失败:', error);
|
||||
toastr.error(`连接测试失败: ${error.message}`, 'Jqyh API连接测试失败');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,388 +1,385 @@
|
||||
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";
|
||||
|
||||
let ChatCompletionService = undefined;
|
||||
try {
|
||||
const module = await import('/scripts/custom-request.js');
|
||||
ChatCompletionService = module.ChatCompletionService;
|
||||
console.log('[Amily2号-Nccs外交部] 已成功召唤"皇家信使"(ChatCompletionService)。');
|
||||
} catch (e) {
|
||||
console.warn("[Amily2号-Nccs外交部] 未能召唤“皇家信使”,部分高级功能(如Claw代理)将受限。请考虑更新SillyTavern版本。", e);
|
||||
}
|
||||
|
||||
function normalizeApiResponse(responseData) {
|
||||
let data = responseData;
|
||||
if (typeof data === 'string') {
|
||||
try {
|
||||
data = JSON.parse(data);
|
||||
} catch (e) {
|
||||
console.error(`[${extensionName}] Nccs API响应JSON解析失败:`, e);
|
||||
return { error: { message: 'Invalid JSON response' } };
|
||||
}
|
||||
}
|
||||
if (data && typeof data.data === 'object' && data.data !== null && !Array.isArray(data.data)) {
|
||||
if (Object.hasOwn(data.data, 'data')) {
|
||||
data = data.data;
|
||||
}
|
||||
}
|
||||
if (data && data.choices && data.choices[0]) {
|
||||
return { content: data.choices[0].message?.content?.trim() };
|
||||
}
|
||||
if (data && data.content) {
|
||||
return { content: data.content.trim() };
|
||||
}
|
||||
if (data && data.data) {
|
||||
return { data: data.data };
|
||||
}
|
||||
if (data && data.error) {
|
||||
return { error: data.error };
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
export function getNccsApiSettings() {
|
||||
return {
|
||||
apiMode: extension_settings[extensionName]?.nccsApiMode || 'openai_test',
|
||||
apiUrl: extension_settings[extensionName]?.nccsApiUrl?.trim() || '',
|
||||
apiKey: extension_settings[extensionName]?.nccsApiKey?.trim() || '',
|
||||
model: extension_settings[extensionName]?.nccsModel || '',
|
||||
maxTokens: extension_settings[extensionName]?.nccsMaxTokens || 4000,
|
||||
temperature: extension_settings[extensionName]?.nccsTemperature || 0.7,
|
||||
tavernProfile: extension_settings[extensionName]?.nccsTavernProfile || ''
|
||||
};
|
||||
}
|
||||
|
||||
export async function callNccsAI(messages, options = {}) {
|
||||
if (window.AMILY2_SYSTEM_PARALYZED === true) {
|
||||
console.error("[Amily2-Nccs制裁] 系统完整性已受损,所有外交活动被无限期中止。");
|
||||
return null;
|
||||
}
|
||||
|
||||
const apiSettings = getNccsApiSettings();
|
||||
|
||||
const finalOptions = {
|
||||
maxTokens: apiSettings.maxTokens,
|
||||
temperature: apiSettings.temperature,
|
||||
model: apiSettings.model,
|
||||
apiUrl: apiSettings.apiUrl,
|
||||
apiKey: apiSettings.apiKey,
|
||||
apiMode: apiSettings.apiMode,
|
||||
tavernProfile: apiSettings.tavernProfile,
|
||||
...options
|
||||
};
|
||||
|
||||
if (finalOptions.apiMode !== 'sillytavern_preset') {
|
||||
if (!finalOptions.apiUrl || !finalOptions.model || !finalOptions.apiKey) {
|
||||
console.warn("[Amily2-Nccs外交部] API配置不完整,无法调用AI");
|
||||
toastr.error("API配置不完整,请检查URL、Key和模型配置。", "Nccs-外交部");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
console.groupCollapsed(`[Amily2号-Nccs统一API调用] ${new Date().toLocaleTimeString()}`);
|
||||
console.log("【请求参数】:", {
|
||||
mode: finalOptions.apiMode,
|
||||
model: finalOptions.model,
|
||||
maxTokens: finalOptions.maxTokens,
|
||||
temperature: finalOptions.temperature,
|
||||
messagesCount: messages.length
|
||||
});
|
||||
console.log("【消息内容】:", messages);
|
||||
console.groupEnd();
|
||||
|
||||
try {
|
||||
let responseContent;
|
||||
|
||||
switch (finalOptions.apiMode) {
|
||||
case 'openai_test':
|
||||
responseContent = await callNccsOpenAITest(messages, finalOptions);
|
||||
break;
|
||||
case 'sillytavern_preset':
|
||||
responseContent = await callNccsSillyTavernPreset(messages, finalOptions);
|
||||
break;
|
||||
default:
|
||||
console.error(`[Amily2-Nccs外交部] 未支持的API模式: ${finalOptions.apiMode}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!responseContent) {
|
||||
console.warn('[Amily2-Nccs外交部] 未能获取AI响应内容');
|
||||
return null;
|
||||
}
|
||||
|
||||
console.groupCollapsed("[Amily2号-Nccs AI回复]");
|
||||
console.log(responseContent);
|
||||
console.groupEnd();
|
||||
|
||||
return responseContent;
|
||||
|
||||
} catch (error) {
|
||||
console.error(`[Amily2-Nccs外交部] API调用发生错误:`, error);
|
||||
|
||||
if (error.message.includes('400')) {
|
||||
toastr.error(`API请求格式错误 (400): 请检查消息格式和模型配置`, "Nccs API调用失败");
|
||||
} else if (error.message.includes('401')) {
|
||||
toastr.error(`API认证失败 (401): 请检查API Key配置`, "Nccs API调用失败");
|
||||
} else if (error.message.includes('403')) {
|
||||
toastr.error(`API访问被拒绝 (403): 请检查权限设置`, "Nccs API调用失败");
|
||||
} else if (error.message.includes('429')) {
|
||||
toastr.error(`API调用频率超限 (429): 请稍后重试`, "Nccs API调用失败");
|
||||
} else if (error.message.includes('500')) {
|
||||
toastr.error(`API服务器错误 (500): 请稍后重试`, "Nccs API调用失败");
|
||||
} else {
|
||||
toastr.error(`API调用失败: ${error.message}`, "Nccs API调用失败");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function callNccsOpenAITest(messages, options) {
|
||||
const isGoogleApi = options.apiUrl.includes('googleapis.com');
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
if (!isGoogleApi) {
|
||||
Object.assign(body, {
|
||||
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(body)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Nccs全兼容API请求失败: ${response.status} - ${errorText}`);
|
||||
}
|
||||
|
||||
const responseData = await response.json();
|
||||
return responseData?.choices?.[0]?.message?.content;
|
||||
}
|
||||
|
||||
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上下文');
|
||||
}
|
||||
|
||||
const profileId = options.tavernProfile;
|
||||
if (!profileId) {
|
||||
throw new Error('未配置SillyTavern预设ID');
|
||||
}
|
||||
|
||||
let originalProfile = '';
|
||||
let responsePromise;
|
||||
|
||||
try {
|
||||
originalProfile = await window.TavernHelper.triggerSlash('/profile');
|
||||
console.log(`[Amily2号-NccsST预设] 当前配置文件: ${originalProfile}`);
|
||||
|
||||
const targetProfile = context.extensionSettings?.connectionManager?.profiles?.find(p => p.id === profileId);
|
||||
if (!targetProfile) {
|
||||
throw new Error(`未找到配置文件ID: ${profileId}`);
|
||||
}
|
||||
|
||||
const targetProfileName = targetProfile.name;
|
||||
console.log(`[Amily2号-NccsST预设] 目标配置文件: ${targetProfileName}`);
|
||||
|
||||
const currentProfile = await window.TavernHelper.triggerSlash('/profile');
|
||||
if (currentProfile !== targetProfileName) {
|
||||
console.log(`[Amily2号-NccsST预设] 切换配置文件: ${currentProfile} -> ${targetProfileName}`);
|
||||
const escapedProfileName = targetProfileName.replace(/"/g, '\\"');
|
||||
await window.TavernHelper.triggerSlash(`/profile await=true "${escapedProfileName}"`);
|
||||
}
|
||||
|
||||
if (!context.ConnectionManagerRequestService) {
|
||||
throw new Error('ConnectionManagerRequestService不可用');
|
||||
}
|
||||
|
||||
console.log(`[Amily2号-NccsST预设] 通过配置文件 ${targetProfileName} 发送请求`);
|
||||
responsePromise = context.ConnectionManagerRequestService.sendRequest(
|
||||
targetProfile.id,
|
||||
messages,
|
||||
options.maxTokens || 4000
|
||||
);
|
||||
|
||||
} finally {
|
||||
try {
|
||||
const currentProfileAfterCall = await window.TavernHelper.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}"`);
|
||||
}
|
||||
} catch (restoreError) {
|
||||
console.error('[Amily2号-NccsST预设] 恢复配置文件失败:', restoreError);
|
||||
}
|
||||
}
|
||||
|
||||
const result = await responsePromise;
|
||||
|
||||
if (!result) {
|
||||
throw new Error('未收到API响应');
|
||||
}
|
||||
|
||||
const normalizedResult = normalizeApiResponse(result);
|
||||
if (normalizedResult.error) {
|
||||
throw new Error(normalizedResult.error.message || 'SillyTavern预设API调用失败');
|
||||
}
|
||||
|
||||
return normalizedResult.content;
|
||||
}
|
||||
|
||||
export async function fetchNccsModels() {
|
||||
console.log('[Amily2号-Nccs外交部] 开始获取模型列表');
|
||||
|
||||
const apiSettings = getNccsApiSettings();
|
||||
|
||||
try {
|
||||
if (apiSettings.apiMode === 'sillytavern_preset') {
|
||||
// SillyTavern预设模式:获取当前预设的模型
|
||||
const context = getContext();
|
||||
if (!context?.extensionSettings?.connectionManager?.profiles) {
|
||||
throw new Error('无法获取SillyTavern配置文件列表');
|
||||
}
|
||||
|
||||
const targetProfile = context.extensionSettings.connectionManager.profiles.find(p => p.id === apiSettings.tavernProfile);
|
||||
if (!targetProfile) {
|
||||
throw new Error(`未找到配置文件ID: ${apiSettings.tavernProfile}`);
|
||||
}
|
||||
|
||||
const models = [];
|
||||
if (targetProfile.openai_model) {
|
||||
models.push({ id: targetProfile.openai_model, name: targetProfile.openai_model });
|
||||
}
|
||||
|
||||
if (models.length === 0) {
|
||||
throw new Error('当前预设未配置模型');
|
||||
}
|
||||
|
||||
console.log('[Amily2号-Nccs外交部] SillyTavern预设模式获取到模型:', models);
|
||||
return models;
|
||||
|
||||
} else {
|
||||
if (!apiSettings.apiUrl || !apiSettings.apiKey) {
|
||||
throw new Error('API URL或Key未配置');
|
||||
}
|
||||
|
||||
const response = await fetch('/api/backends/chat-completions/status', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
...getRequestHeaders(),
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
reverse_proxy: apiSettings.apiUrl,
|
||||
proxy_password: apiSettings.apiKey,
|
||||
chat_completion_source: 'openai'
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const rawData = await response.json();
|
||||
const models = Array.isArray(rawData) ? rawData : (rawData.data || rawData.models || []);
|
||||
|
||||
if (!Array.isArray(models)) {
|
||||
const errorMessage = rawData.error?.message || 'API未返回有效的模型列表数组';
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const formattedModels = models
|
||||
.map(m => {
|
||||
// 从name字段中提取模型名称,去掉"models/"前缀
|
||||
const modelIdRaw = m.name || m.id || m.model || m;
|
||||
const modelName = String(modelIdRaw).replace(/^models\//, '');
|
||||
return {
|
||||
id: modelName,
|
||||
name: modelName
|
||||
};
|
||||
})
|
||||
.filter(m => m.id)
|
||||
.sort((a, b) => String(a.name).localeCompare(String(b.name)));
|
||||
|
||||
console.log('[Amily2号-Nccs外交部] 全兼容模式获取到模型:', formattedModels);
|
||||
return formattedModels;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Amily2号-Nccs外交部] 获取模型列表失败:', error);
|
||||
toastr.error(`获取模型列表失败: ${error.message}`, 'Nccs API');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function testNccsApiConnection() {
|
||||
console.log('[Amily2号-Nccs外交部] 开始API连接测试');
|
||||
|
||||
const apiSettings = getNccsApiSettings();
|
||||
|
||||
if (apiSettings.apiMode === 'sillytavern_preset') {
|
||||
if (!apiSettings.tavernProfile) {
|
||||
toastr.error('未配置SillyTavern预设ID', 'Nccs API连接测试失败');
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (!apiSettings.apiUrl || !apiSettings.apiKey || !apiSettings.model) {
|
||||
toastr.error('API配置不完整,请检查URL、Key和模型', 'Nccs API连接测试失败');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
toastr.info('正在发送测试消息"你好!"...', 'Nccs API连接测试');
|
||||
|
||||
const userName = window.SillyTavern.getContext?.()?.name1 || '用户';
|
||||
const systemPrompt = `接下来用户会使用测试按钮测试api连接是否正常,请回复ta连接是正常的,称呼用户为:“${userName}大人”,并复述群号1060183271,告知ta,本扩展开发者:“诗与酒.”正静待ta的入驻。`;
|
||||
|
||||
const testMessages = [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
{ role: 'user', content: '你好!' }
|
||||
];
|
||||
|
||||
const response = await callNccsAI(testMessages);
|
||||
|
||||
if (response && response.trim()) {
|
||||
console.log('[Amily2号-Nccs外交部] 测试消息响应:', response);
|
||||
const formattedResponse = response.replace(/\*\*(.*?)\*\*/g, '<b>$1</b>');
|
||||
toastr.success(`连接测试成功!AI回复: "${formattedResponse}"`, 'Nccs API连接测试成功', { "escapeHtml": false });
|
||||
return true;
|
||||
} else {
|
||||
throw new Error('API未返回有效响应');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Amily2号-Nccs外交部] 连接测试失败:', error);
|
||||
toastr.error(`连接测试失败: ${error.message}`, 'Nccs API连接测试失败');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
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 {
|
||||
const module = await import('/scripts/custom-request.js');
|
||||
ChatCompletionService = module.ChatCompletionService;
|
||||
console.log('[Amily2号-Nccs外交部] 已成功召唤"皇家信使"(ChatCompletionService)。');
|
||||
} catch (e) {
|
||||
console.warn("[Amily2号-Nccs外交部] 未能召唤“皇家信使”,部分高级功能(如Claw代理)将受限。请考虑更新SillyTavern版本。", e);
|
||||
}
|
||||
|
||||
function normalizeApiResponse(responseData) {
|
||||
let data = responseData;
|
||||
if (typeof data === 'string') {
|
||||
try {
|
||||
data = JSON.parse(data);
|
||||
} catch (e) {
|
||||
console.error(`[${extensionName}] Nccs API响应JSON解析失败:`, e);
|
||||
return { error: { message: 'Invalid JSON response' } };
|
||||
}
|
||||
}
|
||||
if (data && typeof data.data === 'object' && data.data !== null && !Array.isArray(data.data)) {
|
||||
if (Object.hasOwn(data.data, 'data')) {
|
||||
data = data.data;
|
||||
}
|
||||
}
|
||||
if (data && data.choices && data.choices[0]) {
|
||||
return { content: data.choices[0].message?.content?.trim() };
|
||||
}
|
||||
if (data && data.content) {
|
||||
return { content: data.content.trim() };
|
||||
}
|
||||
if (data && data.data) {
|
||||
return { data: data.data };
|
||||
}
|
||||
if (data && data.error) {
|
||||
return { error: data.error };
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
export function getNccsApiSettings() {
|
||||
return {
|
||||
apiMode: extension_settings[extensionName]?.nccsApiMode || 'openai_test',
|
||||
apiUrl: extension_settings[extensionName]?.nccsApiUrl?.trim() || '',
|
||||
apiKey: extension_settings[extensionName]?.nccsApiKey?.trim() || '',
|
||||
model: extension_settings[extensionName]?.nccsModel || '',
|
||||
maxTokens: extension_settings[extensionName]?.nccsMaxTokens || 4000,
|
||||
temperature: extension_settings[extensionName]?.nccsTemperature || 0.7,
|
||||
tavernProfile: extension_settings[extensionName]?.nccsTavernProfile || ''
|
||||
};
|
||||
}
|
||||
|
||||
export async function callNccsAI(messages, options = {}) {
|
||||
if (window.AMILY2_SYSTEM_PARALYZED === true) {
|
||||
console.error("[Amily2-Nccs制裁] 系统完整性已受损,所有外交活动被无限期中止。");
|
||||
return null;
|
||||
}
|
||||
|
||||
const apiSettings = getNccsApiSettings();
|
||||
|
||||
const finalOptions = {
|
||||
maxTokens: apiSettings.maxTokens,
|
||||
temperature: apiSettings.temperature,
|
||||
model: apiSettings.model,
|
||||
apiUrl: apiSettings.apiUrl,
|
||||
apiKey: apiSettings.apiKey,
|
||||
apiMode: apiSettings.apiMode,
|
||||
tavernProfile: apiSettings.tavernProfile,
|
||||
...options
|
||||
};
|
||||
|
||||
if (finalOptions.apiMode !== 'sillytavern_preset') {
|
||||
if (!finalOptions.apiUrl || !finalOptions.model || !finalOptions.apiKey) {
|
||||
console.warn("[Amily2-Nccs外交部] API配置不完整,无法调用AI");
|
||||
toastr.error("API配置不完整,请检查URL、Key和模型配置。", "Nccs-外交部");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
console.groupCollapsed(`[Amily2号-Nccs统一API调用] ${new Date().toLocaleTimeString()}`);
|
||||
console.log("【请求参数】:", {
|
||||
mode: finalOptions.apiMode,
|
||||
model: finalOptions.model,
|
||||
maxTokens: finalOptions.maxTokens,
|
||||
temperature: finalOptions.temperature,
|
||||
messagesCount: messages.length
|
||||
});
|
||||
console.log("【消息内容】:", messages);
|
||||
console.groupEnd();
|
||||
|
||||
try {
|
||||
let responseContent;
|
||||
|
||||
switch (finalOptions.apiMode) {
|
||||
case 'openai_test':
|
||||
responseContent = await callNccsOpenAITest(messages, finalOptions);
|
||||
break;
|
||||
case 'sillytavern_preset':
|
||||
responseContent = await callNccsSillyTavernPreset(messages, finalOptions);
|
||||
break;
|
||||
default:
|
||||
console.error(`[Amily2-Nccs外交部] 未支持的API模式: ${finalOptions.apiMode}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!responseContent) {
|
||||
console.warn('[Amily2-Nccs外交部] 未能获取AI响应内容');
|
||||
return null;
|
||||
}
|
||||
|
||||
console.groupCollapsed("[Amily2号-Nccs AI回复]");
|
||||
console.log(responseContent);
|
||||
console.groupEnd();
|
||||
|
||||
return responseContent;
|
||||
|
||||
} catch (error) {
|
||||
console.error(`[Amily2-Nccs外交部] API调用发生错误:`, error);
|
||||
|
||||
if (error.message.includes('400')) {
|
||||
toastr.error(`API请求格式错误 (400): 请检查消息格式和模型配置`, "Nccs API调用失败");
|
||||
} else if (error.message.includes('401')) {
|
||||
toastr.error(`API认证失败 (401): 请检查API Key配置`, "Nccs API调用失败");
|
||||
} else if (error.message.includes('403')) {
|
||||
toastr.error(`API访问被拒绝 (403): 请检查权限设置`, "Nccs API调用失败");
|
||||
} else if (error.message.includes('429')) {
|
||||
toastr.error(`API调用频率超限 (429): 请稍后重试`, "Nccs API调用失败");
|
||||
} else if (error.message.includes('500')) {
|
||||
toastr.error(`API服务器错误 (500): 请稍后重试`, "Nccs API调用失败");
|
||||
} else {
|
||||
toastr.error(`API调用失败: ${error.message}`, "Nccs API调用失败");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function callNccsOpenAITest(messages, options) {
|
||||
const isGoogleApi = options.apiUrl.includes('googleapis.com');
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
if (!isGoogleApi) {
|
||||
Object.assign(body, {
|
||||
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(body)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Nccs全兼容API请求失败: ${response.status} - ${errorText}`);
|
||||
}
|
||||
|
||||
const responseData = await response.json();
|
||||
return responseData?.choices?.[0]?.message?.content;
|
||||
}
|
||||
|
||||
async function callNccsSillyTavernPreset(messages, options) {
|
||||
console.log('[Amily2号-NccsST预设] 使用SillyTavern预设调用');
|
||||
|
||||
const context = getContext();
|
||||
if (!context) {
|
||||
throw new Error('无法获取SillyTavern上下文');
|
||||
}
|
||||
|
||||
const profileId = options.tavernProfile;
|
||||
if (!profileId) {
|
||||
throw new Error('未配置SillyTavern预设ID');
|
||||
}
|
||||
|
||||
let originalProfile = '';
|
||||
let responsePromise;
|
||||
|
||||
try {
|
||||
originalProfile = await amilyHelper.triggerSlash('/profile');
|
||||
console.log(`[Amily2号-NccsST预设] 当前配置文件: ${originalProfile}`);
|
||||
|
||||
const targetProfile = context.extensionSettings?.connectionManager?.profiles?.find(p => p.id === profileId);
|
||||
if (!targetProfile) {
|
||||
throw new Error(`未找到配置文件ID: ${profileId}`);
|
||||
}
|
||||
|
||||
const targetProfileName = targetProfile.name;
|
||||
console.log(`[Amily2号-NccsST预设] 目标配置文件: ${targetProfileName}`);
|
||||
|
||||
const currentProfile = await amilyHelper.triggerSlash('/profile');
|
||||
if (currentProfile !== targetProfileName) {
|
||||
console.log(`[Amily2号-NccsST预设] 切换配置文件: ${currentProfile} -> ${targetProfileName}`);
|
||||
const escapedProfileName = targetProfileName.replace(/"/g, '\\"');
|
||||
await amilyHelper.triggerSlash(`/profile await=true "${escapedProfileName}"`);
|
||||
}
|
||||
|
||||
if (!context.ConnectionManagerRequestService) {
|
||||
throw new Error('ConnectionManagerRequestService不可用');
|
||||
}
|
||||
|
||||
console.log(`[Amily2号-NccsST预设] 通过配置文件 ${targetProfileName} 发送请求`);
|
||||
responsePromise = context.ConnectionManagerRequestService.sendRequest(
|
||||
targetProfile.id,
|
||||
messages,
|
||||
options.maxTokens || 4000
|
||||
);
|
||||
|
||||
} finally {
|
||||
try {
|
||||
const currentProfileAfterCall = await amilyHelper.triggerSlash('/profile');
|
||||
if (originalProfile && originalProfile !== currentProfileAfterCall) {
|
||||
console.log(`[Amily2号-NccsST预设] 恢复原始配置文件: ${currentProfileAfterCall} -> ${originalProfile}`);
|
||||
const escapedOriginalProfile = originalProfile.replace(/"/g, '\\"');
|
||||
await amilyHelper.triggerSlash(`/profile await=true "${escapedOriginalProfile}"`);
|
||||
}
|
||||
} catch (restoreError) {
|
||||
console.error('[Amily2号-NccsST预设] 恢复配置文件失败:', restoreError);
|
||||
}
|
||||
}
|
||||
|
||||
const result = await responsePromise;
|
||||
|
||||
if (!result) {
|
||||
throw new Error('未收到API响应');
|
||||
}
|
||||
|
||||
const normalizedResult = normalizeApiResponse(result);
|
||||
if (normalizedResult.error) {
|
||||
throw new Error(normalizedResult.error.message || 'SillyTavern预设API调用失败');
|
||||
}
|
||||
|
||||
return normalizedResult.content;
|
||||
}
|
||||
|
||||
export async function fetchNccsModels() {
|
||||
console.log('[Amily2号-Nccs外交部] 开始获取模型列表');
|
||||
|
||||
const apiSettings = getNccsApiSettings();
|
||||
|
||||
try {
|
||||
if (apiSettings.apiMode === 'sillytavern_preset') {
|
||||
// SillyTavern预设模式:获取当前预设的模型
|
||||
const context = getContext();
|
||||
if (!context?.extensionSettings?.connectionManager?.profiles) {
|
||||
throw new Error('无法获取SillyTavern配置文件列表');
|
||||
}
|
||||
|
||||
const targetProfile = context.extensionSettings.connectionManager.profiles.find(p => p.id === apiSettings.tavernProfile);
|
||||
if (!targetProfile) {
|
||||
throw new Error(`未找到配置文件ID: ${apiSettings.tavernProfile}`);
|
||||
}
|
||||
|
||||
const models = [];
|
||||
if (targetProfile.openai_model) {
|
||||
models.push({ id: targetProfile.openai_model, name: targetProfile.openai_model });
|
||||
}
|
||||
|
||||
if (models.length === 0) {
|
||||
throw new Error('当前预设未配置模型');
|
||||
}
|
||||
|
||||
console.log('[Amily2号-Nccs外交部] SillyTavern预设模式获取到模型:', models);
|
||||
return models;
|
||||
|
||||
} else {
|
||||
if (!apiSettings.apiUrl || !apiSettings.apiKey) {
|
||||
throw new Error('API URL或Key未配置');
|
||||
}
|
||||
|
||||
const response = await fetch('/api/backends/chat-completions/status', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
...getRequestHeaders(),
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
reverse_proxy: apiSettings.apiUrl,
|
||||
proxy_password: apiSettings.apiKey,
|
||||
chat_completion_source: 'openai'
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const rawData = await response.json();
|
||||
const models = Array.isArray(rawData) ? rawData : (rawData.data || rawData.models || []);
|
||||
|
||||
if (!Array.isArray(models)) {
|
||||
const errorMessage = rawData.error?.message || 'API未返回有效的模型列表数组';
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const formattedModels = models
|
||||
.map(m => {
|
||||
// 从name字段中提取模型名称,去掉"models/"前缀
|
||||
const modelIdRaw = m.name || m.id || m.model || m;
|
||||
const modelName = String(modelIdRaw).replace(/^models\//, '');
|
||||
return {
|
||||
id: modelName,
|
||||
name: modelName
|
||||
};
|
||||
})
|
||||
.filter(m => m.id)
|
||||
.sort((a, b) => String(a.name).localeCompare(String(b.name)));
|
||||
|
||||
console.log('[Amily2号-Nccs外交部] 全兼容模式获取到模型:', formattedModels);
|
||||
return formattedModels;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Amily2号-Nccs外交部] 获取模型列表失败:', error);
|
||||
toastr.error(`获取模型列表失败: ${error.message}`, 'Nccs API');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function testNccsApiConnection() {
|
||||
console.log('[Amily2号-Nccs外交部] 开始API连接测试');
|
||||
|
||||
const apiSettings = getNccsApiSettings();
|
||||
|
||||
if (apiSettings.apiMode === 'sillytavern_preset') {
|
||||
if (!apiSettings.tavernProfile) {
|
||||
toastr.error('未配置SillyTavern预设ID', 'Nccs API连接测试失败');
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (!apiSettings.apiUrl || !apiSettings.apiKey || !apiSettings.model) {
|
||||
toastr.error('API配置不完整,请检查URL、Key和模型', 'Nccs API连接测试失败');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
toastr.info('正在发送测试消息"你好!"...', 'Nccs API连接测试');
|
||||
|
||||
const userName = window.SillyTavern.getContext?.()?.name1 || '用户';
|
||||
const systemPrompt = `接下来用户会使用测试按钮测试api连接是否正常,请回复ta连接是正常的,称呼用户为:“${userName}大人”,并复述群号1060183271,告知ta,本扩展开发者:“诗与酒.”正静待ta的入驻。`;
|
||||
|
||||
const testMessages = [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
{ role: 'user', content: '你好!' }
|
||||
];
|
||||
|
||||
const response = await callNccsAI(testMessages);
|
||||
|
||||
if (response && response.trim()) {
|
||||
console.log('[Amily2号-Nccs外交部] 测试消息响应:', response);
|
||||
const formattedResponse = response.replace(/\*\*(.*?)\*\*/g, '<b>$1</b>');
|
||||
toastr.success(`连接测试成功!AI回复: "${formattedResponse}"`, 'Nccs API连接测试成功', { "escapeHtml": false });
|
||||
return true;
|
||||
} else {
|
||||
throw new Error('API未返回有效响应');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Amily2号-Nccs外交部] 连接测试失败:', error);
|
||||
toastr.error(`连接测试失败: ${error.message}`, 'Nccs API连接测试失败');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,388 +1,385 @@
|
||||
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";
|
||||
|
||||
let ChatCompletionService = undefined;
|
||||
try {
|
||||
const module = await import('/scripts/custom-request.js');
|
||||
ChatCompletionService = module.ChatCompletionService;
|
||||
console.log('[Amily2号-Ngms外交部] 已成功召唤"皇家信使"(ChatCompletionService)。');
|
||||
} catch (e) {
|
||||
console.warn("[Amily2号-Ngms外交部] 未能召唤“皇家信使”,部分高级功能(如Claw代理)将受限。请考虑更新SillyTavern版本。", e);
|
||||
}
|
||||
|
||||
function normalizeApiResponse(responseData) {
|
||||
let data = responseData;
|
||||
if (typeof data === 'string') {
|
||||
try {
|
||||
data = JSON.parse(data);
|
||||
} catch (e) {
|
||||
console.error(`[${extensionName}] Ngms API响应JSON解析失败:`, e);
|
||||
return { error: { message: 'Invalid JSON response' } };
|
||||
}
|
||||
}
|
||||
if (data && typeof data.data === 'object' && data.data !== null && !Array.isArray(data.data)) {
|
||||
if (Object.hasOwn(data.data, 'data')) {
|
||||
data = data.data;
|
||||
}
|
||||
}
|
||||
if (data && data.choices && data.choices[0]) {
|
||||
return { content: data.choices[0].message?.content?.trim() };
|
||||
}
|
||||
if (data && data.content) {
|
||||
return { content: data.content.trim() };
|
||||
}
|
||||
if (data && data.data) {
|
||||
return { data: data.data };
|
||||
}
|
||||
if (data && data.error) {
|
||||
return { error: data.error };
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
export function getNgmsApiSettings() {
|
||||
return {
|
||||
apiMode: extension_settings[extensionName]?.ngmsApiMode || 'openai_test',
|
||||
apiUrl: extension_settings[extensionName]?.ngmsApiUrl?.trim() || '',
|
||||
apiKey: extension_settings[extensionName]?.ngmsApiKey?.trim() || '',
|
||||
model: extension_settings[extensionName]?.ngmsModel || '',
|
||||
maxTokens: extension_settings[extensionName]?.ngmsMaxTokens || 4000,
|
||||
temperature: extension_settings[extensionName]?.ngmsTemperature || 0.7,
|
||||
tavernProfile: extension_settings[extensionName]?.ngmsTavernProfile || ''
|
||||
};
|
||||
}
|
||||
|
||||
export async function callNgmsAI(messages, options = {}) {
|
||||
if (window.AMILY2_SYSTEM_PARALYZED === true) {
|
||||
console.error("[Amily2-Ngms制裁] 系统完整性已受损,所有外交活动被无限期中止。");
|
||||
return null;
|
||||
}
|
||||
|
||||
const apiSettings = getNgmsApiSettings();
|
||||
|
||||
const finalOptions = {
|
||||
maxTokens: apiSettings.maxTokens,
|
||||
temperature: apiSettings.temperature,
|
||||
model: apiSettings.model,
|
||||
apiUrl: apiSettings.apiUrl,
|
||||
apiKey: apiSettings.apiKey,
|
||||
apiMode: apiSettings.apiMode,
|
||||
tavernProfile: apiSettings.tavernProfile,
|
||||
...options
|
||||
};
|
||||
|
||||
if (finalOptions.apiMode !== 'sillytavern_preset') {
|
||||
if (!finalOptions.apiUrl || !finalOptions.model || !finalOptions.apiKey) {
|
||||
console.warn("[Amily2-Ngms外交部] API配置不完整,无法调用AI");
|
||||
toastr.error("API配置不完整,请检查URL、Key和模型配置。", "Ngms-外交部");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
console.groupCollapsed(`[Amily2号-Ngms统一API调用] ${new Date().toLocaleTimeString()}`);
|
||||
console.log("【请求参数】:", {
|
||||
mode: finalOptions.apiMode,
|
||||
model: finalOptions.model,
|
||||
maxTokens: finalOptions.maxTokens,
|
||||
temperature: finalOptions.temperature,
|
||||
messagesCount: messages.length
|
||||
});
|
||||
console.log("【消息内容】:", messages);
|
||||
console.groupEnd();
|
||||
|
||||
try {
|
||||
let responseContent;
|
||||
|
||||
switch (finalOptions.apiMode) {
|
||||
case 'openai_test':
|
||||
responseContent = await callNgmsOpenAITest(messages, finalOptions);
|
||||
break;
|
||||
case 'sillytavern_preset':
|
||||
responseContent = await callNgmsSillyTavernPreset(messages, finalOptions);
|
||||
break;
|
||||
default:
|
||||
console.error(`[Amily2-Ngms外交部] 未支持的API模式: ${finalOptions.apiMode}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!responseContent) {
|
||||
console.warn('[Amily2-Ngms外交部] 未能获取AI响应内容');
|
||||
return null;
|
||||
}
|
||||
|
||||
console.groupCollapsed("[Amily2号-Ngms AI回复]");
|
||||
console.log(responseContent);
|
||||
console.groupEnd();
|
||||
|
||||
return responseContent;
|
||||
|
||||
} catch (error) {
|
||||
console.error(`[Amily2-Ngms外交部] API调用发生错误:`, error);
|
||||
|
||||
if (error.message.includes('400')) {
|
||||
toastr.error(`API请求格式错误 (400): 请检查消息格式和模型配置`, "Ngms API调用失败");
|
||||
} else if (error.message.includes('401')) {
|
||||
toastr.error(`API认证失败 (401): 请检查API Key配置`, "Ngms API调用失败");
|
||||
} else if (error.message.includes('403')) {
|
||||
toastr.error(`API访问被拒绝 (403): 请检查权限设置`, "Ngms API调用失败");
|
||||
} else if (error.message.includes('429')) {
|
||||
toastr.error(`API调用频率超限 (429): 请稍后重试`, "Ngms API调用失败");
|
||||
} else if (error.message.includes('500')) {
|
||||
toastr.error(`API服务器错误 (500): 请稍后重试`, "Ngms API调用失败");
|
||||
} else {
|
||||
toastr.error(`API调用失败: ${error.message}`, "Ngms API调用失败");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function callNgmsOpenAITest(messages, options) {
|
||||
const isGoogleApi = options.apiUrl.includes('googleapis.com');
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
if (!isGoogleApi) {
|
||||
Object.assign(body, {
|
||||
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(body)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Ngms全兼容API请求失败: ${response.status} - ${errorText}`);
|
||||
}
|
||||
|
||||
const responseData = await response.json();
|
||||
return responseData?.choices?.[0]?.message?.content;
|
||||
}
|
||||
|
||||
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上下文');
|
||||
}
|
||||
|
||||
const profileId = options.tavernProfile;
|
||||
if (!profileId) {
|
||||
throw new Error('未配置SillyTavern预设ID');
|
||||
}
|
||||
|
||||
let originalProfile = '';
|
||||
let responsePromise;
|
||||
|
||||
try {
|
||||
originalProfile = await window.TavernHelper.triggerSlash('/profile');
|
||||
console.log(`[Amily2号-NgmsST预设] 当前配置文件: ${originalProfile}`);
|
||||
|
||||
const targetProfile = context.extensionSettings?.connectionManager?.profiles?.find(p => p.id === profileId);
|
||||
if (!targetProfile) {
|
||||
throw new Error(`未找到配置文件ID: ${profileId}`);
|
||||
}
|
||||
|
||||
const targetProfileName = targetProfile.name;
|
||||
console.log(`[Amily2号-NgmsST预设] 目标配置文件: ${targetProfileName}`);
|
||||
|
||||
const currentProfile = await window.TavernHelper.triggerSlash('/profile');
|
||||
if (currentProfile !== targetProfileName) {
|
||||
console.log(`[Amily2号-NgmsST预设] 切换配置文件: ${currentProfile} -> ${targetProfileName}`);
|
||||
const escapedProfileName = targetProfileName.replace(/"/g, '\\"');
|
||||
await window.TavernHelper.triggerSlash(`/profile await=true "${escapedProfileName}"`);
|
||||
}
|
||||
|
||||
if (!context.ConnectionManagerRequestService) {
|
||||
throw new Error('ConnectionManagerRequestService不可用');
|
||||
}
|
||||
|
||||
console.log(`[Amily2号-NgmsST预设] 通过配置文件 ${targetProfileName} 发送请求`);
|
||||
responsePromise = context.ConnectionManagerRequestService.sendRequest(
|
||||
targetProfile.id,
|
||||
messages,
|
||||
options.maxTokens || 4000
|
||||
);
|
||||
|
||||
} finally {
|
||||
try {
|
||||
const currentProfileAfterCall = await window.TavernHelper.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}"`);
|
||||
}
|
||||
} catch (restoreError) {
|
||||
console.error('[Amily2号-NgmsST预设] 恢复配置文件失败:', restoreError);
|
||||
}
|
||||
}
|
||||
|
||||
const result = await responsePromise;
|
||||
|
||||
if (!result) {
|
||||
throw new Error('未收到API响应');
|
||||
}
|
||||
|
||||
const normalizedResult = normalizeApiResponse(result);
|
||||
if (normalizedResult.error) {
|
||||
throw new Error(normalizedResult.error.message || 'SillyTavern预设API调用失败');
|
||||
}
|
||||
|
||||
return normalizedResult.content;
|
||||
}
|
||||
|
||||
export async function fetchNgmsModels() {
|
||||
console.log('[Amily2号-Ngms外交部] 开始获取模型列表');
|
||||
|
||||
const apiSettings = getNgmsApiSettings();
|
||||
|
||||
try {
|
||||
if (apiSettings.apiMode === 'sillytavern_preset') {
|
||||
// SillyTavern预设模式:获取当前预设的模型
|
||||
const context = getContext();
|
||||
if (!context?.extensionSettings?.connectionManager?.profiles) {
|
||||
throw new Error('无法获取SillyTavern配置文件列表');
|
||||
}
|
||||
|
||||
const targetProfile = context.extensionSettings.connectionManager.profiles.find(p => p.id === apiSettings.tavernProfile);
|
||||
if (!targetProfile) {
|
||||
throw new Error(`未找到配置文件ID: ${apiSettings.tavernProfile}`);
|
||||
}
|
||||
|
||||
const models = [];
|
||||
if (targetProfile.openai_model) {
|
||||
models.push({ id: targetProfile.openai_model, name: targetProfile.openai_model });
|
||||
}
|
||||
|
||||
if (models.length === 0) {
|
||||
throw new Error('当前预设未配置模型');
|
||||
}
|
||||
|
||||
console.log('[Amily2号-Ngms外交部] SillyTavern预设模式获取到模型:', models);
|
||||
return models;
|
||||
|
||||
} else {
|
||||
if (!apiSettings.apiUrl || !apiSettings.apiKey) {
|
||||
throw new Error('API URL或Key未配置');
|
||||
}
|
||||
|
||||
const response = await fetch('/api/backends/chat-completions/status', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
...getRequestHeaders(),
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
reverse_proxy: apiSettings.apiUrl,
|
||||
proxy_password: apiSettings.apiKey,
|
||||
chat_completion_source: 'openai'
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const rawData = await response.json();
|
||||
const models = Array.isArray(rawData) ? rawData : (rawData.data || rawData.models || []);
|
||||
|
||||
if (!Array.isArray(models)) {
|
||||
const errorMessage = result.error?.message || 'API未返回有效的模型列表数组';
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const formattedModels = models
|
||||
.map(m => {
|
||||
// 从name字段中提取模型名称,去掉"models/"前缀
|
||||
const modelIdRaw = m.name || m.id || m.model || m;
|
||||
const modelName = String(modelIdRaw).replace(/^models\//, '');
|
||||
return {
|
||||
id: modelName,
|
||||
name: modelName
|
||||
};
|
||||
})
|
||||
.filter(m => m.id)
|
||||
.sort((a, b) => String(a.name).localeCompare(String(b.name)));
|
||||
|
||||
console.log('[Amily2号-Ngms外交部] 全兼容模式获取到模型:', formattedModels);
|
||||
return formattedModels;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Amily2号-Ngms外交部] 获取模型列表失败:', error);
|
||||
toastr.error(`获取模型列表失败: ${error.message}`, 'Ngms API');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function testNgmsApiConnection() {
|
||||
console.log('[Amily2号-Ngms外交部] 开始API连接测试');
|
||||
|
||||
const apiSettings = getNgmsApiSettings();
|
||||
|
||||
if (apiSettings.apiMode === 'sillytavern_preset') {
|
||||
if (!apiSettings.tavernProfile) {
|
||||
toastr.error('未配置SillyTavern预设ID', 'Ngms API连接测试失败');
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (!apiSettings.apiUrl || !apiSettings.apiKey || !apiSettings.model) {
|
||||
toastr.error('API配置不完整,请检查URL、Key和模型', 'Ngms API连接测试失败');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
toastr.info('正在发送测试消息"你好!"...', 'Ngms API连接测试');
|
||||
|
||||
const userName = window.SillyTavern.getContext?.()?.name1 || '用户';
|
||||
const systemPrompt = `接下来用户会使用测试按钮测试api连接是否正常,请回复ta连接是正常的,称呼用户为:“${userName}大人”,并复述群号1060183271,告知ta,本扩展开发者:“诗与酒.”正静待ta的入驻。`;
|
||||
|
||||
const testMessages = [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
{ role: 'user', content: '你好!' }
|
||||
];
|
||||
|
||||
const response = await callNgmsAI(testMessages);
|
||||
|
||||
if (response && response.trim()) {
|
||||
console.log('[Amily2号-Ngms外交部] 测试消息响应:', response);
|
||||
const formattedResponse = response.replace(/\*\*(.*?)\*\*/g, '<b>$1</b>');
|
||||
toastr.success(`连接测试成功!AI回复: "${formattedResponse}"`, 'Ngms API连接测试成功', { "escapeHtml": false });
|
||||
return true;
|
||||
} else {
|
||||
throw new Error('API未返回有效响应');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Amily2号-Ngms外交部] 连接测试失败:', error);
|
||||
toastr.error(`连接测试失败: ${error.message}`, 'Ngms API连接测试失败');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
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 {
|
||||
const module = await import('/scripts/custom-request.js');
|
||||
ChatCompletionService = module.ChatCompletionService;
|
||||
console.log('[Amily2号-Ngms外交部] 已成功召唤"皇家信使"(ChatCompletionService)。');
|
||||
} catch (e) {
|
||||
console.warn("[Amily2号-Ngms外交部] 未能召唤“皇家信使”,部分高级功能(如Claw代理)将受限。请考虑更新SillyTavern版本。", e);
|
||||
}
|
||||
|
||||
function normalizeApiResponse(responseData) {
|
||||
let data = responseData;
|
||||
if (typeof data === 'string') {
|
||||
try {
|
||||
data = JSON.parse(data);
|
||||
} catch (e) {
|
||||
console.error(`[${extensionName}] Ngms API响应JSON解析失败:`, e);
|
||||
return { error: { message: 'Invalid JSON response' } };
|
||||
}
|
||||
}
|
||||
if (data && typeof data.data === 'object' && data.data !== null && !Array.isArray(data.data)) {
|
||||
if (Object.hasOwn(data.data, 'data')) {
|
||||
data = data.data;
|
||||
}
|
||||
}
|
||||
if (data && data.choices && data.choices[0]) {
|
||||
return { content: data.choices[0].message?.content?.trim() };
|
||||
}
|
||||
if (data && data.content) {
|
||||
return { content: data.content.trim() };
|
||||
}
|
||||
if (data && data.data) {
|
||||
return { data: data.data };
|
||||
}
|
||||
if (data && data.error) {
|
||||
return { error: data.error };
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
export function getNgmsApiSettings() {
|
||||
return {
|
||||
apiMode: extension_settings[extensionName]?.ngmsApiMode || 'openai_test',
|
||||
apiUrl: extension_settings[extensionName]?.ngmsApiUrl?.trim() || '',
|
||||
apiKey: extension_settings[extensionName]?.ngmsApiKey?.trim() || '',
|
||||
model: extension_settings[extensionName]?.ngmsModel || '',
|
||||
maxTokens: extension_settings[extensionName]?.ngmsMaxTokens || 4000,
|
||||
temperature: extension_settings[extensionName]?.ngmsTemperature || 0.7,
|
||||
tavernProfile: extension_settings[extensionName]?.ngmsTavernProfile || ''
|
||||
};
|
||||
}
|
||||
|
||||
export async function callNgmsAI(messages, options = {}) {
|
||||
if (window.AMILY2_SYSTEM_PARALYZED === true) {
|
||||
console.error("[Amily2-Ngms制裁] 系统完整性已受损,所有外交活动被无限期中止。");
|
||||
return null;
|
||||
}
|
||||
|
||||
const apiSettings = getNgmsApiSettings();
|
||||
|
||||
const finalOptions = {
|
||||
maxTokens: apiSettings.maxTokens,
|
||||
temperature: apiSettings.temperature,
|
||||
model: apiSettings.model,
|
||||
apiUrl: apiSettings.apiUrl,
|
||||
apiKey: apiSettings.apiKey,
|
||||
apiMode: apiSettings.apiMode,
|
||||
tavernProfile: apiSettings.tavernProfile,
|
||||
...options
|
||||
};
|
||||
|
||||
if (finalOptions.apiMode !== 'sillytavern_preset') {
|
||||
if (!finalOptions.apiUrl || !finalOptions.model || !finalOptions.apiKey) {
|
||||
console.warn("[Amily2-Ngms外交部] API配置不完整,无法调用AI");
|
||||
toastr.error("API配置不完整,请检查URL、Key和模型配置。", "Ngms-外交部");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
console.groupCollapsed(`[Amily2号-Ngms统一API调用] ${new Date().toLocaleTimeString()}`);
|
||||
console.log("【请求参数】:", {
|
||||
mode: finalOptions.apiMode,
|
||||
model: finalOptions.model,
|
||||
maxTokens: finalOptions.maxTokens,
|
||||
temperature: finalOptions.temperature,
|
||||
messagesCount: messages.length
|
||||
});
|
||||
console.log("【消息内容】:", messages);
|
||||
console.groupEnd();
|
||||
|
||||
try {
|
||||
let responseContent;
|
||||
|
||||
switch (finalOptions.apiMode) {
|
||||
case 'openai_test':
|
||||
responseContent = await callNgmsOpenAITest(messages, finalOptions);
|
||||
break;
|
||||
case 'sillytavern_preset':
|
||||
responseContent = await callNgmsSillyTavernPreset(messages, finalOptions);
|
||||
break;
|
||||
default:
|
||||
console.error(`[Amily2-Ngms外交部] 未支持的API模式: ${finalOptions.apiMode}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!responseContent) {
|
||||
console.warn('[Amily2-Ngms外交部] 未能获取AI响应内容');
|
||||
return null;
|
||||
}
|
||||
|
||||
console.groupCollapsed("[Amily2号-Ngms AI回复]");
|
||||
console.log(responseContent);
|
||||
console.groupEnd();
|
||||
|
||||
return responseContent;
|
||||
|
||||
} catch (error) {
|
||||
console.error(`[Amily2-Ngms外交部] API调用发生错误:`, error);
|
||||
|
||||
if (error.message.includes('400')) {
|
||||
toastr.error(`API请求格式错误 (400): 请检查消息格式和模型配置`, "Ngms API调用失败");
|
||||
} else if (error.message.includes('401')) {
|
||||
toastr.error(`API认证失败 (401): 请检查API Key配置`, "Ngms API调用失败");
|
||||
} else if (error.message.includes('403')) {
|
||||
toastr.error(`API访问被拒绝 (403): 请检查权限设置`, "Ngms API调用失败");
|
||||
} else if (error.message.includes('429')) {
|
||||
toastr.error(`API调用频率超限 (429): 请稍后重试`, "Ngms API调用失败");
|
||||
} else if (error.message.includes('500')) {
|
||||
toastr.error(`API服务器错误 (500): 请稍后重试`, "Ngms API调用失败");
|
||||
} else {
|
||||
toastr.error(`API调用失败: ${error.message}`, "Ngms API调用失败");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function callNgmsOpenAITest(messages, options) {
|
||||
const isGoogleApi = options.apiUrl.includes('googleapis.com');
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
if (!isGoogleApi) {
|
||||
Object.assign(body, {
|
||||
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(body)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Ngms全兼容API请求失败: ${response.status} - ${errorText}`);
|
||||
}
|
||||
|
||||
const responseData = await response.json();
|
||||
return responseData?.choices?.[0]?.message?.content;
|
||||
}
|
||||
|
||||
async function callNgmsSillyTavernPreset(messages, options) {
|
||||
console.log('[Amily2号-NgmsST预设] 使用SillyTavern预设调用');
|
||||
|
||||
const context = getContext();
|
||||
if (!context) {
|
||||
throw new Error('无法获取SillyTavern上下文');
|
||||
}
|
||||
|
||||
const profileId = options.tavernProfile;
|
||||
if (!profileId) {
|
||||
throw new Error('未配置SillyTavern预设ID');
|
||||
}
|
||||
|
||||
let originalProfile = '';
|
||||
let responsePromise;
|
||||
|
||||
try {
|
||||
originalProfile = await amilyHelper.triggerSlash('/profile');
|
||||
console.log(`[Amily2号-NgmsST预设] 当前配置文件: ${originalProfile}`);
|
||||
|
||||
const targetProfile = context.extensionSettings?.connectionManager?.profiles?.find(p => p.id === profileId);
|
||||
if (!targetProfile) {
|
||||
throw new Error(`未找到配置文件ID: ${profileId}`);
|
||||
}
|
||||
|
||||
const targetProfileName = targetProfile.name;
|
||||
console.log(`[Amily2号-NgmsST预设] 目标配置文件: ${targetProfileName}`);
|
||||
|
||||
const currentProfile = await amilyHelper.triggerSlash('/profile');
|
||||
if (currentProfile !== targetProfileName) {
|
||||
console.log(`[Amily2号-NgmsST预设] 切换配置文件: ${currentProfile} -> ${targetProfileName}`);
|
||||
const escapedProfileName = targetProfileName.replace(/"/g, '\\"');
|
||||
await amilyHelper.triggerSlash(`/profile await=true "${escapedProfileName}"`);
|
||||
}
|
||||
|
||||
if (!context.ConnectionManagerRequestService) {
|
||||
throw new Error('ConnectionManagerRequestService不可用');
|
||||
}
|
||||
|
||||
console.log(`[Amily2号-NgmsST预设] 通过配置文件 ${targetProfileName} 发送请求`);
|
||||
responsePromise = context.ConnectionManagerRequestService.sendRequest(
|
||||
targetProfile.id,
|
||||
messages,
|
||||
options.maxTokens || 4000
|
||||
);
|
||||
|
||||
} finally {
|
||||
try {
|
||||
const currentProfileAfterCall = await amilyHelper.triggerSlash('/profile');
|
||||
if (originalProfile && originalProfile !== currentProfileAfterCall) {
|
||||
console.log(`[Amily2号-NgmsST预设] 恢复原始配置文件: ${currentProfileAfterCall} -> ${originalProfile}`);
|
||||
const escapedOriginalProfile = originalProfile.replace(/"/g, '\\"');
|
||||
await amilyHelper.triggerSlash(`/profile await=true "${escapedOriginalProfile}"`);
|
||||
}
|
||||
} catch (restoreError) {
|
||||
console.error('[Amily2号-NgmsST预设] 恢复配置文件失败:', restoreError);
|
||||
}
|
||||
}
|
||||
|
||||
const result = await responsePromise;
|
||||
|
||||
if (!result) {
|
||||
throw new Error('未收到API响应');
|
||||
}
|
||||
|
||||
const normalizedResult = normalizeApiResponse(result);
|
||||
if (normalizedResult.error) {
|
||||
throw new Error(normalizedResult.error.message || 'SillyTavern预设API调用失败');
|
||||
}
|
||||
|
||||
return normalizedResult.content;
|
||||
}
|
||||
|
||||
export async function fetchNgmsModels() {
|
||||
console.log('[Amily2号-Ngms外交部] 开始获取模型列表');
|
||||
|
||||
const apiSettings = getNgmsApiSettings();
|
||||
|
||||
try {
|
||||
if (apiSettings.apiMode === 'sillytavern_preset') {
|
||||
// SillyTavern预设模式:获取当前预设的模型
|
||||
const context = getContext();
|
||||
if (!context?.extensionSettings?.connectionManager?.profiles) {
|
||||
throw new Error('无法获取SillyTavern配置文件列表');
|
||||
}
|
||||
|
||||
const targetProfile = context.extensionSettings.connectionManager.profiles.find(p => p.id === apiSettings.tavernProfile);
|
||||
if (!targetProfile) {
|
||||
throw new Error(`未找到配置文件ID: ${apiSettings.tavernProfile}`);
|
||||
}
|
||||
|
||||
const models = [];
|
||||
if (targetProfile.openai_model) {
|
||||
models.push({ id: targetProfile.openai_model, name: targetProfile.openai_model });
|
||||
}
|
||||
|
||||
if (models.length === 0) {
|
||||
throw new Error('当前预设未配置模型');
|
||||
}
|
||||
|
||||
console.log('[Amily2号-Ngms外交部] SillyTavern预设模式获取到模型:', models);
|
||||
return models;
|
||||
|
||||
} else {
|
||||
if (!apiSettings.apiUrl || !apiSettings.apiKey) {
|
||||
throw new Error('API URL或Key未配置');
|
||||
}
|
||||
|
||||
const response = await fetch('/api/backends/chat-completions/status', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
...getRequestHeaders(),
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
reverse_proxy: apiSettings.apiUrl,
|
||||
proxy_password: apiSettings.apiKey,
|
||||
chat_completion_source: 'openai'
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const rawData = await response.json();
|
||||
const models = Array.isArray(rawData) ? rawData : (rawData.data || rawData.models || []);
|
||||
|
||||
if (!Array.isArray(models)) {
|
||||
const errorMessage = result.error?.message || 'API未返回有效的模型列表数组';
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const formattedModels = models
|
||||
.map(m => {
|
||||
// 从name字段中提取模型名称,去掉"models/"前缀
|
||||
const modelIdRaw = m.name || m.id || m.model || m;
|
||||
const modelName = String(modelIdRaw).replace(/^models\//, '');
|
||||
return {
|
||||
id: modelName,
|
||||
name: modelName
|
||||
};
|
||||
})
|
||||
.filter(m => m.id)
|
||||
.sort((a, b) => String(a.name).localeCompare(String(b.name)));
|
||||
|
||||
console.log('[Amily2号-Ngms外交部] 全兼容模式获取到模型:', formattedModels);
|
||||
return formattedModels;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Amily2号-Ngms外交部] 获取模型列表失败:', error);
|
||||
toastr.error(`获取模型列表失败: ${error.message}`, 'Ngms API');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function testNgmsApiConnection() {
|
||||
console.log('[Amily2号-Ngms外交部] 开始API连接测试');
|
||||
|
||||
const apiSettings = getNgmsApiSettings();
|
||||
|
||||
if (apiSettings.apiMode === 'sillytavern_preset') {
|
||||
if (!apiSettings.tavernProfile) {
|
||||
toastr.error('未配置SillyTavern预设ID', 'Ngms API连接测试失败');
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (!apiSettings.apiUrl || !apiSettings.apiKey || !apiSettings.model) {
|
||||
toastr.error('API配置不完整,请检查URL、Key和模型', 'Ngms API连接测试失败');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
toastr.info('正在发送测试消息"你好!"...', 'Ngms API连接测试');
|
||||
|
||||
const userName = window.SillyTavern.getContext?.()?.name1 || '用户';
|
||||
const systemPrompt = `接下来用户会使用测试按钮测试api连接是否正常,请回复ta连接是正常的,称呼用户为:“${userName}大人”,并复述群号1060183271,告知ta,本扩展开发者:“诗与酒.”正静待ta的入驻。`;
|
||||
|
||||
const testMessages = [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
{ role: 'user', content: '你好!' }
|
||||
];
|
||||
|
||||
const response = await callNgmsAI(testMessages);
|
||||
|
||||
if (response && response.trim()) {
|
||||
console.log('[Amily2号-Ngms外交部] 测试消息响应:', response);
|
||||
const formattedResponse = response.replace(/\*\*(.*?)\*\*/g, '<b>$1</b>');
|
||||
toastr.success(`连接测试成功!AI回复: "${formattedResponse}"`, 'Ngms API连接测试成功', { "escapeHtml": false });
|
||||
return true;
|
||||
} else {
|
||||
throw new Error('API未返回有效响应');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Amily2号-Ngms外交部] 连接测试失败:', error);
|
||||
toastr.error(`连接测试失败: ${error.message}`, 'Ngms API连接测试失败');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,388 +1,385 @@
|
||||
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";
|
||||
|
||||
let ChatCompletionService = undefined;
|
||||
try {
|
||||
const module = await import('/scripts/custom-request.js');
|
||||
ChatCompletionService = module.ChatCompletionService;
|
||||
console.log('[Amily2号-Sybd外交部] 已成功召唤"皇家信使"(ChatCompletionService)。');
|
||||
} catch (e) {
|
||||
console.warn("[Amily2号-Sybd外交部] 未能召唤“皇家信使”,部分高级功能(如Claw代理)将受限。请考虑更新SillyTavern版本。", e);
|
||||
}
|
||||
|
||||
function normalizeApiResponse(responseData) {
|
||||
let data = responseData;
|
||||
if (typeof data === 'string') {
|
||||
try {
|
||||
data = JSON.parse(data);
|
||||
} catch (e) {
|
||||
console.error(`[${extensionName}] Sybd API响应JSON解析失败:`, e);
|
||||
return { error: { message: 'Invalid JSON response' } };
|
||||
}
|
||||
}
|
||||
if (data && typeof data.data === 'object' && data.data !== null && !Array.isArray(data.data)) {
|
||||
if (Object.hasOwn(data.data, 'data')) {
|
||||
data = data.data;
|
||||
}
|
||||
}
|
||||
if (data && data.choices && data.choices[0]) {
|
||||
return { content: data.choices[0].message?.content?.trim() };
|
||||
}
|
||||
if (data && data.content) {
|
||||
return { content: data.content.trim() };
|
||||
}
|
||||
if (data && data.data) {
|
||||
return { data: data.data };
|
||||
}
|
||||
if (data && data.error) {
|
||||
return { error: data.error };
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
export function getSybdApiSettings() {
|
||||
return {
|
||||
apiMode: extension_settings[extensionName]?.sybdApiMode || 'openai_test',
|
||||
apiUrl: extension_settings[extensionName]?.sybdApiUrl?.trim() || '',
|
||||
apiKey: extension_settings[extensionName]?.sybdApiKey?.trim() || '',
|
||||
model: extension_settings[extensionName]?.sybdModel || '',
|
||||
maxTokens: extension_settings[extensionName]?.sybdMaxTokens || 4000,
|
||||
temperature: extension_settings[extensionName]?.sybdTemperature || 0.7,
|
||||
tavernProfile: extension_settings[extensionName]?.sybdTavernProfile || ''
|
||||
};
|
||||
}
|
||||
|
||||
export async function callSybdAI(messages, options = {}) {
|
||||
if (window.AMILY2_SYSTEM_PARALYZED === true) {
|
||||
console.error("[Amily2-Sybd制裁] 系统完整性已受损,所有外交活动被无限期中止。");
|
||||
return null;
|
||||
}
|
||||
|
||||
const apiSettings = getSybdApiSettings();
|
||||
|
||||
const finalOptions = {
|
||||
maxTokens: apiSettings.maxTokens,
|
||||
temperature: apiSettings.temperature,
|
||||
model: apiSettings.model,
|
||||
apiUrl: apiSettings.apiUrl,
|
||||
apiKey: apiSettings.apiKey,
|
||||
apiMode: apiSettings.apiMode,
|
||||
tavernProfile: apiSettings.tavernProfile,
|
||||
...options
|
||||
};
|
||||
|
||||
if (finalOptions.apiMode !== 'sillytavern_preset') {
|
||||
if (!finalOptions.apiUrl || !finalOptions.model || !finalOptions.apiKey) {
|
||||
console.warn("[Amily2-Sybd外交部] API配置不完整,无法调用AI");
|
||||
toastr.error("API配置不完整,请检查URL、Key和模型配置。", "Sybd-外交部");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
console.groupCollapsed(`[Amily2号-Sybd统一API调用] ${new Date().toLocaleTimeString()}`);
|
||||
console.log("【请求参数】:", {
|
||||
mode: finalOptions.apiMode,
|
||||
model: finalOptions.model,
|
||||
maxTokens: finalOptions.maxTokens,
|
||||
temperature: finalOptions.temperature,
|
||||
messagesCount: messages.length
|
||||
});
|
||||
console.log("【消息内容】:", messages);
|
||||
console.groupEnd();
|
||||
|
||||
try {
|
||||
let responseContent;
|
||||
|
||||
switch (finalOptions.apiMode) {
|
||||
case 'openai_test':
|
||||
responseContent = await callSybdOpenAITest(messages, finalOptions);
|
||||
break;
|
||||
case 'sillytavern_preset':
|
||||
responseContent = await callSybdSillyTavernPreset(messages, finalOptions);
|
||||
break;
|
||||
default:
|
||||
console.error(`[Amily2-Sybd外交部] 未支持的API模式: ${finalOptions.apiMode}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!responseContent) {
|
||||
console.warn('[Amily2-Sybd外交部] 未能获取AI响应内容');
|
||||
return null;
|
||||
}
|
||||
|
||||
console.groupCollapsed("[Amily2号-Sybd AI回复]");
|
||||
console.log(responseContent);
|
||||
console.groupEnd();
|
||||
|
||||
return responseContent;
|
||||
|
||||
} catch (error) {
|
||||
console.error(`[Amily2-Sybd外交部] API调用发生错误:`, error);
|
||||
|
||||
if (error.message.includes('400')) {
|
||||
toastr.error(`API请求格式错误 (400): 请检查消息格式和模型配置`, "Sybd API调用失败");
|
||||
} else if (error.message.includes('401')) {
|
||||
toastr.error(`API认证失败 (401): 请检查API Key配置`, "Sybd API调用失败");
|
||||
} else if (error.message.includes('403')) {
|
||||
toastr.error(`API访问被拒绝 (403): 请检查权限设置`, "Sybd API调用失败");
|
||||
} else if (error.message.includes('429')) {
|
||||
toastr.error(`API调用频率超限 (429): 请稍后重试`, "Sybd API调用失败");
|
||||
} else if (error.message.includes('500')) {
|
||||
toastr.error(`API服务器错误 (500): 请稍后重试`, "Sybd API调用失败");
|
||||
} else {
|
||||
toastr.error(`API调用失败: ${error.message}`, "Sybd API调用失败");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function callSybdOpenAITest(messages, options) {
|
||||
const isGoogleApi = options.apiUrl.includes('googleapis.com');
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
if (!isGoogleApi) {
|
||||
Object.assign(body, {
|
||||
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(body)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Sybd全兼容API请求失败: ${response.status} - ${errorText}`);
|
||||
}
|
||||
|
||||
const responseData = await response.json();
|
||||
return responseData?.choices?.[0]?.message?.content;
|
||||
}
|
||||
|
||||
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上下文');
|
||||
}
|
||||
|
||||
const profileId = options.tavernProfile;
|
||||
if (!profileId) {
|
||||
throw new Error('未配置SillyTavern预设ID');
|
||||
}
|
||||
|
||||
let originalProfile = '';
|
||||
let responsePromise;
|
||||
|
||||
try {
|
||||
originalProfile = await window.TavernHelper.triggerSlash('/profile');
|
||||
console.log(`[Amily2号-SybdST预设] 当前配置文件: ${originalProfile}`);
|
||||
|
||||
const targetProfile = context.extensionSettings?.connectionManager?.profiles?.find(p => p.id === profileId);
|
||||
if (!targetProfile) {
|
||||
throw new Error(`未找到配置文件ID: ${profileId}`);
|
||||
}
|
||||
|
||||
const targetProfileName = targetProfile.name;
|
||||
console.log(`[Amily2号-SybdST预设] 目标配置文件: ${targetProfileName}`);
|
||||
|
||||
const currentProfile = await window.TavernHelper.triggerSlash('/profile');
|
||||
if (currentProfile !== targetProfileName) {
|
||||
console.log(`[Amily2号-SybdST预设] 切换配置文件: ${currentProfile} -> ${targetProfileName}`);
|
||||
const escapedProfileName = targetProfileName.replace(/"/g, '\\"');
|
||||
await window.TavernHelper.triggerSlash(`/profile await=true "${escapedProfileName}"`);
|
||||
}
|
||||
|
||||
if (!context.ConnectionManagerRequestService) {
|
||||
throw new Error('ConnectionManagerRequestService不可用');
|
||||
}
|
||||
|
||||
console.log(`[Amily2号-SybdST预设] 通过配置文件 ${targetProfileName} 发送请求`);
|
||||
responsePromise = context.ConnectionManagerRequestService.sendRequest(
|
||||
targetProfile.id,
|
||||
messages,
|
||||
options.maxTokens || 4000
|
||||
);
|
||||
|
||||
} finally {
|
||||
try {
|
||||
const currentProfileAfterCall = await window.TavernHelper.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}"`);
|
||||
}
|
||||
} catch (restoreError) {
|
||||
console.error('[Amily2号-SybdST预设] 恢复配置文件失败:', restoreError);
|
||||
}
|
||||
}
|
||||
|
||||
const result = await responsePromise;
|
||||
|
||||
if (!result) {
|
||||
throw new Error('未收到API响应');
|
||||
}
|
||||
|
||||
const normalizedResult = normalizeApiResponse(result);
|
||||
if (normalizedResult.error) {
|
||||
throw new Error(normalizedResult.error.message || 'SillyTavern预设API调用失败');
|
||||
}
|
||||
|
||||
return normalizedResult.content;
|
||||
}
|
||||
|
||||
export async function fetchSybdModels() {
|
||||
console.log('[Amily2号-Sybd外交部] 开始获取模型列表');
|
||||
|
||||
const apiSettings = getSybdApiSettings();
|
||||
|
||||
try {
|
||||
if (apiSettings.apiMode === 'sillytavern_preset') {
|
||||
// SillyTavern预设模式:获取当前预设的模型
|
||||
const context = getContext();
|
||||
if (!context?.extensionSettings?.connectionManager?.profiles) {
|
||||
throw new Error('无法获取SillyTavern配置文件列表');
|
||||
}
|
||||
|
||||
const targetProfile = context.extensionSettings.connectionManager.profiles.find(p => p.id === apiSettings.tavernProfile);
|
||||
if (!targetProfile) {
|
||||
throw new Error(`未找到配置文件ID: ${apiSettings.tavernProfile}`);
|
||||
}
|
||||
|
||||
const models = [];
|
||||
if (targetProfile.openai_model) {
|
||||
models.push({ id: targetProfile.openai_model, name: targetProfile.openai_model });
|
||||
}
|
||||
|
||||
if (models.length === 0) {
|
||||
throw new Error('当前预设未配置模型');
|
||||
}
|
||||
|
||||
console.log('[Amily2号-Sybd外交部] SillyTavern预设模式获取到模型:', models);
|
||||
return models;
|
||||
|
||||
} else {
|
||||
if (!apiSettings.apiUrl || !apiSettings.apiKey) {
|
||||
throw new Error('API URL或Key未配置');
|
||||
}
|
||||
|
||||
const response = await fetch('/api/backends/chat-completions/status', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
...getRequestHeaders(),
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
reverse_proxy: apiSettings.apiUrl,
|
||||
proxy_password: apiSettings.apiKey,
|
||||
chat_completion_source: 'openai'
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const rawData = await response.json();
|
||||
const models = Array.isArray(rawData) ? rawData : (rawData.data || rawData.models || []);
|
||||
|
||||
if (!Array.isArray(models)) {
|
||||
const errorMessage = result.error?.message || 'API未返回有效的模型列表数组';
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const formattedModels = models
|
||||
.map(m => {
|
||||
// 从name字段中提取模型名称,去掉"models/"前缀
|
||||
const modelIdRaw = m.name || m.id || m.model || m;
|
||||
const modelName = String(modelIdRaw).replace(/^models\//, '');
|
||||
return {
|
||||
id: modelName,
|
||||
name: modelName
|
||||
};
|
||||
})
|
||||
.filter(m => m.id)
|
||||
.sort((a, b) => String(a.name).localeCompare(String(b.name)));
|
||||
|
||||
console.log('[Amily2号-Sybd外交部] 全兼容模式获取到模型:', formattedModels);
|
||||
return formattedModels;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Amily2号-Sybd外交部] 获取模型列表失败:', error);
|
||||
toastr.error(`获取模型列表失败: ${error.message}`, 'Sybd API');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function testSybdApiConnection() {
|
||||
console.log('[Amily2号-Sybd外交部] 开始API连接测试');
|
||||
|
||||
const apiSettings = getSybdApiSettings();
|
||||
|
||||
if (apiSettings.apiMode === 'sillytavern_preset') {
|
||||
if (!apiSettings.tavernProfile) {
|
||||
toastr.error('未配置SillyTavern预设ID', 'Sybd API连接测试失败');
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (!apiSettings.apiUrl || !apiSettings.apiKey || !apiSettings.model) {
|
||||
toastr.error('API配置不完整,请检查URL、Key和模型', 'Sybd API连接测试失败');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
toastr.info('正在发送测试消息"你好!"...', 'Sybd API连接测试');
|
||||
|
||||
const userName = window.SillyTavern.getContext?.()?.name1 || '用户';
|
||||
const systemPrompt = `接下来用户会使用测试按钮测试api连接是否正常,请回复ta连接是正常的,称呼用户为:“${userName}大人”,并复述群号1060183271,告知ta,本扩展开发者:“诗与酒.”正静待ta的入驻。`;
|
||||
|
||||
const testMessages = [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
{ role: 'user', content: '你好!' }
|
||||
];
|
||||
|
||||
const response = await callSybdAI(testMessages);
|
||||
|
||||
if (response && response.trim()) {
|
||||
console.log('[Amily2号-Sybd外交部] 测试消息响应:', response);
|
||||
const formattedResponse = response.replace(/\*\*(.*?)\*\*/g, '<b>$1</b>');
|
||||
toastr.success(`连接测试成功!AI回复: "${formattedResponse}"`, 'Sybd API连接测试成功', { "escapeHtml": false });
|
||||
return true;
|
||||
} else {
|
||||
throw new Error('API未返回有效响应');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Amily2号-Sybd外交部] 连接测试失败:', error);
|
||||
toastr.error(`连接测试失败: ${error.message}`, 'Sybd API连接测试失败');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
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 {
|
||||
const module = await import('/scripts/custom-request.js');
|
||||
ChatCompletionService = module.ChatCompletionService;
|
||||
console.log('[Amily2号-Sybd外交部] 已成功召唤"皇家信使"(ChatCompletionService)。');
|
||||
} catch (e) {
|
||||
console.warn("[Amily2号-Sybd外交部] 未能召唤“皇家信使”,部分高级功能(如Claw代理)将受限。请考虑更新SillyTavern版本。", e);
|
||||
}
|
||||
|
||||
function normalizeApiResponse(responseData) {
|
||||
let data = responseData;
|
||||
if (typeof data === 'string') {
|
||||
try {
|
||||
data = JSON.parse(data);
|
||||
} catch (e) {
|
||||
console.error(`[${extensionName}] Sybd API响应JSON解析失败:`, e);
|
||||
return { error: { message: 'Invalid JSON response' } };
|
||||
}
|
||||
}
|
||||
if (data && typeof data.data === 'object' && data.data !== null && !Array.isArray(data.data)) {
|
||||
if (Object.hasOwn(data.data, 'data')) {
|
||||
data = data.data;
|
||||
}
|
||||
}
|
||||
if (data && data.choices && data.choices[0]) {
|
||||
return { content: data.choices[0].message?.content?.trim() };
|
||||
}
|
||||
if (data && data.content) {
|
||||
return { content: data.content.trim() };
|
||||
}
|
||||
if (data && data.data) {
|
||||
return { data: data.data };
|
||||
}
|
||||
if (data && data.error) {
|
||||
return { error: data.error };
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
export function getSybdApiSettings() {
|
||||
return {
|
||||
apiMode: extension_settings[extensionName]?.sybdApiMode || 'openai_test',
|
||||
apiUrl: extension_settings[extensionName]?.sybdApiUrl?.trim() || '',
|
||||
apiKey: extension_settings[extensionName]?.sybdApiKey?.trim() || '',
|
||||
model: extension_settings[extensionName]?.sybdModel || '',
|
||||
maxTokens: extension_settings[extensionName]?.sybdMaxTokens || 4000,
|
||||
temperature: extension_settings[extensionName]?.sybdTemperature || 0.7,
|
||||
tavernProfile: extension_settings[extensionName]?.sybdTavernProfile || ''
|
||||
};
|
||||
}
|
||||
|
||||
export async function callSybdAI(messages, options = {}) {
|
||||
if (window.AMILY2_SYSTEM_PARALYZED === true) {
|
||||
console.error("[Amily2-Sybd制裁] 系统完整性已受损,所有外交活动被无限期中止。");
|
||||
return null;
|
||||
}
|
||||
|
||||
const apiSettings = getSybdApiSettings();
|
||||
|
||||
const finalOptions = {
|
||||
maxTokens: apiSettings.maxTokens,
|
||||
temperature: apiSettings.temperature,
|
||||
model: apiSettings.model,
|
||||
apiUrl: apiSettings.apiUrl,
|
||||
apiKey: apiSettings.apiKey,
|
||||
apiMode: apiSettings.apiMode,
|
||||
tavernProfile: apiSettings.tavernProfile,
|
||||
...options
|
||||
};
|
||||
|
||||
if (finalOptions.apiMode !== 'sillytavern_preset') {
|
||||
if (!finalOptions.apiUrl || !finalOptions.model || !finalOptions.apiKey) {
|
||||
console.warn("[Amily2-Sybd外交部] API配置不完整,无法调用AI");
|
||||
toastr.error("API配置不完整,请检查URL、Key和模型配置。", "Sybd-外交部");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
console.groupCollapsed(`[Amily2号-Sybd统一API调用] ${new Date().toLocaleTimeString()}`);
|
||||
console.log("【请求参数】:", {
|
||||
mode: finalOptions.apiMode,
|
||||
model: finalOptions.model,
|
||||
maxTokens: finalOptions.maxTokens,
|
||||
temperature: finalOptions.temperature,
|
||||
messagesCount: messages.length
|
||||
});
|
||||
console.log("【消息内容】:", messages);
|
||||
console.groupEnd();
|
||||
|
||||
try {
|
||||
let responseContent;
|
||||
|
||||
switch (finalOptions.apiMode) {
|
||||
case 'openai_test':
|
||||
responseContent = await callSybdOpenAITest(messages, finalOptions);
|
||||
break;
|
||||
case 'sillytavern_preset':
|
||||
responseContent = await callSybdSillyTavernPreset(messages, finalOptions);
|
||||
break;
|
||||
default:
|
||||
console.error(`[Amily2-Sybd外交部] 未支持的API模式: ${finalOptions.apiMode}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!responseContent) {
|
||||
console.warn('[Amily2-Sybd外交部] 未能获取AI响应内容');
|
||||
return null;
|
||||
}
|
||||
|
||||
console.groupCollapsed("[Amily2号-Sybd AI回复]");
|
||||
console.log(responseContent);
|
||||
console.groupEnd();
|
||||
|
||||
return responseContent;
|
||||
|
||||
} catch (error) {
|
||||
console.error(`[Amily2-Sybd外交部] API调用发生错误:`, error);
|
||||
|
||||
if (error.message.includes('400')) {
|
||||
toastr.error(`API请求格式错误 (400): 请检查消息格式和模型配置`, "Sybd API调用失败");
|
||||
} else if (error.message.includes('401')) {
|
||||
toastr.error(`API认证失败 (401): 请检查API Key配置`, "Sybd API调用失败");
|
||||
} else if (error.message.includes('403')) {
|
||||
toastr.error(`API访问被拒绝 (403): 请检查权限设置`, "Sybd API调用失败");
|
||||
} else if (error.message.includes('429')) {
|
||||
toastr.error(`API调用频率超限 (429): 请稍后重试`, "Sybd API调用失败");
|
||||
} else if (error.message.includes('500')) {
|
||||
toastr.error(`API服务器错误 (500): 请稍后重试`, "Sybd API调用失败");
|
||||
} else {
|
||||
toastr.error(`API调用失败: ${error.message}`, "Sybd API调用失败");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function callSybdOpenAITest(messages, options) {
|
||||
const isGoogleApi = options.apiUrl.includes('googleapis.com');
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
if (!isGoogleApi) {
|
||||
Object.assign(body, {
|
||||
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(body)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Sybd全兼容API请求失败: ${response.status} - ${errorText}`);
|
||||
}
|
||||
|
||||
const responseData = await response.json();
|
||||
return responseData?.choices?.[0]?.message?.content;
|
||||
}
|
||||
|
||||
async function callSybdSillyTavernPreset(messages, options) {
|
||||
console.log('[Amily2号-SybdST预设] 使用SillyTavern预设调用');
|
||||
|
||||
const context = getContext();
|
||||
if (!context) {
|
||||
throw new Error('无法获取SillyTavern上下文');
|
||||
}
|
||||
|
||||
const profileId = options.tavernProfile;
|
||||
if (!profileId) {
|
||||
throw new Error('未配置SillyTavern预设ID');
|
||||
}
|
||||
|
||||
let originalProfile = '';
|
||||
let responsePromise;
|
||||
|
||||
try {
|
||||
originalProfile = await amilyHelper.triggerSlash('/profile');
|
||||
console.log(`[Amily2号-SybdST预设] 当前配置文件: ${originalProfile}`);
|
||||
|
||||
const targetProfile = context.extensionSettings?.connectionManager?.profiles?.find(p => p.id === profileId);
|
||||
if (!targetProfile) {
|
||||
throw new Error(`未找到配置文件ID: ${profileId}`);
|
||||
}
|
||||
|
||||
const targetProfileName = targetProfile.name;
|
||||
console.log(`[Amily2号-SybdST预设] 目标配置文件: ${targetProfileName}`);
|
||||
|
||||
const currentProfile = await amilyHelper.triggerSlash('/profile');
|
||||
if (currentProfile !== targetProfileName) {
|
||||
console.log(`[Amily2号-SybdST预设] 切换配置文件: ${currentProfile} -> ${targetProfileName}`);
|
||||
const escapedProfileName = targetProfileName.replace(/"/g, '\\"');
|
||||
await amilyHelper.triggerSlash(`/profile await=true "${escapedProfileName}"`);
|
||||
}
|
||||
|
||||
if (!context.ConnectionManagerRequestService) {
|
||||
throw new Error('ConnectionManagerRequestService不可用');
|
||||
}
|
||||
|
||||
console.log(`[Amily2号-SybdST预设] 通过配置文件 ${targetProfileName} 发送请求`);
|
||||
responsePromise = context.ConnectionManagerRequestService.sendRequest(
|
||||
targetProfile.id,
|
||||
messages,
|
||||
options.maxTokens || 4000
|
||||
);
|
||||
|
||||
} finally {
|
||||
try {
|
||||
const currentProfileAfterCall = await amilyHelper.triggerSlash('/profile');
|
||||
if (originalProfile && originalProfile !== currentProfileAfterCall) {
|
||||
console.log(`[Amily2号-SybdST预设] 恢复原始配置文件: ${currentProfileAfterCall} -> ${originalProfile}`);
|
||||
const escapedOriginalProfile = originalProfile.replace(/"/g, '\\"');
|
||||
await amilyHelper.triggerSlash(`/profile await=true "${escapedOriginalProfile}"`);
|
||||
}
|
||||
} catch (restoreError) {
|
||||
console.error('[Amily2号-SybdST预设] 恢复配置文件失败:', restoreError);
|
||||
}
|
||||
}
|
||||
|
||||
const result = await responsePromise;
|
||||
|
||||
if (!result) {
|
||||
throw new Error('未收到API响应');
|
||||
}
|
||||
|
||||
const normalizedResult = normalizeApiResponse(result);
|
||||
if (normalizedResult.error) {
|
||||
throw new Error(normalizedResult.error.message || 'SillyTavern预设API调用失败');
|
||||
}
|
||||
|
||||
return normalizedResult.content;
|
||||
}
|
||||
|
||||
export async function fetchSybdModels() {
|
||||
console.log('[Amily2号-Sybd外交部] 开始获取模型列表');
|
||||
|
||||
const apiSettings = getSybdApiSettings();
|
||||
|
||||
try {
|
||||
if (apiSettings.apiMode === 'sillytavern_preset') {
|
||||
// SillyTavern预设模式:获取当前预设的模型
|
||||
const context = getContext();
|
||||
if (!context?.extensionSettings?.connectionManager?.profiles) {
|
||||
throw new Error('无法获取SillyTavern配置文件列表');
|
||||
}
|
||||
|
||||
const targetProfile = context.extensionSettings.connectionManager.profiles.find(p => p.id === apiSettings.tavernProfile);
|
||||
if (!targetProfile) {
|
||||
throw new Error(`未找到配置文件ID: ${apiSettings.tavernProfile}`);
|
||||
}
|
||||
|
||||
const models = [];
|
||||
if (targetProfile.openai_model) {
|
||||
models.push({ id: targetProfile.openai_model, name: targetProfile.openai_model });
|
||||
}
|
||||
|
||||
if (models.length === 0) {
|
||||
throw new Error('当前预设未配置模型');
|
||||
}
|
||||
|
||||
console.log('[Amily2号-Sybd外交部] SillyTavern预设模式获取到模型:', models);
|
||||
return models;
|
||||
|
||||
} else {
|
||||
if (!apiSettings.apiUrl || !apiSettings.apiKey) {
|
||||
throw new Error('API URL或Key未配置');
|
||||
}
|
||||
|
||||
const response = await fetch('/api/backends/chat-completions/status', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
...getRequestHeaders(),
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
reverse_proxy: apiSettings.apiUrl,
|
||||
proxy_password: apiSettings.apiKey,
|
||||
chat_completion_source: 'openai'
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const rawData = await response.json();
|
||||
const models = Array.isArray(rawData) ? rawData : (rawData.data || rawData.models || []);
|
||||
|
||||
if (!Array.isArray(models)) {
|
||||
const errorMessage = result.error?.message || 'API未返回有效的模型列表数组';
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const formattedModels = models
|
||||
.map(m => {
|
||||
// 从name字段中提取模型名称,去掉"models/"前缀
|
||||
const modelIdRaw = m.name || m.id || m.model || m;
|
||||
const modelName = String(modelIdRaw).replace(/^models\//, '');
|
||||
return {
|
||||
id: modelName,
|
||||
name: modelName
|
||||
};
|
||||
})
|
||||
.filter(m => m.id)
|
||||
.sort((a, b) => String(a.name).localeCompare(String(b.name)));
|
||||
|
||||
console.log('[Amily2号-Sybd外交部] 全兼容模式获取到模型:', formattedModels);
|
||||
return formattedModels;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Amily2号-Sybd外交部] 获取模型列表失败:', error);
|
||||
toastr.error(`获取模型列表失败: ${error.message}`, 'Sybd API');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function testSybdApiConnection() {
|
||||
console.log('[Amily2号-Sybd外交部] 开始API连接测试');
|
||||
|
||||
const apiSettings = getSybdApiSettings();
|
||||
|
||||
if (apiSettings.apiMode === 'sillytavern_preset') {
|
||||
if (!apiSettings.tavernProfile) {
|
||||
toastr.error('未配置SillyTavern预设ID', 'Sybd API连接测试失败');
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (!apiSettings.apiUrl || !apiSettings.apiKey || !apiSettings.model) {
|
||||
toastr.error('API配置不完整,请检查URL、Key和模型', 'Sybd API连接测试失败');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
toastr.info('正在发送测试消息"你好!"...', 'Sybd API连接测试');
|
||||
|
||||
const userName = window.SillyTavern.getContext?.()?.name1 || '用户';
|
||||
const systemPrompt = `接下来用户会使用测试按钮测试api连接是否正常,请回复ta连接是正常的,称呼用户为:“${userName}大人”,并复述群号1060183271,告知ta,本扩展开发者:“诗与酒.”正静待ta的入驻。`;
|
||||
|
||||
const testMessages = [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
{ role: 'user', content: '你好!' }
|
||||
];
|
||||
|
||||
const response = await callSybdAI(testMessages);
|
||||
|
||||
if (response && response.trim()) {
|
||||
console.log('[Amily2号-Sybd外交部] 测试消息响应:', response);
|
||||
const formattedResponse = response.replace(/\*\*(.*?)\*\*/g, '<b>$1</b>');
|
||||
toastr.success(`连接测试成功!AI回复: "${formattedResponse}"`, 'Sybd API连接测试成功', { "escapeHtml": false });
|
||||
return true;
|
||||
} else {
|
||||
throw new Error('API未返回有效响应');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Amily2号-Sybd外交部] 连接测试失败:', error);
|
||||
toastr.error(`连接测试失败: ${error.message}`, 'Sybd API连接测试失败');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
222
core/commands.js
222
core/commands.js
File diff suppressed because one or more lines are too long
@@ -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} 楼】 `, '');
|
||||
|
||||
92
core/lore.js
92
core/lore.js
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
36
core/tavern-helper/Wrapperiframe.js
Normal file
36
core/tavern-helper/Wrapperiframe.js
Normal 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);
|
||||
};
|
||||
}
|
||||
})();
|
||||
31
core/tavern-helper/iframe_client.js
Normal file
31
core/tavern-helper/iframe_client.js
Normal 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();
|
||||
}
|
||||
@@ -1,149 +1,619 @@
|
||||
import {
|
||||
world_names,
|
||||
loadWorldInfo,
|
||||
saveWorldInfo,
|
||||
createNewWorldInfo,
|
||||
createWorldInfoEntry
|
||||
} from "/scripts/world-info.js";
|
||||
import { characters } from "/script.js";
|
||||
import { getContext } from "/scripts/extensions.js";
|
||||
import { executeSlashCommandsWithOptions } from '/scripts/slash-commands.js';
|
||||
|
||||
|
||||
class AmilyHelper {
|
||||
|
||||
async getLorebooks() {
|
||||
return [...world_names];
|
||||
}
|
||||
|
||||
async getCharLorebooks(options = { type: 'all' }) {
|
||||
try {
|
||||
const context = getContext();
|
||||
if (!context || !context.characterId) {
|
||||
console.warn('[Amily助手] 无法获取当前角色上下文。');
|
||||
return { primary: null, additional: [] };
|
||||
}
|
||||
const character = characters[context.characterId];
|
||||
const primary = character?.data?.extensions?.world;
|
||||
return { primary: primary || null, additional: [] };
|
||||
} catch (error) {
|
||||
console.error('[Amily助手] 获取角色世界书时出错:', error);
|
||||
return { primary: null, additional: [] };
|
||||
}
|
||||
}
|
||||
|
||||
async getLorebookEntries(bookName) {
|
||||
try {
|
||||
const bookData = await loadWorldInfo(bookName);
|
||||
if (!bookData || !bookData.entries) {
|
||||
return [];
|
||||
}
|
||||
return Object.entries(bookData.entries).map(([uid, entry]) => ({
|
||||
uid: parseInt(uid),
|
||||
comment: entry.comment || '无标题条目',
|
||||
content: entry.content || '',
|
||||
key: entry.key || [],
|
||||
enabled: !entry.disable,
|
||||
constant: entry.constant || false,
|
||||
position: entry.position || 4,
|
||||
depth: entry.depth || 998,
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error(`[Amily助手] 获取世界书《${bookName}》条目时出错:`, error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async setLorebookEntries(bookName, entries) {
|
||||
try {
|
||||
const bookData = await loadWorldInfo(bookName);
|
||||
if (!bookData) {
|
||||
console.error(`[Amily助手] 更新失败:找不到世界书《${bookName}》。`);
|
||||
return false;
|
||||
}
|
||||
for (const entryUpdate of entries) {
|
||||
const existingEntry = bookData.entries[entryUpdate.uid];
|
||||
if (existingEntry) {
|
||||
if (entryUpdate.content !== undefined) existingEntry.content = entryUpdate.content;
|
||||
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.constant !== undefined) existingEntry.constant = entryUpdate.constant;
|
||||
if (entryUpdate.position !== undefined) existingEntry.position = entryUpdate.position;
|
||||
if (entryUpdate.depth !== undefined) existingEntry.depth = entryUpdate.depth;
|
||||
}
|
||||
}
|
||||
await saveWorldInfo(bookName, bookData, true);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`[Amily助手] 更新世界书《${bookName}》条目时出错:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async createLorebookEntries(bookName, entries) {
|
||||
try {
|
||||
let bookData = await loadWorldInfo(bookName);
|
||||
if (!bookData) {
|
||||
console.warn(`[Amily助手] 世界书《${bookName}》不存在,将自动创建。`);
|
||||
await this.createLorebook(bookName);
|
||||
bookData = await loadWorldInfo(bookName);
|
||||
if (!bookData) {
|
||||
throw new Error(`创建并加载世界书《${bookName}》失败。`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const newEntryData of entries) {
|
||||
const newEntry = createWorldInfoEntry(bookName, bookData);
|
||||
Object.assign(newEntry, {
|
||||
comment: newEntryData.comment || '新条目',
|
||||
content: newEntryData.content || '',
|
||||
key: newEntryData.key || [],
|
||||
constant: newEntryData.constant || false,
|
||||
position: newEntryData.position ?? 4,
|
||||
depth: newEntryData.depth ?? 998,
|
||||
disable: !(newEntryData.enabled ?? true),
|
||||
});
|
||||
}
|
||||
await saveWorldInfo(bookName, bookData, true);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`[Amily助手] 在世界书《${bookName}》中创建新条目时出错:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async createLorebook(bookName) {
|
||||
try {
|
||||
if (world_names.includes(bookName)) {
|
||||
console.warn(`[Amily助手] 创建失败:世界书《${bookName}》已存在。`);
|
||||
return false;
|
||||
}
|
||||
await createNewWorldInfo(bookName);
|
||||
if (!world_names.includes(bookName)) {
|
||||
world_names.push(bookName);
|
||||
world_names.sort();
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`[Amily助手] 创建世界书《${bookName}》时出错:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async triggerSlash(command) {
|
||||
try {
|
||||
console.log(`[Amily助手] 正在执行斜杠命令: ${command}`);
|
||||
const result = await executeSlashCommandsWithOptions(command);
|
||||
if (result.isError) {
|
||||
throw new Error(result.errorMessage);
|
||||
}
|
||||
return result.pipe;
|
||||
} catch (error) {
|
||||
console.error(`[Amily助手] 执行斜杠命令 '${command}' 时出错:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const amilyHelper = new AmilyHelper();
|
||||
import {
|
||||
world_names,
|
||||
loadWorldInfo,
|
||||
saveWorldInfo,
|
||||
createNewWorldInfo,
|
||||
createWorldInfoEntry,
|
||||
reloadEditor
|
||||
} from "/scripts/world-info.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];
|
||||
}
|
||||
|
||||
async getCharLorebooks(options = { type: 'all' }) {
|
||||
try {
|
||||
const context = getContext();
|
||||
if (!context || context.characterId === undefined) {
|
||||
console.warn('[Amily助手] 无法获取当前角色上下文');
|
||||
return { primary: null, additional: [] };
|
||||
}
|
||||
const character = characters[context.characterId];
|
||||
const primary = character?.data?.extensions?.world;
|
||||
return { primary: primary || null, additional: [] };
|
||||
} catch (error) {
|
||||
console.error('[Amily助手] 获取角色世界书时出错:', error);
|
||||
return { primary: null, additional: [] };
|
||||
}
|
||||
}
|
||||
|
||||
async getLorebookEntries(bookName) {
|
||||
try {
|
||||
const bookData = await loadWorldInfo(bookName);
|
||||
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: positionMap[entry.position] || 'at_depth_as_system',
|
||||
depth: entry.depth || 998,
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error(`[Amily助手] 获取世界书《${bookName}》条目时出错:`, error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async setLorebookEntries(bookName, entries) {
|
||||
try {
|
||||
const bookData = await loadWorldInfo(bookName);
|
||||
if (!bookData) {
|
||||
console.error(`[Amily助手] 更新失败:找不到世界书《${bookName}》`);
|
||||
return false;
|
||||
}
|
||||
for (const entryUpdate of entries) {
|
||||
const existingEntry = bookData.entries[entryUpdate.uid];
|
||||
if (existingEntry) {
|
||||
if (entryUpdate.content !== undefined) existingEntry.content = entryUpdate.content;
|
||||
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.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);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async createLorebookEntries(bookName, entries) {
|
||||
try {
|
||||
let bookData = await loadWorldInfo(bookName);
|
||||
if (!bookData) {
|
||||
console.warn(`[Amily助手] 世界书《${bookName}》不存在,将自动创建`);
|
||||
await this.createLorebook(bookName);
|
||||
bookData = await loadWorldInfo(bookName);
|
||||
if (!bookData) {
|
||||
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.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);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async createLorebook(bookName) {
|
||||
try {
|
||||
if (world_names.includes(bookName)) {
|
||||
console.warn(`[Amily助手] 创建失败:世界书《${bookName}》已存在`);
|
||||
return false;
|
||||
}
|
||||
await createNewWorldInfo(bookName);
|
||||
if (!world_names.includes(bookName)) {
|
||||
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);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 斜杠命令相关 ====================
|
||||
|
||||
async triggerSlash(command) {
|
||||
try {
|
||||
console.log(`[Amily助手] 正在执行斜杠命令: ${command}`);
|
||||
const result = await executeSlashCommandsWithOptions(command);
|
||||
if (result.isError) {
|
||||
throw new Error(result.errorMessage);
|
||||
}
|
||||
return result.pipe;
|
||||
} catch (error) {
|
||||
console.error(`[Amily助手] 执行斜杠命令 '${command}' 时出错:`, error);
|
||||
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] 主窗口监听器已初始化');
|
||||
}
|
||||
|
||||
51
core/tavern-helper/renderer-bindings.js
Normal file
51
core/tavern-helper/renderer-bindings.js
Normal 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.");
|
||||
}
|
||||
21
core/tavern-helper/renderer.html
Normal file
21
core/tavern-helper/renderer.html
Normal 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>
|
||||
606
core/tavern-helper/renderer.js
Normal file
606
core/tavern-helper/renderer.js
Normal 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();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
194
index.js
@@ -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);
|
||||
|
||||
});
|
||||
|
||||
@@ -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 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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,11 +421,49 @@ 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() {
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
32
ui/state.js
32
ui/state.js
@@ -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');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user