4 Commits

33 changed files with 1045 additions and 245 deletions

View File

@@ -1,6 +1,7 @@
import { extension_settings } from '/scripts/extensions.js';
import { extensionName } from '../../utils/settings.js';
import { saveSettingsDebounced } from '/script.js';
import { configManager } from '../../utils/config/ConfigManager.js';
import { world_names } from '/scripts/world-info.js';
import { state } from './cwb_state.js';
import { cwbCompleteDefaultSettings } from './cwb_config.js';
@@ -38,7 +39,7 @@ function saveApiConfig() {
const settings = getSettings();
settings.cwb_api_mode = $panel.find('#cwb-api-mode').val();
settings.cwb_api_url = $panel.find('#cwb-api-url').val().trim();
settings.cwb_api_key = $panel.find('#cwb-api-key').val();
configManager.set('cwb_api_key', $panel.find('#cwb-api-key').val());
settings.cwb_api_model = $panel.find('#cwb-api-model').val();
settings.cwb_tavern_profile = $panel.find('#cwb-tavern-profile').val();
@@ -63,7 +64,7 @@ function saveApiConfig() {
function clearApiConfig() {
const settings = getSettings();
settings.cwb_api_url = '';
settings.cwb_api_key = '';
configManager.set('cwb_api_key', '');
settings.cwb_api_model = '';
saveSettingsDebounced();
state.customApiConfig.url = '';
@@ -283,13 +284,11 @@ export function bindSettingsEvents($settingsPanel) {
$panel.on('input', '#cwb-api-key', function() {
const apiKey = $(this).val();
// 同时更新设置和状态
getSettings().cwb_api_key = apiKey;
// 同时更新设置和状态API Key 经 configManager 写入 localStorage
configManager.set('cwb_api_key', apiKey);
state.customApiConfig.apiKey = apiKey;
saveSettingsDebounced();
console.log('[CWB] API Key已更新 - 设置长度:', getSettings().cwb_api_key?.length || 0, ', 状态长度:', state.customApiConfig.apiKey?.length || 0);
console.log('[CWB] API Key已更新 - 状态长度:', state.customApiConfig.apiKey?.length || 0);
});
$panel.on('change', '#cwb-api-model', function() {
@@ -489,7 +488,7 @@ function updateUiWithSettings() {
}
$panel.find('#cwb-api-url').val(settings.cwb_api_url);
$panel.find('#cwb-api-key').val(settings.cwb_api_key);
$panel.find('#cwb-api-key').val(configManager.get('cwb_api_key') || '');
$panel.find('#cwb-tavern-profile').val(settings.cwb_tavern_profile);
const $modelSelect = $panel.find('#cwb-api-model');
@@ -574,7 +573,7 @@ export function loadSettings() {
state.isIncrementalUpdateEnabled = finalSettings.cwb_incremental_update_enabled;
state.customApiConfig.url = finalSettings.cwb_api_url || '';
state.customApiConfig.apiKey = finalSettings.cwb_api_key || '';
state.customApiConfig.apiKey = configManager.get('cwb_api_key') || '';
state.customApiConfig.model = finalSettings.cwb_api_model || '';
state.currentBreakArmorPrompt = finalSettings.cwb_break_armor_prompt;

View File

@@ -7,6 +7,7 @@
import { extension_settings } from '/scripts/extensions.js';
import { saveSettingsDebounced } from '/script.js';
import { amilyHelper } from '../../core/tavern-helper/main.js';
import { configManager } from '../../utils/config/ConfigManager.js';
const { jQuery: $, SillyTavern } = window;
@@ -675,8 +676,7 @@
$('#cwb-api-key').off('input').on('input', function() {
const value = $(this).val();
extension_settings[extensionName].cwb_api_key = value;
saveSettingsDebounced();
configManager.set('cwb_api_key', value);
});
$('#cwb-model').off('input').on('input', function() {

542
SL/module/SfiGenModule.js Normal file
View File

@@ -0,0 +1,542 @@
import { Module, ModuleBuilder } from './Module.js';
import { extension_settings, getContext } from '../../../../../extensions.js';
import { saveSettingsDebounced, saveChat, reloadCurrentChat, eventSource, event_types } from '../../../../../../script.js';
import { registerSlashCommand } from '../../../../../slash-commands.js';
const extensionName = 'ST-Amily2-Chat-Optimisation-Dev'; // Use main extension name for settings
const sfigenSettingsKey = 'sfigen_settings';
const defaultSettings = {
api_key: '',
model: 'Qwen/Qwen-Image',
negative_prompt: '模糊, 低分辨率, 水印, 文字',
image_size: '1664x928',
steps: 50,
cfg: 4.0,
regex_tag: 'sfigen',
prefix_prompt: ''
};
const builder = new ModuleBuilder()
.name('SfiGen')
.view('assets/siliconflow-image-gen.html')
.strict(true)
.required(['mount']);
export default class SfiGenModule extends Module {
constructor() {
super(builder);
this.settings = {};
}
async init(ctx = {}) {
await super.init(ctx);
this._loadSettings();
return this;
}
_loadSettings() {
if (!extension_settings[extensionName]) {
extension_settings[extensionName] = {};
}
if (!extension_settings[extensionName][sfigenSettingsKey]) {
extension_settings[extensionName][sfigenSettingsKey] = { ...defaultSettings };
}
this.settings = extension_settings[extensionName][sfigenSettingsKey];
// Ensure all default keys exist
for (const key in defaultSettings) {
if (!(key in this.settings)) {
this.settings[key] = defaultSettings[key];
}
}
}
_saveSettings() {
extension_settings[extensionName][sfigenSettingsKey] = this.settings;
saveSettingsDebounced();
}
async mount() {
if (this.el) {
this.el.id = 'amily2_sfigen_panel';
this.el.style.display = 'none';
}
this._bindUI();
this._registerSlashCommand();
this._bindEvents();
this._bindButtonsGlobal();
}
_bindUI() {
const $el = $(this.el);
// Bind inputs
$el.find('#sfigen_api_key').val(this.settings.api_key).on('input', (e) => {
this.settings.api_key = $(e.target).val();
this._saveSettings();
});
$el.find('#sfigen_model').val(this.settings.model).on('input', (e) => {
this.settings.model = $(e.target).val();
this._saveSettings();
});
$el.find('#sfigen_negative_prompt').val(this.settings.negative_prompt).on('input', (e) => {
this.settings.negative_prompt = $(e.target).val();
this._saveSettings();
});
$el.find('#sfigen_image_size').val(this.settings.image_size).on('change', (e) => {
this.settings.image_size = $(e.target).val();
this._saveSettings();
});
$el.find('#sfigen_steps').val(this.settings.steps).on('input', (e) => {
this.settings.steps = $(e.target).val();
this._saveSettings();
});
$el.find('#sfigen_cfg').val(this.settings.cfg).on('input', (e) => {
this.settings.cfg = $(e.target).val();
this._saveSettings();
});
$el.find('#sfigen_regex_tag').val(this.settings.regex_tag).on('input', (e) => {
this.settings.regex_tag = $(e.target).val();
this._saveSettings();
});
$el.find('#sfigen_prefix_prompt').val(this.settings.prefix_prompt).on('input', (e) => {
this.settings.prefix_prompt = $(e.target).val();
this._saveSettings();
});
// Bind style tags
$el.find('.sfigen-style-tag').on('click', (e) => {
const promptToAdd = $(e.target).data('prompt');
const textarea = $el.find('#sfigen_prefix_prompt');
let currentVal = textarea.val().trim();
if (currentVal) {
if (!currentVal.endsWith(',')) {
currentVal += ', ';
} else {
currentVal += ' ';
}
textarea.val(currentVal + promptToAdd);
} else {
textarea.val(promptToAdd);
}
textarea.trigger('input');
$(e.target).css('opacity', '0.5');
setTimeout(() => $(e.target).css('opacity', '1'), 200);
});
// Bind back button
$el.find('#amily2_sfigen_back_to_main').on('click', () => {
$el.hide();
$('#amily2_chat_optimiser > .plugin-features').show();
});
}
async _generateImage(prompt) {
let finalPrompt = prompt;
if (this.settings.prefix_prompt && this.settings.prefix_prompt.trim() !== '') {
finalPrompt = `${this.settings.prefix_prompt.trim()}, ${prompt}`;
}
console.log(`[SfiGen] 开始生成图片,最终提示词:`, finalPrompt);
if (!this.settings.api_key) {
console.warn(`[SfiGen] 未配置 API Key`);
toastr.error('请先在扩展设置中配置 SiliconFlow API Key');
return null;
}
const url = 'https://api.siliconflow.cn/v1/images/generations';
const headers = {
'Authorization': `Bearer ${this.settings.api_key}`,
'Content-Type': 'application/json'
};
const body = {
model: this.settings.model,
prompt: finalPrompt,
negative_prompt: this.settings.negative_prompt,
image_size: this.settings.image_size,
seed: Math.floor(Math.random() * 1000000000),
num_inference_steps: parseInt(this.settings.steps),
cfg: parseFloat(this.settings.cfg)
};
try {
toastr.info('正在生成图片,请稍候...');
const response = await fetch(url, {
method: 'POST',
headers: headers,
body: JSON.stringify(body)
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || `HTTP error! status: ${response.status}`);
}
const data = await response.json();
if (data.images && data.images.length > 0) {
toastr.success('图片生成成功!');
return data.images[0].url;
} else {
throw new Error('API 返回数据中没有图片 URL');
}
} catch (error) {
console.error(`[SfiGen] 生成图片失败:`, error);
toastr.error(`生成图片失败: ${error.message}`);
return null;
}
}
_escapeHtml(unsafe) {
return (unsafe || '').replace(/&/g, "&")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
_processMessageDOM(messageId) {
const messageElement = $(`.mes[mesid="${messageId}"] .mes_text`);
if (!messageElement.length) return;
// 检查是否已经处理过,如果已经有容器,说明已经处理过了,直接返回
if (messageElement.find('.sfigen-image-container').length > 0) {
return;
}
let html = messageElement.html();
const tag = this.settings.regex_tag || 'sfigen';
let newHtml = html;
let hasMatch = false;
// 1. 匹配 [tag: prompt]
const regexPrompt = new RegExp(`\\[${tag}:\\s*([^\\]]+)\\]`, 'gi');
newHtml = newHtml.replace(regexPrompt, (match, prompt) => {
hasMatch = true;
const buttonId = `sfigen-btn-${messageId}-${Math.random().toString(36).substr(2, 9)}`;
const safePrompt = this._escapeHtml(prompt);
const safeMatch = this._escapeHtml(match);
return `<div class="sfigen-image-container" data-message-id="${messageId}" data-prompt="${safePrompt}" data-original-tag="${safeMatch}" style="width: 96%; max-width: 600px; background: var(--SmartThemeBlurTintColor); border-radius: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); overflow: hidden; margin: 20px auto; padding: 15px; text-align: center; position: relative; z-index: 10;"><button id="${buttonId}" class="sfigen-generate-btn" style="background-color: var(--SmartThemeQuoteColor); color: var(--SmartThemeBodyColor); border: 1px solid var(--SmartThemeBorderColor); padding: 8px 20px; border-radius: 8px; cursor: pointer; pointer-events: auto; display: inline-block; font-weight: bold; transition: all 0.2s;"><i class="fa-solid fa-image"></i> 生成图片</button></div>`;
});
// 2. 匹配 [tag_img: prompt | url1,url2]
const regexImg = new RegExp(`\\[${tag}_img:\\s*([^\\]]+)\\]`, 'gi');
newHtml = newHtml.replace(regexImg, (match, content) => {
hasMatch = true;
let prompt = "未知提示词";
let imageList = [];
if (content.includes('|')) {
const parts = content.split('|');
prompt = parts[0].trim();
imageList = parts[1].split(',').map(u => u.trim());
} else {
imageList = content.split(',').map(u => u.trim());
}
const displayUrl = imageList[imageList.length - 1];
const buttonId = `sfigen-btn-${messageId}-${Math.random().toString(36).substr(2, 9)}`;
const safePrompt = this._escapeHtml(prompt);
const safeMatch = this._escapeHtml(match);
let navHtml = '';
if (imageList.length > 1) {
navHtml = `<div style="display: flex; justify-content: center; gap: 10px; margin-top: 10px;">`;
imageList.forEach((url, index) => {
const isActive = index === imageList.length - 1;
navHtml += `<button class="sfigen-nav-btn" data-url="${this._escapeHtml(url)}" style="width: 12px; height: 12px; border-radius: 50%; border: none; background-color: ${isActive ? 'var(--SmartThemeQuoteColor)' : 'var(--SmartThemeBorderColor)'}; cursor: pointer; padding: 0;"></button>`;
});
navHtml += `</div>`;
}
return `<div class="sfigen-image-container" data-message-id="${messageId}" data-prompt="${safePrompt}" data-original-tag="${safeMatch}" data-urls="${this._escapeHtml(imageList.join(','))}" style="width: 96%; max-width: 600px; background: var(--SmartThemeBlurTintColor); border-radius: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); overflow: hidden; margin: 20px auto; padding: 15px; text-align: center; position: relative; z-index: 10;">
<div style="width: calc(100% - 4px); margin: 2px auto 15px auto; border: 2px solid rgba(0,0,0,0.15); border-radius: 8px; overflow: hidden; position: relative; cursor: pointer;" class="sfigen-img-wrapper">
<img src="${this._escapeHtml(displayUrl)}" class="sfigen-display-img" style="width: 100%; display: block; transition: transform 0.3s;" alt="CG" title="点击放大">
<div class="sfigen-img-overlay" style="position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.3); display: flex; justify-content: center; align-items: center; opacity: 0; transition: opacity 0.2s;">
<i class="fa-solid fa-magnifying-glass-plus" style="color: white; font-size: 2em;"></i>
</div>
</div>
${navHtml}
<div style="display: flex; justify-content: center; gap: 10px; margin-top: 15px;">
<button id="${buttonId}" class="sfigen-generate-btn" style="background-color: var(--SmartThemeQuoteColor); color: var(--SmartThemeBodyColor); border: 1px solid var(--SmartThemeBorderColor); padding: 8px 20px; border-radius: 8px; cursor: pointer; pointer-events: auto; display: inline-block; font-weight: bold; transition: all 0.2s;"><i class="fa-solid fa-rotate-right"></i> 再次生成</button>
<button class="sfigen-save-btn" data-url="${this._escapeHtml(displayUrl)}" style="background-color: var(--SmartThemeQuoteColor); color: var(--SmartThemeBodyColor); border: 1px solid var(--SmartThemeBorderColor); padding: 8px 20px; border-radius: 8px; cursor: pointer; pointer-events: auto; display: inline-block; font-weight: bold; transition: all 0.2s;"><i class="fa-solid fa-download"></i> 保存图片</button>
</div>
</div>`;
});
if (hasMatch) {
messageElement.html(newHtml);
}
}
_bindEvents() {
const handleMessageRender = (messageId) => {
setTimeout(() => this._processMessageDOM(messageId), 50);
};
eventSource.on(event_types.USER_MESSAGE_RENDERED, handleMessageRender);
eventSource.on(event_types.CHARACTER_MESSAGE_RENDERED, handleMessageRender);
eventSource.on(event_types.MESSAGE_UPDATED, handleMessageRender);
eventSource.on(event_types.MESSAGE_EDITED, handleMessageRender);
eventSource.on(event_types.MESSAGE_SWIPED, handleMessageRender);
eventSource.on(event_types.CHAT_CHANGED, () => {
setTimeout(() => {
$('.mes').each((_, el) => {
const messageId = $(el).attr('mesid');
if (messageId) {
this._processMessageDOM(messageId);
}
});
}, 500);
});
// Initial processing
setTimeout(() => {
$('.mes').each((_, el) => {
const messageId = $(el).attr('mesid');
if (messageId) {
this._processMessageDOM(messageId);
}
});
}, 1000);
}
_bindButtonsGlobal() {
$(document).off('click', '.sfigen-generate-btn');
$(document).on('click', '.sfigen-generate-btn', async (e) => {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
const btn = $(e.currentTarget);
const container = btn.closest('.sfigen-image-container');
const prompt = container.data('prompt');
const messageId = container.data('message-id');
const originalTag = container.data('original-tag');
btn.prop('disabled', true);
btn.html('<i class="fa-solid fa-spinner fa-spin"></i> 生成中...');
const imageUrl = await this._generateImage(prompt);
if (imageUrl) {
const tag = this.settings.regex_tag || 'sfigen';
let existingUrls = container.data('urls') ? String(container.data('urls')).split(',') : [];
existingUrls.push(imageUrl);
const urlsString = existingUrls.join(',');
const newTag = `[${tag}_img: ${prompt} | ${urlsString}]`;
const context = getContext();
const chat = context.chat;
if (chat && chat[messageId]) {
const message = chat[messageId];
// Fix: Use a more robust replacement strategy
// Sometimes originalTag might have been modified by markdown parser
// So we replace the whole tag block in the original message
const regexPrompt = new RegExp(`\\[${tag}:\\s*([^\\]]+)\\]`, 'gi');
const regexImg = new RegExp(`\\[${tag}_img:\\s*([^\\]]+)\\]`, 'gi');
let replaced = false;
// Try exact match first
if (message.mes.includes(originalTag)) {
message.mes = message.mes.replace(originalTag, newTag);
replaced = true;
}
// If not found, try regex replacement
else {
message.mes = message.mes.replace(regexImg, (match, content) => {
if (content.includes(prompt)) {
replaced = true;
return newTag;
}
return match;
});
if (!replaced) {
message.mes = message.mes.replace(regexPrompt, (match, p) => {
if (p.trim() === prompt.trim()) {
replaced = true;
return newTag;
}
return match;
});
}
}
if (replaced) {
await saveChat();
// 立即在前端替换 DOM显示生成的图片
let navHtml = '';
if (existingUrls.length > 1) {
navHtml = `<div style="display: flex; justify-content: center; gap: 10px; margin-top: 10px;">`;
existingUrls.forEach((url, index) => {
const isActive = index === existingUrls.length - 1;
navHtml += `<button class="sfigen-nav-btn" data-url="${this._escapeHtml(url)}" style="width: 12px; height: 12px; border-radius: 50%; border: none; background-color: ${isActive ? 'var(--SmartThemeQuoteColor)' : 'var(--SmartThemeBorderColor)'}; cursor: pointer; padding: 0;"></button>`;
});
navHtml += `</div>`;
}
const newButtonId = `sfigen-btn-${messageId}-${Math.random().toString(36).substr(2, 9)}`;
const safePrompt = this._escapeHtml(prompt);
const safeNewTag = this._escapeHtml(newTag);
const safeUrlsString = this._escapeHtml(urlsString);
const safeImageUrl = this._escapeHtml(imageUrl);
const finalHtml = `<div class="sfigen-image-container" data-message-id="${messageId}" data-prompt="${safePrompt}" data-original-tag="${safeNewTag}" data-urls="${safeUrlsString}" style="width: 96%; max-width: 600px; background: var(--SmartThemeBlurTintColor); border-radius: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); overflow: hidden; margin: 20px auto; padding: 15px; text-align: center; position: relative; z-index: 10;">
<div style="width: calc(100% - 4px); margin: 2px auto 15px auto; border: 2px solid rgba(0,0,0,0.15); border-radius: 8px; overflow: hidden; position: relative; cursor: pointer;" class="sfigen-img-wrapper">
<img src="${safeImageUrl}" class="sfigen-display-img" style="width: 100%; display: block; transition: transform 0.3s;" alt="CG" title="点击放大">
<div class="sfigen-img-overlay" style="position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.3); display: flex; justify-content: center; align-items: center; opacity: 0; transition: opacity 0.2s;">
<i class="fa-solid fa-magnifying-glass-plus" style="color: white; font-size: 2em;"></i>
</div>
</div>
${navHtml}
<div style="display: flex; justify-content: center; gap: 10px; margin-top: 15px;">
<button id="${newButtonId}" class="sfigen-generate-btn" style="background-color: var(--SmartThemeQuoteColor); color: var(--SmartThemeBodyColor); border: 1px solid var(--SmartThemeBorderColor); padding: 8px 20px; border-radius: 8px; cursor: pointer; pointer-events: auto; display: inline-block; font-weight: bold; transition: all 0.2s;"><i class="fa-solid fa-rotate-right"></i> 再次生成</button>
<button class="sfigen-save-btn" data-url="${safeImageUrl}" style="background-color: var(--SmartThemeQuoteColor); color: var(--SmartThemeBodyColor); border: 1px solid var(--SmartThemeBorderColor); padding: 8px 20px; border-radius: 8px; cursor: pointer; pointer-events: auto; display: inline-block; font-weight: bold; transition: all 0.2s;"><i class="fa-solid fa-download"></i> 保存图片</button>
</div>
</div>`;
container.replaceWith(finalHtml);
} else {
console.warn(`[SfiGen] Could not find tag to replace in message ${messageId}`);
toastr.warning('图片已生成,但无法保存到聊天记录中。');
}
}
} else {
btn.prop('disabled', false);
btn.html('<i class="fa-solid fa-image"></i> 重新生成');
}
});
// Image hover and zoom
$(document).on('mouseenter', '.sfigen-img-wrapper', function() {
$(this).find('.sfigen-img-overlay').css('opacity', '1');
$(this).find('.sfigen-display-img').css('transform', 'scale(1.02)');
}).on('mouseleave', '.sfigen-img-wrapper', function() {
$(this).find('.sfigen-img-overlay').css('opacity', '0');
$(this).find('.sfigen-display-img').css('transform', 'scale(1)');
});
$(document).on('click', '.sfigen-img-wrapper', function(e) {
e.stopPropagation();
const imgUrl = $(this).find('img').attr('src');
const overlay = $(`
<div id="sfigen-zoom-overlay" style="position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0,0,0,0.9); z-index: 9999; display: flex; justify-content: center; align-items: center; cursor: zoom-out; opacity: 0; transition: opacity 0.3s;">
<img src="${imgUrl}" style="max-width: 95%; max-height: 95%; object-fit: contain; border-radius: 8px; box-shadow: 0 0 20px rgba(0,0,0,0.5); transform: scale(0.9); transition: transform 0.3s;">
<div style="position: absolute; top: 20px; right: 20px; color: white; font-size: 24px; cursor: pointer;"><i class="fa-solid fa-xmark"></i></div>
</div>
`);
$('body').append(overlay);
setTimeout(() => {
overlay.css('opacity', '1');
overlay.find('img').css('transform', 'scale(1)');
}, 10);
overlay.on('click', function() {
overlay.css('opacity', '0');
overlay.find('img').css('transform', 'scale(0.9)');
setTimeout(() => overlay.remove(), 300);
});
});
// Save image
$(document).on('click', '.sfigen-save-btn', async function(e) {
e.preventDefault();
e.stopPropagation();
const url = $(this).data('url');
try {
const response = await fetch(url);
const blob = await response.blob();
const downloadUrl = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.style.display = 'none';
a.href = downloadUrl;
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
a.download = `sfigen_${timestamp}.png`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(downloadUrl);
document.body.removeChild(a);
toastr.success('图片已保存到默认下载目录');
} catch (error) {
console.error(`[SfiGen] 保存图片失败:`, error);
toastr.error('保存图片失败');
}
});
// Nav buttons
$(document).on('click', '.sfigen-nav-btn', function(e) {
e.preventDefault();
e.stopPropagation();
const btn = $(this);
const container = btn.closest('.sfigen-image-container');
const targetUrl = btn.data('url');
container.find('.sfigen-display-img').attr('src', targetUrl);
container.find('.sfigen-save-btn').data('url', targetUrl);
container.find('.sfigen-nav-btn').css('background-color', 'var(--SmartThemeBorderColor)');
btn.css('background-color', 'var(--SmartThemeQuoteColor)');
});
}
_registerSlashCommand() {
registerSlashCommand('sfigen', async (args, value) => {
if (!value) {
toastr.warning('请提供提示词。例如: /sfigen 一个可爱的猫咪');
return;
}
const imageUrl = await this._generateImage(value);
if (imageUrl) {
const context = getContext();
const message = `<img src="${imageUrl}" alt="Generated Image" style="max-width: 100%; border-radius: 8px;" />`;
context.chat.push({
name: 'System',
is_user: false,
is_system: true,
mes: message,
send_date: Date.now(),
});
await saveChat();
if (typeof window.updateChat === 'function') {
window.updateChat();
} else if (typeof window.updateMessageBlock === 'function') {
window.updateMessageBlock(context.chat.length - 1, context.chat[context.chat.length - 1]);
} else {
await reloadCurrentChat();
}
}
}, [], '使用 SiliconFlow 生成图片', true, true);
}
}

View File

@@ -20,6 +20,7 @@ import GlossaryModule from './GlossaryModule.js';
import RendererModule from './RendererModule.js';
import SuperMemoryModule from './SuperMemoryModule.js';
import ApiConfigModule from './ApiConfigModule.js';
import SfiGenModule from './SfiGenModule.js';
export function registerAllModules() {
registry.register('AdditionalFeatures', () => new AdditionalFeaturesModule());
@@ -33,4 +34,5 @@ export function registerAllModules() {
registry.register('Renderer', () => new RendererModule());
registry.register('SuperMemory', () => new SuperMemoryModule());
registry.register('ApiConfig', () => new ApiConfigModule());
registry.register('SfiGen', () => new SfiGenModule());
}

View File

@@ -64,6 +64,11 @@
- **分步填表上下文丢失修复**:修复了 `core/table-system/secondary-filler.js``getHistoryContext` 函数的切片索引错误Off-by-one error确保紧挨着目标楼层的那条关键历史消息能够被正确提取并发送给 AI提供完整的上下文因果关系。
以下为更新内容:
- **硅基生图模块集成**
- 在“附加功能”面板中新增“硅基生图”入口,与“前端渲染”按钮平行排列。
- 支持在聊天消息中通过 `[sfigen: 提示词]` 标签一键生成图片,并支持多张图片切换、放大预览和保存到本地。
- 修复了编辑消息后生图 UI 重复渲染或消失的问题,确保 DOM 更新的稳定性。
- 修复了图片 URL 无法正确保存到聊天记录的问题。
- **自动构建器优化**
- **多会话管理**:支持创建、切换和删除多个独立的构建会话,方便用户同时进行多个角色的构建任务。
- **状态持久化**:动态规则、聊天记录和任务状态现在会保存在本地存储中,刷新页面或关闭窗口后不会丢失。

View File

@@ -223,7 +223,10 @@
<button id="amily2_open_text_optimization" class="menu_button wide_button"><i class="fas fa-cogs"></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>
</div>
<div class="button-group" style="display: flex; justify-content: space-between; gap: 8px; margin-top: 8px;">
<button id="amily2_open_renderer" class="menu_button wide_button"><i class="fas fa-paint-brush"></i> 前端渲染</button>
<button id="amily2_open_sfigen" class="menu_button wide_button"><i class="fas fa-image"></i> 硅基生图</button>
</div>
</fieldset>

View File

@@ -74,15 +74,14 @@
</div>
</fieldset>
<!-- 新建/编辑 Profile 弹窗 -->
<div id="amily2_profile_modal" style="display:none; position:fixed; inset:0; background:rgba(0,0,0,0.6); z-index:9999; align-items:center; justify-content:center;">
<div style="background:var(--SmartThemeBlurTintColor); border:1px solid var(--SmartThemeBorderColor); border-radius:8px; padding:20px; width:min(500px,94vw); max-height:88vh; overflow-y:auto;">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:14px;">
<strong id="amily2_profile_modal_title"><i class="fas fa-key"></i> 新建连接配置</strong>
<button id="amily2_profile_modal_close" class="menu_button small_button secondary interactable"></button>
</div>
<!-- 新建/编辑 Profile 表单details 折叠) -->
<details id="amily2_profile_form_details" class="settings-group amily2-profile-form">
<summary>
<i id="amily2_profile_form_icon" class="fas fa-plus"></i>
<span id="amily2_profile_modal_title">新建连接配置</span>
</summary>
<div style="padding-top:10px;">
<!-- 类型选择 -->
<div class="amily2_settings_block">
<label for="amily2_pf_type">配置类型</label>
@@ -111,7 +110,7 @@
<label for="amily2_pf_url">API 地址</label>
<input id="amily2_pf_url" type="text" class="text_pole" placeholder="https://api.example.com/v1" />
</div>
<!-- Google 专属提示(选 Google 时显示) -->
<!-- Google 专属提示 -->
<div id="amily2_pf_google_note" style="display:none; margin-bottom:8px;">
<small class="notes" style="display:block; padding:6px 10px; background:var(--black10a); border-radius:4px; border-left:3px solid #4285f4;">
<i class="fas fa-info-circle" style="color:#4285f4;"></i>
@@ -125,15 +124,12 @@
<small class="notes">留空则不修改现有 Key。</small>
</div>
<!-- 模型选择(带获取按钮) -->
<!-- 模型选择 -->
<div class="amily2_settings_block">
<label for="amily2_pf_model">模型</label>
<div style="display:flex; gap:6px; align-items:stretch;">
<input id="amily2_pf_model" type="text" class="text_pole"
list="amily2_pf_model_list"
placeholder="手动填写或点击「获取」"
style="flex:1;" />
<datalist id="amily2_pf_model_list"></datalist>
<input id="amily2_pf_model" type="text" class="text_pole" placeholder="手动填写或点击「获取」" style="flex:1;" />
<select id="amily2_pf_model_select" class="text_pole" style="flex:1; display:none;"></select>
<button id="amily2_pf_fetch_models" class="menu_button small_button interactable" type="button" title="从 API 获取可用模型列表(需先填写地址和 Key">
<i class="fas fa-list"></i> 获取
</button>
@@ -148,7 +144,7 @@
<span id="amily2_pf_test_result" style="font-size:0.85em;"></span>
</div>
<!-- Chat 高级参数(折叠) -->
<!-- Chat 高级参数 -->
<div id="amily2_pf_chat_params">
<details class="amily2_advanced_section" style="margin-top:4px;">
<summary style="cursor:pointer; font-size:0.88em; color:var(--SmartThemeQuoteColor); user-select:none; padding:4px 0;">
@@ -163,11 +159,18 @@
<label for="amily2_pf_temperature">温度Temperature</label>
<input id="amily2_pf_temperature" type="number" class="text_pole" min="0" max="2" step="0.1" value="1.0" />
</div>
<div class="amily2_settings_block" style="flex-direction:row; align-items:center; gap:8px;">
<input id="amily2_pf_fake_stream" type="checkbox" />
<label for="amily2_pf_fake_stream">
启用假流式(防 CF 超时)
<small class="notes" style="display:block; font-weight:normal;">以 stream:true 接收 SSE 后拼接,适用于经 CloudFlare 免费代理的接口</small>
</label>
</div>
</div>
</details>
</div>
<!-- Embedding 高级参数(折叠) -->
<!-- Embedding 高级参数 -->
<div id="amily2_pf_embedding_params" style="display:none;">
<details class="amily2_advanced_section" style="margin-top:4px;">
<summary style="cursor:pointer; font-size:0.88em; color:var(--SmartThemeQuoteColor); user-select:none; padding:4px 0;">
@@ -209,11 +212,13 @@
</div>
<!-- 操作按钮 -->
<div style="display:flex; gap:8px; margin-top:16px; justify-content:flex-end;">
<button id="amily2_profile_modal_cancel" class="menu_button secondary interactable">取消</button>
<div style="display:flex; gap:8px; margin-top:16px;">
<button id="amily2_profile_modal_cancel" class="menu_button secondary interactable">
<i class="fas fa-times"></i> 取消
</button>
<button id="amily2_profile_modal_save" class="menu_button interactable">
<i class="fas fa-save"></i> 保存
</button>
</div>
</div>
</div>
</details>

View File

@@ -0,0 +1,77 @@
<div class="amily2-header">
<button id="amily2_sfigen_back_to_main" class="menu_button secondary small_button interactable">
<i class="fas fa-arrow-left"></i> 返回主殿
</button>
<div class="additional-features-title interactable" title="SiliconFlow Image Gen">
<i class="fas fa-image"></i> 硅基流动生图
</div>
</div>
<hr class="header-divider">
<fieldset class="settings-group">
<legend><i class="fas fa-cog"></i> 基础配置</legend>
<div class="flex-container">
<label for="sfigen_api_key">API Key (Bearer Token):</label>
<input id="sfigen_api_key" class="text_pole" type="password" placeholder="sk-..." />
</div>
<div class="flex-container">
<label for="sfigen_model">Model (模型):</label>
<input id="sfigen_model" class="text_pole" type="text" value="Qwen/Qwen-Image" />
</div>
<div class="flex-container">
<label for="sfigen_negative_prompt">Negative Prompt (反向提示词):</label>
<input id="sfigen_negative_prompt" class="text_pole" type="text" value="模糊, 低分辨率, 水印, 文字" />
</div>
<div class="flex-container">
<label for="sfigen_image_size">Image Size (分辨率):</label>
<select id="sfigen_image_size" class="text_pole">
<option value="1024x1024">1024x1024</option>
<option value="512x1024">512x1024</option>
<option value="768x512">768x512</option>
<option value="768x1024">768x1024</option>
<option value="1024x576">1024x576</option>
<option value="576x1024">576x1024</option>
<option value="1664x928" selected>1664x928</option>
</select>
</div>
<div class="flex-container">
<label for="sfigen_steps">Steps (步数):</label>
<input id="sfigen_steps" class="text_pole" type="number" value="50" min="1" max="100" />
</div>
<div class="flex-container">
<label for="sfigen_cfg">CFG Scale:</label>
<input id="sfigen_cfg" class="text_pole" type="number" value="4.0" step="0.1" min="1.0" max="20.0" />
</div>
<div class="flex-container">
<label for="sfigen_regex_tag">触发标签 (Tag):</label>
<input id="sfigen_regex_tag" class="text_pole" type="text" value="sfigen" title="例如填入 sfigen则会抓取 [sfigen: 提示词] 标签" />
</div>
</fieldset>
<fieldset class="settings-group">
<legend><i class="fas fa-paint-brush"></i> 风格预设</legend>
<div class="flex-container" style="flex-direction: column; align-items: flex-start;">
<label for="sfigen_prefix_prompt">固定前缀提示词 (Prefix Prompt):</label>
<div id="sfigen_style_tags" style="display: flex; flex-wrap: wrap; gap: 8px; margin: 8px 0;">
<span class="sfigen-style-tag" data-prompt="masterpiece, best quality, high detail anime art, sharp line art, 8K, ultra HD" style="background: var(--SmartThemeQuoteColor); color: var(--SmartThemeBodyColor); padding: 4px 12px; border-radius: 16px; cursor: pointer; font-size: 0.85em; border: 1px solid var(--SmartThemeBorderColor); user-select: none; transition: opacity 0.2s;">日系高清二次元</span>
<span class="sfigen-style-tag" data-prompt="doujinshi style, illustration, vibrant colors, detailed background, pixiv" style="background: var(--SmartThemeQuoteColor); color: var(--SmartThemeBodyColor); padding: 4px 12px; border-radius: 16px; cursor: pointer; font-size: 0.85em; border: 1px solid var(--SmartThemeBorderColor); user-select: none; transition: opacity 0.2s;">同人插画风</span>
<span class="sfigen-style-tag" data-prompt="ancient chinese style, hanfu, traditional clothes, ink painting style, wuxia" style="background: var(--SmartThemeQuoteColor); color: var(--SmartThemeBodyColor); padding: 4px 12px; border-radius: 16px; cursor: pointer; font-size: 0.85em; border: 1px solid var(--SmartThemeBorderColor); user-select: none; transition: opacity 0.2s;">古风</span>
<span class="sfigen-style-tag" data-prompt="photorealistic, realistic, RAW photo, 8k uhd, dslr, soft lighting, high quality" style="background: var(--SmartThemeQuoteColor); color: var(--SmartThemeBodyColor); padding: 4px 12px; border-radius: 16px; cursor: pointer; font-size: 0.85em; border: 1px solid var(--SmartThemeBorderColor); user-select: none; transition: opacity 0.2s;">写实摄影</span>
<span class="sfigen-style-tag" data-prompt="cyberpunk style, neon lights, futuristic, sci-fi, dark city" style="background: var(--SmartThemeQuoteColor); color: var(--SmartThemeBodyColor); padding: 4px 12px; border-radius: 16px; cursor: pointer; font-size: 0.85em; border: 1px solid var(--SmartThemeBorderColor); user-select: none; transition: opacity 0.2s;">赛博朋克</span>
<span class="sfigen-style-tag" data-prompt="watercolor painting, soft edges, artistic, brush strokes" style="background: var(--SmartThemeQuoteColor); color: var(--SmartThemeBodyColor); padding: 4px 12px; border-radius: 16px; cursor: pointer; font-size: 0.85em; border: 1px solid var(--SmartThemeBorderColor); user-select: none; transition: opacity 0.2s;">水彩画</span>
<span class="sfigen-style-tag" data-prompt="clear skin texture, obvious body contour, soft warm dim lamp shadow" style="background: var(--SmartThemeQuoteColor); color: var(--SmartThemeBodyColor); padding: 4px 12px; border-radius: 16px; cursor: pointer; font-size: 0.85em; border: 1px solid var(--SmartThemeBorderColor); user-select: none; transition: opacity 0.2s;">质感光影</span>
<span class="sfigen-style-tag" data-prompt="1girl, solo, beautiful face, detailed eyes" style="background: var(--SmartThemeQuoteColor); color: var(--SmartThemeBodyColor); padding: 4px 12px; border-radius: 16px; cursor: pointer; font-size: 0.85em; border: 1px solid var(--SmartThemeBorderColor); user-select: none; transition: opacity 0.2s;">单人特写</span>
</div>
<textarea id="sfigen_prefix_prompt" class="text_pole" rows="3" placeholder="点击上方标签快速插入,或在此手动输入..." style="width: 100%; box-sizing: border-box;"></textarea>
</div>
</fieldset>
<fieldset class="settings-group">
<legend><i class="fas fa-info-circle"></i> 使用说明</legend>
<small>
<b>仅需填入硅基流动密钥0.3元(赠金亦可,模型默认)一张图。</b><br><br>
<b>使用方法 1</b> 在聊天框输入 <code>/sfigen 你的提示词</code><br>
<b>使用方法 2</b> 让 AI 在回复中输出 <code>[sfigen: 生图提示词]</code>,插件会自动将其替换为生图按钮。<br>
<b>固定前缀:</b> 每次生成时,会自动将“固定前缀提示词”加在您的提示词前面,以保证画风统一。
</small>
</fieldset>

View File

@@ -751,3 +751,24 @@ hr.header-divider {
transform: scale(1);
}
}
/* === Profile 表单details 折叠) === */
.amily2-profile-form > summary {
cursor: pointer;
user-select: none;
list-style: none;
display: flex;
align-items: center;
gap: 6px;
font-weight: bold;
}
.amily2-profile-form > summary::-webkit-details-marker {
display: none;
}
.amily2-profile-form[open] > summary {
margin-bottom: 4px;
padding-bottom: 8px;
border-bottom: 1px solid var(--SmartThemeBorderColor);
}

View File

@@ -1,6 +1,7 @@
import { extension_settings, getContext } from "/scripts/extensions.js";
import { characters } from "/script.js";
import { getSlotProfile } from './api/api-resolver.js';
import { configManager } from '../utils/config/ConfigManager.js';
import { world_names } from "/scripts/world-info.js";
import { extensionName } from "../utils/settings.js";
import { extractContentByTag, replaceContentByTag, extractFullTagBlock } from '../utils/tagProcessor.js';
@@ -449,12 +450,44 @@ export async function getApiSettings(slot = 'main') {
// 温度 / MaxTokens 读面板值profile-sync 保留了这些输入框)
maxTokens: s.maxTokens ?? profile.maxTokens ?? 65500,
temperature: s.temperature ?? profile.temperature ?? 1.0,
fakeStream: profile.fakeStream ?? false,
tavernProfile: '',
};
}
// 降级:读旧 DOM 面板配置
// 降级:按槽位读取各自的独立配置
const settings = extension_settings[extensionName] || {};
// plotOpt 槽有独立 API 面板(剧情优化),优先读其专属设置
if (slot === 'plotOpt') {
const apiMode = settings.plotOpt_apiMode || 'openai_test';
if (apiMode === 'sillytavern_preset') {
const context = getContext();
const profileId = settings.plotOpt_tavernProfile || '';
const stProfile = context.extensionSettings?.connectionManager?.profiles?.find(p => p.id === profileId);
return {
apiProvider: 'sillytavern_preset',
apiUrl: '',
apiKey: '',
model: stProfile?.openai_model || 'Preset Model',
maxTokens: settings.plotOpt_max_tokens ?? 65500,
temperature: settings.plotOpt_temperature ?? 1.0,
tavernProfile: profileId,
};
}
return {
apiProvider: apiMode,
apiUrl: settings.plotOpt_apiUrl?.trim() || '',
apiKey: configManager.get('plotOpt_apiKey') || '',
model: document.getElementById('amily2_opt_model')?.value?.trim()
|| settings.plotOpt_model || '',
maxTokens: settings.plotOpt_max_tokens ?? 65500,
temperature: settings.plotOpt_temperature ?? 1.0,
tavernProfile: '',
};
}
// main 槽(及其余未明确处理的槽):读主面板 DOM 配置
const apiProvider = document.getElementById('amily2_api_provider')?.value || 'openai';
let model;

View File

@@ -2,6 +2,8 @@ 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';
import { getSlotProfile, providerToApiMode } from './api-resolver.js';
import { configManager } from '../../utils/config/ConfigManager.js';
let ChatCompletionService = undefined;
try {
@@ -42,15 +44,32 @@ function normalizeApiResponse(responseData) {
return data;
}
export function getSybdApiSettings() {
export async function getSybdApiSettings() {
const s = extension_settings[extensionName] || {};
// 优先读取 'sybd' 槽位分配的 Profile
const profile = await getSlotProfile('sybd');
if (profile) {
return {
apiMode: providerToApiMode(profile.provider),
apiUrl: profile.apiUrl,
apiKey: profile.apiKey ?? '',
model: profile.model,
maxTokens: s.sybdMaxTokens ?? profile.maxTokens ?? 4000,
temperature: s.sybdTemperature ?? profile.temperature ?? 0.7,
tavernProfile: '',
};
}
// 降级:读旧 extension_settings 字段
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 || ''
apiMode: s.sybdApiMode || 'openai_test',
apiUrl: s.sybdApiUrl?.trim() || '',
apiKey: configManager.get('sybdApiKey') || '',
model: s.sybdModel || '',
maxTokens: s.sybdMaxTokens || 4000,
temperature: s.sybdTemperature || 0.7,
tavernProfile: s.sybdTavernProfile || '',
};
}
@@ -60,7 +79,7 @@ export async function callSybdAI(messages, options = {}) {
return null;
}
const apiSettings = getSybdApiSettings();
const apiSettings = await getSybdApiSettings();
const finalOptions = {
maxTokens: apiSettings.maxTokens,
@@ -257,8 +276,8 @@ async function callSybdSillyTavernPreset(messages, options) {
export async function fetchSybdModels() {
console.log('[Amily2号-Sybd外交部] 开始获取模型列表');
const apiSettings = getSybdApiSettings();
const apiSettings = await getSybdApiSettings();
try {
if (apiSettings.apiMode === 'sillytavern_preset') {
@@ -340,8 +359,8 @@ export async function fetchSybdModels() {
export async function testSybdApiConnection() {
console.log('[Amily2号-Sybd外交部] 开始API连接测试');
const apiSettings = getSybdApiSettings();
const apiSettings = await getSybdApiSettings();
if (apiSettings.apiMode === 'sillytavern_preset') {
if (!apiSettings.tavernProfile) {

View File

@@ -46,7 +46,7 @@ import { renderTables } from '../../ui/table-bindings.js';
async function processMessageUpdate(messageId) {
TableManager.clearHighlights();
const settings = extension_settings[extensionName];
const settings = extension_settings[extensionName] || {};
const tableSystemEnabled = settings.table_system_enabled !== false;
if (!tableSystemEnabled) {
log('【表格服务】表格系统总开关已关闭,跳过所有表格处理。', 'info');

View File

@@ -22,7 +22,7 @@ const MAX_RETRIES = 2;
async function getWorldBookContext() {
const settings = extension_settings[extensionName];
const settings = extension_settings[extensionName] || {};
if (!settings.table_worldbook_enabled) {
return '';
}
@@ -114,7 +114,7 @@ function updateButtonState(state, batchNum = 0, attemptNum = 0) {
async function callTableModel(messages) {
try {
const settings = extension_settings[extensionName];
const settings = extension_settings[extensionName] || {};
if (settings.nccsEnabled) {
log('使用 Nccs API 进行表格填充...', 'info');
@@ -141,7 +141,7 @@ async function callTableModel(messages) {
function getRawMessagesForSummary(startFloor, endFloor) {
const context = getContext();
const chat = context.chat;
const settings = extension_settings[extensionName];
const settings = extension_settings[extensionName] || {};
const historySlice = chat.slice(startFloor - 1, endFloor);
if (historySlice.length === 0) return null;
@@ -319,7 +319,7 @@ export function startBatchFilling() {
const button = fillButton();
if (!button) return;
const settings = extension_settings[extensionName];
const settings = extension_settings[extensionName] || {};
const tableSystemEnabled = settings.table_system_enabled !== false;
if (!tableSystemEnabled) {
log('表格系统总开关已关闭,跳过批量填表。', 'info');
@@ -387,7 +387,7 @@ export function startBatchFilling() {
export async function startFloorRangeFilling(startFloor, endFloor) {
const settings = extension_settings[extensionName];
const settings = extension_settings[extensionName] || {};
const tableSystemEnabled = settings.table_system_enabled !== false;
if (!tableSystemEnabled) {
log('表格系统总开关已关闭,跳过楼层填表。', 'info');

View File

@@ -1264,7 +1264,7 @@ export function getAiFlowTemplateForInjection() {
}
export async function updateTableFromText(textContent, options = {}) {
const settings = extension_settings[extensionName];
const settings = extension_settings[extensionName] || {};
if (settings.table_system_enabled === false) {
log('表格系统总开关已关闭,跳过 <Amily2Edit> 标签处理。', 'info');
return;
@@ -1575,7 +1575,7 @@ export async function rollbackState() {
export async function rollbackAndRefill() {
// 检查表格系统总开关
const settings = extension_settings[extensionName];
const settings = extension_settings[extensionName] || {};
if (settings.table_system_enabled === false) {
log('表格系统总开关已关闭,跳过回退填表。', 'info');
toastr.info('表格系统总开关已关闭,无法执行回退填表。');

View File

@@ -8,7 +8,7 @@ import { callAI, generateRandomSeed } from '../api.js';
import { callNccsAI } from '../api/NccsApi.js';
export async function reorganizeTableContent(selectedTableIndices) {
const settings = extension_settings[extensionName];
const settings = extension_settings[extensionName] || {};
if (settings.table_system_enabled === false) {
toastr.warning('表格系统总开关已关闭。');
@@ -20,12 +20,6 @@ export async function reorganizeTableContent(selectedTableIndices) {
return;
}
const { apiUrl, apiKey, model, temperature, maxTokens, forceProxyForCustomApi } = settings;
if (!apiUrl || !model) {
toastr.error("主API的URL或模型未配置重新整理功能无法启动。", "Amily2-重新整理");
return;
}
try {
toastr.info('正在重新整理表格内容...', 'Amily2-重新整理');

View File

@@ -13,7 +13,7 @@ import { safeLorebookEntries } from '../tavernhelper-compatibility.js';
async function getWorldBookContext() {
const settings = extension_settings[extensionName];
const settings = extension_settings[extensionName] || {};
if (!settings.table_worldbook_enabled) {
return '';
@@ -67,7 +67,7 @@ async function getWorldBookContext() {
export async function fillWithSecondaryApi(latestMessage, forceRun = false) {
clearHighlights();
const settings = extension_settings[extensionName];
const settings = extension_settings[extensionName] || {};
// 总开关关闭时,分步填表同样禁用
if (settings.table_system_enabled === false) {
@@ -92,14 +92,6 @@ export async function fillWithSecondaryApi(latestMessage, forceRun = false) {
return;
}
const { apiUrl, apiKey, model, temperature, maxTokens, forceProxyForCustomApi } = settings;
if (!apiUrl || !model) {
if (!window.secondaryApiUrlWarned) {
toastr.error("主API的URL或模型未配置分步填表功能无法启动。", "Amily2-分步填表");
window.secondaryApiUrlWarned = true;
}
return;
}
try {
const bufferSize = parseInt(settings.secondary_filler_buffer || 0, 10);

View File

@@ -131,6 +131,7 @@ export {
};
export const tableSystemDefaultSettings = {
table_system_enabled: true,
table_injection_enabled: false,
injection: {

View File

@@ -1,6 +1,8 @@
import { extension_settings, getContext } from "/scripts/extensions.js";
import { saveSettingsDebounced, eventSource, event_types } from "/script.js";
import { extensionName } from "../utils/settings.js";
import { configManager } from '../utils/config/ConfigManager.js';
import { SENSITIVE_KEYS } from '../utils/config/sensitive-keys.js';
import { safeLorebooks, safeLorebookEntries, safeUpdateLorebookEntries } from '../core/tavernhelper-compatibility.js';
import { testSybdApiConnection, fetchSybdModels } from '../core/api/SybdApi.js';
import { handleFileUpload, processNovel } from './index.js';
@@ -29,18 +31,21 @@ function loadSettingsToUI() {
const inputs = container.querySelectorAll('[data-setting-key]');
inputs.forEach(target => {
const key = target.dataset.settingKey;
const value = settings[key];
// 敏感字段从 configManagerlocalStorage读取其余从 extension_settings 读取
const value = SENSITIVE_KEYS.has(key) ? configManager.get(key) : settings[key];
if (value === undefined) {
let defaultValue;
if (target.type === 'checkbox') {
defaultValue = target.checked;
} else if (target.type === 'range') {
defaultValue = target.dataset.type === 'float' ? parseFloat(target.value) : parseInt(target.value, 10);
} else {
defaultValue = target.value;
if (value === undefined || value === null || value === '') {
if (!SENSITIVE_KEYS.has(key)) {
let defaultValue;
if (target.type === 'checkbox') {
defaultValue = target.checked;
} else if (target.type === 'range') {
defaultValue = target.dataset.type === 'float' ? parseFloat(target.value) : parseInt(target.value, 10);
} else {
defaultValue = target.value;
}
updateAndSaveSetting(key, defaultValue);
}
updateAndSaveSetting(key, defaultValue);
return;
};
@@ -90,8 +95,13 @@ function bindAutoSaveEvents() {
case 'float': value = parseFloat(value); break;
case 'boolean': value = (typeof value === 'boolean') ? value : (value === 'true'); break;
}
updateAndSaveSetting(key, value);
// 敏感字段API Key经 configManager 写入 localStorage
if (SENSITIVE_KEYS.has(key)) {
configManager.set(key, value);
} else {
updateAndSaveSetting(key, value);
}
if (key === 'sybdApiMode') {
updateConfigVisibility(value);

View File

@@ -568,11 +568,12 @@ async function onPlotGenerationAfterCommands(type, params, dryRun) {
if (globalSettings?.plotOpt_enabled === false) return false;
const isJqyhEnabled = globalSettings?.jqyhEnabled === true;
const hasMainProfile = !!apiProfileManager.getAssignment('main') || !!apiProfileManager.getAssignment('plotOpt');
const isMainApiConfigured = hasMainProfile || !!globalSettings?.apiUrl || !!globalSettings?.tavernProfile;
const hasProfile = !!apiProfileManager.getAssignment('main') || !!apiProfileManager.getAssignment('plotOpt');
const hasLegacyConfig = !!globalSettings?.apiUrl || !!globalSettings?.tavernProfile
|| !!globalSettings?.plotOpt_apiUrl || !!globalSettings?.plotOpt_tavernProfile;
if (!isJqyhEnabled && !isMainApiConfigured) {
console.log("[Amily2-剧情优化] 优化已启用,但Jqyh API已禁用且主API未配置(无 Profile 分配亦无旧设置)。");
if (!isJqyhEnabled && !hasProfile && !hasLegacyConfig) {
console.log("[Amily2-剧情优化] 优化已启用,但未配置任何可用的 API(无 Profile 分配亦无独立配置)。");
return false;
}

View File

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

View File

@@ -80,11 +80,8 @@ export function bindApiConfigPanel(container) {
// 弹窗:测试连接
$c.find('#amily2_pf_test_conn').on('click', () => _testConnection($c));
// 弹窗:关闭
$c.find('#amily2_profile_modal_close, #amily2_profile_modal_cancel').on('click', () => closeModal($c));
$c.find('#amily2_profile_modal').on('click', function (e) {
if (e.target === this) closeModal($c);
});
// 表单:取消
$c.find('#amily2_profile_modal_cancel').on('click', () => closeModal($c));
// 弹窗:保存
$c.find('#amily2_profile_modal_save').on('click', () => saveProfile($c));
@@ -329,13 +326,13 @@ export function renderSlotAssignments($c) {
async function openModal($c, id) {
_editingId = id;
const $modal = $c.find('#amily2_profile_modal');
if (id) {
// 编辑模式
const p = apiProfileManager.getProfile(id);
if (!p) return;
$c.find('#amily2_profile_modal_title').html('<i class="fas fa-edit"></i> 编辑连接配置');
$c.find('#amily2_profile_modal_title').text('编辑连接配置');
$c.find('#amily2_profile_form_icon').attr('class', 'fas fa-edit');
$c.find('#amily2_pf_type').val(p.type).prop('disabled', true); // 不允许修改类型
$c.find('#amily2_pf_name').val(p.name);
$c.find('#amily2_pf_provider').val(p.provider);
@@ -346,6 +343,7 @@ async function openModal($c, id) {
if (p.type === 'chat') {
$c.find('#amily2_pf_max_tokens').val(p.maxTokens);
$c.find('#amily2_pf_temperature').val(p.temperature);
$c.find('#amily2_pf_fake_stream').prop('checked', p.fakeStream ?? false);
} else if (p.type === 'embedding') {
$c.find('#amily2_pf_dimensions').val(p.dimensions ?? '');
$c.find('#amily2_pf_encoding_format').val(p.encodingFormat);
@@ -357,13 +355,15 @@ async function openModal($c, id) {
_handleProviderChange($c, p.provider);
} else {
// 新建模式
$c.find('#amily2_profile_modal_title').html('<i class="fas fa-plus"></i> 新建连接配置');
$c.find('#amily2_profile_modal_title').text('新建连接配置');
$c.find('#amily2_profile_form_icon').attr('class', 'fas fa-plus');
$c.find('#amily2_pf_type').val('chat').prop('disabled', false);
$c.find('#amily2_pf_name, #amily2_pf_url, #amily2_pf_key, #amily2_pf_model').val('');
$c.find('#amily2_pf_provider').val('openai');
_handleProviderChange($c, 'openai');
$c.find('#amily2_pf_max_tokens').val(65500);
$c.find('#amily2_pf_temperature').val(1.0);
$c.find('#amily2_pf_fake_stream').prop('checked', false);
$c.find('#amily2_pf_dimensions').val('');
$c.find('#amily2_pf_encoding_format').val('float');
$c.find('#amily2_pf_top_n').val(5);
@@ -371,15 +371,18 @@ async function openModal($c, id) {
_switchParamSections($c, 'chat');
}
// 清空上次测试结果和模型列表缓存
// 清空上次测试结果,重置模型选择器为手动输入状态
$c.find('#amily2_pf_test_result').text('');
$c.find('#amily2_pf_model_list').empty();
$c.find('#amily2_pf_model_select').hide().empty();
$c.find('#amily2_pf_model').show();
$modal.css('display', 'flex');
const $details = $c.find('#amily2_profile_form_details');
$details.prop('open', true);
$details[0].scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
function closeModal($c) {
$c.find('#amily2_profile_modal').hide();
$c.find('#amily2_profile_form_details').prop('open', false);
$c.find('#amily2_pf_type').prop('disabled', false);
_editingId = null;
}
@@ -390,7 +393,8 @@ async function saveProfile($c) {
const provider = $c.find('#amily2_pf_provider').val();
const apiUrl = $c.find('#amily2_pf_url').val().trim();
const apiKey = $c.find('#amily2_pf_key').val();
const model = $c.find('#amily2_pf_model').val().trim();
const $sel = $c.find('#amily2_pf_model_select');
const model = ($sel.is(':visible') ? $sel.val() : $c.find('#amily2_pf_model').val()).trim();
if (!name) { toastr.warning('请填写配置名称。'); return; }
@@ -399,6 +403,7 @@ async function saveProfile($c) {
if (type === 'chat') {
data.maxTokens = parseInt($c.find('#amily2_pf_max_tokens').val(), 10) || 65500;
data.temperature = parseFloat($c.find('#amily2_pf_temperature').val()) || 1.0;
data.fakeStream = $c.find('#amily2_pf_fake_stream').prop('checked');
} else if (type === 'embedding') {
const dim = $c.find('#amily2_pf_dimensions').val();
data.dimensions = dim ? parseInt(dim, 10) : null;
@@ -440,9 +445,14 @@ async function saveProfile($c) {
async function _fetchModels($c) {
const apiUrl = $c.find('#amily2_pf_url').val().trim();
const apiKey = $c.find('#amily2_pf_key').val().trim();
const provider = $c.find('#amily2_pf_provider').val();
// 编辑模式下 Key 不回显,字段为空时从 ApiKeyStore 读取已存储的 Key
let apiKey = $c.find('#amily2_pf_key').val().trim();
if (!apiKey && _editingId) {
apiKey = await apiProfileManager.getKey(_editingId) ?? '';
}
if (!apiUrl) { toastr.warning('请先填写 API 地址。'); return; }
const $btn = $c.find('#amily2_pf_fetch_models').prop('disabled', true);
@@ -495,7 +505,8 @@ async function _fetchModels($c) {
}
const rawData = await resp.json();
// ST 返回原始数组或包含 data/models 字段的对象
const list = Array.isArray(rawData) ? rawData : (rawData.data ?? rawData.models ?? []);
const rawList = Array.isArray(rawData) ? rawData : (rawData.data ?? rawData.models ?? []);
const list = Array.isArray(rawList) ? rawList : [];
models = list.map(m => m.id ?? m.name ?? m).filter(m => typeof m === 'string' && m);
}
@@ -504,11 +515,12 @@ async function _fetchModels($c) {
return;
}
const $dl = $c.find('#amily2_pf_model_list');
$dl.html(models.map(m => `<option value="${_escapeHtml(m)}">`).join(''));
const $modelInput = $c.find('#amily2_pf_model');
if (!$modelInput.val()) $modelInput.val(models[0]);
const currentVal = $c.find('#amily2_pf_model').val().trim();
const $sel = $c.find('#amily2_pf_model_select');
$sel.html(models.map(m => `<option value="${_escapeHtml(m)}">${_escapeHtml(m)}</option>`).join(''));
if (currentVal && models.includes(currentVal)) $sel.val(currentVal);
$c.find('#amily2_pf_model').hide();
$sel.show();
toastr.success(`已获取 ${models.length} 个可用模型。`);
} catch (e) {
@@ -520,9 +532,14 @@ async function _fetchModels($c) {
async function _testConnection($c) {
const apiUrl = $c.find('#amily2_pf_url').val().trim();
const apiKey = $c.find('#amily2_pf_key').val().trim();
const provider = $c.find('#amily2_pf_provider').val();
// 编辑模式下 Key 不回显,字段为空时从 ApiKeyStore 读取已存储的 Key
let apiKey = $c.find('#amily2_pf_key').val().trim();
if (!apiKey && _editingId) {
apiKey = await apiProfileManager.getKey(_editingId) ?? '';
}
if (!apiUrl) { toastr.warning('请先填写 API 地址。'); return; }
const $btn = $c.find('#amily2_pf_test_conn').prop('disabled', true);
@@ -568,8 +585,39 @@ async function _testConnection($c) {
if (modelsResp.ok) {
const rawData = await modelsResp.json();
const list = Array.isArray(rawData) ? rawData : (rawData.data ?? rawData.models ?? []);
const rawList = Array.isArray(rawData) ? rawData : (rawData.data ?? rawData.models ?? []);
const list = Array.isArray(rawList) ? rawList : [];
const count = list.length;
// chat 类型额外发一次假补全,验证 completion 端点也能正常鉴权
const type = $c.find('#amily2_pf_type').val();
const $sel = $c.find('#amily2_pf_model_select');
const model = ($sel.is(':visible') ? $sel.val() : $c.find('#amily2_pf_model').val()).trim();
if (type === 'chat' && model) {
$result.text('模型列表 ✓,正在验证补全端点…').css('color', 'var(--SmartThemeQuoteColor)');
const genResp = await fetch('/api/backends/chat-completions/generate', {
method: 'POST',
headers: { ...getRequestHeaders(), 'Content-Type': 'application/json' },
body: JSON.stringify({
reverse_proxy: apiUrl,
proxy_password: apiKey,
chat_completion_source: 'openai',
model,
messages: [{ role: 'user', content: 'Hi' }],
max_tokens: 1,
stream: false,
}),
});
if (!genResp.ok) {
const genErr = await genResp.json().catch(() => ({}));
const genMsg = genErr?.error?.message || `补全端点返回 HTTP ${genResp.status}`;
$result.text(`模型列表 ✓,补全失败:${genMsg}`).css('color', 'var(--warning-color)');
toastr.warning(`补全端点测试失败:${genMsg}`);
return;
}
}
$result.text(`连接成功${count ? `${count} 个可用模型` : ''}`).css('color', 'var(--green)');
toastr.success('连接测试通过!');
return;

View File

@@ -4,6 +4,7 @@ import { defaultSettings, extensionName, saveSettings, extensionBasePath } from
import { pluginAuthStatus, activatePluginAuthorization, getPasswordForDate } from "../utils/auth.js";
import { fetchModels, testApiConnection } from "../core/api.js";
import { safeLorebooks, safeCharLorebooks, safeLorebookEntries } from "../core/tavernhelper-compatibility.js";
import { configManager } from '../utils/config/ConfigManager.js';
import { setAvailableModels, populateModelDropdown, getLatestUpdateInfo } from "./state.js";
import { fixCommand, testReplyChecker } from "../core/commands.js";
@@ -801,7 +802,7 @@ export function bindModalEvents() {
container
.off("click.amily2.chamber_nav")
.on("click.amily2.chamber_nav",
"#amily2_open_text_optimization, #amily2_open_plot_optimization, #amily2_open_additional_features, #amily2_open_rag_palace, #amily2_open_memorisation_forms, #amily2_open_character_world_book, #amily2_open_world_editor, #amily2_open_glossary, #amily2_open_renderer, #amily2_open_super_memory, #amily2_open_auto_char_card, #amily2_open_api_config, #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_text_optimization, #amily2_back_to_main_from_cwb, #amily2_back_to_main_from_world_editor, #amily2_back_to_main_from_glossary, #amily2_renderer_back_button, #amily2_back_to_main_from_super_memory, #amily2_back_to_main_from_api_config", function () {
"#amily2_open_text_optimization, #amily2_open_plot_optimization, #amily2_open_additional_features, #amily2_open_rag_palace, #amily2_open_memorisation_forms, #amily2_open_character_world_book, #amily2_open_world_editor, #amily2_open_glossary, #amily2_open_renderer, #amily2_open_super_memory, #amily2_open_auto_char_card, #amily2_open_api_config, #amily2_open_sfigen, #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_text_optimization, #amily2_back_to_main_from_cwb, #amily2_back_to_main_from_world_editor, #amily2_back_to_main_from_glossary, #amily2_renderer_back_button, #amily2_back_to_main_from_super_memory, #amily2_back_to_main_from_api_config, #amily2_sfigen_back_to_main", function () {
if (!pluginAuthStatus.authorized) return;
const mainPanel = container.find('.plugin-features');
@@ -816,6 +817,7 @@ export function bindModalEvents() {
const rendererPanel = container.find('#amily2_renderer_panel');
const superMemoryPanel = container.find('#amily2_super_memory_panel');
const apiConfigPanel = container.find('#amily2_api_config_panel');
const sfigenPanel = container.find('#amily2_sfigen_panel');
mainPanel.hide();
additionalPanel.hide();
@@ -829,6 +831,7 @@ export function bindModalEvents() {
rendererPanel.hide();
superMemoryPanel.hide();
apiConfigPanel.hide();
sfigenPanel.hide();
switch (this.id) {
case 'amily2_open_text_optimization':
@@ -876,6 +879,9 @@ export function bindModalEvents() {
case 'amily2_open_api_config':
apiConfigPanel.show();
break;
case 'amily2_open_sfigen':
sfigenPanel.show();
break;
case 'amily2_back_to_main_settings':
case 'amily2_back_to_main_from_hanlinyuan':
case 'amily2_back_to_main_from_forms':
@@ -887,6 +893,7 @@ export function bindModalEvents() {
case 'amily2_renderer_back_button':
case 'amily2_back_to_main_from_super_memory':
case 'amily2_back_to_main_from_api_config':
case 'amily2_sfigen_back_to_main':
mainPanel.show();
break;
}
@@ -1032,7 +1039,12 @@ export function bindModalEvents() {
.on("change.amily2.text", "#amily2_api_url, #amily2_api_key, #amily2_optimization_target_tag", function () {
if (!pluginAuthStatus.authorized) return;
const key = snakeToCamel(this.id.replace("amily2_", ""));
updateAndSaveSetting(key, this.value);
// apiKey 是敏感字段,必须经 configManager 写入 localStorage
if (key === 'apiKey') {
configManager.set(key, this.value);
} else {
updateAndSaveSetting(key, this.value);
}
toastr.success(`配置 [${key}] 已自动保存!`, "Amily2号");
});

View File

@@ -6,6 +6,7 @@ import {
} from "../utils/settings.js";
import { showHtmlModal } from './page-window.js';
import { applyExclusionRules, extractBlocksByTags } from '../core/utils/rag-tag-extractor.js';
import { configManager } from '../utils/config/ConfigManager.js';
import {
getAvailableWorldbooks, getLoresForWorldbook,
@@ -459,16 +460,23 @@ function bindNgmsApiEvents() {
// API配置字段绑定
const apiFields = [
{ id: 'amily2_ngms_api_url', key: 'ngmsApiUrl' },
{ id: 'amily2_ngms_api_key', key: 'ngmsApiKey' },
{ id: 'amily2_ngms_api_key', key: 'ngmsApiKey', sensitive: true },
{ id: 'amily2_ngms_model', key: 'ngmsModel' }
];
apiFields.forEach(field => {
const element = document.getElementById(field.id);
if (element) {
element.value = extension_settings[extensionName][field.key] || '';
// 敏感字段API Key从 configManagerlocalStorage读取
element.value = field.sensitive
? (configManager.get(field.key) || '')
: (extension_settings[extensionName][field.key] || '');
element.addEventListener('change', function() {
updateAndSaveSetting(field.key, this.value);
if (field.sensitive) {
configManager.set(field.key, this.value);
} else {
updateAndSaveSetting(field.key, this.value);
}
});
}
});

View File

@@ -13,6 +13,8 @@ import { testConcurrentApiConnection, fetchConcurrentModels } from '../core/api/
import { safeLorebooks, safeCharLorebooks, safeLorebookEntries } from "../core/tavernhelper-compatibility.js";
import { createDrawer } from '../ui/drawer.js';
import { pluginAuthStatus } from "../utils/auth.js";
import { configManager } from '../utils/config/ConfigManager.js';
import { SENSITIVE_KEYS } from '../utils/config/sensitive-keys.js';
// ========== Prompt Cache (module-level state) ==========
@@ -161,6 +163,9 @@ async function opt_saveSetting(key, value) {
console.error(`[${extensionName}] 保存角色数据失败:`, error);
toastr.error('无法保存角色卡设置,请检查控制台。');
}
} else if (SENSITIVE_KEYS.has(key)) {
// 敏感字段API Key经 configManager 写入 localStorage
configManager.set(key, value);
} else {
if (!extension_settings[extensionName]) {
extension_settings[extensionName] = {};
@@ -622,7 +627,8 @@ function opt_loadSettings(panel) {
panel.find('#amily2_opt_worldbook_enabled').prop('checked', settings.plotOpt_worldbookEnabled);
panel.find('#amily2_opt_new_memory_logic_enabled').prop('checked', settings.plotOpt_newMemoryLogicEnabled);
panel.find('#amily2_opt_api_url').val(settings.plotOpt_apiUrl);
panel.find('#amily2_opt_api_key').val(settings.plotOpt_apiKey);
// plotOpt_apiKey 是敏感字段,从 configManagerlocalStorage读取
panel.find('#amily2_opt_api_key').val(configManager.get('plotOpt_apiKey') || '');
const modelInput = panel.find('#amily2_opt_model');
const modelSelect = panel.find('#amily2_opt_model_select');
@@ -701,14 +707,17 @@ function bindConcurrentApiEvents() {
const fields = [
{ id: 'amily2_plotOpt_concurrentApiProvider', key: 'plotOpt_concurrentApiProvider' },
{ id: 'amily2_plotOpt_concurrentApiUrl', key: 'plotOpt_concurrentApiUrl' },
{ id: 'amily2_plotOpt_concurrentApiKey', key: 'plotOpt_concurrentApiKey' },
{ id: 'amily2_plotOpt_concurrentApiKey', key: 'plotOpt_concurrentApiKey', sensitive: true },
{ id: 'amily2_plotOpt_concurrentModel', key: 'plotOpt_concurrentModel' }
];
fields.forEach(field => {
const element = document.getElementById(field.id);
if (element) {
element.value = settings[field.key] || '';
// 敏感字段API Key从 configManagerlocalStorage读取
element.value = field.sensitive
? (configManager.get(field.key) || '')
: (settings[field.key] || '');
}
});
@@ -787,9 +796,13 @@ function bindConcurrentApiEvents() {
const element = document.getElementById(field.id);
if (element) {
element.addEventListener('change', function() {
if (!extension_settings[extensionName]) extension_settings[extensionName] = {};
extension_settings[extensionName][field.key] = this.value;
saveSettingsDebounced();
if (field.sensitive) {
configManager.set(field.key, this.value);
} else {
if (!extension_settings[extensionName]) extension_settings[extensionName] = {};
extension_settings[extensionName][field.key] = this.value;
saveSettingsDebounced();
}
});
}
});
@@ -1384,16 +1397,23 @@ function bindJqyhApiEvents() {
// API配置字段绑定
const apiFields = [
{ id: 'amily2_jqyh_api_url', key: 'jqyhApiUrl' },
{ id: 'amily2_jqyh_api_key', key: 'jqyhApiKey' },
{ id: 'amily2_jqyh_api_key', key: 'jqyhApiKey', sensitive: true },
{ id: 'amily2_jqyh_model', key: 'jqyhModel' }
];
apiFields.forEach(field => {
const element = document.getElementById(field.id);
if (element) {
element.value = extension_settings[extensionName][field.key] || '';
// 敏感字段API Key从 configManagerlocalStorage读取
element.value = field.sensitive
? (configManager.get(field.key) || '')
: (extension_settings[extensionName][field.key] || '');
element.addEventListener('change', function() {
updateAndSaveSetting(field.key, this.value);
if (field.sensitive) {
configManager.set(field.key, this.value);
} else {
updateAndSaveSetting(field.key, this.value);
}
});
}
});

View File

@@ -29,6 +29,10 @@ import { testNccsApiConnection } from '../core/api/NccsApi.js';
// 用于通过子元素定位父 block 的选择器
const BLOCK_SEL = '.amily2_settings_block, .control-group, .amily2_opt_settings_block';
// 每个槽位在回填 Profile 值前的 DOM 字段快照(用于取消分配时还原)
// 结构:{ [slot]: { [selector]: value } }
const _fieldSnapshots = {};
const CARD_CLASS = 'amily2_profile_status_card';
const CARD_SLOT_ATTR = 'data-card-slot';
const HIDDEN_ATTR = 'data-profile-hidden';
@@ -115,11 +119,37 @@ export async function syncSlot(slot) {
_removeCard(slot);
_restoreHidden(slot);
if (!profile) return;
if (!profile) {
// 取消分配:将 DOM 字段值还原为分配 Profile 前的快照,
// 防止残留的 Profile 回填值(尤其是 '••••••••' 的 Key 占位符)
// 因 blur 事件被误存入 extension_settings / localStorage。
const snap = _fieldSnapshots[slot];
if (snap) {
for (const [sel, val] of Object.entries(snap)) {
const el = document.querySelector(sel);
if (el) el.value = val;
}
delete _fieldSnapshots[slot];
}
return;
}
const container = _resolveContainer(config.container);
if (!container) return;
// 回填前先快照各字段当前值(即 extension_settings / configManager 中的真实值),
// 以便取消分配时能还原,避免 Profile 值污染旧配置。
const snap = {};
for (const sel of Object.values(config.fields || {})) {
const el = document.querySelector(sel);
if (el) snap[sel] = el.value;
}
if (config.keyField) {
const keyEl = document.querySelector(config.keyField);
if (keyEl) snap[config.keyField] = keyEl.value;
}
_fieldSnapshots[slot] = snap;
// 回填值(向下兼容:部分代码仍从 DOM 读取 fallback
for (const [key, sel] of Object.entries(config.fields || {})) {
const el = document.querySelector(sel);

View File

@@ -2,6 +2,7 @@ import { extension_settings } from "/scripts/extensions.js";
import { characters, this_chid } from '/script.js';
import { extensionName, defaultSettings } from "../utils/settings.js";
import { pluginAuthStatus } from "../utils/auth.js";
import { configManager } from '../utils/config/ConfigManager.js';
@@ -82,7 +83,7 @@ export function updateUI() {
$("#amily2_api_provider").val(settings.apiProvider || 'openai');
$("#amily2_api_url").val(settings.apiUrl);
$("#amily2_api_url").attr('type', 'text');
$("#amily2_api_key").val(settings.apiKey);
$("#amily2_api_key").val(configManager.get('apiKey') || '');
$("#amily2_model").val(settings.model);
$("#amily2_preset_selector").val(settings.tavernProfile);

View File

@@ -13,10 +13,23 @@ import { characters, this_chid, eventSource, event_types } from "/script.js";
import { fetchNccsModels, testNccsApiConnection } from '../core/api/NccsApi.js';
import { showGraphVisualization } from '../core/relationship-graph/visualizer.js';
import { escapeHTML } from '../utils/utils.js';
import { configManager } from '../utils/config/ConfigManager.js';
const isTouchDevice = () => window.matchMedia('(pointer: coarse)').matches;
const getAllTablesContainer = () => document.getElementById('all-tables-container');
function getLiveExtensionSettings() {
if (!extension_settings[extensionName]) {
extension_settings[extensionName] = {};
}
return extension_settings[extensionName];
}
function isTableSystemEnabled() {
return getLiveExtensionSettings().table_system_enabled !== false;
}
let isResizing = false;
let activeTableIndex = 0; // 【V155.0】当前激活的表格索引
@@ -767,7 +780,7 @@ export function renderTables() {
function openTableRuleEditor() {
const settings = extension_settings[extensionName];
const settings = getLiveExtensionSettings();
const tags = settings.table_tags_to_extract || '';
const exclusionRules = settings.table_exclusion_rules || [];
@@ -1010,8 +1023,6 @@ function openRuleEditor(tableIndex) {
function bindInjectionSettings() {
const settings = extension_settings[extensionName];
const masterSwitchCheckbox = document.getElementById('table-system-master-switch');
const enabledCheckbox = document.getElementById('table-injection-enabled');
const optimizationCheckbox = document.getElementById('context-optimization-enabled'); // 【V144.0】
@@ -1023,6 +1034,15 @@ function bindInjectionSettings() {
return;
}
const getLiveSettings = () => {
const liveSettings = getLiveExtensionSettings();
if (!liveSettings.injection) {
liveSettings.injection = { position: 1, depth: 0, role: 0 };
}
return liveSettings;
};
const updateInjectionUI = () => {
const position = positionSelect.value;
const masterEnabled = masterSwitchCheckbox.checked;
@@ -1076,6 +1096,7 @@ function bindInjectionSettings() {
}
};
const settings = getLiveSettings();
masterSwitchCheckbox.checked = settings.table_system_enabled !== false;
enabledCheckbox.checked = settings.table_injection_enabled;
if (optimizationCheckbox) { // 【V144.0】
@@ -1094,7 +1115,8 @@ function bindInjectionSettings() {
if (masterSwitchCheckbox.dataset.eventsBound) return;
masterSwitchCheckbox.addEventListener('change', () => {
settings.table_system_enabled = masterSwitchCheckbox.checked;
const currentSettings = getLiveSettings();
currentSettings.table_system_enabled = masterSwitchCheckbox.checked;
saveSettingsDebounced();
updateInjectionUI();
@@ -1104,35 +1126,40 @@ function bindInjectionSettings() {
});
enabledCheckbox.addEventListener('change', () => {
settings.table_injection_enabled = enabledCheckbox.checked;
const currentSettings = getLiveSettings();
currentSettings.table_injection_enabled = enabledCheckbox.checked;
saveSettingsDebounced();
});
// 【V144.0】
if (optimizationCheckbox) {
optimizationCheckbox.addEventListener('change', () => {
settings.context_optimization_enabled = optimizationCheckbox.checked;
const currentSettings = getLiveSettings();
currentSettings.context_optimization_enabled = optimizationCheckbox.checked;
saveSettingsDebounced();
toastr.info(`上下文优化(世界书合并)已${optimizationCheckbox.checked ? '启用' : '禁用'}`);
});
}
positionSelect.addEventListener('change', () => {
settings.injection.position = parseInt(positionSelect.value, 10);
const currentSettings = getLiveSettings();
currentSettings.injection.position = parseInt(positionSelect.value, 10);
saveSettingsDebounced();
updateInjectionUI();
});
depthInput.addEventListener('input', () => {
settings.injection.depth = parseInt(depthInput.value, 10);
const currentSettings = getLiveSettings();
currentSettings.injection.depth = parseInt(depthInput.value, 10);
saveSettingsDebounced();
});
roleRadioGroup.forEach(radio => {
radio.addEventListener('change', () => {
if (radio.checked) {
settings.injection.role = parseInt(radio.value, 10);
const currentSettings = getLiveSettings();
currentSettings.injection.role = parseInt(radio.value, 10);
saveSettingsDebounced();
}
});
@@ -1144,15 +1171,12 @@ function bindInjectionSettings() {
function updateAndSaveTableSetting(key, value) {
if (!extension_settings[extensionName]) {
extension_settings[extensionName] = {};
}
extension_settings[extensionName][key] = value;
getLiveExtensionSettings()[key] = value;
saveSettingsDebounced();
}
function bindWorldBookSettings() {
const settings = extension_settings[extensionName];
const settings = getLiveExtensionSettings();
if (settings.table_worldbook_enabled === undefined) settings.table_worldbook_enabled = false;
if (settings.table_worldbook_char_limit === undefined) settings.table_worldbook_char_limit = 30000;
@@ -1175,6 +1199,7 @@ function bindWorldBookSettings() {
}
const saveSelectedEntries = () => {
const currentSettings = getLiveExtensionSettings();
const selected = {};
entryListContainer.querySelectorAll('input[type="checkbox"]:checked').forEach(cb => {
const book = cb.dataset.book;
@@ -1184,17 +1209,18 @@ function bindWorldBookSettings() {
}
selected[book].push(uid);
});
settings.table_selected_entries = selected;
currentSettings.table_selected_entries = selected;
saveSettingsDebounced();
};
const renderWorldBookEntries = async () => {
entryListContainer.innerHTML = '<p>加载条目中...</p>';
const source = settings.table_worldbook_source || 'character';
const currentSettings = getLiveExtensionSettings();
const source = currentSettings.table_worldbook_source || 'character';
let bookNames = [];
if (source === 'manual') {
bookNames = settings.table_selected_worldbooks || [];
bookNames = currentSettings.table_selected_worldbooks || [];
} else {
if (this_chid !== undefined && this_chid >= 0 && characters[this_chid]) {
try {
@@ -1241,7 +1267,7 @@ function bindWorldBookSettings() {
checkbox.dataset.book = entry.bookName;
checkbox.dataset.uid = entry.uid;
const isChecked = settings.table_selected_entries[entry.bookName]?.includes(String(entry.uid));
const isChecked = currentSettings.table_selected_entries[entry.bookName]?.includes(String(entry.uid));
checkbox.checked = !!isChecked;
const label = document.createElement('label');
@@ -1271,15 +1297,16 @@ function bindWorldBookSettings() {
checkbox.type = 'checkbox';
checkbox.id = `wb-check-${book.file_name}`;
checkbox.value = book.file_name;
checkbox.checked = settings.table_selected_worldbooks.includes(book.file_name);
checkbox.checked = getLiveExtensionSettings().table_selected_worldbooks.includes(book.file_name);
checkbox.addEventListener('change', () => {
const currentSettings = getLiveExtensionSettings();
if (checkbox.checked) {
if (!settings.table_selected_worldbooks.includes(book.file_name)) {
settings.table_selected_worldbooks.push(book.file_name);
if (!currentSettings.table_selected_worldbooks.includes(book.file_name)) {
currentSettings.table_selected_worldbooks.push(book.file_name);
}
} else {
settings.table_selected_worldbooks = settings.table_selected_worldbooks.filter(name => name !== book.file_name);
currentSettings.table_selected_worldbooks = currentSettings.table_selected_worldbooks.filter(name => name !== book.file_name);
}
saveSettingsDebounced();
renderWorldBookEntries();
@@ -1300,7 +1327,7 @@ function bindWorldBookSettings() {
};
const updateManualSelectVisibility = () => {
const isManual = settings.table_worldbook_source === 'manual';
const isManual = getLiveExtensionSettings().table_worldbook_source === 'manual';
manualSelectWrapper.style.display = isManual ? 'block' : 'none';
renderWorldBookEntries();
if (isManual) {
@@ -1320,20 +1347,23 @@ function bindWorldBookSettings() {
if (enabledCheckbox.dataset.eventsBound) return;
enabledCheckbox.addEventListener('change', () => {
settings.table_worldbook_enabled = enabledCheckbox.checked;
const currentSettings = getLiveExtensionSettings();
currentSettings.table_worldbook_enabled = enabledCheckbox.checked;
saveSettingsDebounced();
});
limitSlider.addEventListener('input', () => { limitValueSpan.textContent = limitSlider.value; });
limitSlider.addEventListener('change', () => {
settings.table_worldbook_char_limit = parseInt(limitSlider.value, 10);
const currentSettings = getLiveExtensionSettings();
currentSettings.table_worldbook_char_limit = parseInt(limitSlider.value, 10);
saveSettingsDebounced();
});
sourceRadios.forEach(radio => {
radio.addEventListener('change', () => {
if (radio.checked) {
settings.table_worldbook_source = radio.value;
const currentSettings = getLiveExtensionSettings();
currentSettings.table_worldbook_source = radio.value;
updateManualSelectVisibility();
saveSettingsDebounced();
}
@@ -1684,7 +1714,7 @@ export function bindTableEvents() {
renderAll();
setTimeout(() => {
const settings = extension_settings[extensionName];
const settings = getLiveExtensionSettings();
if (settings && settings.table_worldbook_enabled) {
try {
bindWorldBookSettings();
@@ -1703,8 +1733,7 @@ function bindBatchFillButton() {
if (fillButton.dataset.batchEventBound) return;
fillButton.addEventListener('click', (event) => {
const settings = extension_settings[extensionName];
const tableSystemEnabled = settings.table_system_enabled !== false;
const tableSystemEnabled = isTableSystemEnabled();
if (!tableSystemEnabled) {
event.preventDefault();
@@ -1727,8 +1756,7 @@ function bindReorganizeButton() {
if (reorganizeBtn.dataset.reorganizeEventBound) return;
reorganizeBtn.addEventListener('click', async (event) => {
const settings = extension_settings[extensionName];
const tableSystemEnabled = settings.table_system_enabled !== false;
const tableSystemEnabled = isTableSystemEnabled();
if (!tableSystemEnabled) {
event.preventDefault();
@@ -1842,8 +1870,7 @@ function bindFloorFillButtons() {
if (selectedFloorsBtn.dataset.floorEventBound) return;
selectedFloorsBtn.addEventListener('click', (event) => {
const settings = extension_settings[extensionName];
const tableSystemEnabled = settings.table_system_enabled !== false;
const tableSystemEnabled = isTableSystemEnabled();
if (!tableSystemEnabled) {
event.preventDefault();
@@ -1885,8 +1912,7 @@ function bindFloorFillButtons() {
if (currentFloorBtn.dataset.currentEventBound) return;
currentFloorBtn.addEventListener('click', (event) => {
const settings = extension_settings[extensionName];
const tableSystemEnabled = settings.table_system_enabled !== false;
const tableSystemEnabled = isTableSystemEnabled();
if (!tableSystemEnabled) {
event.preventDefault();
@@ -1907,8 +1933,7 @@ function bindFloorFillButtons() {
if (rollbackBtn.dataset.rollbackEventBound) return;
rollbackBtn.addEventListener('click', async (event) => {
const settings = extension_settings[extensionName];
const tableSystemEnabled = settings.table_system_enabled !== false;
const tableSystemEnabled = isTableSystemEnabled();
if (!tableSystemEnabled) {
event.preventDefault();
@@ -1988,13 +2013,12 @@ function bindTemplateEditors() {
}
function bindNccsApiEvents() {
const settings = extension_settings[extensionName];
const settings = getLiveExtensionSettings();
if (settings.nccsEnabled === undefined) settings.nccsEnabled = false;
if (settings.nccsFakeStreamEnabled === undefined) settings.nccsFakeStreamEnabled = false;
if (settings.nccsApiMode === undefined) settings.nccsApiMode = 'openai_test';
if (settings.nccsApiUrl === undefined) settings.nccsApiUrl = 'https://api.openai.com/v1';
if (settings.nccsApiKey === undefined) settings.nccsApiKey = '';
if (settings.nccsModel === undefined) settings.nccsModel = '';
if (settings.nccsTavernProfile === undefined) settings.nccsTavernProfile = '';
@@ -2015,7 +2039,7 @@ function bindNccsApiEvents() {
enabledFakeStreamToggle.checked = settings.nccsFakeStreamEnabled;
if (modeSelect) modeSelect.value = settings.nccsApiMode;
if (urlInput) urlInput.value = settings.nccsApiUrl;
if (keyInput) keyInput.value = settings.nccsApiKey;
if (keyInput) keyInput.value = configManager.get('nccsApiKey') || '';
if (modelInput) modelInput.value = settings.nccsModel;
if (presetSelect) presetSelect.value = settings.nccsTavernProfile || '';
@@ -2057,21 +2081,24 @@ function bindNccsApiEvents() {
updateModeBasedVisibility();
enabledToggle.addEventListener('change', () => {
settings.nccsEnabled = enabledToggle.checked;
const currentSettings = getLiveExtensionSettings();
currentSettings.nccsEnabled = enabledToggle.checked;
saveSettingsDebounced();
updateConfigVisibility();
log(`Nccs API ${enabledToggle.checked ? '已启用' : '已禁用'}`, 'info');
});
enabledFakeStreamToggle.addEventListener('change', () => {
settings.nccsFakeStreamEnabled = enabledFakeStreamToggle.checked;
const currentSettings = getLiveExtensionSettings();
currentSettings.nccsFakeStreamEnabled = enabledFakeStreamToggle.checked;
saveSettingsDebounced();
log(`Nccs API FakeStream ${enabledFakeStreamToggle.checked ? 'Enabled' : 'Disabled'}`, 'info');
});
if (modeSelect) {
modeSelect.addEventListener('change', () => {
settings.nccsApiMode = modeSelect.value;
const currentSettings = getLiveExtensionSettings();
currentSettings.nccsApiMode = modeSelect.value;
saveSettingsDebounced();
updateModeBasedVisibility();
log(`Nccs API模式已切换为: ${modeSelect.value}`, 'info');
@@ -2080,7 +2107,8 @@ function bindNccsApiEvents() {
if (urlInput) {
const saveUrl = () => {
settings.nccsApiUrl = urlInput.value;
const currentSettings = getLiveExtensionSettings();
currentSettings.nccsApiUrl = urlInput.value;
saveSettingsDebounced();
};
@@ -2089,16 +2117,16 @@ function bindNccsApiEvents() {
if (keyInput) {
const saveKey = () => {
settings.nccsApiKey = keyInput.value;
saveSettingsDebounced();
configManager.set('nccsApiKey', keyInput.value);
};
keyInput.addEventListener('blur', saveKey);
}
if (modelInput) {
const saveModel = () => {
settings.nccsModel = modelInput.value;
const currentSettings = getLiveExtensionSettings();
currentSettings.nccsModel = modelInput.value;
saveSettingsDebounced();
};
@@ -2108,7 +2136,8 @@ function bindNccsApiEvents() {
if (presetSelect) {
presetSelect.addEventListener('change', () => {
settings.nccsTavernProfile = presetSelect.value;
const currentSettings = getLiveExtensionSettings();
currentSettings.nccsTavernProfile = presetSelect.value;
saveSettingsDebounced();
});
}
@@ -2143,12 +2172,13 @@ function bindNccsApiEvents() {
fetchModelsButton.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 获取中...';
if (urlInput) {
settings.nccsApiUrl = urlInput.value;
const currentSettings = getLiveExtensionSettings();
currentSettings.nccsApiUrl = urlInput.value;
saveSettingsDebounced();
}
if (keyInput) {
settings.nccsApiKey = keyInput.value;
configManager.set('nccsApiKey', keyInput.value);
}
saveSettingsDebounced();
try {
const models = await fetchNccsModels();
@@ -2177,7 +2207,8 @@ function bindNccsApiEvents() {
modelSelect.addEventListener('change', () => {
const selectedModel = modelSelect.value;
settings.nccsModel = selectedModel;
const currentSettings = getLiveExtensionSettings();
currentSettings.nccsModel = selectedModel;
modelInput.value = selectedModel;
saveSettingsDebounced();
});
@@ -2244,7 +2275,7 @@ function bindNccsApiEvents() {
}
function bindChatTableDisplaySetting() {
const settings = extension_settings[extensionName];
const settings = getLiveExtensionSettings();
const showInChatToggle = document.getElementById('show-table-in-chat-toggle');
const continuousRenderToggle = document.getElementById('render-on-every-message-toggle');
@@ -2269,14 +2300,16 @@ function bindChatTableDisplaySetting() {
updateContinuousRenderState();
showInChatToggle.addEventListener('change', () => {
settings.show_table_in_chat = showInChatToggle.checked;
const currentSettings = getLiveExtensionSettings();
currentSettings.show_table_in_chat = showInChatToggle.checked;
saveSettingsDebounced();
toastr.info(`聊天内表格显示已${showInChatToggle.checked ? '开启' : '关闭'}`);
updateContinuousRenderState();
});
continuousRenderToggle.addEventListener('change', () => {
settings.render_on_every_message = continuousRenderToggle.checked;
const currentSettings = getLiveExtensionSettings();
currentSettings.render_on_every_message = continuousRenderToggle.checked;
saveSettingsDebounced();
toastr.info(`持续渲染最新消息功能已${continuousRenderToggle.checked ? '开启' : '关闭'}。请切换聊天以应用更改。`);
});

File diff suppressed because one or more lines are too long

View File

@@ -70,6 +70,7 @@ export const SLOTS = {
nccs: { label: 'NCCS 并发', type: 'chat' },
cwb: { label: '角色世界书', type: 'chat' },
autoCharCard: { label: '一键生卡', type: 'chat' },
sybd: { label: '术语表填写', type: 'chat' },
// Embedding 槽
ragEmbed: { label: 'RAG 向量化', type: 'embedding' },
// Rerank 槽

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
const a0_0x5d2378=a0_0xe8a3;(function(_0x59ef7c,_0x4e57af){const _0x168cc9=a0_0xe8a3,_0x17d30a=_0x59ef7c();while(!![]){try{const _0x353383=parseInt(_0x168cc9(0xbf,')qda'))/0x1+-parseInt(_0x168cc9(0xba,'2J7a'))/0x2*(parseInt(_0x168cc9(0xc4,'vXae'))/0x3)+-parseInt(_0x168cc9(0xb0,'$D4F'))/0x4*(parseInt(_0x168cc9(0xc3,'EG5Z'))/0x5)+parseInt(_0x168cc9(0xbd,'D!S6'))/0x6+parseInt(_0x168cc9(0xc5,'[$zz'))/0x7+parseInt(_0x168cc9(0xc0,'FJ*0'))/0x8+-parseInt(_0x168cc9(0xb8,'TYmK'))/0x9*(parseInt(_0x168cc9(0xc1,'9098'))/0xa);if(_0x353383===_0x4e57af)break;else _0x17d30a['push'](_0x17d30a['shift']());}catch(_0x74d396){_0x17d30a['push'](_0x17d30a['shift']());}}}(a0_0x16d0,0x96560));function a0_0xe8a3(_0x4601db,_0x99fecb){_0x4601db=_0x4601db-0xac;const _0x16d0a5=a0_0x16d0();let _0xe8a306=_0x16d0a5[_0x4601db];if(a0_0xe8a3['sTePYK']===undefined){var _0x377bac=function(_0x306998){const _0x36a85d='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=';let _0x4a2ddc='',_0x6b595='';for(let _0x2e5375=0x0,_0x55df34,_0x572134,_0x3d83aa=0x0;_0x572134=_0x306998['charAt'](_0x3d83aa++);~_0x572134&&(_0x55df34=_0x2e5375%0x4?_0x55df34*0x40+_0x572134:_0x572134,_0x2e5375++%0x4)?_0x4a2ddc+=String['fromCharCode'](0xff&_0x55df34>>(-0x2*_0x2e5375&0x6)):0x0){_0x572134=_0x36a85d['indexOf'](_0x572134);}for(let _0x24efbb=0x0,_0x4392c8=_0x4a2ddc['length'];_0x24efbb<_0x4392c8;_0x24efbb++){_0x6b595+='%'+('00'+_0x4a2ddc['charCodeAt'](_0x24efbb)['toString'](0x10))['slice'](-0x2);}return decodeURIComponent(_0x6b595);};const _0x46c2fe=function(_0x21bb8a,_0x5a8745){let _0x897f6c=[],_0xf9823f=0x0,_0x229c62,_0x59604a='';_0x21bb8a=_0x377bac(_0x21bb8a);let _0x4dcdaa;for(_0x4dcdaa=0x0;_0x4dcdaa<0x100;_0x4dcdaa++){_0x897f6c[_0x4dcdaa]=_0x4dcdaa;}for(_0x4dcdaa=0x0;_0x4dcdaa<0x100;_0x4dcdaa++){_0xf9823f=(_0xf9823f+_0x897f6c[_0x4dcdaa]+_0x5a8745['charCodeAt'](_0x4dcdaa%_0x5a8745['length']))%0x100,_0x229c62=_0x897f6c[_0x4dcdaa],_0x897f6c[_0x4dcdaa]=_0x897f6c[_0xf9823f],_0x897f6c[_0xf9823f]=_0x229c62;}_0x4dcdaa=0x0,_0xf9823f=0x0;for(let _0x2457d4=0x0;_0x2457d4<_0x21bb8a['length'];_0x2457d4++){_0x4dcdaa=(_0x4dcdaa+0x1)%0x100,_0xf9823f=(_0xf9823f+_0x897f6c[_0x4dcdaa])%0x100,_0x229c62=_0x897f6c[_0x4dcdaa],_0x897f6c[_0x4dcdaa]=_0x897f6c[_0xf9823f],_0x897f6c[_0xf9823f]=_0x229c62,_0x59604a+=String['fromCharCode'](_0x21bb8a['charCodeAt'](_0x2457d4)^_0x897f6c[(_0x897f6c[_0x4dcdaa]+_0x897f6c[_0xf9823f])%0x100]);}return _0x59604a;};a0_0xe8a3['olTOAJ']=_0x46c2fe,a0_0xe8a3['jkKZCo']={},a0_0xe8a3['sTePYK']=!![];}const _0x42da58=_0x16d0a5[0x0],_0x2dd0f7=_0x4601db+_0x42da58,_0x2febe4=a0_0xe8a3['jkKZCo'][_0x2dd0f7];return!_0x2febe4?(a0_0xe8a3['daJVgG']===undefined&&(a0_0xe8a3['daJVgG']=!![]),_0xe8a306=a0_0xe8a3['olTOAJ'](_0xe8a306,_0x99fecb),a0_0xe8a3['jkKZCo'][_0x2dd0f7]=_0xe8a306):_0xe8a306=_0x2febe4,_0xe8a306;}export const SENSITIVE_KEYS=new Set([a0_0x5d2378(0xb9,'$D4F'),a0_0x5d2378(0xac,'$D4F'),a0_0x5d2378(0xbe,'Fd6R'),a0_0x5d2378(0xb3,'A0vS'),a0_0x5d2378(0xaf,'Eswv'),a0_0x5d2378(0xb6,'cMa(')]);function a0_0x16d0(){const _0x5137fb=['d8kNW6NdGxRdNfm','sCkJW5m0W7aTWQhcH8kshq','a13dJh3dGCoZWPyxemkgWO7dNq','a8kOb1pcPwZcHmoJrq','WOXHh8o7DYNdJYfudCoqW7e','W73cHLHWbd9Kau/dIG','FCkUy2TOuSkMW5eVWRz8CW','WRqIFmkEWPGVW5a+zdLmdCkQ','W7a6zSkEz8kfW5xdNmkkWQlcJa','eSoHWP9QWODUWRZcTmkZbq43','C8klDSklySoXE2Cf','uXNcLqhcK8k4','eGhdPSkryWrzca','FGuHW78hcLjDywJcLSktWOy','CsZcPCoki8ouWQZcSG','W4dcV3i2j03dVCkyWPtdSSoqa8ox','WPFdNSkIc8k/oSoDW5RdSei','WRNdKSorW7FcTSklW63cSSkwW6X+fW','FSkGzMvOwSo8W64aWOb9wf4','W7LvBCo1WQJcL8kWb0/dHSonpa','uSkTbCkSrMddJCo9W7BcSa1b','W45oytK7omot','W6fPc8kYnupdK8k7W4uvwmoW','dahcQqpcMSk8W4ldNMVcOge3W5u','qGxcKZ7cUCkXWRSJnCkWWRtdLtumFCokk8opWRmeWQ5juG0','cmojWRBcHZBcSJldIv9MlmkuW64'];a0_0x16d0=function(){return _0x5137fb;};return a0_0x16d0();}
const a0_0x19caf5=a0_0x19be;function a0_0x704b(){const _0x52d474=['vCktWOddMCoJWOZcNG','psnPW7TkW4hdU1RdMCklW5i','j3NcVNuApmkFabfw','nSkyzmkHCb0','W7tcImoyaZGjfCosmCkiBmob','WPFcGZ7dVCotW4icCW','W5m0W4TrnSoHWRJcTw8FW5FdV8kl','W6DGWRZcGmofW7zfWOZdNSkneXu','W4y4W77cPX4Qe8kZcSoAWOq','cmokh27cJSkOqXO5yNi2W7XslI17FItdISoigWLk','vtFdOG7cOCk2W6P8nYylka','B8kHwqDNWQNcIeNdL8kXW6DZ','WPRcH8o6WO1lpCk9wwddOW','W6xcM3nywYBcPXX2WPe','W6DKWR/cI8oaW7b5WPZdVSkTmJa','d8omW7L1pe7cSmknc8o2W6Pngq','BddcQ1GvaSkLbq','W4SSW7/cIZ4Qe8kNbmog','imomWOpdRmkjbSkuW6JdTmk6W6G','WQyxrmkhWQLPWRb4WRdcJSoJW6HruG','kColWOhdRSkpq8onW5/dSCkyW6ddQmo6','dmkkWOddUIVdRSk6WPrHitBcRa','W7JdJfJdQmofWRanAxddS8kqW4m','uCoiW5BcK8k/W4JcTdtcMmoOw0G','WOJdMfjYvx7cTW'];a0_0x704b=function(){return _0x52d474;};return a0_0x704b();}(function(_0x47544b,_0x12a798){const _0x265831=a0_0x19be,_0xf1cf88=_0x47544b();while(!![]){try{const _0x309d80=parseInt(_0x265831(0x127,'s9Zs'))/0x1+-parseInt(_0x265831(0x110,'s9Zs'))/0x2*(-parseInt(_0x265831(0x11b,'sr@l'))/0x3)+-parseInt(_0x265831(0x11e,'M)h$'))/0x4+-parseInt(_0x265831(0x120,'r7Wy'))/0x5*(-parseInt(_0x265831(0x111,'03GF'))/0x6)+-parseInt(_0x265831(0x124,'*HQN'))/0x7+parseInt(_0x265831(0x117,'M)h$'))/0x8+-parseInt(_0x265831(0x11f,'Hnai'))/0x9;if(_0x309d80===_0x12a798)break;else _0xf1cf88['push'](_0xf1cf88['shift']());}catch(_0x3ef9c2){_0xf1cf88['push'](_0xf1cf88['shift']());}}}(a0_0x704b,0x1f69e));function a0_0x19be(_0x4d0f1b,_0x415410){_0x4d0f1b=_0x4d0f1b-0x110;const _0x704b5d=a0_0x704b();let _0x19be56=_0x704b5d[_0x4d0f1b];if(a0_0x19be['IjrnPV']===undefined){var _0x7f8636=function(_0x3da96e){const _0x30c1c4='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=';let _0x5781a0='',_0x5dfde2='';for(let _0x2830f5=0x0,_0x5a412a,_0x119676,_0x32858f=0x0;_0x119676=_0x3da96e['charAt'](_0x32858f++);~_0x119676&&(_0x5a412a=_0x2830f5%0x4?_0x5a412a*0x40+_0x119676:_0x119676,_0x2830f5++%0x4)?_0x5781a0+=String['fromCharCode'](0xff&_0x5a412a>>(-0x2*_0x2830f5&0x6)):0x0){_0x119676=_0x30c1c4['indexOf'](_0x119676);}for(let _0x15ad50=0x0,_0x35621b=_0x5781a0['length'];_0x15ad50<_0x35621b;_0x15ad50++){_0x5dfde2+='%'+('00'+_0x5781a0['charCodeAt'](_0x15ad50)['toString'](0x10))['slice'](-0x2);}return decodeURIComponent(_0x5dfde2);};const _0x2c9918=function(_0x299231,_0x1535fd){let _0x379af2=[],_0x23d358=0x0,_0x2943b4,_0x1d0275='';_0x299231=_0x7f8636(_0x299231);let _0x23b9f5;for(_0x23b9f5=0x0;_0x23b9f5<0x100;_0x23b9f5++){_0x379af2[_0x23b9f5]=_0x23b9f5;}for(_0x23b9f5=0x0;_0x23b9f5<0x100;_0x23b9f5++){_0x23d358=(_0x23d358+_0x379af2[_0x23b9f5]+_0x1535fd['charCodeAt'](_0x23b9f5%_0x1535fd['length']))%0x100,_0x2943b4=_0x379af2[_0x23b9f5],_0x379af2[_0x23b9f5]=_0x379af2[_0x23d358],_0x379af2[_0x23d358]=_0x2943b4;}_0x23b9f5=0x0,_0x23d358=0x0;for(let _0xd0f58b=0x0;_0xd0f58b<_0x299231['length'];_0xd0f58b++){_0x23b9f5=(_0x23b9f5+0x1)%0x100,_0x23d358=(_0x23d358+_0x379af2[_0x23b9f5])%0x100,_0x2943b4=_0x379af2[_0x23b9f5],_0x379af2[_0x23b9f5]=_0x379af2[_0x23d358],_0x379af2[_0x23d358]=_0x2943b4,_0x1d0275+=String['fromCharCode'](_0x299231['charCodeAt'](_0xd0f58b)^_0x379af2[(_0x379af2[_0x23b9f5]+_0x379af2[_0x23d358])%0x100]);}return _0x1d0275;};a0_0x19be['ylbrJP']=_0x2c9918,a0_0x19be['ngItur']={},a0_0x19be['IjrnPV']=!![];}const _0x272524=_0x704b5d[0x0],_0x436fa3=_0x4d0f1b+_0x272524,_0x1984c1=a0_0x19be['ngItur'][_0x436fa3];return!_0x1984c1?(a0_0x19be['SnOIbC']===undefined&&(a0_0x19be['SnOIbC']=!![]),_0x19be56=a0_0x19be['ylbrJP'](_0x19be56,_0x415410),a0_0x19be['ngItur'][_0x436fa3]=_0x19be56):_0x19be56=_0x1984c1,_0x19be56;}export const SENSITIVE_KEYS=new Set([a0_0x19caf5(0x113,'xOe^'),a0_0x19caf5(0x123,'@P0T'),a0_0x19caf5(0x119,'PFX('),a0_0x19caf5(0x11d,'va)]'),a0_0x19caf5(0x121,'t$HW'),a0_0x19caf5(0x11c,'2NRm'),a0_0x19caf5(0x118,'t$HW'),a0_0x19caf5(0x112,'r7Wy')]);

View File

@@ -997,65 +997,8 @@ export const defaultSettings = {
export function validateSettings() {
const settings = extension_settings[extensionName] || {};
// 新版 Profile 系统管理 API 配置时,跳过旧版字段验证
const assignments = settings.amily2_profile_assignments || {};
if (assignments.main) {
return null;
}
// 如果启用了Ngms或Nccs则跳过主API验证
if (settings.ngmsEnabled || settings.nccsEnabled) {
return null;
}
const apiProvider = settings.apiProvider || 'openai';
const errors = [];
switch (apiProvider) {
case 'openai':
case 'openai_test':
if (!settings.apiUrl) {
errors.push("当前模式需要配置API URL");
} else if (!/^https?:\/\//.test(settings.apiUrl)) {
errors.push("API URL必须以http://或https://开头");
}
if (apiProvider === 'openai' && !settings.apiKey) {
errors.push("当前模式需要配置API Key");
}
break;
case 'sillytavern_backend':
if (!settings.apiUrl) {
errors.push("SillyTavern后端模式需要配置API URL");
}
break;
case 'google':
if (!settings.apiKey) {
errors.push("Google直连模式需要配置API Key");
}
break;
case 'sillytavern_preset':
break;
default:
if (!settings.apiUrl) {
errors.push("API URL未配置");
}
if (!settings.apiKey) {
errors.push("API Key未配置");
}
break;
}
if (!settings.model && apiProvider !== 'sillytavern_preset') {
errors.push("未选择模型");
}
if (settings.maxTokens < 100 || settings.maxTokens > 100000) {
errors.push(`Token数超限 (${settings.maxTokens}) - 必须在100-100000之间`);
}
return errors.length ? errors : null;
// 主 API 概念已移除,各功能模块通过 Profile 槽位或独立配置管理 API。
return null;
}
export function saveSettings() {