mirror of
https://github.com/Wx-2025/ST-Amily2-Chat-Optimisation.git
synced 2026-06-06 10:25:51 +00:00
Compare commits
9 Commits
0c5ac2c70b
...
2.1.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
31d00f4330 | ||
|
|
13d05651f3 | ||
|
|
544937bb91 | ||
|
|
8d590073f4 | ||
|
|
58ff3c3faf | ||
|
|
c50e1a9425 | ||
|
|
2291a871eb | ||
|
|
bddda1802f | ||
|
|
1fdbe62142 |
@@ -679,11 +679,6 @@ export async function callCustomOpenAI(messages) {
|
||||
const headers = { ...getRequestHeaders(), 'Content-Type': 'application/json' };
|
||||
const body = JSON.stringify(requestBody);
|
||||
|
||||
console.groupCollapsed(`[CWB] API Call @ ${new Date().toLocaleTimeString()}`);
|
||||
console.log('Request URL:', fullApiUrl);
|
||||
console.log('Request Headers:', headers);
|
||||
console.log('Request Body:', requestBody);
|
||||
|
||||
try {
|
||||
const response = await fetch(fullApiUrl, {
|
||||
method: 'POST',
|
||||
@@ -693,27 +688,19 @@ export async function callCustomOpenAI(messages) {
|
||||
|
||||
if (!response.ok) {
|
||||
const errTxt = await response.text();
|
||||
console.error('API Error Response:', errTxt);
|
||||
throw new Error(`API请求失败: ${response.status} ${errTxt}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
console.log('API Full Response:', data);
|
||||
|
||||
|
||||
if (data.choices && data.choices[0]?.message?.content) {
|
||||
console.log('Extracted Content:', data.choices[0].message.content.trim());
|
||||
console.groupEnd();
|
||||
return data.choices[0].message.content.trim();
|
||||
}
|
||||
|
||||
|
||||
throw new Error('API响应格式不正确。');
|
||||
|
||||
} catch (error) {
|
||||
console.error('API Call Failed:', error);
|
||||
console.error('[CWB] API Call Failed:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
if (console.groupEnd) {
|
||||
console.groupEnd();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { generateRandomSeed } from '../../core/api.js';
|
||||
import { getChatIdentifier } from '../../core/lore.js';
|
||||
import { safeLorebookEntries } from '../../core/tavernhelper-compatibility.js';
|
||||
import { amilyHelper } from '../../core/tavern-helper/main.js';
|
||||
import { resolveHistoriographyRuleConfig } from '../../utils/config/RuleProfileManager.js';
|
||||
|
||||
const { SillyTavern, jQuery, characters } = window;
|
||||
|
||||
@@ -127,9 +128,10 @@ function processChatMessages(messages) {
|
||||
return messages.map(msg => `${msg.is_user ? SillyTavern?.name1 || '用户' : msg.name || '角色'}: ${msg.message}`).join('\n\n');
|
||||
}
|
||||
|
||||
const useTagExtraction = mainSettings.historiographyTagExtractionEnabled ?? false;
|
||||
const tagsToExtract = useTagExtraction ? (mainSettings.historiographyTags || '').split(',').map(t => t.trim()).filter(Boolean) : [];
|
||||
const exclusionRules = mainSettings.historiographyExclusionRules || [];
|
||||
const historiographyRuleConfig = resolveHistoriographyRuleConfig(mainSettings);
|
||||
const useTagExtraction = historiographyRuleConfig.tagExtractionEnabled ?? false;
|
||||
const tagsToExtract = useTagExtraction ? (historiographyRuleConfig.tags || '').split(',').map(t => t.trim()).filter(Boolean) : [];
|
||||
const exclusionRules = historiographyRuleConfig.exclusionRules || [];
|
||||
|
||||
logDebug(`[CWB] 标签提取: ${useTagExtraction}, 标签: ${tagsToExtract.join(', ')}, 排除规则: ${exclusionRules.length}`);
|
||||
|
||||
|
||||
@@ -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 = '';
|
||||
@@ -86,6 +87,13 @@ function saveBreakArmorPrompt() {
|
||||
showToastr('success', '破甲预设已保存!');
|
||||
}
|
||||
|
||||
function autosaveBreakArmorPrompt() {
|
||||
const newPrompt = $panel.find('#cwb-break-armor-prompt-textarea').val();
|
||||
getSettings().cwb_break_armor_prompt = newPrompt;
|
||||
state.currentBreakArmorPrompt = newPrompt;
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
function resetBreakArmorPrompt() {
|
||||
getSettings().cwb_break_armor_prompt = cwbCompleteDefaultSettings.cwb_break_armor_prompt;
|
||||
state.currentBreakArmorPrompt = cwbCompleteDefaultSettings.cwb_break_armor_prompt;
|
||||
@@ -106,6 +114,13 @@ function saveCharCardPrompt() {
|
||||
showToastr('success', '角色卡预设已保存!');
|
||||
}
|
||||
|
||||
function autosaveCharCardPrompt() {
|
||||
const newPrompt = $panel.find('#cwb-char-card-prompt-textarea').val();
|
||||
getSettings().cwb_char_card_prompt = newPrompt;
|
||||
state.currentCharCardPrompt = newPrompt;
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
function resetCharCardPrompt() {
|
||||
getSettings().cwb_char_card_prompt = cwbCompleteDefaultSettings.cwb_char_card_prompt;
|
||||
state.currentCharCardPrompt = cwbCompleteDefaultSettings.cwb_char_card_prompt;
|
||||
@@ -128,6 +143,16 @@ function saveAutoUpdateThreshold() {
|
||||
}
|
||||
}
|
||||
|
||||
function autosaveAutoUpdateThreshold() {
|
||||
const valStr = $panel.find('#cwb-auto-update-threshold').val();
|
||||
const newT = parseInt(valStr, 10);
|
||||
if (!isNaN(newT) && newT >= 1) {
|
||||
getSettings().cwb_auto_update_threshold = newT;
|
||||
state.autoUpdateThreshold = newT;
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
}
|
||||
|
||||
function saveScanDepth() {
|
||||
const valStr = $panel.find('#cwb-scan-depth').val();
|
||||
const newT = parseInt(valStr, 10);
|
||||
@@ -142,6 +167,16 @@ function saveScanDepth() {
|
||||
}
|
||||
}
|
||||
|
||||
function autosaveScanDepth() {
|
||||
const valStr = $panel.find('#cwb-scan-depth').val();
|
||||
const newT = parseInt(valStr, 10);
|
||||
if (!isNaN(newT) && newT >= 1) {
|
||||
getSettings().cwb_scan_depth = newT;
|
||||
state.scanDepth = newT;
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
}
|
||||
|
||||
function bindWorldBookSettings() {
|
||||
const MAX_RETRIES = 10;
|
||||
const RETRY_DELAY = 200;
|
||||
@@ -283,16 +318,13 @@ 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);
|
||||
updateApiStatusDisplay($panel);
|
||||
});
|
||||
|
||||
$panel.on('change', '#cwb-api-model', function() {
|
||||
$panel.on('input change', '#cwb-api-model', function(event) {
|
||||
const model = $(this).val();
|
||||
|
||||
// 同时更新设置和状态
|
||||
@@ -304,11 +336,16 @@ export function bindSettingsEvents($settingsPanel) {
|
||||
|
||||
console.log('[CWB] 模型已更新 - 设置:', getSettings().cwb_api_model, ', 状态:', state.customApiConfig.model);
|
||||
|
||||
if (model) {
|
||||
if (model && event.type === 'change') {
|
||||
showToastr('success', `模型已选择: ${model}`);
|
||||
}
|
||||
});
|
||||
|
||||
$panel.on('input change', '#cwb-break-armor-prompt-textarea', autosaveBreakArmorPrompt);
|
||||
$panel.on('input change', '#cwb-char-card-prompt-textarea', autosaveCharCardPrompt);
|
||||
$panel.on('input change', '#cwb-auto-update-threshold', autosaveAutoUpdateThreshold);
|
||||
$panel.on('input change', '#cwb-scan-depth', autosaveScanDepth);
|
||||
|
||||
$panel.on('click', '#cwb-load-models', () => fetchModelsAndConnect($panel));
|
||||
|
||||
$panel.on('click', '#cwb-save-break-armor-prompt', saveBreakArmorPrompt);
|
||||
@@ -489,7 +526,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 +611,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;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
22
SL/module/RuleConfigModule.js
Normal file
22
SL/module/RuleConfigModule.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Module, ModuleBuilder } from './Module.js';
|
||||
import { bindRuleConfigPanel } from '../../ui/rule-config-bindings.js';
|
||||
|
||||
const builder = new ModuleBuilder()
|
||||
.name('RuleConfig')
|
||||
.view('assets/rule-config-panel.html')
|
||||
.strict(true)
|
||||
.required(['mount']);
|
||||
|
||||
export default class RuleConfigModule extends Module {
|
||||
constructor() {
|
||||
super(builder);
|
||||
}
|
||||
|
||||
async mount() {
|
||||
if (this.el) {
|
||||
this.el.id = 'amily2_rule_config_panel';
|
||||
this.el.style.display = 'none';
|
||||
}
|
||||
bindRuleConfigPanel($(this.el));
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,7 @@ 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
|
||||
import { extensionName } from '../../utils/settings.js';
|
||||
const sfigenSettingsKey = 'sfigen_settings';
|
||||
|
||||
const defaultSettings = {
|
||||
|
||||
@@ -16,7 +16,8 @@ export default class TableModule extends Module {
|
||||
if (this.el) {
|
||||
this.el.id = 'amily2_memorisation_forms_panel';
|
||||
this.el.style.display = 'none';
|
||||
this.el.dataset.module = 'TableModule';
|
||||
}
|
||||
bindTableEvents();
|
||||
bindTableEvents(this.el);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 RuleConfigModule from './RuleConfigModule.js';
|
||||
import SfiGenModule from './SfiGenModule.js';
|
||||
|
||||
export function registerAllModules() {
|
||||
@@ -34,5 +35,6 @@ export function registerAllModules() {
|
||||
registry.register('Renderer', () => new RendererModule());
|
||||
registry.register('SuperMemory', () => new SuperMemoryModule());
|
||||
registry.register('ApiConfig', () => new ApiConfigModule());
|
||||
registry.register('RuleConfig', () => new RuleConfigModule());
|
||||
registry.register('SfiGen', () => new SfiGenModule());
|
||||
}
|
||||
|
||||
21
TODO.md
21
TODO.md
@@ -80,4 +80,23 @@
|
||||
- **史官系统 (Historiographer) 优化**:
|
||||
- **Ngms API 强制参数**:在 `core/api/Ngms_api.js` 中,移除了旧版 UI 中的温度和最大 Token 设置,强制将默认温度设为 `1.0`,最大 Token 设为 `30000`,以确保总结任务的稳定性和完整性。
|
||||
- **总结失败自动重试**:在 `core/historiographer.js` 中为“微言录”和“宏史卷”的生成过程添加了自定义重试逻辑。用户可在 UI 中设置重试次数,当 AI 返回空内容时,系统会自动等待并重试,降低了因 API 波动导致的总结失败率。
|
||||
- **时间跨度标识优化**:修改了 `utils/settings.js` 中的“微言录”和“宏史卷”提示词,强制要求 AI 在提取时间时加入相对时间跨度标识 `(Xd)`(如 `2023-09-15(2d)-星期五-15:00`),以解决长篇剧情中因缺乏具体日期导致的时间线混乱问题。
|
||||
- **时间跨度标识优化**:修改了 `utils/settings.js` 中的”微言录”和”宏史卷”提示词,强制要求 AI 在提取时间时加入相对时间跨度标识 `(Xd)`(如 `2023-09-15(2d)-星期五-15:00`),以解决长篇剧情中因缺乏具体日期导致的时间线混乱问题。
|
||||
|
||||
### 2.1.0 (2026/04/18)
|
||||
|
||||
以下为更新内容:
|
||||
- **提取规则配置通用化重构**:
|
||||
- `RuleProfileManager` 新增功能槽(SLOTS)+ 统一分配(assignments)机制,参照 `ApiProfileManager` 的架构模式,将提取规则预设存储在 `extension_settings`(settings.json)中实现云同步。
|
||||
- 定义四个功能槽:`table`(表格提取)、`historiography`(史官/总结提取)、`condensation`(翰林院·浓缩)、`queryPreprocessing`(翰林院·查询预处理)。
|
||||
- 新增 `resolveSlotRuleConfig(slot)` 统一解析接口,优先读取 assignments 分配,回退兼容旧字段。
|
||||
- 新增一次性迁移逻辑:自动将旧版分散的 `table_rule_profile_id`、`historiographyRuleProfileId`、`condensation.ruleProfileId`、`queryPreprocessing.ruleProfileId` 迁移到统一的 `ruleProfileAssignments`。
|
||||
- **消费方 UI 统一改为下拉选单**:
|
||||
- **表格系统**:移除”启用独立提取规则”开关和”配置规则”弹窗,替换为规则配置下拉选单,onChange 即时生效。
|
||||
- **史官系统**:移除标签提取开关、标签输入框和”内容排除”弹窗按钮,替换为规则配置下拉选单。
|
||||
- **翰林院(浓缩 + 查询预处理)**:移除各自的规则配置弹窗按钮,分别替换为独立的规则配置下拉选单;修复了翰林院旧弹窗存在的 HTML 注入隐患。
|
||||
- 新建/编辑规则统一通过「规则配置中心」面板完成。
|
||||
- **遗留代码清理**:
|
||||
- 删除 `openTableRuleEditor`(table-bindings.js)、`showHistoriographyExclusionRulesModal`(historiography-bindings.js)、`showRulesModal` / `saveHanlinRuleProfile` / `syncHanlinLinkedRuleProfile`(hanlinyuan-bindings.js)等旧弹窗函数。
|
||||
- 删除 `saveHistoriographyRuleProfile` / `syncHistoriographyLinkedRuleProfile` 等旧同步函数。
|
||||
- 移除 `table_independent_rules_enabled` 开关判断,批量填表和分步填表改为检查 resolved config 是否有实际内容。
|
||||
- 修复 `previewCondensation` 引用已移除 DOM 元素(`hly-tag-extraction-toggle` / `hly-tag-input`)的问题,改为从 resolved config 读取。
|
||||
|
||||
@@ -241,20 +241,12 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="hly-control-block" style="flex-direction: row; justify-content: space-between; align-items: center; margin-top: 10px;">
|
||||
<label for="historiography-tag-extraction-toggle">标签提取</label>
|
||||
<label class="hly-toggle-switch">
|
||||
<input type="checkbox" id="historiography-tag-extraction-toggle" data-setting-key="condensation.tagExtractionEnabled" data-type="boolean">
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div id="historiography-tag-input-container" class="hly-control-block" style="display: none;">
|
||||
<label for="historiography-tag-input">输入标签 (以逗号分隔):</label>
|
||||
<textarea id="historiography-tag-input" class="hly-imperial-brush" rows="2" placeholder="例如: content,details" data-setting-key="condensation.tags" data-type="string"></textarea>
|
||||
<div class="hly-control-block" style="margin-top: 10px;">
|
||||
<label style="font-weight: bold;">提取规则配置</label>
|
||||
<select id="historiography-rule-profile-select" class="hly-imperial-brush" style="width: 100%;"></select>
|
||||
<small class="hly-notes">选择在「规则配置中心」里创建的提取规则,应用于微言录和宏史卷总结。</small>
|
||||
</div>
|
||||
<div class="hly-button-group" style="justify-content: flex-start; margin-top: 10px; display: flex; align-items: center; gap: 20px;">
|
||||
<button id="historiography-exclusion-rules-btn" class="hly-action-button">内容排除</button>
|
||||
|
||||
<div class="auto-control-pair" style="margin-bottom: 0;">
|
||||
<label for="historiography_auto_summary_interactive" title="开启后,“自动巡录”将弹出交互窗口确认,而不是在后台静默运行。">交互式巡录:</label>
|
||||
<label class="toggle-switch">
|
||||
|
||||
@@ -252,16 +252,10 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="table-independent-rules-container" class="control-block-with-switch" style="margin-bottom: 10px; display: none; flex-direction: column; align-items: flex-start; gap: 8px;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; width: 100%;">
|
||||
<label for="table-independent-rules-enabled">启用独立提取规则</label>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="table-independent-rules-enabled">
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
<button id="table-configure-rules-btn" class="menu_button small_button" style="display: none;"><i class="fas fa-cog"></i> 配置规则</button>
|
||||
<small class="notes">启用后,分步填表和批量填表将使用下方配置的专属规则,而非微言录的规则。</small>
|
||||
<div class="control-block-with-switch" style="margin-bottom: 10px; display: flex; flex-direction: column; align-items: flex-start; gap: 8px;">
|
||||
<label style="font-weight: bold;">提取规则配置</label>
|
||||
<select id="table-rule-profile-select" class="text_pole" style="width: 100%;"></select>
|
||||
<small class="notes">选择在「规则配置中心」里创建的提取规则,应用于分步填表和批量填表。未选择时使用默认行为。</small>
|
||||
</div>
|
||||
<div class="action-center-buttons" style="gap: 8px;">
|
||||
<button id="amily2-open-relationship-graph-btn" class="menu_button accent small_button interactable"><i class="fas fa-project-diagram"></i> 关系图谱</button>
|
||||
|
||||
@@ -251,19 +251,10 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hly-control-block" style="flex-direction: row; justify-content: space-between; align-items: center;">
|
||||
<label for="hly-tag-extraction-toggle">标签提取</label>
|
||||
<label class="hly-toggle-switch">
|
||||
<input type="checkbox" id="hly-tag-extraction-toggle" data-setting-key="condensation.tagExtractionEnabled" data-type="boolean">
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div id="hly-tag-input-container" class="hly-control-block" style="display: none;">
|
||||
<label for="hly-tag-input">输入标签 (以逗号分隔):</label>
|
||||
<textarea id="hly-tag-input" class="hly-imperial-brush" rows="2" placeholder="例如: content,details,摘要" data-setting-key="condensation.tags" data-type="string"></textarea>
|
||||
</div>
|
||||
<div class="hly-button-group" style="justify-content: flex-start;">
|
||||
<button id="hly-exclusion-rules-btn" class="hly-action-button">内容排除</button>
|
||||
<div class="hly-control-block" style="margin-top: 8px;">
|
||||
<label style="font-weight: bold;">提取规则配置</label>
|
||||
<select id="hly-condensation-rule-profile-select" class="hly-imperial-brush" style="width: 100%;"></select>
|
||||
<small class="hly-notes">选择在「规则配置中心」里创建的提取规则,应用于浓缩处理。</small>
|
||||
</div>
|
||||
<div class="hly-button-group">
|
||||
<button class="hly-action-button success" onclick="startHLYCondensation()"> 开始凝识</button>
|
||||
@@ -405,7 +396,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div id="hly-kb-list-local" class="hly-kb-list">
|
||||
<p id="hly-kb-list-local-placeholder" style="color: #888;">当前角色没有知识库。通过“书库编纂”中的功能可自动创建。</p>
|
||||
<p id="hly-kb-list-local-placeholder" style="color: #888;">当前角色没有知识库。通过“书库编纂"中的功能可自动创建。</p>
|
||||
<!-- Local KBs will be populated here -->
|
||||
</div>
|
||||
<div class="hly-button-group" style="margin-top: 15px;">
|
||||
@@ -456,10 +447,11 @@
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="hly-button-group" style="justify-content: flex-start;">
|
||||
<button id="hly-query-preprocessing-rules-btn" class="hly-action-button">配置处理规则</button>
|
||||
<div class="hly-control-block" style="margin-top: 8px;">
|
||||
<label style="font-weight: bold;">处理规则配置</label>
|
||||
<select id="hly-query-preprocessing-rule-profile-select" class="hly-imperial-brush" style="width: 100%;"></select>
|
||||
<small class="hly-notes">选择在「规则配置中心」里创建的提取规则,应用于查询预处理(标签提取 + 内容排除)。</small>
|
||||
</div>
|
||||
<small class="hly-notes">此功能类似于“凝识法则”,可对您最近的几条聊天记录(即用于检索的文本)进行标签提取和内容排除,以生成更纯净、更高效的检索查询。</small>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="hly-settings-group">
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
<div class="additional-features-title">
|
||||
<i class="fas fa-key"></i> API 连接配置
|
||||
</div>
|
||||
<button id="amily2_back_to_main_from_api_config" class="menu_button secondary small_button interactable">
|
||||
返回主殿 <i class="fas fa-arrow-right"></i>
|
||||
<button id="amily2_back_to_main_from_api_config" class="menu_button secondary small_button interactable amily2-vbtn">
|
||||
<span class="vbtn-icon"><i class="fas fa-arrow-right"></i></span><span class="vbtn-label">返回主殿</span>
|
||||
</button>
|
||||
</div>
|
||||
<hr class="header-divider" style="margin-top: 5px; margin-bottom: 10px;">
|
||||
@@ -26,10 +26,19 @@
|
||||
<label>当前密钥对指纹</label>
|
||||
<div style="display:flex; gap:6px; align-items:center;">
|
||||
<code id="amily2_keypair_fingerprint" style="flex:1; padding:4px 8px; background:var(--black30a); border-radius:4px; font-size:0.85em;">(未生成)</code>
|
||||
<button id="amily2_generate_keypair" class="menu_button interactable small_button" title="生成新密钥对(会清除所有已加密的 Key)">
|
||||
<i class="fas fa-sync-alt"></i> 重新生成
|
||||
<button id="amily2_generate_keypair" class="menu_button interactable small_button amily2-vbtn" title="生成新密钥对(会清除所有已加密的 Key)">
|
||||
<span class="vbtn-icon"><i class="fas fa-sync-alt"></i></span><span class="vbtn-label">重新生成</span>
|
||||
</button>
|
||||
</div>
|
||||
<div style="display:flex; gap:6px; margin-top:6px; flex-wrap:wrap;">
|
||||
<button id="amily2_export_key_bundle" class="menu_button interactable small_button amily2-vbtn" title="导出当前设备的私钥包,用于新设备恢复解密权限">
|
||||
<span class="vbtn-icon"><i class="fas fa-download"></i></span><span class="vbtn-label">导出私钥</span>
|
||||
</button>
|
||||
<button id="amily2_import_key_bundle" class="menu_button interactable small_button amily2-vbtn" title="导入先前导出的私钥包,恢复云同步密钥的解密能力">
|
||||
<span class="vbtn-icon"><i class="fas fa-upload"></i></span><span class="vbtn-label">导入私钥</span>
|
||||
</button>
|
||||
<input id="amily2_import_key_bundle_input" type="file" accept=".json,application/json" style="display:none;" />
|
||||
</div>
|
||||
<small class="notes" style="color: var(--warning-color);">
|
||||
⚠️ 重新生成密钥对后,所有已加密存储的 API Key 将失效,需重新输入。
|
||||
</small>
|
||||
@@ -43,17 +52,17 @@
|
||||
|
||||
<div style="display:flex; gap:6px; margin-bottom:10px; flex-wrap:wrap;">
|
||||
<button class="menu_button small_button amily2_profile_type_filter active" data-type="all">全部</button>
|
||||
<button class="menu_button small_button amily2_profile_type_filter" data-type="chat">
|
||||
<i class="fas fa-comments"></i> 对话模型
|
||||
<button class="menu_button small_button amily2_profile_type_filter amily2-vbtn" data-type="chat">
|
||||
<span class="vbtn-icon"><i class="fas fa-comments"></i></span><span class="vbtn-label">对话模型</span>
|
||||
</button>
|
||||
<button class="menu_button small_button amily2_profile_type_filter" data-type="embedding">
|
||||
<i class="fas fa-project-diagram"></i> 向量嵌入
|
||||
<button class="menu_button small_button amily2_profile_type_filter amily2-vbtn" data-type="embedding">
|
||||
<span class="vbtn-icon"><i class="fas fa-project-diagram"></i></span><span class="vbtn-label">向量嵌入</span>
|
||||
</button>
|
||||
<button class="menu_button small_button amily2_profile_type_filter" data-type="rerank">
|
||||
<i class="fas fa-sort-amount-down"></i> 重排序
|
||||
<button class="menu_button small_button amily2_profile_type_filter amily2-vbtn" data-type="rerank">
|
||||
<span class="vbtn-icon"><i class="fas fa-sort-amount-down"></i></span><span class="vbtn-label">重排序</span>
|
||||
</button>
|
||||
<button id="amily2_add_profile" class="menu_button small_button interactable" style="margin-left:auto;">
|
||||
<i class="fas fa-plus"></i> 新建配置
|
||||
<button id="amily2_add_profile" class="menu_button small_button interactable amily2-vbtn" style="margin-left:auto;">
|
||||
<span class="vbtn-icon"><i class="fas fa-plus"></i></span><span class="vbtn-label">新建配置</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -130,16 +139,16 @@
|
||||
<div style="display:flex; gap:6px; align-items:stretch;">
|
||||
<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 id="amily2_pf_fetch_models" class="menu_button small_button interactable amily2-vbtn" type="button" title="从 API 获取可用模型列表(需先填写地址和 Key)">
|
||||
<span class="vbtn-icon"><i class="fas fa-list"></i></span><span class="vbtn-label">获取</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 测试连接 -->
|
||||
<div style="display:flex; align-items:center; gap:10px; margin-bottom:10px;">
|
||||
<button id="amily2_pf_test_conn" class="menu_button small_button interactable" type="button">
|
||||
<i class="fas fa-plug"></i> 测试连接
|
||||
<button id="amily2_pf_test_conn" class="menu_button small_button interactable amily2-vbtn" type="button">
|
||||
<span class="vbtn-icon"><i class="fas fa-plug"></i></span><span class="vbtn-label">测试连接</span>
|
||||
</button>
|
||||
<span id="amily2_pf_test_result" style="font-size:0.85em;"></span>
|
||||
</div>
|
||||
@@ -159,6 +168,13 @@
|
||||
<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>
|
||||
@@ -206,11 +222,11 @@
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<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 id="amily2_profile_modal_cancel" class="menu_button secondary interactable amily2-vbtn">
|
||||
<span class="vbtn-icon"><i class="fas fa-times"></i></span><span class="vbtn-label">取消</span>
|
||||
</button>
|
||||
<button id="amily2_profile_modal_save" class="menu_button interactable">
|
||||
<i class="fas fa-save"></i> 保存
|
||||
<button id="amily2_profile_modal_save" class="menu_button interactable amily2-vbtn">
|
||||
<span class="vbtn-icon"><i class="fas fa-save"></i></span><span class="vbtn-label">保存</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
80
assets/rule-config-panel.html
Normal file
80
assets/rule-config-panel.html
Normal file
@@ -0,0 +1,80 @@
|
||||
<div class="settings-group" id="amily2_rule_config_panel_root">
|
||||
<fieldset class="settings-group">
|
||||
<legend><i class="fas fa-list-check"></i> 规则配置中心</legend>
|
||||
<div class="amily2-rule-layout">
|
||||
<div class="amily2-rule-sidebar">
|
||||
<div style="display:flex; gap:8px; margin-bottom:10px;">
|
||||
<button id="amily2_rule_profile_new" class="menu_button small_button amily2-vbtn"><span class="vbtn-icon"><i class="fas fa-plus"></i></span><span class="vbtn-label">新建</span></button>
|
||||
</div>
|
||||
<div id="amily2_rule_profile_list" style="display:flex; flex-direction:column; gap:8px;"></div>
|
||||
</div>
|
||||
<div class="amily2-rule-main">
|
||||
<div class="amily2_settings_block">
|
||||
<label for="amily2_rule_profile_name">配置名称</label>
|
||||
<input id="amily2_rule_profile_name" class="text_pole" type="text" placeholder="例如:通用提取规则">
|
||||
</div>
|
||||
<div class="amily2_settings_block" style="margin-top:10px;">
|
||||
<label><input id="amily2_rule_profile_tag_toggle" type="checkbox"> 启用标签提取</label>
|
||||
</div>
|
||||
<div id="amily2_rule_profile_tags_wrap" class="amily2_settings_block" style="display:none; margin-top:10px;">
|
||||
<label for="amily2_rule_profile_tags">标签列表</label>
|
||||
<textarea id="amily2_rule_profile_tags" class="text_pole" rows="3" placeholder="例如:content,details,summary"></textarea>
|
||||
</div>
|
||||
<div class="amily2_settings_block" style="margin-top:10px;">
|
||||
<label>排除规则</label>
|
||||
<div id="amily2_rule_profile_rules" style="display:flex; flex-direction:column; gap:8px; margin:8px 0;"></div>
|
||||
<button id="amily2_rule_profile_add_rule" class="menu_button small_button amily2-vbtn"><span class="vbtn-icon"><i class="fas fa-plus"></i></span><span class="vbtn-label">添加规则</span></button>
|
||||
</div>
|
||||
<div class="amily2-rule-actions">
|
||||
<button id="amily2_rule_profile_save" class="menu_button menu_button_primary amily2-vbtn"><span class="vbtn-icon"><i class="fas fa-save"></i></span><span class="vbtn-label">保存</span></button>
|
||||
<button id="amily2_rule_profile_delete" class="menu_button danger amily2-vbtn"><span class="vbtn-icon"><i class="fas fa-trash-alt"></i></span><span class="vbtn-label">删除</span></button>
|
||||
<button id="amily2_back_to_main_from_rule_config" class="menu_button amily2-vbtn"><span class="vbtn-icon"><i class="fas fa-arrow-left"></i></span><span class="vbtn-label">返回</span></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
<style>
|
||||
#amily2_rule_config_panel .amily2-rule-row,
|
||||
#amily2_rule_config_panel_root .amily2-rule-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr auto;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
#amily2_rule_config_panel_root .amily2-rule-layout {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
#amily2_rule_config_panel_root .amily2-rule-sidebar {
|
||||
width: 260px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
#amily2_rule_config_panel_root .amily2-rule-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
#amily2_rule_config_panel_root .amily2-rule-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
#amily2_rule_config_panel_root .amily2-rule-sidebar {
|
||||
width: 100%;
|
||||
}
|
||||
#amily2_rule_config_panel_root .amily2-rule-actions > .amily2-vbtn {
|
||||
flex: 1 1 calc(33.333% - 8px);
|
||||
min-width: 72px;
|
||||
}
|
||||
#amily2_rule_config_panel_root .amily2-rule-row {
|
||||
grid-template-columns: 1fr 1fr !important;
|
||||
}
|
||||
#amily2_rule_config_panel_root .amily2-rule-row > :last-child {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -772,3 +772,18 @@ hr.header-divider {
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid var(--SmartThemeBorderColor);
|
||||
}
|
||||
|
||||
/* 图标在上、文字在下的垂直按钮 */
|
||||
.amily2-vbtn {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 3px;
|
||||
padding: 8px 12px;
|
||||
min-width: 56px;
|
||||
text-align: center;
|
||||
line-height: 1;
|
||||
}
|
||||
.amily2-vbtn .vbtn-icon { font-size: 1.15em; }
|
||||
.amily2-vbtn .vbtn-label { font-size: 0.82em; white-space: nowrap; }
|
||||
|
||||
@@ -179,9 +179,12 @@ class Amily2Updater {
|
||||
if (this.compareVersions(this.latestVersion, this.currentVersion) > 0) {
|
||||
$updateIndicator.show();
|
||||
$updateButton.attr('title', `发现新版本 ${this.latestVersion}!点击查看详情`);
|
||||
const safeVersion = /^[\w.+\-]{1,40}$/.test(String(this.latestVersion ?? '')) ? this.latestVersion : '未知';
|
||||
$updateButtonNew
|
||||
.show()
|
||||
.html(`<i class="fas fa-gift"></i> 新版 ${this.latestVersion}`)
|
||||
.empty()
|
||||
.append($('<i>').addClass('fas fa-gift'))
|
||||
.append(document.createTextNode(` 新版 ${safeVersion}`))
|
||||
.off('click')
|
||||
.on('click', () => this.showUpdateConfirmDialog());
|
||||
} else {
|
||||
|
||||
66
core/api.js
66
core/api.js
@@ -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 { getSlotProfile, providerToApiMode } 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';
|
||||
@@ -329,10 +330,12 @@ async function fetchGoogleDirectModels(apiUrl, apiKey) {
|
||||
const GOOGLE_API_BASE_URL = 'https://generativelanguage.googleapis.com';
|
||||
|
||||
const fetchGoogleModels = async (version) => {
|
||||
const url = `${GOOGLE_API_BASE_URL}/${version}/models?key=${apiKey}`;
|
||||
const url = `${GOOGLE_API_BASE_URL}/${version}/models`;
|
||||
console.log(`[Amily2号-使节团] 正在从 Google API (${version}) 获取模型列表: ${url}`);
|
||||
|
||||
const response = await fetch(url);
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: { 'x-goog-api-key': apiKey },
|
||||
});
|
||||
if (!response.ok) {
|
||||
console.warn(`获取 Google API (${version}) 模型列表失败: ${response.status}`);
|
||||
return [];
|
||||
@@ -441,20 +444,56 @@ export async function getApiSettings(slot = 'main') {
|
||||
// 优先读取槽位分配的 Profile(仅接管连接参数)
|
||||
const profile = await getSlotProfile(slot);
|
||||
if (profile) {
|
||||
const resolvedProvider = profile.provider === 'sillytavern_backend'
|
||||
? 'sillytavern_backend'
|
||||
: providerToApiMode(profile.provider);
|
||||
|
||||
return {
|
||||
apiProvider: profile.provider,
|
||||
apiProvider: resolvedProvider,
|
||||
apiUrl: profile.apiUrl,
|
||||
apiKey: profile.apiKey ?? '',
|
||||
model: profile.model,
|
||||
// 温度 / 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;
|
||||
@@ -489,13 +528,15 @@ export async function testApiConnection() {
|
||||
|
||||
try {
|
||||
const apiSettings = await getApiSettings();
|
||||
const apiProvider = apiSettings.apiProvider || 'openai';
|
||||
const requiresApiKey = !['sillytavern_backend', 'sillytavern_preset'].includes(apiProvider);
|
||||
|
||||
if (apiSettings.apiProvider === 'sillytavern_preset') {
|
||||
if (apiProvider === 'sillytavern_preset') {
|
||||
if (!apiSettings.tavernProfile) {
|
||||
throw new Error("请先在下方选择一个SillyTavern预设");
|
||||
}
|
||||
} else {
|
||||
if (!apiSettings.apiUrl || !apiSettings.apiKey || !apiSettings.model) {
|
||||
if (!apiSettings.apiUrl || !apiSettings.model) {
|
||||
throw new Error("API配置不完整,请检查URL、Key和模型选择");
|
||||
}
|
||||
}
|
||||
@@ -706,12 +747,13 @@ async function callGoogleDirect(messages, options) {
|
||||
const GOOGLE_API_BASE_URL = 'https://generativelanguage.googleapis.com';
|
||||
|
||||
const apiVersion = options.model.includes('gemini-1.5') ? 'v1beta' : 'v1';
|
||||
const finalApiUrl = `${GOOGLE_API_BASE_URL}/${apiVersion}/models/${options.model}:generateContent?key=${options.apiKey}`;
|
||||
|
||||
const finalApiUrl = `${GOOGLE_API_BASE_URL}/${apiVersion}/models/${options.model}:generateContent`;
|
||||
|
||||
console.log(`[Amily2号-Google直连] API地址: ${finalApiUrl}`);
|
||||
|
||||
const headers = {
|
||||
"Content-Type": "application/json"
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
"x-goog-api-key": options.apiKey,
|
||||
};
|
||||
|
||||
const requestBody = JSON.stringify(convertToGoogleRequest({
|
||||
|
||||
@@ -485,7 +485,8 @@ Example:
|
||||
.replace(/<\/thinking>/gi, '');
|
||||
|
||||
const toolNames = Object.keys(tools);
|
||||
const toolRegex = new RegExp(`<(${toolNames.join('|')})(?:\\s+[^>]*)?>[\\s\\S]*?<\\/\\1>`, 'gi');
|
||||
const escapedToolNames = toolNames.map(n => String(n).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
|
||||
const toolRegex = new RegExp(`<(${escapedToolNames.join('|')})(?:\\s+[^>]*)?>[\\s\\S]*?<\\/\\1>`, 'gi');
|
||||
cleanContent = cleanContent.replace(toolRegex, '').trim();
|
||||
|
||||
if (cleanContent) {
|
||||
|
||||
@@ -18,6 +18,7 @@ import { getPresetPrompts, getMixedOrder } from '../PresetSettings/index.js';
|
||||
import { callAI, generateRandomSeed } from "./api.js";
|
||||
import { callNgmsAI } from "./api/Ngms_api.js";
|
||||
import { executeAutoHide } from "./autoHideManager.js";
|
||||
import { resolveHistoriographyRuleConfig } from "../utils/config/RuleProfileManager.js";
|
||||
|
||||
let reloadEditor = () => {
|
||||
console.warn("[大史官] reloadEditor 函数不可用,可能是旧版本。已使用空函数代替。");
|
||||
@@ -302,9 +303,10 @@ function getRawMessagesForSummary(startFloor, endFloor) {
|
||||
const userName = context.name1 || '用户';
|
||||
const characterName = context.name2 || '角色';
|
||||
|
||||
const useTagExtraction = settings.historiographyTagExtractionEnabled ?? false;
|
||||
const tagsToExtract = useTagExtraction ? (settings.historiographyTags || '').split(',').map(t => t.trim()).filter(Boolean) : [];
|
||||
const exclusionRules = settings.historiographyExclusionRules || [];
|
||||
const historiographyRuleConfig = resolveHistoriographyRuleConfig(settings);
|
||||
const useTagExtraction = historiographyRuleConfig.tagExtractionEnabled ?? false;
|
||||
const tagsToExtract = useTagExtraction ? (historiographyRuleConfig.tags || '').split(',').map(t => t.trim()).filter(Boolean) : [];
|
||||
const exclusionRules = historiographyRuleConfig.exclusionRules || [];
|
||||
|
||||
const messages = historySlice.map((msg, index) => {
|
||||
let content = msg.mes;
|
||||
|
||||
@@ -329,7 +329,7 @@ export async function getPlotOptimizedWorldbookContent(context, apiSettings, isC
|
||||
selectedWorldbooks: apiSettings.plotOpt_selectedWorldbooks,
|
||||
autoSelectWorldbooks: apiSettings.plotOpt_autoSelectWorldbooks || [],
|
||||
worldbookCharLimit: apiSettings.plotOpt_worldbookCharLimit,
|
||||
contextLimit: apiSettings.plotOpt_contextLimit || 5,
|
||||
contextLimit: apiSettings.plotOpt_contextLimit ?? apiSettings.plotOpt_contextTurnCount ?? 5,
|
||||
enabledWorldbookEntries: apiSettings.plotOpt_enabledWorldbookEntries,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -9,20 +9,20 @@ import {
|
||||
buildGoogleEmbeddingApiUrl
|
||||
} from './utils/googleAdapter.js';
|
||||
import { getSlotProfile } from './api/api-resolver.js';
|
||||
import { extensionName } from '../utils/settings.js';
|
||||
|
||||
const MODULE_NAME = 'hanlinyuan-rag-core';
|
||||
const GOOGLE_API_BASE_URL = 'https://generativelanguage.googleapis.com';
|
||||
|
||||
function getSettings() {
|
||||
const context = SillyTavern.getContext();
|
||||
if (!context || !context.extensionSettings || !context.extensionSettings[MODULE_NAME]) {
|
||||
console.error('[翰林院-API] 无法获取设置,API调用可能失败。');
|
||||
return {
|
||||
retrieval: {},
|
||||
rerank: {}
|
||||
};
|
||||
}
|
||||
return context.extensionSettings[MODULE_NAME];
|
||||
const root = extension_settings[extensionName];
|
||||
const nested = root && root[MODULE_NAME];
|
||||
if (nested) return nested;
|
||||
// 读侧兼容:若迁移尚未触发(极早期调用),回退至旧顶层位置,避免空配置。
|
||||
const legacy = extension_settings[MODULE_NAME];
|
||||
if (legacy) return legacy;
|
||||
console.error('[翰林院-API] 无法获取设置,API调用可能失败。');
|
||||
return { retrieval: {}, rerank: {} };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -112,9 +112,11 @@ export async function fetchEmbeddingModels(overrideSettings = null) {
|
||||
if (!apiKey) throw new Error("Google直连模式需要API Key。");
|
||||
|
||||
const fetchGoogleModels = async (version) => {
|
||||
const url = `${GOOGLE_API_BASE_URL}/${version}/models?key=${apiKey}`;
|
||||
const url = `${GOOGLE_API_BASE_URL}/${version}/models`;
|
||||
console.log(`[翰林院] 正在从 Google API (${version}) 获取模型列表: ${url}`);
|
||||
const response = await fetch(url);
|
||||
const response = await fetch(url, {
|
||||
headers: { 'x-goog-api-key': apiKey },
|
||||
});
|
||||
if (!response.ok) {
|
||||
console.warn(`获取 Google API (${version}) 模型列表失败: ${response.status}`);
|
||||
return [];
|
||||
@@ -345,8 +347,8 @@ export async function getEmbeddings(texts, signal = null) {
|
||||
console.log('[翰林院-API] 使用Google直连模式获取向量。');
|
||||
if (!apiKey) throw new Error('Google直连模式需要API Key。');
|
||||
|
||||
// 使用适配器构建URL和请求体
|
||||
const googleUrl = `${buildGoogleEmbeddingApiUrl(GOOGLE_API_BASE_URL, embeddingModel)}?key=${apiKey}`;
|
||||
// 使用适配器构建URL和请求体;Key 通过 x-goog-api-key 头传递避免 URL 泄露
|
||||
const googleUrl = buildGoogleEmbeddingApiUrl(GOOGLE_API_BASE_URL, embeddingModel);
|
||||
const googleBody = buildGoogleEmbeddingRequest(batch, embeddingModel);
|
||||
|
||||
console.log(`[翰林院-API] 发送到 Google API 的请求 URL: ${googleUrl}`);
|
||||
@@ -356,6 +358,7 @@ export async function getEmbeddings(texts, signal = null) {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-goog-api-key': apiKey,
|
||||
},
|
||||
body: JSON.stringify(googleBody),
|
||||
signal: signal,
|
||||
|
||||
@@ -4,13 +4,17 @@ import {
|
||||
extension_prompt_roles,
|
||||
setExtensionPrompt,
|
||||
eventSource,
|
||||
event_types
|
||||
event_types,
|
||||
saveSettingsDebounced
|
||||
} from '/script.js';
|
||||
import { extension_settings } from '/scripts/extensions.js';
|
||||
|
||||
import * as ContextUtils from './utils/context-utils.js';
|
||||
import { getCollectionIdInfo, getCharacterId, getCharacterStableId } from './utils/context-utils.js';
|
||||
import { defaultSettings as ragDefaultSettings } from './rag-settings.js';
|
||||
import { extractBlocksByTags, applyExclusionRules } from './utils/rag-tag-extractor.js';
|
||||
import { resolveQueryPreprocessingRuleConfig } from '../utils/config/RuleProfileManager.js';
|
||||
import { extensionName } from '../utils/settings.js';
|
||||
import * as IngestionManager from './ingestion-manager.js';
|
||||
import {
|
||||
getEmbeddings,
|
||||
@@ -79,12 +83,23 @@ function containsPinyinMatch(text, query) {
|
||||
|
||||
|
||||
function highlightSearchMatch(text, query) {
|
||||
const safeText = String(text ?? '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
if (!query || !query.trim()) {
|
||||
return text;
|
||||
return safeText;
|
||||
}
|
||||
|
||||
const regex = new RegExp(`(${query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
|
||||
return text.replace(regex, '<mark class="search-highlight">$1</mark>');
|
||||
const safeQuery = String(query)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
const regex = new RegExp(`(${safeQuery.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
|
||||
return safeText.replace(regex, '<mark class="search-highlight">$1</mark>');
|
||||
}
|
||||
|
||||
function debounce(func, wait) {
|
||||
@@ -136,6 +151,7 @@ function initialize() {
|
||||
console.error('[翰林院] 未能获取SillyTavern上下文,初始化失败。');
|
||||
return;
|
||||
}
|
||||
migrateLegacyRagSettings();
|
||||
settings = getSettings();
|
||||
if (!window.hanlinyuanRagProcessor) {
|
||||
window.hanlinyuanRagProcessor = {};
|
||||
@@ -284,17 +300,16 @@ async function ingestTextToHanlinyuan(text, source = 'manual', metadata = {}, pr
|
||||
}
|
||||
|
||||
function getSettings() {
|
||||
if (!context || !context.extensionSettings) {
|
||||
|
||||
return structuredClone(ragDefaultSettings);
|
||||
if (!extension_settings[extensionName]) {
|
||||
extension_settings[extensionName] = {};
|
||||
}
|
||||
|
||||
const root = extension_settings[extensionName];
|
||||
|
||||
let s = context.extensionSettings[MODULE_NAME];
|
||||
let s = root[MODULE_NAME];
|
||||
|
||||
if (!s) {
|
||||
s = {};
|
||||
context.extensionSettings[MODULE_NAME] = s;
|
||||
root[MODULE_NAME] = s;
|
||||
}
|
||||
|
||||
if (s.condensationHistory === undefined) {
|
||||
@@ -331,16 +346,49 @@ function getSettings() {
|
||||
}
|
||||
|
||||
function saveSettings() {
|
||||
|
||||
if (context) context.saveSettingsDebounced();
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
function resetSettings() {
|
||||
|
||||
if (context) {
|
||||
context.extensionSettings[MODULE_NAME] = structuredClone(ragDefaultSettings);
|
||||
saveSettings();
|
||||
if (!extension_settings[extensionName]) {
|
||||
extension_settings[extensionName] = {};
|
||||
}
|
||||
extension_settings[extensionName][MODULE_NAME] = structuredClone(ragDefaultSettings);
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function migrateLegacyRagSettings() {
|
||||
const legacy = extension_settings[MODULE_NAME];
|
||||
if (!legacy || typeof legacy !== 'object') return;
|
||||
|
||||
if (!extension_settings[extensionName]) {
|
||||
extension_settings[extensionName] = {};
|
||||
}
|
||||
const root = extension_settings[extensionName];
|
||||
|
||||
// legacy 是用户此前实际交互过的真数据来源;nested 可能已被 super-memory 等模块用默认值填过,
|
||||
// 因此采用 legacy-优先的深合并:legacy 中的叶子值覆盖 nested,nested 中 legacy 没有的键保留。
|
||||
if (!root[MODULE_NAME] || typeof root[MODULE_NAME] !== 'object') {
|
||||
root[MODULE_NAME] = legacy;
|
||||
console.log(`[翰林院] 已迁移旧版 '${MODULE_NAME}' 设置到 extension_settings['${extensionName}']。`);
|
||||
} else {
|
||||
const merged = root[MODULE_NAME];
|
||||
const overlayLegacy = (src, dst) => {
|
||||
for (const key of Object.keys(src)) {
|
||||
const sv = src[key];
|
||||
if (sv && typeof sv === 'object' && !Array.isArray(sv) && dst[key] && typeof dst[key] === 'object' && !Array.isArray(dst[key])) {
|
||||
overlayLegacy(sv, dst[key]);
|
||||
} else {
|
||||
dst[key] = sv;
|
||||
}
|
||||
}
|
||||
};
|
||||
overlayLegacy(legacy, merged);
|
||||
console.log(`[翰林院] 发现新旧两处配置;已将顶层 '${MODULE_NAME}' 深合并覆盖到 extension_settings['${extensionName}']。`);
|
||||
}
|
||||
|
||||
delete extension_settings[MODULE_NAME];
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
function showNotification(message, type = 'info') {
|
||||
@@ -1324,7 +1372,7 @@ function preprocessQueryText(queryText) {
|
||||
}
|
||||
|
||||
let processedText = queryText;
|
||||
const { tagExtractionEnabled, tags, exclusionRules } = settings.queryPreprocessing;
|
||||
const { tagExtractionEnabled, tags, exclusionRules } = resolveQueryPreprocessingRuleConfig(settings);
|
||||
|
||||
if (tagExtractionEnabled && tags) {
|
||||
const tagsToExtract = tags.split(',').map(t => t.trim()).filter(Boolean);
|
||||
@@ -1438,7 +1486,7 @@ async function rearrangeChat(chat, contextSize, abort, type) {
|
||||
const queryMessages = chat.slice(-settings.advanced.queryMessageCount);
|
||||
if (queryMessages.length === 0) return;
|
||||
|
||||
const queryPreprocessingSettings = settings.queryPreprocessing;
|
||||
const queryPreprocessingSettings = resolveQueryPreprocessingRuleConfig(settings);
|
||||
let queryText = '';
|
||||
const relevantTexts = [];
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { getContext, extension_settings } from "/scripts/extensions.js";
|
||||
import { saveSettingsDebounced } from "/script.js";
|
||||
import { getCharacterStableId } from "../utils/context-utils.js";
|
||||
import { getMemoryState } from "../table-system/manager.js";
|
||||
import { extensionName } from "../../utils/settings.js";
|
||||
@@ -160,26 +161,62 @@ export function getRelatedNodes(nodeId, maxDepth = 1) {
|
||||
return related;
|
||||
}
|
||||
|
||||
function getGraphStore(create = false) {
|
||||
if (!extension_settings[extensionName]) {
|
||||
if (!create) return null;
|
||||
extension_settings[extensionName] = {};
|
||||
}
|
||||
const root = extension_settings[extensionName];
|
||||
if (!root.relationship_graphs) {
|
||||
if (!create) return null;
|
||||
root.relationship_graphs = {};
|
||||
}
|
||||
return root.relationship_graphs;
|
||||
}
|
||||
|
||||
function migrateLegacyRelationshipGraphs() {
|
||||
const legacy = extension_settings.relationship_graphs;
|
||||
if (!legacy || typeof legacy !== 'object') return;
|
||||
|
||||
if (!extension_settings[extensionName]) {
|
||||
extension_settings[extensionName] = {};
|
||||
}
|
||||
const root = extension_settings[extensionName];
|
||||
|
||||
if (!root.relationship_graphs) {
|
||||
root.relationship_graphs = legacy;
|
||||
console.log(`[关系图谱] 已迁移旧版 'relationship_graphs' 到 extension_settings['${extensionName}']。`);
|
||||
} else {
|
||||
console.log(`[关系图谱] 发现遗留顶层 'relationship_graphs',但新位置已存在;合并遗留数据并清理顶层。`);
|
||||
for (const [cid, data] of Object.entries(legacy)) {
|
||||
if (!root.relationship_graphs[cid]) {
|
||||
root.relationship_graphs[cid] = data;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
delete extension_settings.relationship_graphs;
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
export async function saveGraph() {
|
||||
const context = getContext();
|
||||
const charId = getCharacterStableId();
|
||||
if (!charId) return;
|
||||
|
||||
if (!context.extensionSettings.relationship_graphs) {
|
||||
context.extensionSettings.relationship_graphs = {};
|
||||
}
|
||||
|
||||
context.extensionSettings.relationship_graphs[charId] = graphData;
|
||||
context.saveSettingsDebounced();
|
||||
const store = getGraphStore(true);
|
||||
if (!store) return;
|
||||
|
||||
store[charId] = graphData;
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
export async function loadGraph() {
|
||||
const context = getContext();
|
||||
const charId = getCharacterStableId();
|
||||
if (!charId) return;
|
||||
|
||||
if (context.extensionSettings.relationship_graphs && context.extensionSettings.relationship_graphs[charId]) {
|
||||
graphData = context.extensionSettings.relationship_graphs[charId];
|
||||
const store = getGraphStore(false);
|
||||
if (store && store[charId]) {
|
||||
graphData = store[charId];
|
||||
console.log(`[关系图谱] 已加载角色 ${charId} 的图谱: ${graphData.nodes.length} 个节点, ${graphData.edges.length} 条边。`);
|
||||
} else {
|
||||
graphData = { nodes: [], edges: [] };
|
||||
@@ -188,6 +225,7 @@ export async function loadGraph() {
|
||||
|
||||
const context = getContext();
|
||||
if (context) {
|
||||
migrateLegacyRelationshipGraphs();
|
||||
loadGraph();
|
||||
document.addEventListener('AMILY2_TABLE_UPDATED', (e) => {
|
||||
const { tableName } = e.detail;
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
import { getBatchFillerFlowTemplate, convertTablesToCsvString, updateTableFromText, saveStateToMessage, getMemoryState } from './table-system/manager.js';
|
||||
import { saveChat } from "/script.js";
|
||||
import { renderTables } from '../ui/table-bindings.js';
|
||||
import { resolveHistoriographyRuleConfig } from "../utils/config/RuleProfileManager.js";
|
||||
|
||||
import { getPresetPrompts, getMixedOrder } from '../PresetSettings/index.js';
|
||||
import { callAI, generateRandomSeed } from './api.js';
|
||||
@@ -423,14 +424,15 @@ export async function processPlotOptimization(currentUserMessage, contextMessage
|
||||
}
|
||||
|
||||
let history = '';
|
||||
const contextLimit = settings.plotOpt_contextLimit || 0;
|
||||
const contextLimit = settings.plotOpt_contextLimit ?? settings.plotOpt_contextTurnCount ?? 0;
|
||||
if (contextLimit > 0 && contextMessages.length > 0) {
|
||||
const historyMessages = contextMessages.slice(-contextLimit);
|
||||
|
||||
// 复刻 Historiographer 的标签提取与内容排除逻辑
|
||||
const useTagExtraction = settings.historiographyTagExtractionEnabled ?? false;
|
||||
const tagsToExtract = useTagExtraction ? (settings.historiographyTags || '').split(',').map(t => t.trim()).filter(Boolean) : [];
|
||||
const exclusionRules = settings.historiographyExclusionRules || [];
|
||||
const historiographyRuleConfig = resolveHistoriographyRuleConfig(settings);
|
||||
const useTagExtraction = historiographyRuleConfig.tagExtractionEnabled ?? false;
|
||||
const tagsToExtract = useTagExtraction ? (historiographyRuleConfig.tags || '').split(',').map(t => t.trim()).filter(Boolean) : [];
|
||||
const exclusionRules = historiographyRuleConfig.exclusionRules || [];
|
||||
|
||||
history = historyMessages
|
||||
.map(msg => {
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -9,6 +9,7 @@ import { getPresetPrompts, getMixedOrder } from '../../PresetSettings/index.js';
|
||||
import { callAI, generateRandomSeed } from '../api.js';
|
||||
import { callNccsAI } from '../api/NccsApi.js';
|
||||
import { extractBlocksByTags, applyExclusionRules } from '../utils/rag-tag-extractor.js';
|
||||
import { resolveTableRuleConfig } from '../../utils/config/RuleProfileManager.js';
|
||||
|
||||
import { getBatchFillerRuleTemplate, getBatchFillerFlowTemplate, convertTablesToCsvString } from './manager.js';
|
||||
|
||||
@@ -22,7 +23,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 +115,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 +142,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;
|
||||
@@ -152,10 +153,11 @@ function getRawMessagesForSummary(startFloor, endFloor) {
|
||||
let tagsToExtract = [];
|
||||
let exclusionRules = [];
|
||||
|
||||
if (settings.table_independent_rules_enabled) {
|
||||
log('批量填表:使用独立提取规则。', 'info');
|
||||
tagsToExtract = (settings.table_tags_to_extract || '').split(',').map(t => t.trim()).filter(Boolean);
|
||||
exclusionRules = settings.table_exclusion_rules || [];
|
||||
const tableRuleConfig = resolveTableRuleConfig(settings);
|
||||
if (tableRuleConfig.tags || (tableRuleConfig.exclusionRules && tableRuleConfig.exclusionRules.length)) {
|
||||
log('批量填表:使用提取规则配置。', 'info');
|
||||
tagsToExtract = (tableRuleConfig.tags || '').split(',').map(t => t.trim()).filter(Boolean);
|
||||
exclusionRules = tableRuleConfig.exclusionRules || [];
|
||||
}
|
||||
|
||||
const messages = historySlice.map((msg, index) => {
|
||||
@@ -319,7 +321,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 +389,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');
|
||||
|
||||
@@ -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('表格系统总开关已关闭,无法执行回退填表。');
|
||||
|
||||
@@ -4,11 +4,11 @@ import { renderTables } from '../../ui/table-bindings.js';
|
||||
import { extensionName } from "../../utils/settings.js";
|
||||
import { convertTablesToCsvString, convertSelectedTablesToCsvString, saveStateToMessage, getMemoryState, updateTableFromText, getBatchFillerRuleTemplate, getBatchFillerFlowTemplate } from './manager.js';
|
||||
import { getPresetPrompts, getMixedOrder } from '../../PresetSettings/index.js';
|
||||
import { callAI, generateRandomSeed, getApiSettings } from '../api.js';
|
||||
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,13 +20,6 @@ export async function reorganizeTableContent(selectedTableIndices) {
|
||||
return;
|
||||
}
|
||||
|
||||
const resolvedApi = await getApiSettings('main');
|
||||
const { apiUrl, apiKey, model, temperature, maxTokens, forceProxyForCustomApi } = resolvedApi ?? settings;
|
||||
if (!apiUrl || !model) {
|
||||
toastr.error("主API的URL或模型未配置,重新整理功能无法启动。", "Amily2-重新整理");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
toastr.info('正在重新整理表格内容...', 'Amily2-重新整理');
|
||||
|
||||
|
||||
@@ -6,14 +6,15 @@ import { updateOrInsertTableInChat } from '../../ui/message-table-renderer.js';
|
||||
import { extensionName } from "../../utils/settings.js";
|
||||
import { updateTableFromText, getBatchFillerRuleTemplate, getBatchFillerFlowTemplate, convertTablesToCsvString, saveStateToMessage, getMemoryState, clearHighlights } from './manager.js';
|
||||
import { getPresetPrompts, getMixedOrder } from '../../PresetSettings/index.js';
|
||||
import { callAI, generateRandomSeed, getApiSettings } from '../api.js';
|
||||
import { callAI, generateRandomSeed } from '../api.js';
|
||||
import { callNccsAI } from '../api/NccsApi.js';
|
||||
import { extractBlocksByTags, applyExclusionRules } from '../utils/rag-tag-extractor.js';
|
||||
import { resolveTableRuleConfig } from '../../utils/config/RuleProfileManager.js';
|
||||
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 +68,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,15 +93,6 @@ export async function fillWithSecondaryApi(latestMessage, forceRun = false) {
|
||||
return;
|
||||
}
|
||||
|
||||
const resolvedApi = await getApiSettings('main');
|
||||
const { apiUrl, apiKey, model, temperature, maxTokens, forceProxyForCustomApi } = resolvedApi ?? 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);
|
||||
@@ -180,9 +172,10 @@ export async function fillWithSecondaryApi(latestMessage, forceRun = false) {
|
||||
|
||||
let tagsToExtract = [];
|
||||
let exclusionRules = [];
|
||||
if (settings.table_independent_rules_enabled) {
|
||||
tagsToExtract = (settings.table_tags_to_extract || '').split(',').map(t => t.trim()).filter(Boolean);
|
||||
exclusionRules = settings.table_exclusion_rules || [];
|
||||
const tableRuleConfig = resolveTableRuleConfig(settings);
|
||||
if (tableRuleConfig.tags || (tableRuleConfig.exclusionRules && tableRuleConfig.exclusionRules.length)) {
|
||||
tagsToExtract = (tableRuleConfig.tags || '').split(',').map(t => t.trim()).filter(Boolean);
|
||||
exclusionRules = tableRuleConfig.exclusionRules || [];
|
||||
}
|
||||
|
||||
let coreContentText = "";
|
||||
|
||||
@@ -131,6 +131,7 @@ export {
|
||||
};
|
||||
|
||||
export const tableSystemDefaultSettings = {
|
||||
table_system_enabled: true,
|
||||
table_injection_enabled: false,
|
||||
|
||||
injection: {
|
||||
|
||||
@@ -137,7 +137,12 @@ export function progressTracker(operationId, maxAttempts) {
|
||||
container.style.backgroundColor = 'rgba(80,0,0,0.9)';
|
||||
progress.style.display = 'none';
|
||||
info.style.whiteSpace = 'pre-wrap';
|
||||
info.innerHTML = `<span style="color:#ff9494">错误详情:</span>\n${errorMsg}`;
|
||||
info.innerHTML = '';
|
||||
const label = document.createElement('span');
|
||||
label.style.color = '#ff9494';
|
||||
label.textContent = '错误详情:';
|
||||
info.appendChild(label);
|
||||
info.appendChild(document.createTextNode('\n' + String(errorMsg ?? '')));
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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];
|
||||
// 敏感字段从 configManager(localStorage)读取,其余从 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);
|
||||
@@ -311,7 +321,7 @@ async function renderWorldBookEntries() {
|
||||
</div>
|
||||
<div class="entry-content-display">${renderContent(entry.content)}</div>
|
||||
<div class="entry-content-editor" style="display: none;">
|
||||
<textarea class="text_pole" style="width: 98%; min-height: 150px;">${entry.content}</textarea>
|
||||
<textarea class="text_pole" style="width: 98%; min-height: 150px;">${escapeHTML(entry.content || '')}</textarea>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -367,7 +377,12 @@ async function renderWorldBookEntries() {
|
||||
|
||||
} catch (error) {
|
||||
console.error('加载世界书条目失败:', error);
|
||||
container.innerHTML = `<p style="text-align:center; color: #ff8a8a;">加载失败: ${error.message}</p>`;
|
||||
const p = document.createElement('p');
|
||||
p.style.textAlign = 'center';
|
||||
p.style.color = '#ff8a8a';
|
||||
p.textContent = `加载失败: ${error?.message ?? '未知错误'}`;
|
||||
container.innerHTML = '';
|
||||
container.appendChild(p);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import './SL/bus/Amily2Bus.js'
|
||||
import './utils/config/ConfigManager.js'
|
||||
import './utils/config/api-key-store/ApiKeyStore.js'
|
||||
import './utils/config/ApiProfileManager.js'
|
||||
import './utils/config/RuleProfileManager.js'
|
||||
import './core/table-system/TableSystemService.js'
|
||||
|
||||
// Re-exports (重新导出供 index.js 使用)
|
||||
@@ -23,6 +24,7 @@ export { characters, this_chid, eventSource, event_types, saveSettingsDebounced
|
||||
// Core Systems
|
||||
export { injectTableData, generateTableContent } from "./core/table-system/injector.js";
|
||||
export { initialize as initializeRagProcessor } from "./core/rag-processor.js";
|
||||
export { loadSettingsToUI as loadHanlinyuanSettingsToUI } from "./ui/hanlinyuan-bindings.js";
|
||||
export { loadTables, clearHighlights, rollbackAndRefill, rollbackState, commitPendingDeletions, saveStateToMessage, getMemoryState, clearUpdatedTables } from './core/table-system/manager.js';
|
||||
export { fillWithSecondaryApi } from './core/table-system/secondary-filler.js';
|
||||
export { renderTables } from './ui/table-bindings.js';
|
||||
@@ -33,7 +35,9 @@ export { pluginVersion, extensionName, defaultSettings } from './utils/settings.
|
||||
export { configManager } from './utils/config/ConfigManager.js';
|
||||
export { apiKeyStore } from './utils/config/api-key-store/ApiKeyStore.js';
|
||||
export { apiProfileManager, PROFILE_TYPES, SLOTS } from './utils/config/ApiProfileManager.js';
|
||||
export { ruleProfileManager, RULE_SLOTS, resolveSlotRuleConfig, resolveCondensationRuleConfig, resolveQueryPreprocessingRuleConfig, resolveTableRuleConfig, resolveHistoriographyRuleConfig, resolveRuleConfig } from './utils/config/RuleProfileManager.js';
|
||||
export { bindApiConfigPanel } from './ui/api-config-bindings.js';
|
||||
export { bindRuleConfigPanel } from './ui/rule-config-bindings.js';
|
||||
export { checkAuthorization, refreshUserInfo } from './utils/auth.js';
|
||||
export { tableSystemDefaultSettings } from './core/table-system/settings.js';
|
||||
export { manageLorebookEntriesForChat } from './core/lore.js';
|
||||
|
||||
22
index.js
22
index.js
@@ -8,6 +8,7 @@ import {
|
||||
characters, this_chid, eventSource, event_types, saveSettingsDebounced,
|
||||
injectTableData, generateTableContent,
|
||||
initializeRagProcessor,
|
||||
loadHanlinyuanSettingsToUI,
|
||||
loadTables, clearHighlights, rollbackAndRefill, rollbackState, commitPendingDeletions, saveStateToMessage, getMemoryState, clearUpdatedTables,
|
||||
fillWithSecondaryApi,
|
||||
renderTables,
|
||||
@@ -568,11 +569,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;
|
||||
}
|
||||
|
||||
@@ -609,7 +611,7 @@ async function onPlotGenerationAfterCommands(type, params, dryRun) {
|
||||
}, 100);
|
||||
});
|
||||
|
||||
const contextTurnCount = globalSettings.plotOpt_contextLimit || 10;
|
||||
const contextTurnCount = globalSettings.plotOpt_contextLimit ?? globalSettings.plotOpt_contextTurnCount ?? 10;
|
||||
const contextSource = isFromTextarea ? context.chat : context.chat.slice(0, -1);
|
||||
const slicedContext = contextTurnCount > 0 ? contextSource.slice(-contextTurnCount) : contextSource;
|
||||
|
||||
@@ -772,6 +774,15 @@ function initializeRagAndInjection() {
|
||||
console.error('[Amily2-翰林院] RAG处理器初始化失败:', error);
|
||||
}
|
||||
|
||||
// 此时 ST settings hydration 已完成,且 RAG 第二次 init 拿到的是真实 saved settings 引用。
|
||||
// mount 阶段那次 loadSettingsToUI 跑得过早(hydration 之前),UI 拿到的是默认值;
|
||||
// 在此重跑一次以让翰林院面板显示真实持久化值。
|
||||
try {
|
||||
loadHanlinyuanSettingsToUI();
|
||||
} catch (error) {
|
||||
console.error('[Amily2-翰林院] 步骤五重载面板设置失败:', error);
|
||||
}
|
||||
|
||||
console.log("[Amily2号-开国大典] 步骤六:智能冲突检测与注入策略...");
|
||||
console.log('[Amily2-策略] 采用“完全主导”策略,覆盖 `vectors_rearrangeChat`。');
|
||||
window['vectors_rearrangeChat'] = executeAmily2Injection;
|
||||
@@ -879,6 +890,7 @@ jQuery(async () => {
|
||||
initializeAmilyHelper();
|
||||
mergePluginSettings();
|
||||
configManager.migrate(); // 将 extension_settings 中残留的敏感字段迁移到 localStorage
|
||||
await configManager.init();
|
||||
|
||||
let attempts = 0;
|
||||
const maxAttempts = 100;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "Amily2号聊天优化助手",
|
||||
"display_name": "Amily2号助手",
|
||||
"version": "2.0.1",
|
||||
"version": "2.1.1",
|
||||
"author": "Wx-2025",
|
||||
"description": "一个拥有独立UI的智能引擎,正文优化、自动总结、记忆表格、rag向量、隐藏楼层、剧情推进等多功能整合。",
|
||||
"minSillyTavernVersion": "1.10.0",
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
import { apiProfileManager, PROFILE_TYPES, SLOTS } from '../utils/config/ApiProfileManager.js';
|
||||
import { apiKeyStore } from '../utils/config/api-key-store/ApiKeyStore.js';
|
||||
import { configManager } from '../utils/config/ConfigManager.js';
|
||||
import { getRequestHeaders, saveSettingsDebounced } from '/script.js';
|
||||
import { extension_settings } from '/scripts/extensions.js';
|
||||
import { extensionName } from '../utils/settings.js';
|
||||
@@ -97,6 +98,7 @@ function _bindStorageMode($c) {
|
||||
const $select = $c.find('#amily2_keystore_mode');
|
||||
const $cloud = $c.find('#amily2_cloud_key_section');
|
||||
const $note = $c.find('#amily2_keystore_mode_note');
|
||||
const $importInput = $c.find('#amily2_import_key_bundle_input');
|
||||
|
||||
const MODE_NOTES = {
|
||||
local: '本地存储:API Key 仅存于本设备浏览器,绝不上传服务端。换设备需重新填写。',
|
||||
@@ -124,6 +126,9 @@ function _bindStorageMode($c) {
|
||||
|
||||
try {
|
||||
await apiKeyStore.setMode(newMode);
|
||||
if (newMode === 'cloud') {
|
||||
await configManager.syncSensitiveCache({ force: true });
|
||||
}
|
||||
$cloud.toggle(newMode === 'cloud');
|
||||
$note.text(MODE_NOTES[newMode]);
|
||||
if (newMode === 'cloud') _refreshFingerprint($c);
|
||||
@@ -142,6 +147,43 @@ function _bindStorageMode($c) {
|
||||
_refreshFingerprint($c);
|
||||
toastr.warning('新密钥对已生成,请重新输入各 Profile 的 API Key。');
|
||||
});
|
||||
|
||||
$c.find('#amily2_export_key_bundle').on('click', async () => {
|
||||
try {
|
||||
const bundle = await apiKeyStore.exportPrivateKeyBundle();
|
||||
_downloadJson(
|
||||
`amily2-keystore-${_timestampForFilename()}.json`,
|
||||
bundle
|
||||
);
|
||||
toastr.success('私钥包已导出,请妥善保管。');
|
||||
} catch (e) {
|
||||
console.error('[ApiConfig] 导出私钥包失败:', e);
|
||||
toastr.error(e.message || '导出私钥包失败。');
|
||||
}
|
||||
});
|
||||
|
||||
$c.find('#amily2_import_key_bundle').on('click', () => {
|
||||
$importInput.val('');
|
||||
$importInput.trigger('click');
|
||||
});
|
||||
|
||||
$importInput.on('change', async function () {
|
||||
const file = this.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
try {
|
||||
const text = await file.text();
|
||||
await apiKeyStore.importPrivateKeyBundle(text);
|
||||
await configManager.syncSensitiveCache({ force: true });
|
||||
await _refreshFingerprint($c);
|
||||
toastr.success('私钥包导入成功,已尝试恢复云同步的 API Key 缓存。');
|
||||
} catch (e) {
|
||||
console.error('[ApiConfig] 导入私钥包失败:', e);
|
||||
toastr.error(e.message || '导入私钥包失败。');
|
||||
} finally {
|
||||
$importInput.val('');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function _refreshFingerprint($c) {
|
||||
@@ -149,6 +191,24 @@ async function _refreshFingerprint($c) {
|
||||
$c.find('#amily2_keypair_fingerprint').text(fp);
|
||||
}
|
||||
|
||||
function _downloadJson(filename, data) {
|
||||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
function _timestampForFilename() {
|
||||
const now = new Date();
|
||||
const pad = n => String(n).padStart(2, '0');
|
||||
return `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
|
||||
}
|
||||
|
||||
// ── Profile 列表渲染 ──────────────────────────────────────────────────────────
|
||||
|
||||
export function renderProfileList($c) {
|
||||
@@ -343,6 +403,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);
|
||||
@@ -362,6 +423,7 @@ async function openModal($c, id) {
|
||||
_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);
|
||||
@@ -401,6 +463,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;
|
||||
@@ -459,10 +522,11 @@ async function _fetchModels($c) {
|
||||
let models;
|
||||
|
||||
if (provider === 'google') {
|
||||
// Google 用原生 API,以 ?key= 传参,返回 models[] 而非 data[]
|
||||
// Google 用原生 API,Key 通过 x-goog-api-key 头传递避免 URL 泄露
|
||||
if (!apiKey) { toastr.warning('请先填写 Google API Key。'); return; }
|
||||
const resp = await fetch(
|
||||
`https://generativelanguage.googleapis.com/v1beta/models?key=${encodeURIComponent(apiKey)}`
|
||||
'https://generativelanguage.googleapis.com/v1beta/models',
|
||||
{ headers: { 'x-goog-api-key': apiKey } }
|
||||
);
|
||||
if (!resp.ok) {
|
||||
const status = resp.status;
|
||||
@@ -551,7 +615,8 @@ async function _testConnection($c) {
|
||||
return;
|
||||
}
|
||||
const resp = await fetch(
|
||||
`https://generativelanguage.googleapis.com/v1beta/models?key=${encodeURIComponent(apiKey)}`
|
||||
'https://generativelanguage.googleapis.com/v1beta/models',
|
||||
{ headers: { 'x-goog-api-key': apiKey } }
|
||||
);
|
||||
if (resp.ok) {
|
||||
const data = await resp.json();
|
||||
@@ -582,8 +647,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;
|
||||
|
||||
@@ -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";
|
||||
@@ -434,6 +435,10 @@ export function bindModalEvents() {
|
||||
bindAmily2ModalWorldBookSettings();
|
||||
|
||||
const container = $("#amily2_drawer_content").length ? $("#amily2_drawer_content") : $("#amily2_chat_optimiser");
|
||||
const apiConfigButton = container.find('#amily2_open_api_config');
|
||||
if (apiConfigButton.length && !container.find('#amily2_open_rule_config').length) {
|
||||
apiConfigButton.after(' <button id="amily2_open_rule_config" class="menu_button wide_button"><i class="fas fa-list-check"></i> 规则配置</button>');
|
||||
}
|
||||
|
||||
// Collapsible sections logic
|
||||
container.find('.collapsible-legend').each(function() {
|
||||
@@ -801,7 +806,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_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 () {
|
||||
"#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_rule_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_back_to_main_from_rule_config, #amily2_sfigen_back_to_main", function () {
|
||||
if (!pluginAuthStatus.authorized) return;
|
||||
|
||||
const mainPanel = container.find('.plugin-features');
|
||||
@@ -816,6 +821,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 ruleConfigPanel = container.find('#amily2_rule_config_panel');
|
||||
const sfigenPanel = container.find('#amily2_sfigen_panel');
|
||||
|
||||
mainPanel.hide();
|
||||
@@ -830,6 +836,7 @@ export function bindModalEvents() {
|
||||
rendererPanel.hide();
|
||||
superMemoryPanel.hide();
|
||||
apiConfigPanel.hide();
|
||||
ruleConfigPanel.hide();
|
||||
sfigenPanel.hide();
|
||||
|
||||
switch (this.id) {
|
||||
@@ -878,6 +885,9 @@ export function bindModalEvents() {
|
||||
case 'amily2_open_api_config':
|
||||
apiConfigPanel.show();
|
||||
break;
|
||||
case 'amily2_open_rule_config':
|
||||
ruleConfigPanel.show();
|
||||
break;
|
||||
case 'amily2_open_sfigen':
|
||||
sfigenPanel.show();
|
||||
break;
|
||||
@@ -892,6 +902,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_back_to_main_from_rule_config':
|
||||
case 'amily2_sfigen_back_to_main':
|
||||
mainPanel.show();
|
||||
break;
|
||||
@@ -1034,11 +1045,16 @@ export function bindModalEvents() {
|
||||
});
|
||||
|
||||
container
|
||||
.off("change.amily2.text")
|
||||
.on("change.amily2.text", "#amily2_api_url, #amily2_api_key, #amily2_optimization_target_tag", function () {
|
||||
.off("input.amily2.text change.amily2.text")
|
||||
.on("input.amily2.text 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号");
|
||||
});
|
||||
|
||||
@@ -1076,6 +1092,25 @@ export function bindModalEvents() {
|
||||
},
|
||||
);
|
||||
|
||||
container
|
||||
.off("input.amily2.number change.amily2.number")
|
||||
.on(
|
||||
"input.amily2.number change.amily2.number",
|
||||
"#amily2_max_tokens, #amily2_temperature, #amily2_context_messages",
|
||||
function () {
|
||||
if (!pluginAuthStatus.authorized) return;
|
||||
const key = snakeToCamel(this.id.replace("amily2_", ""));
|
||||
const value = this.id.includes("temperature")
|
||||
? parseFloat(this.value)
|
||||
: parseInt(this.value, 10);
|
||||
|
||||
if (Number.isNaN(value)) return;
|
||||
|
||||
$(`#${this.id}_value`).text(value);
|
||||
updateAndSaveSetting(key, value);
|
||||
},
|
||||
);
|
||||
|
||||
const promptMap = {
|
||||
mainPrompt: "#amily2_main_prompt",
|
||||
systemPrompt: "#amily2_system_prompt",
|
||||
@@ -1097,6 +1132,14 @@ export function bindModalEvents() {
|
||||
.off("change.amily2.prompt_selector")
|
||||
.on("change.amily2.prompt_selector", selector, updateEditorView);
|
||||
|
||||
container
|
||||
.off("input.amily2.unified_editor change.amily2.unified_editor")
|
||||
.on("input.amily2.unified_editor change.amily2.unified_editor", editor, function () {
|
||||
const selectedKey = $(selector).val();
|
||||
if (!selectedKey) return;
|
||||
updateAndSaveSetting(selectedKey, $(this).val());
|
||||
});
|
||||
|
||||
container
|
||||
.off("click.amily2.unified_save")
|
||||
.on("click.amily2.unified_save", unifiedSaveButton, function () {
|
||||
@@ -1119,8 +1162,8 @@ export function bindModalEvents() {
|
||||
});
|
||||
|
||||
container
|
||||
.off("change.amily2.lore_settings")
|
||||
.on("change.amily2.lore_settings",
|
||||
.off("input.amily2.lore_settings change.amily2.lore_settings")
|
||||
.on("input.amily2.lore_settings change.amily2.lore_settings",
|
||||
'select[id^="amily2_lore_"], input#amily2_lore_depth_input',
|
||||
function () {
|
||||
if (!pluginAuthStatus.authorized) return;
|
||||
|
||||
@@ -7,6 +7,7 @@ import * as ContextUtils from '../core/utils/context-utils.js';
|
||||
import * as IngestionManager from '../core/ingestion-manager.js';
|
||||
import { showContentModal, showHtmlModal } from './page-window.js';
|
||||
import { extractBlocksByTags, applyExclusionRules } from '../core/utils/rag-tag-extractor.js';
|
||||
import { ruleProfileManager, resolveCondensationRuleConfig } from '../utils/config/RuleProfileManager.js';
|
||||
import {
|
||||
filterWorldbooks,
|
||||
filterWorldbookEntries,
|
||||
@@ -16,6 +17,34 @@ import {
|
||||
|
||||
'use strict';
|
||||
|
||||
function escapeTextareaContent(text) {
|
||||
return String(text ?? '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
}
|
||||
|
||||
function escapeAttribute(text) {
|
||||
return String(text ?? '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
}
|
||||
|
||||
|
||||
function _populateHlyRuleProfileSelect(select, slot, detail) {
|
||||
const profiles = detail?.profiles ?? ruleProfileManager.listProfiles();
|
||||
const assigned = detail?.assignments?.[slot] ?? ruleProfileManager.getAssignment(slot) ?? '';
|
||||
select.innerHTML = [
|
||||
'<option value="">— 未分配 —</option>',
|
||||
...profiles.map(p =>
|
||||
`<option value="${p.id}" ${p.id === assigned ? 'selected' : ''}>${escapeTextareaContent(p.name || p.id)}</option>`
|
||||
),
|
||||
].join('');
|
||||
}
|
||||
|
||||
|
||||
function setupGlobalEventHandlers() {
|
||||
|
||||
window.saveHLYSettings = () => saveSettingsFromUI(false); // false表示非自动保存
|
||||
@@ -46,6 +75,17 @@ function updateAndSaveSetting(key, value) {
|
||||
current[keys[keys.length - 1]] = value;
|
||||
|
||||
HanlinyuanCore.saveSettings();
|
||||
|
||||
if (key === 'condensation.tagExtractionEnabled') {
|
||||
syncHanlinLinkedRuleProfile('condensation', { tagExtractionEnabled: value });
|
||||
} else if (key === 'condensation.tags') {
|
||||
syncHanlinLinkedRuleProfile('condensation', { tags: value });
|
||||
} else if (key === 'queryPreprocessing.tagExtractionEnabled') {
|
||||
syncHanlinLinkedRuleProfile('queryPreprocessing', { tagExtractionEnabled: value });
|
||||
} else if (key === 'queryPreprocessing.tags') {
|
||||
syncHanlinLinkedRuleProfile('queryPreprocessing', { tags: value });
|
||||
}
|
||||
|
||||
log(`[自动保存] 设置项 '${key}' 已更新为: ${JSON.stringify(value)}`, 'success');
|
||||
}
|
||||
|
||||
@@ -121,14 +161,26 @@ export function bindHanlinyuanEvents() {
|
||||
|
||||
// 确保核心已经初始化
|
||||
if (HanlinyuanCore.initialize) {
|
||||
HanlinyuanCore.initialize();
|
||||
try {
|
||||
HanlinyuanCore.initialize();
|
||||
} catch (e) {
|
||||
console.error('[翰林院-枢纽] 核心初始化抛出异常:', e);
|
||||
}
|
||||
} else {
|
||||
console.error('[翰林院-枢纽] 核心法典未能提供初始化圣旨!');
|
||||
return;
|
||||
}
|
||||
|
||||
loadSettingsToUI();
|
||||
loadWorldbookList(); // 【新增】加载书库列表
|
||||
try {
|
||||
loadSettingsToUI();
|
||||
} catch (e) {
|
||||
console.error('[翰林院-枢纽] loadSettingsToUI 抛出异常:', e);
|
||||
}
|
||||
try {
|
||||
loadWorldbookList();
|
||||
} catch (e) {
|
||||
console.error('[翰林院-枢纽] loadWorldbookList 抛出异常:', e);
|
||||
}
|
||||
log('[翰林院-枢纽] 已成功连接各部,政令畅通。', 'info');
|
||||
const fileInput = document.getElementById('hanlinyuan-ingest-novel-file-input');
|
||||
const fileNameSpan = document.getElementById('hanlinyuan-ingest-novel-file-name');
|
||||
@@ -351,18 +403,34 @@ function bindInternalUIEvents() {
|
||||
librarySelect.addEventListener('change', handleWorldbookSelectionChange);
|
||||
}
|
||||
|
||||
// 为规则配置按钮绑定通用弹窗事件
|
||||
const condensationRulesBtn = document.getElementById('hly-exclusion-rules-btn');
|
||||
if (condensationRulesBtn) {
|
||||
condensationRulesBtn.addEventListener('click', () => showRulesModal('condensation'));
|
||||
// 浓缩 — 提取规则下拉选单
|
||||
const condensationRuleSelect = document.getElementById('hly-condensation-rule-profile-select');
|
||||
if (condensationRuleSelect) {
|
||||
_populateHlyRuleProfileSelect(condensationRuleSelect, 'condensation');
|
||||
condensationRuleSelect.addEventListener('change', () => {
|
||||
ruleProfileManager.setAssignment('condensation', condensationRuleSelect.value || null);
|
||||
const name = condensationRuleSelect.selectedOptions[0]?.textContent || '';
|
||||
toastr.info(condensationRuleSelect.value ? `浓缩提取规则已切换为「${name}」` : '浓缩提取规则已取消分配');
|
||||
});
|
||||
}
|
||||
|
||||
// 为“检索预处理”的配置按钮绑定事件
|
||||
const queryPreprocessingBtn = document.getElementById('hly-query-preprocessing-rules-btn');
|
||||
if (queryPreprocessingBtn) {
|
||||
queryPreprocessingBtn.addEventListener('click', () => showRulesModal('queryPreprocessing'));
|
||||
// 查询预处理 — 提取规则下拉选单
|
||||
const queryPrepRuleSelect = document.getElementById('hly-query-preprocessing-rule-profile-select');
|
||||
if (queryPrepRuleSelect) {
|
||||
_populateHlyRuleProfileSelect(queryPrepRuleSelect, 'queryPreprocessing');
|
||||
queryPrepRuleSelect.addEventListener('change', () => {
|
||||
ruleProfileManager.setAssignment('queryPreprocessing', queryPrepRuleSelect.value || null);
|
||||
const name = queryPrepRuleSelect.selectedOptions[0]?.textContent || '';
|
||||
toastr.info(queryPrepRuleSelect.value ? `查询预处理规则已切换为「${name}」` : '查询预处理规则已取消分配');
|
||||
});
|
||||
}
|
||||
|
||||
// 规则配置中心保存/删除后自动刷新翰林院下拉选单
|
||||
document.addEventListener('amily2:ruleProfilesChanged', (e) => {
|
||||
if (condensationRuleSelect) _populateHlyRuleProfileSelect(condensationRuleSelect, 'condensation', e.detail);
|
||||
if (queryPrepRuleSelect) _populateHlyRuleProfileSelect(queryPrepRuleSelect, 'queryPreprocessing', e.detail);
|
||||
});
|
||||
|
||||
// 为自定义多选下拉框绑定事件
|
||||
const multiSelectBtn = document.getElementById('hly-hist-entry-multiselect-btn');
|
||||
const optionsContainer = document.getElementById('hly-hist-entry-multiselect-options');
|
||||
@@ -547,7 +615,7 @@ function handleApiModeChange() {
|
||||
}
|
||||
}
|
||||
|
||||
function loadSettingsToUI() {
|
||||
export function loadSettingsToUI() {
|
||||
const settings = HanlinyuanCore.getSettings();
|
||||
if (!settings) return;
|
||||
|
||||
@@ -593,14 +661,17 @@ function loadSettingsToUI() {
|
||||
histMaxRetriesEl.value = settings.historiographyMaxRetries ?? 2;
|
||||
}
|
||||
|
||||
// 新增:加载标签提取设置
|
||||
// 注:hly-tag-extraction-toggle / hly-tag-input / hly-tag-input-container 已从 HTML 移除,
|
||||
// 标签提取规则改由 RuleProfileManager 管理。此处保留兼容性 null 检查,避免抛错吞掉后续段落加载。
|
||||
const tagExtractionToggle = document.getElementById('hly-tag-extraction-toggle');
|
||||
const tagInput = document.getElementById('hly-tag-input');
|
||||
const tagInputContainer = document.getElementById('hly-tag-input-container');
|
||||
|
||||
tagExtractionToggle.checked = settings.condensation.tagExtractionEnabled;
|
||||
tagInput.value = settings.condensation.tags; // 直接使用从核心获取的值
|
||||
tagInputContainer.style.display = tagExtractionToggle.checked ? 'block' : 'none';
|
||||
if (tagExtractionToggle) tagExtractionToggle.checked = settings.condensation.tagExtractionEnabled;
|
||||
if (tagInput) tagInput.value = settings.condensation.tags;
|
||||
if (tagInputContainer && tagExtractionToggle) {
|
||||
tagInputContainer.style.display = tagExtractionToggle.checked ? 'block' : 'none';
|
||||
}
|
||||
|
||||
// Rerank 设置
|
||||
document.getElementById('hly-rerank-enabled').checked = settings.rerank.enabled;
|
||||
@@ -864,8 +935,8 @@ async function renderKnowledgeBases() {
|
||||
|
||||
} catch (error) {
|
||||
console.error('[翰林院-枢纽] 渲染知识库列表失败:', error);
|
||||
localContainer.innerHTML = `<p class="hly-notes log-error"><i>加载失败: ${error.message}</i></p>`;
|
||||
globalContainer.innerHTML = `<p class="hly-notes log-error"><i>加载失败: ${error.message}</i></p>`;
|
||||
localContainer.innerHTML = `<p class="hly-notes log-error"><i>加载失败: ${escapeTextareaContent(error.message)}</i></p>`;
|
||||
globalContainer.innerHTML = `<p class="hly-notes log-error"><i>加载失败: ${escapeTextareaContent(error.message)}</i></p>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -964,8 +1035,8 @@ function _createKbItemElement(id, kb, scope, vectorCount) {
|
||||
|
||||
item.innerHTML = `
|
||||
<div class="hly-kb-name-container">
|
||||
<input type="checkbox" class="hly-kb-item-checkbox" data-kb-id="${id}">
|
||||
<span class="hly-kb-name" title="ID: ${id}">${kb.name} (${vectorCount}条)</span>
|
||||
<input type="checkbox" class="hly-kb-item-checkbox" data-kb-id="${escapeAttribute(id)}">
|
||||
<span class="hly-kb-name" title="ID: ${escapeAttribute(id)}">${escapeTextareaContent(kb.name || '')} (${Number(vectorCount) || 0}条)</span>
|
||||
</div>
|
||||
<div class="hly-kb-actions">
|
||||
${moveButtonHtml}
|
||||
@@ -1473,11 +1544,11 @@ function updateEntryOptions(query, allEntries) {
|
||||
filteredEntries.forEach(entry => {
|
||||
const displayText = query ?
|
||||
highlightSearchMatch(entry.comment, query) :
|
||||
entry.comment;
|
||||
escapeTextareaContent(entry.comment);
|
||||
|
||||
const optionHtml = `
|
||||
<label class="hly-multiselect-option" title="${entry.comment} (Key: ${entry.key})">
|
||||
<input type="checkbox" class="hly-hist-entry-checkbox" value="${entry.key}">
|
||||
<label class="hly-multiselect-option" title="${escapeAttribute(entry.comment)} (Key: ${escapeAttribute(entry.key)})">
|
||||
<input type="checkbox" class="hly-hist-entry-checkbox" value="${escapeAttribute(entry.key)}">
|
||||
<span>${displayText}</span>
|
||||
</label>`;
|
||||
optionsContainer.insertAdjacentHTML('beforeend', optionHtml);
|
||||
@@ -1563,134 +1634,20 @@ API端点: ${settings.retrieval.apiEndpoint}
|
||||
log(`查询宝库状态失败: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
function showRulesModal(type) {
|
||||
const settings = HanlinyuanCore.getSettings();
|
||||
const config = settings[type];
|
||||
if (!config) {
|
||||
console.error(`[翰林院-枢纽] 未找到类型为 "${type}" 的配置项。`);
|
||||
return;
|
||||
}
|
||||
|
||||
const title = type === 'condensation' ? '编辑凝识内容排除规则' : '编辑检索内容排除规则';
|
||||
const rules = config.exclusionRules || [];
|
||||
|
||||
const createRuleRowHtml = (rule = { start: '', end: '' }, index) => `
|
||||
<div class="hly-exclusion-rule-row" data-index="${index}">
|
||||
<input type="text" class="hly-imperial-brush" value="${(rule.start || '').replace(/"/g, '"')}" placeholder="开始字符串, 如 <!--">
|
||||
<span style="margin: 0 5px;">到</span>
|
||||
<input type="text" class="hly-imperial-brush" value="${(rule.end || '').replace(/"/g, '"')}" placeholder="结束字符串, 如 -->">
|
||||
<button class="hly-delete-rule-btn" title="删除此规则">×</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const rulesHtml = rules.map(createRuleRowHtml).join('');
|
||||
|
||||
// 标签提取部分只在“检索预处理”设置中显示
|
||||
const tagExtractionFieldset = type === 'queryPreprocessing' ? `
|
||||
<fieldset class="hly-settings-group">
|
||||
<legend><i class="fas fa-tags"></i> 标签提取</legend>
|
||||
<div class="hly-control-block" style="flex-direction: row; justify-content: space-between; align-items: center;">
|
||||
<label for="hly-modal-tag-extraction-enabled">启用标签提取</label>
|
||||
<label class="hly-toggle-switch">
|
||||
<input type="checkbox" id="hly-modal-tag-extraction-enabled" ${config.tagExtractionEnabled ? 'checked' : ''}>
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div id="hly-modal-tag-input-container" class="hly-control-block" style="display: ${config.tagExtractionEnabled ? 'block' : 'none'};">
|
||||
<label for="hly-modal-tag-input">输入标签 (以逗号分隔):</label>
|
||||
<textarea id="hly-modal-tag-input" class="hly-imperial-brush" rows="2" placeholder="例如: content,details,摘要">${config.tags || ''}</textarea>
|
||||
</div>
|
||||
</fieldset>
|
||||
` : '';
|
||||
|
||||
const modalHtml = `
|
||||
<div id="hly-rules-modal-container">
|
||||
${tagExtractionFieldset}
|
||||
<fieldset class="hly-settings-group">
|
||||
<legend><i class="fas fa-ban"></i> 内容排除规则</legend>
|
||||
<p class="hly-notes">在这里定义需要从提取内容中排除的文本片段。例如,排除HTML注释,可以设置开始字符串为 \`<!--\`,结束字符串为 \`-->\`。</p>
|
||||
<div id="hly-rules-list">${rulesHtml.length > 0 ? rulesHtml : '<p class="hly-notes" style="text-align:center;">暂无规则</p>'}</div>
|
||||
<button id="hly-add-rule-btn" class="hly-action-button" style="margin-top: 10px;">
|
||||
<i class="fas fa-plus"></i> 添加新规则
|
||||
</button>
|
||||
</fieldset>
|
||||
</div>
|
||||
<style>
|
||||
.hly-exclusion-rule-row { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
|
||||
.hly-exclusion-rule-row input { flex-grow: 1; }
|
||||
.hly-delete-rule-btn { background: #c0392b; color: white; border: none; border-radius: 50%; width: 24px; height: 24px; cursor: pointer; font-size: 16px; line-height: 24px; text-align: center; padding: 0; flex-shrink: 0; }
|
||||
</style>
|
||||
`;
|
||||
|
||||
showHtmlModal(title, modalHtml, {
|
||||
okText: '保存规则',
|
||||
onOk: (dialogElement) => {
|
||||
const newRules = [];
|
||||
dialogElement.find('.hly-exclusion-rule-row').each(function () {
|
||||
const start = $(this).find('input').eq(0).val().trim();
|
||||
const end = $(this).find('input').eq(1).val().trim();
|
||||
// 规则必须至少有一个开始字符串
|
||||
if (start) {
|
||||
newRules.push({ start, end });
|
||||
}
|
||||
});
|
||||
|
||||
const newConfig = { ...config, exclusionRules: newRules };
|
||||
|
||||
// 如果是检索预处理设置,则同时保存标签提取的设置
|
||||
if (type === 'queryPreprocessing') {
|
||||
newConfig.tagExtractionEnabled = dialogElement.find('#hly-modal-tag-extraction-enabled').is(':checked');
|
||||
newConfig.tags = dialogElement.find('#hly-modal-tag-input').val();
|
||||
}
|
||||
|
||||
updateAndSaveSetting(type, newConfig);
|
||||
toastr.success('规则已保存。', '圣旨已达');
|
||||
},
|
||||
onShow: (dialogElement) => {
|
||||
const rulesList = dialogElement.find('#hly-rules-list');
|
||||
|
||||
dialogElement.find('#hly-add-rule-btn').on('click', () => {
|
||||
const newIndex = rulesList.children('.hly-exclusion-rule-row').length;
|
||||
const newRowHtml = createRuleRowHtml(undefined, newIndex);
|
||||
if (rulesList.find('p').length > 0) {
|
||||
rulesList.html(newRowHtml);
|
||||
} else {
|
||||
rulesList.append(newRowHtml);
|
||||
}
|
||||
});
|
||||
|
||||
rulesList.on('click', '.hly-delete-rule-btn', function () {
|
||||
$(this).closest('.hly-exclusion-rule-row').remove();
|
||||
if (rulesList.children().length === 0) {
|
||||
rulesList.html('<p class="hly-notes" style="text-align:center;">暂无规则</p>');
|
||||
}
|
||||
});
|
||||
|
||||
// 如果是检索预处理设置,则绑定标签提取的UI事件
|
||||
if (type === 'queryPreprocessing') {
|
||||
const tagToggle = dialogElement.find('#hly-modal-tag-extraction-enabled');
|
||||
const tagInputContainer = dialogElement.find('#hly-modal-tag-input-container');
|
||||
tagToggle.on('change', () => {
|
||||
tagInputContainer.css('display', tagToggle.is(':checked') ? 'block' : 'none');
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function previewCondensation() {
|
||||
const resultsEl = document.getElementById('hly-condensation-results');
|
||||
try {
|
||||
// 1. 获取UI设置和新规则
|
||||
const settings = HanlinyuanCore.getSettings();
|
||||
const exclusionRules = settings.condensation.exclusionRules || [];
|
||||
const condensationRuleConfig = resolveCondensationRuleConfig(settings);
|
||||
const exclusionRules = condensationRuleConfig.exclusionRules || [];
|
||||
const overrideMessageTypes = {
|
||||
user: document.getElementById('hly-include-user').checked,
|
||||
ai: document.getElementById('hly-include-ai').checked,
|
||||
};
|
||||
const useTagExtraction = document.getElementById('hly-tag-extraction-toggle').checked;
|
||||
const useTagExtraction = condensationRuleConfig.tagExtractionEnabled;
|
||||
const tagsToExtract = useTagExtraction
|
||||
? document.getElementById('hly-tag-input').value.split(',').map(t => t.trim()).filter(Boolean)
|
||||
? (condensationRuleConfig.tags || '').split(',').map(t => t.trim()).filter(Boolean)
|
||||
: [];
|
||||
|
||||
// 2. 获取原始消息
|
||||
@@ -1758,7 +1715,7 @@ function previewCondensation() {
|
||||
<textarea class="hly-preview-textarea"
|
||||
data-floor="${item.floor}"
|
||||
data-is-user="${item.is_user}"
|
||||
data-send-date="${item.send_date}">${item.content}</textarea>
|
||||
data-send-date="${item.send_date}">${escapeTextareaContent(item.content)}</textarea>
|
||||
</div>
|
||||
</details>
|
||||
<button class="hly-preview-delete-btn-v2" data-target="${item.id}" title="删除此条">×</button>
|
||||
@@ -1836,7 +1793,7 @@ function log(message, type = 'info') {
|
||||
}
|
||||
|
||||
p.className = `hly-log-entry ${colorClass}`;
|
||||
p.innerHTML = `<i class="fa-solid ${icon}"></i> [${timestamp}] ${message}`;
|
||||
p.innerHTML = `<i class="fa-solid ${escapeAttribute(icon)}"></i> [${escapeTextareaContent(timestamp)}] ${escapeTextareaContent(message)}`;
|
||||
|
||||
// 移除初始的占位符
|
||||
const placeholder = logOutput.querySelector('.hly-log-placeholder');
|
||||
|
||||
@@ -5,7 +5,8 @@ import {
|
||||
saveSettings,
|
||||
} 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 { ruleProfileManager, resolveHistoriographyRuleConfig } from '../utils/config/RuleProfileManager.js';
|
||||
|
||||
import {
|
||||
getAvailableWorldbooks, getLoresForWorldbook,
|
||||
@@ -16,6 +17,25 @@ import {
|
||||
|
||||
import { getNgmsApiSettings, testNgmsApiConnection, fetchNgmsModels } from "../core/api/Ngms_api.js";
|
||||
|
||||
function getHistoriographyRuleConfig() {
|
||||
return resolveHistoriographyRuleConfig(extension_settings[extensionName] || {});
|
||||
}
|
||||
|
||||
function _escapeHtml(text) {
|
||||
return String(text ?? '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
}
|
||||
|
||||
function _populateHistRuleProfileSelect(select, detail) {
|
||||
const profiles = detail?.profiles ?? ruleProfileManager.listProfiles();
|
||||
const assigned = detail?.assignments?.historiography ?? ruleProfileManager.getAssignment('historiography') ?? '';
|
||||
select.innerHTML = [
|
||||
'<option value="">— 未分配 —</option>',
|
||||
...profiles.map(p =>
|
||||
`<option value="${p.id}" ${p.id === assigned ? 'selected' : ''}>${_escapeHtml(p.name || p.id)}</option>`
|
||||
),
|
||||
].join('');
|
||||
}
|
||||
|
||||
|
||||
function setupPromptEditor(type) {
|
||||
const selector = document.getElementById(
|
||||
@@ -198,29 +218,19 @@ export function bindHistoriographyEvents() {
|
||||
saveSettings();
|
||||
});
|
||||
|
||||
// ========== 🏷️ 标签与排除规则绑定 (新增) ==========
|
||||
const tagExtractionToggle = document.getElementById("historiography-tag-extraction-toggle");
|
||||
const tagInputContainer = document.getElementById("historiography-tag-input-container");
|
||||
const tagInput = document.getElementById("historiography-tag-input");
|
||||
const exclusionRulesBtn = document.getElementById("historiography-exclusion-rules-btn");
|
||||
|
||||
tagExtractionToggle.checked = extension_settings[extensionName].historiographyTagExtractionEnabled ?? false;
|
||||
tagInput.value = extension_settings[extensionName].historiographyTags ?? '';
|
||||
tagInputContainer.style.display = tagExtractionToggle.checked ? 'block' : 'none';
|
||||
|
||||
tagExtractionToggle.addEventListener("change", (event) => {
|
||||
const isEnabled = event.target.checked;
|
||||
extension_settings[extensionName].historiographyTagExtractionEnabled = isEnabled;
|
||||
tagInputContainer.style.display = isEnabled ? 'block' : 'none';
|
||||
saveSettings();
|
||||
});
|
||||
|
||||
tagInput.addEventListener("change", (event) => {
|
||||
extension_settings[extensionName].historiographyTags = event.target.value;
|
||||
saveSettings();
|
||||
});
|
||||
|
||||
exclusionRulesBtn.addEventListener("click", showHistoriographyExclusionRulesModal);
|
||||
// ========== 提取规则下拉选单 ==========
|
||||
const histRuleSelect = document.getElementById("historiography-rule-profile-select");
|
||||
if (histRuleSelect) {
|
||||
_populateHistRuleProfileSelect(histRuleSelect);
|
||||
histRuleSelect.addEventListener("change", () => {
|
||||
ruleProfileManager.setAssignment('historiography', histRuleSelect.value || null);
|
||||
const name = histRuleSelect.selectedOptions[0]?.textContent || '';
|
||||
toastr.info(histRuleSelect.value ? `史官提取规则已切换为「${name}」` : '史官提取规则已取消分配');
|
||||
});
|
||||
document.addEventListener('amily2:ruleProfilesChanged', (e) => {
|
||||
_populateHistRuleProfileSelect(histRuleSelect, e.detail);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
const expeditionExecuteBtn = document.getElementById("amily2_mhb_small_expedition_execute");
|
||||
@@ -459,16 +469,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)从 configManager(localStorage)读取
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -608,62 +625,3 @@ async function loadNgmsTavernPresets() {
|
||||
}
|
||||
}
|
||||
|
||||
function showHistoriographyExclusionRulesModal() {
|
||||
const rules = extension_settings[extensionName].historiographyExclusionRules || [];
|
||||
|
||||
const createRuleRowHtml = (rule = { start: '', end: '' }, index) => `
|
||||
<div class="hly-exclusion-rule-row" data-index="${index}">
|
||||
<input type="text" class="hly-imperial-brush" value="${rule.start}" placeholder="开始字符, 如 <!--">
|
||||
<span>到</span>
|
||||
<input type="text" class="hly-imperial-brush" value="${rule.end}" placeholder="结束字符, 如 -->">
|
||||
<button class="hly-delete-rule-btn" title="删除此规则">×</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const rulesHtml = rules.map(createRuleRowHtml).join('');
|
||||
|
||||
const modalHtml = `
|
||||
<div id="historiography-exclusion-rules-container">
|
||||
<p class="hly-notes">在这里定义需要从提取内容中排除的文本片段。例如,排除HTML注释,可以设置开始字符为 \`<!--\`,结束字符为 \`-->\`。</p>
|
||||
<div id="historiography-rules-list">${rulesHtml}</div>
|
||||
<button id="historiography-add-rule-btn" class="hly-action-button" style="margin-top: 10px;">
|
||||
<i class="fas fa-plus"></i> 添加新规则
|
||||
</button>
|
||||
</div>
|
||||
<style>
|
||||
.hly-exclusion-rule-row { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
|
||||
.hly-exclusion-rule-row input { flex-grow: 1; }
|
||||
.hly-delete-rule-btn { background: #c0392b; color: white; border: none; border-radius: 50%; width: 24px; height: 24px; cursor: pointer; font-size: 16px; line-height: 24px; text-align: center; padding: 0; }
|
||||
</style>
|
||||
`;
|
||||
|
||||
showHtmlModal('编辑内容排除规则', modalHtml, {
|
||||
okText: '保存规则',
|
||||
onOk: (dialogElement) => {
|
||||
const newRules = [];
|
||||
dialogElement.find('.hly-exclusion-rule-row').each(function() {
|
||||
const start = $(this).find('input').eq(0).val().trim();
|
||||
const end = $(this).find('input').eq(1).val().trim();
|
||||
if (start && end) {
|
||||
newRules.push({ start, end });
|
||||
}
|
||||
});
|
||||
extension_settings[extensionName].historiographyExclusionRules = newRules;
|
||||
saveSettings();
|
||||
toastr.success('内容排除规则已保存。', '圣旨已达');
|
||||
},
|
||||
onShow: (dialogElement) => {
|
||||
const rulesList = dialogElement.find('#historiography-rules-list');
|
||||
|
||||
dialogElement.find('#historiography-add-rule-btn').on('click', () => {
|
||||
const newIndex = rulesList.children().length;
|
||||
const newRowHtml = createRuleRowHtml({ start: '', end: '' }, newIndex);
|
||||
rulesList.append(newRowHtml);
|
||||
});
|
||||
|
||||
rulesList.on('click', '.hly-delete-rule-btn', function() {
|
||||
$(this).closest('.hly-exclusion-rule-row').remove();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { getMemoryState, getHighlights } from '../core/table-system/manager.js';
|
||||
import { extension_settings } from '/scripts/extensions.js';
|
||||
import { extensionName } from '../utils/settings.js';
|
||||
import { getContext } from '/scripts/extensions.js';
|
||||
import { escapeHTML } from '../utils/utils.js';
|
||||
|
||||
const TABLE_CONTAINER_ID = 'amily2-chat-table-container';
|
||||
const isTouchDevice = () => window.matchMedia('(pointer: coarse)').matches;
|
||||
@@ -366,10 +367,11 @@ function renderTablesToHtml(tables, highlights) {
|
||||
const icon = getTableIcon(table.name);
|
||||
|
||||
// 侧边栏按钮 (现在包含文字)
|
||||
const safeTableName = escapeHTML(table.name || '');
|
||||
sidebarHtml += `
|
||||
<div class="amily2-game-tab ${isActive}" data-target="game-panel-${index}" title="${table.name}">
|
||||
<div class="amily2-game-tab ${isActive}" data-target="game-panel-${index}" title="${safeTableName}">
|
||||
<i class="fas ${icon}"></i>
|
||||
<span class="tab-text">${table.name}</span>
|
||||
<span class="tab-text">${safeTableName}</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -380,7 +382,7 @@ function renderTablesToHtml(tables, highlights) {
|
||||
const theadHtml = `
|
||||
<thead>
|
||||
<tr>
|
||||
${table.headers.map(header => `<th>${header}</th>`).join('')}
|
||||
${table.headers.map(header => `<th>${escapeHTML(String(header ?? ''))}</th>`).join('')}
|
||||
</tr>
|
||||
</thead>
|
||||
`;
|
||||
@@ -396,7 +398,7 @@ function renderTablesToHtml(tables, highlights) {
|
||||
const highlightKey = `${table.originalIndex}-${rowIndex}-${colIndex}`;
|
||||
const isHighlighted = highlights.has(highlightKey);
|
||||
const style = isHighlighted ? 'style="color: #00ff7f; font-weight: bold;"' : '';
|
||||
tbodyHtml += `<td ${style}>${cell}</td>`;
|
||||
tbodyHtml += `<td ${style}>${escapeHTML(String(cell ?? ''))}</td>`;
|
||||
});
|
||||
tbodyHtml += '</tr>';
|
||||
});
|
||||
@@ -413,7 +415,7 @@ function renderTablesToHtml(tables, highlights) {
|
||||
|
||||
contentHtml += `
|
||||
<div id="game-panel-${index}" class="amily2-game-panel ${isActive}">
|
||||
<div class="amily2-panel-title"><i class="fas ${icon}"></i> ${table.name}</div>
|
||||
<div class="amily2-panel-title"><i class="fas ${icon}"></i> ${safeTableName}</div>
|
||||
${tableHtml}
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -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] = {};
|
||||
@@ -179,6 +184,25 @@ function opt_getMergedSettings() {
|
||||
return { ...globalSettings, ...characterSettings };
|
||||
}
|
||||
|
||||
function bindInputLikeSave(element, handler) {
|
||||
if (!element) return;
|
||||
element.oninput = handler;
|
||||
element.onchange = handler;
|
||||
}
|
||||
|
||||
function syncModelMirror(inputElement, selectElement) {
|
||||
if (!inputElement || !selectElement) return;
|
||||
const value = inputElement.value || '';
|
||||
if (!value) return;
|
||||
|
||||
let option = Array.from(selectElement.options || []).find(item => item.value === value);
|
||||
if (!option) {
|
||||
option = new Option(value, value, true, true);
|
||||
selectElement.add(option);
|
||||
}
|
||||
selectElement.value = value;
|
||||
}
|
||||
|
||||
|
||||
|
||||
function opt_bindSlider(panel, sliderId, displayId) {
|
||||
@@ -622,7 +646,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 是敏感字段,从 configManager(localStorage)读取
|
||||
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');
|
||||
@@ -635,14 +660,15 @@ function opt_loadSettings(panel) {
|
||||
modelSelect.append(new Option('<-请先获取模型', '', true, true));
|
||||
}
|
||||
|
||||
syncModelMirror(modelInput.get(0), modelSelect.get(0));
|
||||
panel.find('#amily2_opt_max_tokens').val(settings.plotOpt_max_tokens);
|
||||
panel.find('#amily2_opt_temperature').val(settings.plotOpt_temperature);
|
||||
panel.find('#amily2_opt_top_p').val(settings.plotOpt_top_p);
|
||||
panel.find('#amily2_opt_presence_penalty').val(settings.plotOpt_presence_penalty);
|
||||
panel.find('#amily2_opt_frequency_penalty').val(settings.plotOpt_frequency_penalty);
|
||||
panel.find('#amily2_opt_context_turn_count').val(settings.plotOpt_contextTurnCount);
|
||||
const contextLimit = settings.plotOpt_contextLimit ?? settings.plotOpt_contextTurnCount ?? defaultSettings.plotOpt_contextLimit;
|
||||
panel.find('#amily2_opt_worldbook_char_limit').val(settings.plotOpt_worldbookCharLimit);
|
||||
panel.find('#amily2_opt_context_limit').val(settings.plotOpt_contextLimit);
|
||||
panel.find('#amily2_opt_context_limit').val(contextLimit);
|
||||
|
||||
panel.find('#amily2_opt_rate_main').val(settings.plotOpt_rateMain);
|
||||
panel.find('#amily2_opt_rate_personal').val(settings.plotOpt_ratePersonal);
|
||||
@@ -674,7 +700,6 @@ function opt_loadSettings(panel) {
|
||||
opt_bindSlider(panel, '#amily2_opt_top_p', '#amily2_opt_top_p_value');
|
||||
opt_bindSlider(panel, '#amily2_opt_presence_penalty', '#amily2_opt_presence_penalty_value');
|
||||
opt_bindSlider(panel, '#amily2_opt_frequency_penalty', '#amily2_opt_frequency_penalty_value');
|
||||
opt_bindSlider(panel, '#amily2_opt_context_turn_count', '#amily2_opt_context_turn_count_value');
|
||||
opt_bindSlider(panel, '#amily2_opt_worldbook_char_limit', '#amily2_opt_worldbook_char_limit_value');
|
||||
opt_bindSlider(panel, '#amily2_opt_context_limit', '#amily2_opt_context_limit_value');
|
||||
|
||||
@@ -701,14 +726,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)从 configManager(localStorage)读取
|
||||
element.value = field.sensitive
|
||||
? (configManager.get(field.key) || '')
|
||||
: (settings[field.key] || '');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -786,11 +814,22 @@ function bindConcurrentApiEvents() {
|
||||
fields.forEach(field => {
|
||||
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();
|
||||
});
|
||||
const saveField = function() {
|
||||
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();
|
||||
if (field.key === 'plotOpt_concurrentModel') {
|
||||
syncModelMirror(
|
||||
document.getElementById('amily2_plotOpt_concurrentModel'),
|
||||
document.getElementById('amily2_plotOpt_concurrentModel_select')
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
bindInputLikeSave(element, saveField);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1004,12 +1043,32 @@ function bindConcurrentWorldbookEvents() {
|
||||
});
|
||||
}
|
||||
|
||||
function opt_purgeGarbageKeys() {
|
||||
const store = extension_settings[extensionName];
|
||||
if (!store) return;
|
||||
let removed = 0;
|
||||
for (const key of Object.keys(store)) {
|
||||
// 历史 bug 造成的污染 key:handleSettingChange 误把世界书/条目复选框当作设置项,
|
||||
// 生成形如 plotOpt_amily2-opt-wb-*、plotOpt_amily2-opt-entry-*、plotOpt_amily2-opt-concurrent-wb-* 的键
|
||||
if (/^plotOpt_amily2-opt-/.test(key)) {
|
||||
delete store[key];
|
||||
removed++;
|
||||
}
|
||||
}
|
||||
if (removed > 0) {
|
||||
console.log(`[${extensionName}] 清理残留的 ${removed} 条无效 plotOpt_* 设置键。`);
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
}
|
||||
|
||||
export function initializePlotOptimizationBindings() {
|
||||
const panel = $('#amily2_plot_optimization_panel');
|
||||
if (panel.length === 0 || panel.data('events-bound')) {
|
||||
return;
|
||||
}
|
||||
|
||||
opt_purgeGarbageKeys();
|
||||
|
||||
// Tab switching logic
|
||||
panel.find('.sinan-navigation-deck').on('click', '.sinan-nav-item', function() {
|
||||
const tabButton = $(this);
|
||||
@@ -1140,7 +1199,11 @@ export function initializePlotOptimizationBindings() {
|
||||
|
||||
const handleSettingChange = function(element) {
|
||||
const el = $(element);
|
||||
const key_part = (element.name || element.id).replace('amily2_opt_', '');
|
||||
const rawName = element.name || element.id || '';
|
||||
// 仅处理下划线前缀的真实设置项;动态生成的世界书/条目复选框用连字符命名(amily2-opt-wb-*、amily2-opt-entry-*),
|
||||
// 它们有自己的专属 handler,若被此处捕获会生成 plotOpt_amily2-opt-... 的垃圾 key 污染 settings
|
||||
if (!rawName.startsWith('amily2_opt_')) return;
|
||||
const key_part = rawName.replace('amily2_opt_', '');
|
||||
const key = 'plotOpt_' + key_part.replace(/_([a-z])/g, (g) => g[1].toUpperCase());
|
||||
|
||||
let value = element.type === 'checkbox' ? element.checked : el.val();
|
||||
@@ -1179,6 +1242,13 @@ export function initializePlotOptimizationBindings() {
|
||||
handleSettingChange(this);
|
||||
});
|
||||
|
||||
panel.on('input.amily2_opt change.amily2_opt', '#amily2_opt_model', function() {
|
||||
syncModelMirror(
|
||||
panel.find('#amily2_opt_model').get(0),
|
||||
panel.find('#amily2_opt_model_select').get(0)
|
||||
);
|
||||
});
|
||||
|
||||
panel.on('change.amily2_opt', '#amily2_opt_model_select', function() {
|
||||
const selectedModel = $(this).val();
|
||||
if (selectedModel) {
|
||||
@@ -1384,17 +1454,31 @@ 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] || '';
|
||||
element.addEventListener('change', function() {
|
||||
updateAndSaveSetting(field.key, this.value);
|
||||
});
|
||||
// 敏感字段(API Key)从 configManager(localStorage)读取
|
||||
element.value = field.sensitive
|
||||
? (configManager.get(field.key) || '')
|
||||
: (extension_settings[extensionName][field.key] || '');
|
||||
const saveField = function() {
|
||||
if (field.sensitive) {
|
||||
configManager.set(field.key, this.value);
|
||||
} else {
|
||||
updateAndSaveSetting(field.key, this.value);
|
||||
if (field.key === 'jqyhModel') {
|
||||
syncModelMirror(
|
||||
document.getElementById('amily2_jqyh_model'),
|
||||
document.getElementById('amily2_jqyh_model_select')
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
bindInputLikeSave(element, saveField);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
@@ -342,7 +372,8 @@ async function _fetchSlotModels(slot, card) {
|
||||
return;
|
||||
}
|
||||
const resp = await fetch(
|
||||
`https://generativelanguage.googleapis.com/v1beta/models?key=${encodeURIComponent(profile.apiKey)}`
|
||||
'https://generativelanguage.googleapis.com/v1beta/models',
|
||||
{ headers: { 'x-goog-api-key': profile.apiKey } }
|
||||
);
|
||||
if (!resp.ok) {
|
||||
$result.text(`失败:HTTP ${resp.status}`).css('color', 'var(--warning-color)');
|
||||
|
||||
151
ui/rule-config-bindings.js
Normal file
151
ui/rule-config-bindings.js
Normal file
@@ -0,0 +1,151 @@
|
||||
import { ruleProfileManager } from '../utils/config/RuleProfileManager.js';
|
||||
|
||||
let currentEditingId = null;
|
||||
|
||||
function createEmptyProfile() {
|
||||
return {
|
||||
id: '',
|
||||
name: '',
|
||||
tagExtractionEnabled: false,
|
||||
tags: '',
|
||||
exclusionRules: [],
|
||||
};
|
||||
}
|
||||
|
||||
function createRuleRow(rule = { start: '', end: '' }, index = 0) {
|
||||
return `
|
||||
<div class="amily2-rule-row" data-index="${index}">
|
||||
<input type="text" class="text_pole amily2-rule-start" value="${escapeHtml(rule.start || '')}" placeholder="起始标记">
|
||||
<input type="text" class="text_pole amily2-rule-end" value="${escapeHtml(rule.end || '')}" placeholder="结束标记">
|
||||
<button type="button" class="menu_button danger small_button amily2-rule-remove">
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
return String(text ?? '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
|
||||
function renderRules(container, exclusionRules = []) {
|
||||
const list = container.find('#amily2_rule_profile_rules');
|
||||
if (!exclusionRules.length) {
|
||||
list.html('<p class="notes">当前没有排除规则。</p>');
|
||||
return;
|
||||
}
|
||||
list.html(exclusionRules.map((rule, index) => createRuleRow(rule, index)).join(''));
|
||||
}
|
||||
|
||||
function collectProfile(container) {
|
||||
const exclusionRules = [];
|
||||
container.find('.amily2-rule-row').each(function () {
|
||||
const start = $(this).find('.amily2-rule-start').val().trim();
|
||||
const end = $(this).find('.amily2-rule-end').val().trim();
|
||||
if (start) {
|
||||
exclusionRules.push({ start, end });
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
id: currentEditingId || '',
|
||||
name: container.find('#amily2_rule_profile_name').val().trim(),
|
||||
tagExtractionEnabled: container.find('#amily2_rule_profile_tag_toggle').is(':checked'),
|
||||
tags: container.find('#amily2_rule_profile_tags').val(),
|
||||
exclusionRules,
|
||||
};
|
||||
}
|
||||
|
||||
function renderProfileList(container) {
|
||||
const list = container.find('#amily2_rule_profile_list');
|
||||
const profiles = ruleProfileManager.listProfiles();
|
||||
|
||||
if (!profiles.length) {
|
||||
list.html('<p class="notes">还没有规则配置。</p>');
|
||||
return;
|
||||
}
|
||||
|
||||
list.html(profiles.map(profile => `
|
||||
<button type="button" class="menu_button wide_button amily2-rule-profile-item" data-id="${profile.id}">
|
||||
<span>${escapeHtml(profile.name || profile.id)}</span>
|
||||
</button>
|
||||
`).join(''));
|
||||
}
|
||||
|
||||
function fillEditor(container, profile) {
|
||||
const current = profile || createEmptyProfile();
|
||||
currentEditingId = current.id || null;
|
||||
container.find('#amily2_rule_profile_name').val(current.name || '');
|
||||
container.find('#amily2_rule_profile_tag_toggle').prop('checked', !!current.tagExtractionEnabled);
|
||||
container.find('#amily2_rule_profile_tags').val(current.tags || '');
|
||||
container.find('#amily2_rule_profile_tags_wrap').toggle(!!current.tagExtractionEnabled);
|
||||
renderRules(container, current.exclusionRules || []);
|
||||
}
|
||||
|
||||
export function bindRuleConfigPanel(container) {
|
||||
const $c = $(container);
|
||||
|
||||
renderProfileList($c);
|
||||
fillEditor($c, createEmptyProfile());
|
||||
|
||||
$c.off('.ruleConfig');
|
||||
|
||||
$c.on('click.ruleConfig', '#amily2_rule_profile_new', () => {
|
||||
fillEditor($c, createEmptyProfile());
|
||||
});
|
||||
|
||||
$c.on('click.ruleConfig', '.amily2-rule-profile-item', function () {
|
||||
const profile = ruleProfileManager.getProfile($(this).data('id'));
|
||||
if (profile) {
|
||||
fillEditor($c, profile);
|
||||
}
|
||||
});
|
||||
|
||||
$c.on('change.ruleConfig', '#amily2_rule_profile_tag_toggle', function () {
|
||||
$c.find('#amily2_rule_profile_tags_wrap').toggle(this.checked);
|
||||
});
|
||||
|
||||
$c.on('click.ruleConfig', '#amily2_rule_profile_add_rule', () => {
|
||||
const rules = collectProfile($c).exclusionRules;
|
||||
rules.push({ start: '', end: '' });
|
||||
renderRules($c, rules);
|
||||
});
|
||||
|
||||
$c.on('click.ruleConfig', '.amily2-rule-remove', function () {
|
||||
$(this).closest('.amily2-rule-row').remove();
|
||||
if ($c.find('.amily2-rule-row').length === 0) {
|
||||
renderRules($c, []);
|
||||
}
|
||||
});
|
||||
|
||||
$c.on('click.ruleConfig', '#amily2_rule_profile_save', () => {
|
||||
const profile = collectProfile($c);
|
||||
if (!profile.name) {
|
||||
toastr.warning('请先填写规则配置名称。');
|
||||
return;
|
||||
}
|
||||
const saved = ruleProfileManager.saveProfile(profile);
|
||||
fillEditor($c, saved);
|
||||
renderProfileList($c);
|
||||
|
||||
toastr.success('规则配置已保存。');
|
||||
});
|
||||
|
||||
$c.on('click.ruleConfig', '#amily2_rule_profile_delete', () => {
|
||||
if (!currentEditingId) {
|
||||
return;
|
||||
}
|
||||
if (!confirm('删除当前规则配置?引用它的位置会回退到旧配置。')) {
|
||||
return;
|
||||
}
|
||||
ruleProfileManager.deleteProfile(currentEditingId);
|
||||
fillEditor($c, createEmptyProfile());
|
||||
renderProfileList($c);
|
||||
|
||||
toastr.success('规则配置已删除。');
|
||||
});
|
||||
}
|
||||
24
ui/state.js
24
ui/state.js
@@ -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);
|
||||
|
||||
@@ -197,10 +198,20 @@ export function updatePlotOptimizationUI() {
|
||||
const settings = getMergedPlotOptSettings();
|
||||
if (!settings) return;
|
||||
|
||||
const contextLimit = settings.plotOpt_contextLimit ?? settings.plotOpt_contextTurnCount ?? defaultSettings.plotOpt_contextLimit;
|
||||
const worldbookCharLimit = settings.plotOpt_worldbookCharLimit ?? defaultSettings.plotOpt_worldbookCharLimit;
|
||||
const worldbookEnabled = settings.plotOpt_worldbookEnabled ?? settings.plotOpt_worldbook_enabled ?? defaultSettings.plotOpt_worldbookEnabled;
|
||||
let tableEnabledValue = settings.plotOpt_tableEnabled;
|
||||
if (tableEnabledValue === true) {
|
||||
tableEnabledValue = 'main';
|
||||
} else if (tableEnabledValue === false || tableEnabledValue === undefined) {
|
||||
tableEnabledValue = 'disabled';
|
||||
}
|
||||
|
||||
$('#amily2_opt_enabled').prop('checked', settings.plotOpt_enabled);
|
||||
$('#amily2_opt_ejs_enabled').prop('checked', settings.plotOpt_ejsEnabled);
|
||||
$('#amily2_opt_worldbook_enabled').prop('checked', settings.plotOpt_worldbook_enabled);
|
||||
$('#amily2_opt_table_enabled').prop('checked', settings.plotOpt_tableEnabled);
|
||||
$('#amily2_opt_worldbook_enabled').prop('checked', worldbookEnabled);
|
||||
$('#amily2_opt_table_enabled').val(tableEnabledValue);
|
||||
|
||||
$('#amily2_opt_main_prompt').val(settings.plotOpt_mainPrompt);
|
||||
$('#amily2_opt_system_prompt').val(settings.plotOpt_systemPrompt);
|
||||
@@ -212,13 +223,12 @@ export function updatePlotOptimizationUI() {
|
||||
$('#amily2_opt_rate_cuckold').val(settings.plotOpt_rateCuckold);
|
||||
|
||||
const sliders = {
|
||||
'#amily2_opt_context_limit': 'plotOpt_contextLimit',
|
||||
'#amily2_opt_worldbook_char_limit': 'plotOpt_worldbookCharLimit',
|
||||
'#amily2_opt_context_limit': contextLimit,
|
||||
'#amily2_opt_worldbook_char_limit': worldbookCharLimit,
|
||||
};
|
||||
|
||||
for (const sliderId in sliders) {
|
||||
const key = sliders[sliderId];
|
||||
const value = settings[key];
|
||||
const value = sliders[sliderId];
|
||||
const valueDisplayId = `${sliderId}_value`;
|
||||
|
||||
if (value !== undefined) {
|
||||
|
||||
@@ -13,10 +13,44 @@ 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';
|
||||
import { ruleProfileManager } from '../utils/config/RuleProfileManager.js';
|
||||
import { bindTableTemplateEditors } from './table/template-bindings.js';
|
||||
import { bindNccsApiEvents as bindNccsApiSettingsEvents } from './table/nccs-bindings.js';
|
||||
import { bindChatTableDisplaySetting as bindChatTableDisplaySettings } from './table/chat-display-bindings.js';
|
||||
|
||||
const isTouchDevice = () => window.matchMedia('(pointer: coarse)').matches;
|
||||
const getAllTablesContainer = () => document.getElementById('all-tables-container');
|
||||
|
||||
/**
|
||||
* 通用:填充规则配置下拉选单
|
||||
* @param {HTMLSelectElement} select
|
||||
* @param {string} slot — RULE_SLOTS 中的功能槽名
|
||||
*/
|
||||
function _populateRuleProfileSelect(select, slot, detail) {
|
||||
const profiles = detail?.profiles ?? ruleProfileManager.listProfiles();
|
||||
const assigned = detail?.assignments?.[slot] ?? ruleProfileManager.getAssignment(slot) ?? '';
|
||||
const options = [
|
||||
'<option value="">— 未分配 —</option>',
|
||||
...profiles.map(p =>
|
||||
`<option value="${p.id}" ${p.id === assigned ? 'selected' : ''}>${escapeHTML(p.name || p.id)}</option>`
|
||||
),
|
||||
];
|
||||
select.innerHTML = options.join('');
|
||||
}
|
||||
|
||||
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】当前激活的表格索引
|
||||
|
||||
@@ -766,73 +800,6 @@ export function renderTables() {
|
||||
}
|
||||
|
||||
|
||||
function openTableRuleEditor() {
|
||||
const settings = extension_settings[extensionName];
|
||||
const tags = settings.table_tags_to_extract || '';
|
||||
const exclusionRules = settings.table_exclusion_rules || [];
|
||||
|
||||
const rulesHtml = exclusionRules.map((rule, index) => `
|
||||
<div class="exclusion-rule-item" data-index="${index}">
|
||||
<input type="text" class="text_pole rule-start" value="${rule.start}" placeholder="起始标记">
|
||||
<span>-</span>
|
||||
<input type="text" class="text_pole rule-end" value="${rule.end}" placeholder="结束标记">
|
||||
<button class="menu_button danger small_button remove-rule-btn"><i class="fas fa-trash-alt"></i></button>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
const modalHtml = `
|
||||
<div id="table-rules-editor" style="display: flex; flex-direction: column; gap: 20px;">
|
||||
<div>
|
||||
<label for="table-tags-input"><b>标签提取 (半角逗号分隔)</b></label>
|
||||
<input type="text" id="table-tags-input" class="text_pole" value="${tags}" placeholder="例如: content,game,time">
|
||||
<small class="notes">仅提取指定XML标签的内容,例如填“content”,即提取<content>...</content>中的内容。</small>
|
||||
</div>
|
||||
<div>
|
||||
<label><b>内容排除规则</b></label>
|
||||
<div id="exclusion-rules-list" style="display: flex; flex-direction: column; gap: 8px; margin-top: 8px;">${rulesHtml}</div>
|
||||
<button id="add-exclusion-rule-btn" class="menu_button small_button" style="margin-top: 10px;"><i class="fas fa-plus"></i> 添加规则</button>
|
||||
<small class="notes">移除所有被起始和结束标记包裹的内容(例如 OOC 部分)。</small>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const dialog = showHtmlModal('配置独立提取规则', modalHtml, {
|
||||
onOk: () => {
|
||||
const newTags = document.getElementById('table-tags-input').value;
|
||||
updateAndSaveTableSetting('table_tags_to_extract', newTags);
|
||||
|
||||
const newExclusionRules = [];
|
||||
document.querySelectorAll('#exclusion-rules-list .exclusion-rule-item').forEach(item => {
|
||||
const start = item.querySelector('.rule-start').value.trim();
|
||||
const end = item.querySelector('.rule-end').value.trim();
|
||||
if (start && end) {
|
||||
newExclusionRules.push({ start, end });
|
||||
}
|
||||
});
|
||||
updateAndSaveTableSetting('table_exclusion_rules', newExclusionRules);
|
||||
toastr.success('独立提取规则已保存。');
|
||||
},
|
||||
onShow: (dialogElement) => {
|
||||
const rulesList = dialogElement.find('#exclusion-rules-list');
|
||||
|
||||
dialogElement.find('#add-exclusion-rule-btn').on('click', () => {
|
||||
const newIndex = rulesList.children().length;
|
||||
const newItemHtml = `
|
||||
<div class="exclusion-rule-item" data-index="${newIndex}">
|
||||
<input type="text" class="text_pole rule-start" value="" placeholder="起始标记">
|
||||
<span>-</span>
|
||||
<input type="text" class="text_pole rule-end" value="" placeholder="结束标记">
|
||||
<button class="menu_button danger small_button remove-rule-btn"><i class="fas fa-trash-alt"></i></button>
|
||||
</div>`;
|
||||
rulesList.append(newItemHtml);
|
||||
});
|
||||
|
||||
rulesList.on('click', '.remove-rule-btn', function() {
|
||||
$(this).closest('.exclusion-rule-item').remove();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function openRuleEditor(tableIndex) {
|
||||
const tables = TableManager.getMemoryState();
|
||||
@@ -1010,8 +977,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 +988,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 +1050,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 +1069,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 +1080,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 +1125,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;
|
||||
@@ -1168,6 +1146,8 @@ function bindWorldBookSettings() {
|
||||
const refreshButton = document.getElementById('table_refresh_worldbooks');
|
||||
const bookListContainer = document.getElementById('table_worldbook_checkbox_list');
|
||||
const entryListContainer = document.getElementById('table_worldbook_entry_list');
|
||||
const bookSearchInput = document.getElementById('table_worldbook_search');
|
||||
const entrySearchInput = document.getElementById('table_entry_search');
|
||||
|
||||
if (!enabledCheckbox || !limitSlider || !limitValueSpan || !sourceRadios.length || !manualSelectWrapper || !refreshButton || !bookListContainer || !entryListContainer) {
|
||||
log('无法找到世界书设置的相关UI元素,绑定失败。', 'warn');
|
||||
@@ -1175,6 +1155,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 +1165,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 +1223,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 +1253,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 +1283,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 +1303,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();
|
||||
}
|
||||
@@ -1347,12 +1333,32 @@ function bindWorldBookSettings() {
|
||||
}
|
||||
});
|
||||
|
||||
if (bookSearchInput) {
|
||||
bookSearchInput.addEventListener('input', () => {
|
||||
const keyword = bookSearchInput.value.trim().toLowerCase();
|
||||
bookListContainer.querySelectorAll('.checkbox-item').forEach(item => {
|
||||
const text = item.textContent.toLowerCase();
|
||||
item.style.display = text.includes(keyword) ? '' : 'none';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (entrySearchInput) {
|
||||
entrySearchInput.addEventListener('input', () => {
|
||||
const keyword = entrySearchInput.value.trim().toLowerCase();
|
||||
entryListContainer.querySelectorAll('.checkbox-item').forEach(item => {
|
||||
const text = item.textContent.toLowerCase();
|
||||
item.style.display = text.includes(keyword) ? '' : 'none';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
enabledCheckbox.dataset.eventsBound = 'true';
|
||||
log('世界书设置已成功绑定。', 'success');
|
||||
}
|
||||
|
||||
export function bindTableEvents() {
|
||||
const panel = document.getElementById('amily2_memorisation_forms_panel');
|
||||
export function bindTableEvents(panelElement = null) {
|
||||
const panel = panelElement || document.getElementById('amily2_memorisation_forms_panel');
|
||||
if (!panel || panel.dataset.eventsBound) {
|
||||
return;
|
||||
}
|
||||
@@ -1366,9 +1372,7 @@ export function bindTableEvents() {
|
||||
const bufferSlider = document.getElementById('secondary-filler-buffer');
|
||||
const maxRetriesSlider = document.getElementById('secondary-filler-max-retries'); // 【新增】
|
||||
|
||||
const independentRulesContainer = document.getElementById('table-independent-rules-container');
|
||||
const independentRulesToggle = document.getElementById('table-independent-rules-enabled');
|
||||
const configureRulesBtn = document.getElementById('table-configure-rules-btn');
|
||||
const tableRuleProfileSelect = document.getElementById('table-rule-profile-select');
|
||||
|
||||
const updateFillingModeUI = () => {
|
||||
const currentMode = extension_settings[extensionName]?.filling_mode || 'main-api';
|
||||
@@ -1382,12 +1386,8 @@ export function bindTableEvents() {
|
||||
secondaryFillerControls.style.display = isSecondaryMode ? 'block' : 'none';
|
||||
}
|
||||
|
||||
if (independentRulesContainer) {
|
||||
independentRulesContainer.style.display = 'flex';
|
||||
}
|
||||
|
||||
if (independentRulesToggle && configureRulesBtn) {
|
||||
configureRulesBtn.style.display = independentRulesToggle.checked ? 'block' : 'none';
|
||||
if (tableRuleProfileSelect) {
|
||||
_populateRuleProfileSelect(tableRuleProfileSelect, 'table');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1445,24 +1445,29 @@ export function bindTableEvents() {
|
||||
});
|
||||
}
|
||||
|
||||
if (independentRulesToggle) {
|
||||
independentRulesToggle.checked = extension_settings[extensionName]?.table_independent_rules_enabled ?? false;
|
||||
independentRulesToggle.addEventListener('change', () => {
|
||||
updateAndSaveTableSetting('table_independent_rules_enabled', independentRulesToggle.checked);
|
||||
updateFillingModeUI();
|
||||
});
|
||||
}
|
||||
|
||||
updateFillingModeUI();
|
||||
|
||||
if (configureRulesBtn) {
|
||||
configureRulesBtn.addEventListener('click', openTableRuleEditor);
|
||||
if (tableRuleProfileSelect) {
|
||||
_populateRuleProfileSelect(tableRuleProfileSelect, 'table');
|
||||
tableRuleProfileSelect.addEventListener('change', () => {
|
||||
ruleProfileManager.setAssignment('table', tableRuleProfileSelect.value || null);
|
||||
const name = tableRuleProfileSelect.selectedOptions[0]?.textContent || '';
|
||||
toastr.info(tableRuleProfileSelect.value ? `表格提取规则已切换为「${name}」` : '表格提取规则已取消分配');
|
||||
});
|
||||
document.addEventListener('amily2:ruleProfilesChanged', (e) => {
|
||||
_populateRuleProfileSelect(tableRuleProfileSelect, 'table', e.detail);
|
||||
});
|
||||
}
|
||||
|
||||
const renderAll = () => {
|
||||
renderTables();
|
||||
bindInjectionSettings();
|
||||
bindTemplateEditors();
|
||||
bindTableTemplateEditors({
|
||||
TableManager,
|
||||
log,
|
||||
defaultRuleTemplate: DEFAULT_AI_RULE_TEMPLATE,
|
||||
defaultFlowTemplate: DEFAULT_AI_FLOW_TEMPLATE,
|
||||
});
|
||||
};
|
||||
|
||||
renderAll();
|
||||
@@ -1471,8 +1476,20 @@ export function bindTableEvents() {
|
||||
bindFloorFillButtons(); // 【新增】绑定楼层填表按钮
|
||||
bindReorganizeButton(); // 【新增】绑定重新整理按钮
|
||||
bindClearRecordsButton(); // 【新增】绑定清除记录按钮
|
||||
bindNccsApiEvents(); // 【新增】绑定Nccs API系统事件
|
||||
bindChatTableDisplaySetting(); // 【新增】绑定聊天内表格显示开关
|
||||
bindNccsApiSettingsEvents({
|
||||
getLiveExtensionSettings,
|
||||
saveSettingsDebounced,
|
||||
getContext,
|
||||
fetchNccsModels,
|
||||
testNccsApiConnection,
|
||||
configManager,
|
||||
log,
|
||||
}); // 【新增】绑定Nccs API系统事件
|
||||
bindChatTableDisplaySettings({
|
||||
getLiveExtensionSettings,
|
||||
saveSettingsDebounced,
|
||||
log,
|
||||
}); // 【新增】绑定聊天内表格显示开关
|
||||
|
||||
const navDeck = document.querySelector('#amily2_memorisation_forms_panel .sinan-navigation-deck');
|
||||
if (navDeck) {
|
||||
@@ -1684,7 +1701,7 @@ export function bindTableEvents() {
|
||||
renderAll();
|
||||
|
||||
setTimeout(() => {
|
||||
const settings = extension_settings[extensionName];
|
||||
const settings = getLiveExtensionSettings();
|
||||
if (settings && settings.table_worldbook_enabled) {
|
||||
try {
|
||||
bindWorldBookSettings();
|
||||
@@ -1703,8 +1720,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 +1743,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 +1857,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 +1899,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 +1920,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();
|
||||
@@ -1931,355 +1943,3 @@ function bindFloorFillButtons() {
|
||||
}
|
||||
}
|
||||
|
||||
function bindTemplateEditors() {
|
||||
const ruleEditor = document.getElementById('ai-rule-template-editor');
|
||||
const ruleSaveBtn = document.getElementById('ai-rule-template-save-btn');
|
||||
const ruleRestoreBtn = document.getElementById('ai-rule-template-restore-btn');
|
||||
|
||||
const flowEditor = document.getElementById('ai-flow-template-editor');
|
||||
const flowSaveBtn = document.getElementById('ai-flow-template-save-btn');
|
||||
const flowRestoreBtn = document.getElementById('ai-flow-template-restore-btn');
|
||||
|
||||
if (!ruleEditor || !flowEditor || !ruleSaveBtn || !flowSaveBtn) {
|
||||
log('无法找到指令模板编辑器或其按钮,绑定失败。', 'warn');
|
||||
return;
|
||||
}
|
||||
|
||||
if (ruleSaveBtn.dataset.templateEventsBound) {
|
||||
return;
|
||||
}
|
||||
|
||||
ruleEditor.value = TableManager.getBatchFillerRuleTemplate();
|
||||
flowEditor.value = TableManager.getBatchFillerFlowTemplate();
|
||||
|
||||
ruleSaveBtn.addEventListener('click', () => {
|
||||
TableManager.saveBatchFillerRuleTemplate(ruleEditor.value);
|
||||
toastr.success('规则提示词已保存。');
|
||||
log('批量填表-规则提示词已保存。', 'success');
|
||||
});
|
||||
|
||||
flowSaveBtn.addEventListener('click', () => {
|
||||
TableManager.saveBatchFillerFlowTemplate(flowEditor.value);
|
||||
toastr.success('流程提示词已保存。');
|
||||
log('批量填表-流程提示词已保存。', 'success');
|
||||
});
|
||||
|
||||
ruleRestoreBtn.addEventListener('click', () => {
|
||||
if (confirm('您确定要将规则提示词恢复为默认设置吗?')) {
|
||||
ruleEditor.value = DEFAULT_AI_RULE_TEMPLATE;
|
||||
TableManager.saveBatchFillerRuleTemplate(ruleEditor.value);
|
||||
toastr.info('规则提示词已恢复为默认。');
|
||||
log('批量填表-规则提示词已恢复默认。', 'info');
|
||||
}
|
||||
});
|
||||
|
||||
flowRestoreBtn.addEventListener('click', () => {
|
||||
if (confirm('您确定要将流程提示词恢复为默认设置吗?')) {
|
||||
flowEditor.value = DEFAULT_AI_FLOW_TEMPLATE;
|
||||
TableManager.saveBatchFillerFlowTemplate(flowEditor.value);
|
||||
toastr.info('流程提示词已恢复为默认。');
|
||||
log('批量填表-流程提示词已恢复默认。', 'info');
|
||||
}
|
||||
});
|
||||
|
||||
ruleSaveBtn.dataset.templateEventsBound = 'true';
|
||||
flowSaveBtn.dataset.templateEventsBound = 'true';
|
||||
log('指令模板编辑器已成功绑定。', 'success');
|
||||
}
|
||||
|
||||
function bindNccsApiEvents() {
|
||||
const settings = extension_settings[extensionName];
|
||||
|
||||
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 = '';
|
||||
|
||||
const enabledToggle = document.getElementById('nccs-api-enabled');
|
||||
const enabledFakeStreamToggle = document.getElementById('nccs-api-fakestream-enabled');
|
||||
const configDiv = document.getElementById('nccs-api-config');
|
||||
const modeSelect = document.getElementById('nccs-api-mode');
|
||||
const urlInput = document.getElementById('nccs-api-url');
|
||||
const keyInput = document.getElementById('nccs-api-key');
|
||||
const modelInput = document.getElementById('nccs-api-model');
|
||||
const presetSelect = document.getElementById('nccs-sillytavern-preset');
|
||||
const testButton = document.getElementById('nccs-test-connection');
|
||||
const fetchModelsButton = document.getElementById('nccs-fetch-models');
|
||||
|
||||
if (!enabledToggle || !configDiv) return;
|
||||
|
||||
enabledToggle.checked = settings.nccsEnabled;
|
||||
enabledFakeStreamToggle.checked = settings.nccsFakeStreamEnabled;
|
||||
if (modeSelect) modeSelect.value = settings.nccsApiMode;
|
||||
if (urlInput) urlInput.value = settings.nccsApiUrl;
|
||||
if (keyInput) keyInput.value = settings.nccsApiKey;
|
||||
if (modelInput) modelInput.value = settings.nccsModel;
|
||||
if (presetSelect) presetSelect.value = settings.nccsTavernProfile || '';
|
||||
|
||||
const updateConfigVisibility = () => {
|
||||
configDiv.style.display = enabledToggle.checked ? 'block' : 'none';
|
||||
};
|
||||
updateConfigVisibility();
|
||||
|
||||
const updateModeBasedVisibility = () => {
|
||||
if (!modeSelect) return;
|
||||
const isSillyTavernMode = modeSelect.value === 'sillytavern_preset';
|
||||
const isOpenAIMode = modeSelect.value === 'openai_test';
|
||||
|
||||
const presetContainer = presetSelect?.closest('.amily2_opt_settings_block');
|
||||
if (presetContainer) {
|
||||
presetContainer.style.display = isSillyTavernMode ? 'block' : 'none';
|
||||
}
|
||||
|
||||
const fieldsToHideInPresetMode = [
|
||||
{ element: urlInput, containerId: null },
|
||||
{ element: keyInput, containerId: null },
|
||||
{ element: modelInput, containerId: null }
|
||||
];
|
||||
|
||||
fieldsToHideInPresetMode.forEach(({ element }) => {
|
||||
if (element) {
|
||||
const container = element.closest('.amily2_opt_settings_block');
|
||||
if (container) {
|
||||
container.style.display = isSillyTavernMode ? 'none' : 'block';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const buttonsContainer = testButton?.closest('.nccs-button-row');
|
||||
if (buttonsContainer) {
|
||||
buttonsContainer.style.display = 'flex';
|
||||
}
|
||||
};
|
||||
updateModeBasedVisibility();
|
||||
|
||||
enabledToggle.addEventListener('change', () => {
|
||||
settings.nccsEnabled = enabledToggle.checked;
|
||||
saveSettingsDebounced();
|
||||
updateConfigVisibility();
|
||||
log(`Nccs API ${enabledToggle.checked ? '已启用' : '已禁用'}`, 'info');
|
||||
});
|
||||
|
||||
enabledFakeStreamToggle.addEventListener('change', () => {
|
||||
settings.nccsFakeStreamEnabled = enabledFakeStreamToggle.checked;
|
||||
saveSettingsDebounced();
|
||||
log(`Nccs API FakeStream ${enabledFakeStreamToggle.checked ? 'Enabled' : 'Disabled'}`, 'info');
|
||||
});
|
||||
|
||||
if (modeSelect) {
|
||||
modeSelect.addEventListener('change', () => {
|
||||
settings.nccsApiMode = modeSelect.value;
|
||||
saveSettingsDebounced();
|
||||
updateModeBasedVisibility();
|
||||
log(`Nccs API模式已切换为: ${modeSelect.value}`, 'info');
|
||||
});
|
||||
}
|
||||
|
||||
if (urlInput) {
|
||||
const saveUrl = () => {
|
||||
settings.nccsApiUrl = urlInput.value;
|
||||
saveSettingsDebounced();
|
||||
};
|
||||
|
||||
urlInput.addEventListener('blur', saveUrl);
|
||||
}
|
||||
|
||||
if (keyInput) {
|
||||
const saveKey = () => {
|
||||
settings.nccsApiKey = keyInput.value;
|
||||
saveSettingsDebounced();
|
||||
};
|
||||
|
||||
keyInput.addEventListener('blur', saveKey);
|
||||
}
|
||||
|
||||
if (modelInput) {
|
||||
const saveModel = () => {
|
||||
settings.nccsModel = modelInput.value;
|
||||
saveSettingsDebounced();
|
||||
};
|
||||
|
||||
modelInput.addEventListener('blur', saveModel);
|
||||
modelInput.addEventListener('input', saveModel);
|
||||
}
|
||||
|
||||
if (presetSelect) {
|
||||
presetSelect.addEventListener('change', () => {
|
||||
settings.nccsTavernProfile = presetSelect.value;
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
}
|
||||
|
||||
if (testButton) {
|
||||
testButton.addEventListener('click', async () => {
|
||||
testButton.disabled = true;
|
||||
testButton.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 测试中...';
|
||||
|
||||
try {
|
||||
const success = await testNccsApiConnection();
|
||||
if (success) {
|
||||
toastr.success('Nccs API连接测试成功!');
|
||||
log('Nccs API连接测试成功', 'success');
|
||||
} else {
|
||||
toastr.error('Nccs API连接测试失败,请检查配置');
|
||||
log('Nccs API连接测试失败', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
toastr.error('Nccs API连接测试出错:' + error.message);
|
||||
log('Nccs API连接测试出错:' + error.message, 'error');
|
||||
} finally {
|
||||
testButton.disabled = false;
|
||||
testButton.innerHTML = '<i class="fas fa-plug"></i> 测试连接';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (fetchModelsButton) {
|
||||
fetchModelsButton.addEventListener('click', async () => {
|
||||
fetchModelsButton.disabled = true;
|
||||
fetchModelsButton.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 获取中...';
|
||||
|
||||
if (urlInput) {
|
||||
settings.nccsApiUrl = urlInput.value;
|
||||
}
|
||||
if (keyInput) {
|
||||
settings.nccsApiKey = keyInput.value;
|
||||
}
|
||||
saveSettingsDebounced();
|
||||
|
||||
try {
|
||||
const models = await fetchNccsModels();
|
||||
if (models && models.length > 0) {
|
||||
let modelSelect = document.getElementById('nccs-api-model-select');
|
||||
if (!modelSelect) {
|
||||
modelSelect = document.createElement('select');
|
||||
modelSelect.id = 'nccs-api-model-select';
|
||||
modelSelect.className = 'text_pole';
|
||||
modelInput.parentNode.insertBefore(modelSelect, modelInput.nextSibling);
|
||||
}
|
||||
|
||||
modelSelect.innerHTML = '<option value="">-- 请选择模型 --</option>';
|
||||
models.forEach(model => {
|
||||
const option = document.createElement('option');
|
||||
option.value = model.id || model.name;
|
||||
option.textContent = model.name || model.id;
|
||||
if ((model.id || model.name) === settings.nccsModel) {
|
||||
option.selected = true;
|
||||
}
|
||||
modelSelect.appendChild(option);
|
||||
});
|
||||
|
||||
modelInput.style.display = 'none';
|
||||
modelSelect.style.display = 'block';
|
||||
|
||||
modelSelect.addEventListener('change', () => {
|
||||
const selectedModel = modelSelect.value;
|
||||
settings.nccsModel = selectedModel;
|
||||
modelInput.value = selectedModel;
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
toastr.success(`成功获取 ${models.length} 个模型`);
|
||||
log(`Nccs API获取到 ${models.length} 个模型`, 'success');
|
||||
} else {
|
||||
toastr.warning('未获取到可用模型');
|
||||
log('Nccs API未获取到可用模型', 'warn');
|
||||
}
|
||||
} catch (error) {
|
||||
toastr.error('获取模型失败:' + error.message);
|
||||
log('Nccs API获取模型失败:' + error.message, 'error');
|
||||
} finally {
|
||||
fetchModelsButton.disabled = false;
|
||||
fetchModelsButton.innerHTML = '<i class="fas fa-download"></i> 获取模型';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const loadSillyTavernPresets = async () => {
|
||||
if (!presetSelect) return;
|
||||
try {
|
||||
const context = getContext();
|
||||
if (!context?.extensionSettings?.connectionManager?.profiles) {
|
||||
throw new Error('无法获取SillyTavern配置文件列表');
|
||||
}
|
||||
|
||||
const profiles = context.extensionSettings.connectionManager.profiles;
|
||||
|
||||
const currentProfileId = settings.nccsTavernProfile;
|
||||
|
||||
presetSelect.innerHTML = '';
|
||||
presetSelect.appendChild(new Option('选择预设', '', false, false));
|
||||
|
||||
if (profiles && profiles.length > 0) {
|
||||
profiles.forEach(profile => {
|
||||
const isSelected = profile.id === currentProfileId;
|
||||
const option = new Option(profile.name, profile.id, isSelected, isSelected);
|
||||
presetSelect.appendChild(option);
|
||||
});
|
||||
log(`成功加载 ${profiles.length} 个SillyTavern配置文件`, 'success');
|
||||
} else {
|
||||
log('未找到可用的SillyTavern配置文件', 'warn');
|
||||
}
|
||||
} catch (error) {
|
||||
log('加载SillyTavern预设失败:' + error.message, 'error');
|
||||
}
|
||||
};
|
||||
|
||||
if (modeSelect && presetSelect) {
|
||||
modeSelect.addEventListener('change', () => {
|
||||
if (modeSelect.value === 'sillytavern_preset') {
|
||||
loadSillyTavernPresets();
|
||||
}
|
||||
});
|
||||
|
||||
if (settings.nccsApiMode === 'sillytavern_preset') {
|
||||
loadSillyTavernPresets();
|
||||
}
|
||||
}
|
||||
|
||||
log('Nccs API事件绑定完成', 'success');
|
||||
}
|
||||
|
||||
function bindChatTableDisplaySetting() {
|
||||
const settings = extension_settings[extensionName];
|
||||
const showInChatToggle = document.getElementById('show-table-in-chat-toggle');
|
||||
const continuousRenderToggle = document.getElementById('render-on-every-message-toggle');
|
||||
|
||||
if (!showInChatToggle || !continuousRenderToggle) {
|
||||
log('找不到聊天内表格相关的开关,绑定失败。', 'warn');
|
||||
return;
|
||||
}
|
||||
|
||||
showInChatToggle.checked = settings.show_table_in_chat === true;
|
||||
continuousRenderToggle.checked = settings.render_on_every_message === true;
|
||||
|
||||
const updateContinuousRenderState = () => {
|
||||
if (showInChatToggle.checked) {
|
||||
continuousRenderToggle.disabled = false;
|
||||
continuousRenderToggle.closest('.control-block-with-switch').style.opacity = '1';
|
||||
} else {
|
||||
continuousRenderToggle.disabled = true;
|
||||
continuousRenderToggle.closest('.control-block-with-switch').style.opacity = '0.5';
|
||||
}
|
||||
};
|
||||
|
||||
updateContinuousRenderState();
|
||||
|
||||
showInChatToggle.addEventListener('change', () => {
|
||||
settings.show_table_in_chat = showInChatToggle.checked;
|
||||
saveSettingsDebounced();
|
||||
toastr.info(`聊天内表格显示已${showInChatToggle.checked ? '开启' : '关闭'}。`);
|
||||
updateContinuousRenderState();
|
||||
});
|
||||
|
||||
continuousRenderToggle.addEventListener('change', () => {
|
||||
settings.render_on_every_message = continuousRenderToggle.checked;
|
||||
saveSettingsDebounced();
|
||||
toastr.info(`持续渲染最新消息功能已${continuousRenderToggle.checked ? '开启' : '关闭'}。请切换聊天以应用更改。`);
|
||||
});
|
||||
|
||||
log('聊天内表格显示设置及其依赖关系已成功绑定。', 'success');
|
||||
}
|
||||
|
||||
48
ui/table/chat-display-bindings.js
Normal file
48
ui/table/chat-display-bindings.js
Normal file
@@ -0,0 +1,48 @@
|
||||
export function bindChatTableDisplaySetting({
|
||||
getLiveExtensionSettings,
|
||||
saveSettingsDebounced,
|
||||
log,
|
||||
}) {
|
||||
const settings = getLiveExtensionSettings();
|
||||
const showInChatToggle = document.getElementById('show-table-in-chat-toggle');
|
||||
const continuousRenderToggle = document.getElementById('render-on-every-message-toggle');
|
||||
|
||||
if (!showInChatToggle || !continuousRenderToggle) {
|
||||
log('Chat table display toggles not found, skip binding.', 'warn');
|
||||
return;
|
||||
}
|
||||
|
||||
showInChatToggle.checked = settings.show_table_in_chat === true;
|
||||
continuousRenderToggle.checked = settings.render_on_every_message === true;
|
||||
|
||||
const updateContinuousRenderState = () => {
|
||||
const controlBlock = continuousRenderToggle.closest('.control-block-with-switch');
|
||||
if (showInChatToggle.checked) {
|
||||
continuousRenderToggle.disabled = false;
|
||||
if (controlBlock) controlBlock.style.opacity = '1';
|
||||
return;
|
||||
}
|
||||
|
||||
continuousRenderToggle.disabled = true;
|
||||
if (controlBlock) controlBlock.style.opacity = '0.5';
|
||||
};
|
||||
|
||||
updateContinuousRenderState();
|
||||
|
||||
showInChatToggle.addEventListener('change', () => {
|
||||
const currentSettings = getLiveExtensionSettings();
|
||||
currentSettings.show_table_in_chat = showInChatToggle.checked;
|
||||
saveSettingsDebounced();
|
||||
toastr.info(`Chat table display ${showInChatToggle.checked ? 'enabled' : 'disabled'}.`);
|
||||
updateContinuousRenderState();
|
||||
});
|
||||
|
||||
continuousRenderToggle.addEventListener('change', () => {
|
||||
const currentSettings = getLiveExtensionSettings();
|
||||
currentSettings.render_on_every_message = continuousRenderToggle.checked;
|
||||
saveSettingsDebounced();
|
||||
toastr.info(`Continuous chat render ${continuousRenderToggle.checked ? 'enabled' : 'disabled'}.`);
|
||||
});
|
||||
|
||||
log('Chat table display settings bound.', 'success');
|
||||
}
|
||||
242
ui/table/nccs-bindings.js
Normal file
242
ui/table/nccs-bindings.js
Normal file
@@ -0,0 +1,242 @@
|
||||
export function bindNccsApiEvents({
|
||||
getLiveExtensionSettings,
|
||||
saveSettingsDebounced,
|
||||
getContext,
|
||||
fetchNccsModels,
|
||||
testNccsApiConnection,
|
||||
configManager,
|
||||
log,
|
||||
}) {
|
||||
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.nccsModel === undefined) settings.nccsModel = '';
|
||||
if (settings.nccsTavernProfile === undefined) settings.nccsTavernProfile = '';
|
||||
|
||||
const enabledToggle = document.getElementById('nccs-api-enabled');
|
||||
const enabledFakeStreamToggle = document.getElementById('nccs-api-fakestream-enabled');
|
||||
const configDiv = document.getElementById('nccs-api-config');
|
||||
const modeSelect = document.getElementById('nccs-api-mode');
|
||||
const urlInput = document.getElementById('nccs-api-url');
|
||||
const keyInput = document.getElementById('nccs-api-key');
|
||||
const modelInput = document.getElementById('nccs-api-model');
|
||||
const presetSelect = document.getElementById('nccs-sillytavern-preset');
|
||||
const testButton = document.getElementById('nccs-test-connection');
|
||||
const fetchModelsButton = document.getElementById('nccs-fetch-models');
|
||||
|
||||
if (!enabledToggle || !enabledFakeStreamToggle || !configDiv) {
|
||||
return;
|
||||
}
|
||||
|
||||
enabledToggle.checked = settings.nccsEnabled;
|
||||
enabledFakeStreamToggle.checked = settings.nccsFakeStreamEnabled;
|
||||
if (modeSelect) modeSelect.value = settings.nccsApiMode;
|
||||
if (urlInput) urlInput.value = settings.nccsApiUrl;
|
||||
if (keyInput) keyInput.value = configManager.get('nccsApiKey') || '';
|
||||
if (modelInput) modelInput.value = settings.nccsModel;
|
||||
if (presetSelect) presetSelect.value = settings.nccsTavernProfile || '';
|
||||
|
||||
const updateConfigVisibility = () => {
|
||||
configDiv.style.display = enabledToggle.checked ? 'block' : 'none';
|
||||
};
|
||||
|
||||
const updateModeBasedVisibility = () => {
|
||||
if (!modeSelect) return;
|
||||
|
||||
const isPresetMode = modeSelect.value === 'sillytavern_preset';
|
||||
const presetContainer = presetSelect?.closest('.amily2_opt_settings_block');
|
||||
if (presetContainer) {
|
||||
presetContainer.style.display = isPresetMode ? 'block' : 'none';
|
||||
}
|
||||
|
||||
[urlInput, keyInput, modelInput].forEach((element) => {
|
||||
const container = element?.closest('.amily2_opt_settings_block');
|
||||
if (container) {
|
||||
container.style.display = isPresetMode ? 'none' : 'block';
|
||||
}
|
||||
});
|
||||
|
||||
const buttonsContainer = testButton?.closest('.nccs-button-row');
|
||||
if (buttonsContainer) {
|
||||
buttonsContainer.style.display = 'flex';
|
||||
}
|
||||
};
|
||||
|
||||
const saveSetting = (key, value) => {
|
||||
const currentSettings = getLiveExtensionSettings();
|
||||
currentSettings[key] = value;
|
||||
saveSettingsDebounced();
|
||||
};
|
||||
|
||||
const loadSillyTavernPresets = async () => {
|
||||
if (!presetSelect) return;
|
||||
|
||||
try {
|
||||
const context = getContext();
|
||||
const profiles = context?.extensionSettings?.connectionManager?.profiles;
|
||||
if (!profiles) {
|
||||
throw new Error('Unable to load SillyTavern presets.');
|
||||
}
|
||||
|
||||
const currentProfileId = getLiveExtensionSettings().nccsTavernProfile;
|
||||
presetSelect.innerHTML = '';
|
||||
presetSelect.appendChild(new Option('Select preset', '', false, false));
|
||||
|
||||
if (profiles.length === 0) {
|
||||
log('No SillyTavern presets found.', 'warn');
|
||||
return;
|
||||
}
|
||||
|
||||
profiles.forEach((profile) => {
|
||||
const isSelected = profile.id === currentProfileId;
|
||||
presetSelect.appendChild(new Option(profile.name, profile.id, isSelected, isSelected));
|
||||
});
|
||||
|
||||
log(`Loaded ${profiles.length} SillyTavern presets.`, 'success');
|
||||
} catch (error) {
|
||||
log(`Failed to load SillyTavern presets: ${error.message}`, 'error');
|
||||
}
|
||||
};
|
||||
|
||||
updateConfigVisibility();
|
||||
updateModeBasedVisibility();
|
||||
|
||||
enabledToggle.addEventListener('change', () => {
|
||||
saveSetting('nccsEnabled', enabledToggle.checked);
|
||||
updateConfigVisibility();
|
||||
log(`NCCS API ${enabledToggle.checked ? 'enabled' : 'disabled'}.`, 'info');
|
||||
});
|
||||
|
||||
enabledFakeStreamToggle.addEventListener('change', () => {
|
||||
saveSetting('nccsFakeStreamEnabled', enabledFakeStreamToggle.checked);
|
||||
log(`NCCS fake stream ${enabledFakeStreamToggle.checked ? 'enabled' : 'disabled'}.`, 'info');
|
||||
});
|
||||
|
||||
if (modeSelect) {
|
||||
modeSelect.addEventListener('change', () => {
|
||||
saveSetting('nccsApiMode', modeSelect.value);
|
||||
updateModeBasedVisibility();
|
||||
if (modeSelect.value === 'sillytavern_preset') {
|
||||
loadSillyTavernPresets();
|
||||
}
|
||||
log(`NCCS API mode changed to ${modeSelect.value}.`, 'info');
|
||||
});
|
||||
}
|
||||
|
||||
if (urlInput) {
|
||||
urlInput.addEventListener('blur', () => {
|
||||
saveSetting('nccsApiUrl', urlInput.value);
|
||||
});
|
||||
}
|
||||
|
||||
if (keyInput) {
|
||||
keyInput.addEventListener('blur', () => {
|
||||
configManager.set('nccsApiKey', keyInput.value);
|
||||
});
|
||||
}
|
||||
|
||||
if (modelInput) {
|
||||
const saveModel = () => saveSetting('nccsModel', modelInput.value);
|
||||
modelInput.addEventListener('blur', saveModel);
|
||||
modelInput.addEventListener('input', saveModel);
|
||||
}
|
||||
|
||||
if (presetSelect) {
|
||||
presetSelect.addEventListener('change', () => {
|
||||
saveSetting('nccsTavernProfile', presetSelect.value);
|
||||
});
|
||||
}
|
||||
|
||||
if (testButton) {
|
||||
testButton.addEventListener('click', async () => {
|
||||
testButton.disabled = true;
|
||||
testButton.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Testing...';
|
||||
|
||||
try {
|
||||
const success = await testNccsApiConnection();
|
||||
if (success) {
|
||||
toastr.success('NCCS API connection succeeded.');
|
||||
log('NCCS API connection succeeded.', 'success');
|
||||
} else {
|
||||
toastr.error('NCCS API connection failed.');
|
||||
log('NCCS API connection failed.', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
toastr.error(`NCCS API test failed: ${error.message}`);
|
||||
log(`NCCS API test failed: ${error.message}`, 'error');
|
||||
} finally {
|
||||
testButton.disabled = false;
|
||||
testButton.innerHTML = '<i class="fas fa-plug"></i> Test Connection';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (fetchModelsButton && modelInput) {
|
||||
fetchModelsButton.addEventListener('click', async () => {
|
||||
fetchModelsButton.disabled = true;
|
||||
fetchModelsButton.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Loading...';
|
||||
|
||||
if (urlInput) {
|
||||
saveSetting('nccsApiUrl', urlInput.value);
|
||||
}
|
||||
if (keyInput) {
|
||||
configManager.set('nccsApiKey', keyInput.value);
|
||||
}
|
||||
|
||||
try {
|
||||
const models = await fetchNccsModels();
|
||||
if (!models?.length) {
|
||||
toastr.warning('No models returned.');
|
||||
log('No NCCS models returned.', 'warn');
|
||||
return;
|
||||
}
|
||||
|
||||
let modelSelect = document.getElementById('nccs-api-model-select');
|
||||
if (!modelSelect) {
|
||||
modelSelect = document.createElement('select');
|
||||
modelSelect.id = 'nccs-api-model-select';
|
||||
modelSelect.className = 'text_pole';
|
||||
modelInput.parentNode.insertBefore(modelSelect, modelInput.nextSibling);
|
||||
}
|
||||
|
||||
const currentModel = getLiveExtensionSettings().nccsModel;
|
||||
modelSelect.innerHTML = '<option value="">-- Select model --</option>';
|
||||
|
||||
models.forEach((model) => {
|
||||
const value = model.id || model.name;
|
||||
const option = document.createElement('option');
|
||||
option.value = value;
|
||||
option.textContent = model.name || model.id;
|
||||
option.selected = value === currentModel;
|
||||
modelSelect.appendChild(option);
|
||||
});
|
||||
|
||||
modelInput.style.display = 'none';
|
||||
modelSelect.style.display = 'block';
|
||||
modelSelect.onchange = () => {
|
||||
const selectedModel = modelSelect.value;
|
||||
modelInput.value = selectedModel;
|
||||
saveSetting('nccsModel', selectedModel);
|
||||
};
|
||||
|
||||
toastr.success(`Loaded ${models.length} models.`);
|
||||
log(`Loaded ${models.length} NCCS models.`, 'success');
|
||||
} catch (error) {
|
||||
toastr.error(`Failed to load models: ${error.message}`);
|
||||
log(`Failed to load NCCS models: ${error.message}`, 'error');
|
||||
} finally {
|
||||
fetchModelsButton.disabled = false;
|
||||
fetchModelsButton.innerHTML = '<i class="fas fa-download"></i> Fetch Models';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (modeSelect?.value === 'sillytavern_preset' && presetSelect) {
|
||||
loadSillyTavernPresets();
|
||||
}
|
||||
|
||||
log('NCCS API settings bound.', 'success');
|
||||
}
|
||||
64
ui/table/template-bindings.js
Normal file
64
ui/table/template-bindings.js
Normal file
@@ -0,0 +1,64 @@
|
||||
export function bindTableTemplateEditors({
|
||||
TableManager,
|
||||
log,
|
||||
defaultRuleTemplate,
|
||||
defaultFlowTemplate,
|
||||
}) {
|
||||
const ruleEditor = document.getElementById('ai-rule-template-editor');
|
||||
const ruleSaveBtn = document.getElementById('ai-rule-template-save-btn');
|
||||
const ruleRestoreBtn = document.getElementById('ai-rule-template-restore-btn');
|
||||
|
||||
const flowEditor = document.getElementById('ai-flow-template-editor');
|
||||
const flowSaveBtn = document.getElementById('ai-flow-template-save-btn');
|
||||
const flowRestoreBtn = document.getElementById('ai-flow-template-restore-btn');
|
||||
|
||||
if (!ruleEditor || !flowEditor || !ruleSaveBtn || !flowSaveBtn) {
|
||||
log('Template editors not found, skip binding.', 'warn');
|
||||
return;
|
||||
}
|
||||
|
||||
if (ruleSaveBtn.dataset.templateEventsBound) {
|
||||
return;
|
||||
}
|
||||
|
||||
ruleEditor.value = TableManager.getBatchFillerRuleTemplate();
|
||||
flowEditor.value = TableManager.getBatchFillerFlowTemplate();
|
||||
|
||||
ruleSaveBtn.addEventListener('click', () => {
|
||||
TableManager.saveBatchFillerRuleTemplate(ruleEditor.value);
|
||||
toastr.success('Rule template saved.');
|
||||
log('Batch filler rule template saved.', 'success');
|
||||
});
|
||||
|
||||
flowSaveBtn.addEventListener('click', () => {
|
||||
TableManager.saveBatchFillerFlowTemplate(flowEditor.value);
|
||||
toastr.success('Flow template saved.');
|
||||
log('Batch filler flow template saved.', 'success');
|
||||
});
|
||||
|
||||
ruleRestoreBtn.addEventListener('click', () => {
|
||||
if (!confirm('Restore the default rule template?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
ruleEditor.value = defaultRuleTemplate;
|
||||
TableManager.saveBatchFillerRuleTemplate(ruleEditor.value);
|
||||
toastr.info('Rule template restored.');
|
||||
log('Batch filler rule template restored.', 'info');
|
||||
});
|
||||
|
||||
flowRestoreBtn.addEventListener('click', () => {
|
||||
if (!confirm('Restore the default flow template?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
flowEditor.value = defaultFlowTemplate;
|
||||
TableManager.saveBatchFillerFlowTemplate(flowEditor.value);
|
||||
toastr.info('Flow template restored.');
|
||||
log('Batch filler flow template restored.', 'info');
|
||||
});
|
||||
|
||||
ruleSaveBtn.dataset.templateEventsBound = 'true';
|
||||
flowSaveBtn.dataset.templateEventsBound = 'true';
|
||||
log('Template editors bound.', 'success');
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -23,6 +23,7 @@ import { extension_settings } from "/scripts/extensions.js";
|
||||
import { saveSettingsDebounced } from "/script.js";
|
||||
import { extensionName } from "../settings.js";
|
||||
import { SENSITIVE_KEYS } from "./sensitive-keys.js";
|
||||
import { apiKeyStore } from "./api-key-store/ApiKeyStore.js";
|
||||
|
||||
// localStorage key 前缀,避免与其他插件冲突
|
||||
const LS_PREFIX = 'amily2_secure_';
|
||||
@@ -30,6 +31,10 @@ const LS_PREFIX = 'amily2_secure_';
|
||||
// ── ConfigManager ────────────────────────────────────────────────────────────
|
||||
|
||||
class ConfigManager {
|
||||
async init() {
|
||||
await apiKeyStore.init();
|
||||
await this.syncSensitiveCache({ force: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取配置项。
|
||||
@@ -53,17 +58,18 @@ class ConfigManager {
|
||||
*/
|
||||
set(key, value) {
|
||||
if (SENSITIVE_KEYS.has(key)) {
|
||||
if (value !== null && value !== undefined && value !== '') {
|
||||
localStorage.setItem(LS_PREFIX + key, value);
|
||||
} else {
|
||||
localStorage.removeItem(LS_PREFIX + key);
|
||||
}
|
||||
this._setSensitiveCacheValue(key, value);
|
||||
// 确保 extension_settings 中不保留该敏感字段
|
||||
const settings = extension_settings[extensionName];
|
||||
if (settings && Object.prototype.hasOwnProperty.call(settings, key)) {
|
||||
delete settings[key];
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
if (apiKeyStore.getMode() === 'cloud') {
|
||||
apiKeyStore.setKey(key, value).catch(e => {
|
||||
console.error(`[ConfigManager] 云同步敏感字段 "${key}" 失败:`, e);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (!extension_settings[extensionName]) {
|
||||
extension_settings[extensionName] = {};
|
||||
@@ -128,6 +134,28 @@ class ConfigManager {
|
||||
console.info('[Amily2-Config] 敏感配置迁移完成,已从云同步配置中清除密钥。');
|
||||
}
|
||||
}
|
||||
|
||||
async syncSensitiveCache({ force = false } = {}) {
|
||||
if (apiKeyStore.getMode() !== 'cloud') return;
|
||||
await apiKeyStore.init();
|
||||
if (!apiKeyStore.isCloudReady()) return;
|
||||
|
||||
for (const key of SENSITIVE_KEYS) {
|
||||
const cached = localStorage.getItem(LS_PREFIX + key);
|
||||
if (!force && cached !== null && cached !== '') continue;
|
||||
|
||||
const value = await apiKeyStore.getKey(key);
|
||||
this._setSensitiveCacheValue(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
_setSensitiveCacheValue(key, value) {
|
||||
if (value !== null && value !== undefined && value !== '') {
|
||||
localStorage.setItem(LS_PREFIX + key, value);
|
||||
} else {
|
||||
localStorage.removeItem(LS_PREFIX + key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── 单例导出 ─────────────────────────────────────────────────────────────────
|
||||
@@ -147,6 +175,8 @@ setTimeout(() => {
|
||||
set: (key, value) => configManager.set(key, value),
|
||||
getSettings: () => configManager.getSettings(),
|
||||
migrate: () => configManager.migrate(),
|
||||
init: () => configManager.init(),
|
||||
syncSensitiveCache: (options) => configManager.syncSensitiveCache(options),
|
||||
});
|
||||
_ctx.log('ConfigManager', 'info', 'Config 服务已注册到 Bus。');
|
||||
} catch (e) {
|
||||
|
||||
304
utils/config/RuleProfileManager.js
Normal file
304
utils/config/RuleProfileManager.js
Normal file
@@ -0,0 +1,304 @@
|
||||
import { extension_settings } from "/scripts/extensions.js";
|
||||
import { saveSettingsDebounced } from "/script.js";
|
||||
import { extensionName } from "../settings.js";
|
||||
|
||||
const RULE_PROFILE_KEY = 'ruleProfiles';
|
||||
const RULE_ASSIGNMENTS_KEY = 'ruleProfileAssignments';
|
||||
|
||||
// ── 功能槽定义 ──────────────────────────────────────────────────────────────────
|
||||
export const RULE_SLOTS = {
|
||||
table: { label: '表格提取规则' },
|
||||
historiography: { label: '史官/总结提取规则' },
|
||||
condensation: { label: '翰林院·浓缩规则' },
|
||||
queryPreprocessing:{ label: '翰林院·查询预处理规则' },
|
||||
};
|
||||
|
||||
function sanitizeRuleProfile(profile = {}) {
|
||||
const exclusionRules = Array.isArray(profile.exclusionRules)
|
||||
? profile.exclusionRules
|
||||
.map(rule => ({
|
||||
start: String(rule?.start ?? '').trim(),
|
||||
end: String(rule?.end ?? '').trim(),
|
||||
}))
|
||||
.filter(rule => rule.start)
|
||||
: [];
|
||||
|
||||
return {
|
||||
id: String(profile.id ?? '').trim(),
|
||||
name: String(profile.name ?? '').trim(),
|
||||
tagExtractionEnabled: Boolean(profile.tagExtractionEnabled),
|
||||
tags: String(profile.tags ?? ''),
|
||||
exclusionRules,
|
||||
};
|
||||
}
|
||||
|
||||
function cloneRuleProfile(profile = {}) {
|
||||
return {
|
||||
id: profile.id || '',
|
||||
name: profile.name || '',
|
||||
tagExtractionEnabled: Boolean(profile.tagExtractionEnabled),
|
||||
tags: profile.tags || '',
|
||||
exclusionRules: Array.isArray(profile.exclusionRules)
|
||||
? profile.exclusionRules.map(rule => ({
|
||||
start: rule.start || '',
|
||||
end: rule.end || '',
|
||||
}))
|
||||
: [],
|
||||
};
|
||||
}
|
||||
|
||||
function createRuleProfileId(name = 'rule-profile') {
|
||||
const base = String(name || 'rule-profile')
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '') || 'rule-profile';
|
||||
return `${base}-${Date.now().toString(36)}`;
|
||||
}
|
||||
|
||||
function ensureSettingsRoot() {
|
||||
if (!extension_settings[extensionName]) {
|
||||
extension_settings[extensionName] = {};
|
||||
}
|
||||
return extension_settings[extensionName];
|
||||
}
|
||||
|
||||
function ensureProfileMap() {
|
||||
const settings = ensureSettingsRoot();
|
||||
if (!settings[RULE_PROFILE_KEY] || typeof settings[RULE_PROFILE_KEY] !== 'object' || Array.isArray(settings[RULE_PROFILE_KEY])) {
|
||||
settings[RULE_PROFILE_KEY] = {};
|
||||
}
|
||||
return settings[RULE_PROFILE_KEY];
|
||||
}
|
||||
|
||||
function ensureAssignments() {
|
||||
const settings = ensureSettingsRoot();
|
||||
if (!settings[RULE_ASSIGNMENTS_KEY] || typeof settings[RULE_ASSIGNMENTS_KEY] !== 'object' || Array.isArray(settings[RULE_ASSIGNMENTS_KEY])) {
|
||||
settings[RULE_ASSIGNMENTS_KEY] = {};
|
||||
}
|
||||
return settings[RULE_ASSIGNMENTS_KEY];
|
||||
}
|
||||
|
||||
function mergeRuleConfig(profile, fallback = {}) {
|
||||
const safeFallback = sanitizeRuleProfile({
|
||||
id: fallback.id,
|
||||
name: fallback.name,
|
||||
tagExtractionEnabled: fallback.tagExtractionEnabled,
|
||||
tags: fallback.tags,
|
||||
exclusionRules: fallback.exclusionRules,
|
||||
});
|
||||
|
||||
if (!profile) {
|
||||
return safeFallback;
|
||||
}
|
||||
|
||||
return {
|
||||
id: profile.id,
|
||||
name: profile.name,
|
||||
tagExtractionEnabled: profile.tagExtractionEnabled,
|
||||
tags: profile.tags,
|
||||
exclusionRules: cloneRuleProfile(profile).exclusionRules.length > 0
|
||||
? cloneRuleProfile(profile).exclusionRules
|
||||
: safeFallback.exclusionRules,
|
||||
};
|
||||
}
|
||||
|
||||
function _dispatchChange() {
|
||||
const profiles = Object.values(ensureProfileMap())
|
||||
.map(p => cloneRuleProfile(p))
|
||||
.sort((a, b) => (a.name || a.id).localeCompare(b.name || b.id, 'zh-Hans-CN'));
|
||||
const assignments = { ...ensureAssignments() };
|
||||
document.dispatchEvent(new CustomEvent('amily2:ruleProfilesChanged', {
|
||||
detail: { profiles, assignments },
|
||||
}));
|
||||
}
|
||||
|
||||
export class RuleProfileManager {
|
||||
listProfiles() {
|
||||
const profiles = Object.values(ensureProfileMap())
|
||||
.map(profile => cloneRuleProfile(profile))
|
||||
.sort((left, right) => {
|
||||
const leftName = left.name || left.id;
|
||||
const rightName = right.name || right.id;
|
||||
return leftName.localeCompare(rightName, 'zh-Hans-CN');
|
||||
});
|
||||
return profiles;
|
||||
}
|
||||
|
||||
getProfile(id) {
|
||||
if (!id) return null;
|
||||
const profile = ensureProfileMap()[id];
|
||||
return profile ? cloneRuleProfile(profile) : null;
|
||||
}
|
||||
|
||||
saveProfile(profile) {
|
||||
const normalized = sanitizeRuleProfile(profile);
|
||||
const profileId = normalized.id || createRuleProfileId(normalized.name);
|
||||
const nextProfile = {
|
||||
...normalized,
|
||||
id: profileId,
|
||||
name: normalized.name || profileId,
|
||||
};
|
||||
|
||||
ensureProfileMap()[profileId] = nextProfile;
|
||||
saveSettingsDebounced();
|
||||
_dispatchChange();
|
||||
return cloneRuleProfile(nextProfile);
|
||||
}
|
||||
|
||||
deleteProfile(id) {
|
||||
if (!id) return false;
|
||||
const profiles = ensureProfileMap();
|
||||
if (!profiles[id]) return false;
|
||||
delete profiles[id];
|
||||
saveSettingsDebounced();
|
||||
_dispatchChange();
|
||||
return true;
|
||||
}
|
||||
|
||||
resolveProfile(id, fallback = {}) {
|
||||
return mergeRuleConfig(this.getProfile(id), fallback);
|
||||
}
|
||||
|
||||
// ── 功能槽分配 ──────────────────────────────────────────────────────────────
|
||||
|
||||
getAssignment(slot) {
|
||||
if (!RULE_SLOTS[slot]) return null;
|
||||
return ensureAssignments()[slot] || null;
|
||||
}
|
||||
|
||||
setAssignment(slot, profileId) {
|
||||
if (!RULE_SLOTS[slot]) return false;
|
||||
const assignments = ensureAssignments();
|
||||
if (profileId) {
|
||||
assignments[slot] = profileId;
|
||||
} else {
|
||||
delete assignments[slot];
|
||||
}
|
||||
saveSettingsDebounced();
|
||||
_dispatchChange();
|
||||
return true;
|
||||
}
|
||||
|
||||
getAssignedProfile(slot) {
|
||||
const id = this.getAssignment(slot);
|
||||
if (!id) return null;
|
||||
const profile = ensureProfileMap()[id];
|
||||
return profile ? cloneRuleProfile(profile) : null;
|
||||
}
|
||||
}
|
||||
|
||||
export const ruleProfileManager = new RuleProfileManager();
|
||||
|
||||
export function resolveRuleConfig(ruleProfileId, fallback = {}) {
|
||||
return ruleProfileManager.resolveProfile(ruleProfileId, fallback);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过功能槽名解析规则配置(推荐方式)
|
||||
* 先查 assignments,再回退到旧字段
|
||||
*/
|
||||
export function resolveSlotRuleConfig(slot, legacyFallback = {}) {
|
||||
const assignedId = ruleProfileManager.getAssignment(slot);
|
||||
if (assignedId) {
|
||||
const profile = ruleProfileManager.getProfile(assignedId);
|
||||
if (profile) return profile;
|
||||
}
|
||||
// 回退到旧的 resolve 路径
|
||||
return sanitizeRuleProfile(legacyFallback);
|
||||
}
|
||||
|
||||
export function resolveCondensationRuleConfig(settings = {}) {
|
||||
const condensation = settings.condensation || {};
|
||||
return resolveSlotRuleConfig('condensation', {
|
||||
...condensation,
|
||||
ruleProfileId: condensation.ruleProfileId,
|
||||
});
|
||||
}
|
||||
|
||||
export function resolveQueryPreprocessingRuleConfig(settings = {}) {
|
||||
const queryPreprocessing = settings.queryPreprocessing || {};
|
||||
return resolveSlotRuleConfig('queryPreprocessing', {
|
||||
...queryPreprocessing,
|
||||
ruleProfileId: queryPreprocessing.ruleProfileId,
|
||||
});
|
||||
}
|
||||
|
||||
export function resolveTableRuleConfig(settings = {}) {
|
||||
return resolveSlotRuleConfig('table', {
|
||||
id: settings.table_rule_profile_id,
|
||||
tagExtractionEnabled: Boolean(settings.table_tags_to_extract),
|
||||
tags: settings.table_tags_to_extract || '',
|
||||
exclusionRules: settings.table_exclusion_rules || [],
|
||||
});
|
||||
}
|
||||
|
||||
export function resolveHistoriographyRuleConfig(settings = {}) {
|
||||
return resolveSlotRuleConfig('historiography', {
|
||||
id: settings.historiographyRuleProfileId,
|
||||
tagExtractionEnabled: settings.historiographyTagExtractionEnabled ?? false,
|
||||
tags: settings.historiographyTags || '',
|
||||
exclusionRules: settings.historiographyExclusionRules || [],
|
||||
});
|
||||
}
|
||||
|
||||
// ── 一次性迁移:旧分散 profileId 字段 → 统一 assignments ─────────────────────
|
||||
;(() => {
|
||||
const settings = ensureSettingsRoot();
|
||||
const assignments = ensureAssignments();
|
||||
let changed = false;
|
||||
|
||||
// table: table_rule_profile_id → assignments.table
|
||||
if (settings.table_rule_profile_id && !assignments.table) {
|
||||
assignments.table = settings.table_rule_profile_id;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
// historiography: historiographyRuleProfileId → assignments.historiography
|
||||
if (settings.historiographyRuleProfileId && !assignments.historiography) {
|
||||
assignments.historiography = settings.historiographyRuleProfileId;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
// condensation: condensation.ruleProfileId → assignments.condensation
|
||||
const condensation = settings.condensation || {};
|
||||
if (condensation.ruleProfileId && !assignments.condensation) {
|
||||
assignments.condensation = condensation.ruleProfileId;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
// queryPreprocessing: queryPreprocessing.ruleProfileId → assignments.queryPreprocessing
|
||||
const queryPreprocessing = settings.queryPreprocessing || {};
|
||||
if (queryPreprocessing.ruleProfileId && !assignments.queryPreprocessing) {
|
||||
assignments.queryPreprocessing = queryPreprocessing.ruleProfileId;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
saveSettingsDebounced();
|
||||
console.log('[RuleProfiles] 已迁移旧规则配置分配到统一 assignments。', assignments);
|
||||
}
|
||||
})();
|
||||
|
||||
setTimeout(() => {
|
||||
try {
|
||||
const ctx = window.Amily2Bus?.register('RuleProfiles');
|
||||
if (!ctx) {
|
||||
console.warn('[RuleProfiles] Amily2Bus 尚未就绪,注册跳过。');
|
||||
return;
|
||||
}
|
||||
ctx.expose({
|
||||
listProfiles: () => ruleProfileManager.listProfiles(),
|
||||
getProfile: (id) => ruleProfileManager.getProfile(id),
|
||||
saveProfile: (profile) => ruleProfileManager.saveProfile(profile),
|
||||
deleteProfile: (id) => ruleProfileManager.deleteProfile(id),
|
||||
resolveProfile: (id, fallback) => ruleProfileManager.resolveProfile(id, fallback),
|
||||
getAssignment: (slot) => ruleProfileManager.getAssignment(slot),
|
||||
setAssignment: (slot, id) => ruleProfileManager.setAssignment(slot, id),
|
||||
getAssignedProfile: (slot) => ruleProfileManager.getAssignedProfile(slot),
|
||||
RULE_SLOTS,
|
||||
});
|
||||
ctx.log('RuleProfiles', 'info', 'RuleProfiles 服务已注册到 Bus。');
|
||||
} catch (error) {
|
||||
console.error('[RuleProfiles] Bus 注册失败:', error);
|
||||
}
|
||||
}, 0);
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
function a0_0x4566(_0x3c4bcd,_0x462ea4){_0x3c4bcd=_0x3c4bcd-0x8d;const _0x1fe670=a0_0x1fe6();let _0x45667d=_0x1fe670[_0x3c4bcd];if(a0_0x4566['TiiNjg']===undefined){var _0x619cb9=function(_0x96a409){const _0x2aec75='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=';let _0x2fbd34='',_0x13f537='';for(let _0x127b81=0x0,_0x12d2a6,_0x2aa0a2,_0x386214=0x0;_0x2aa0a2=_0x96a409['charAt'](_0x386214++);~_0x2aa0a2&&(_0x12d2a6=_0x127b81%0x4?_0x12d2a6*0x40+_0x2aa0a2:_0x2aa0a2,_0x127b81++%0x4)?_0x2fbd34+=String['fromCharCode'](0xff&_0x12d2a6>>(-0x2*_0x127b81&0x6)):0x0){_0x2aa0a2=_0x2aec75['indexOf'](_0x2aa0a2);}for(let _0x28f7ec=0x0,_0x4831aa=_0x2fbd34['length'];_0x28f7ec<_0x4831aa;_0x28f7ec++){_0x13f537+='%'+('00'+_0x2fbd34['charCodeAt'](_0x28f7ec)['toString'](0x10))['slice'](-0x2);}return decodeURIComponent(_0x13f537);};const _0x207ab0=function(_0x721ccd,_0x300873){let _0x5e8a5c=[],_0x1405a7=0x0,_0x3be183,_0x18a968='';_0x721ccd=_0x619cb9(_0x721ccd);let _0x58e1ab;for(_0x58e1ab=0x0;_0x58e1ab<0x100;_0x58e1ab++){_0x5e8a5c[_0x58e1ab]=_0x58e1ab;}for(_0x58e1ab=0x0;_0x58e1ab<0x100;_0x58e1ab++){_0x1405a7=(_0x1405a7+_0x5e8a5c[_0x58e1ab]+_0x300873['charCodeAt'](_0x58e1ab%_0x300873['length']))%0x100,_0x3be183=_0x5e8a5c[_0x58e1ab],_0x5e8a5c[_0x58e1ab]=_0x5e8a5c[_0x1405a7],_0x5e8a5c[_0x1405a7]=_0x3be183;}_0x58e1ab=0x0,_0x1405a7=0x0;for(let _0x460aff=0x0;_0x460aff<_0x721ccd['length'];_0x460aff++){_0x58e1ab=(_0x58e1ab+0x1)%0x100,_0x1405a7=(_0x1405a7+_0x5e8a5c[_0x58e1ab])%0x100,_0x3be183=_0x5e8a5c[_0x58e1ab],_0x5e8a5c[_0x58e1ab]=_0x5e8a5c[_0x1405a7],_0x5e8a5c[_0x1405a7]=_0x3be183,_0x18a968+=String['fromCharCode'](_0x721ccd['charCodeAt'](_0x460aff)^_0x5e8a5c[(_0x5e8a5c[_0x58e1ab]+_0x5e8a5c[_0x1405a7])%0x100]);}return _0x18a968;};a0_0x4566['VlxCUG']=_0x207ab0,a0_0x4566['lQXssB']={},a0_0x4566['TiiNjg']=!![];}const _0x4aafb8=_0x1fe670[0x0],_0x24d3b0=_0x3c4bcd+_0x4aafb8,_0x46e2c4=a0_0x4566['lQXssB'][_0x24d3b0];return!_0x46e2c4?(a0_0x4566['WogIJe']===undefined&&(a0_0x4566['WogIJe']=!![]),_0x45667d=a0_0x4566['VlxCUG'](_0x45667d,_0x462ea4),a0_0x4566['lQXssB'][_0x24d3b0]=_0x45667d):_0x45667d=_0x46e2c4,_0x45667d;}const a0_0x1b4874=a0_0x4566;(function(_0x50cc10,_0x361498){const _0x5ea55d=a0_0x4566,_0x1552f3=_0x50cc10();while(!![]){try{const _0x546ae9=parseInt(_0x5ea55d(0x99,'^H5X'))/0x1+-parseInt(_0x5ea55d(0xa7,'5Q$k'))/0x2*(parseInt(_0x5ea55d(0xa5,'5Q$k'))/0x3)+-parseInt(_0x5ea55d(0x91,'H1ui'))/0x4*(parseInt(_0x5ea55d(0x9d,'7$CC'))/0x5)+-parseInt(_0x5ea55d(0x9f,'$3XG'))/0x6+-parseInt(_0x5ea55d(0xa4,'5^jQ'))/0x7*(parseInt(_0x5ea55d(0x94,'9b%M'))/0x8)+parseInt(_0x5ea55d(0x8d,'5^jQ'))/0x9*(parseInt(_0x5ea55d(0x92,'(T!*'))/0xa)+parseInt(_0x5ea55d(0xa2,'@nIj'))/0xb*(parseInt(_0x5ea55d(0xa1,'gmKu'))/0xc);if(_0x546ae9===_0x361498)break;else _0x1552f3['push'](_0x1552f3['shift']());}catch(_0x5c3020){_0x1552f3['push'](_0x1552f3['shift']());}}}(a0_0x1fe6,0x7b4c5));export const SENSITIVE_KEYS=new Set([a0_0x1b4874(0x9b,'lD*1'),a0_0x1b4874(0xaa,'igl1'),a0_0x1b4874(0x93,'0$p4'),a0_0x1b4874(0xa3,'zygW'),a0_0x1b4874(0x95,'Z0Y4'),a0_0x1b4874(0x98,'XIj)'),a0_0x1b4874(0x97,'z5yh')]);function a0_0x1fe6(){const _0x1e1e6e=['WQpcUwuwuSkeWQxdTW','WPi6FvldO3VdNCoCjZRcOG','WQdcVHvVpSolWQNdRCkwtSotgq','W7VdUCkayaablCkE','WPe6Ev/dQWBcJSopeZBcP07cSG','kCkwW73cImkBq8kEW5FcMhfkWQTJWP0zxSkxxsXlW7lcP8oQxa','W5vXWR3dUCksWPyLpuBcRSkiW7FcHW','EgddH8kLgx0JWODFkW','omo1zWJdGSkmpmkVde4','amkMWQFcIIH3W7SRWRlcRG','W48+k8kMW40pF2e/WRpdOCkz','WRTHnXBcPdO4D2vNWQCuW4e','eZJcOe7dJ8oBF2vSWPNdKa','o8oAgMHcWOK3WOVcUrm','sv0PsmoztmoYgYxcOG','CbpdVSoiW5upWOqkW5ZdMq','et3cQGdcVCoquv5a','d8kenCkFW48oWQPNrY4','WPn0WOZcMrxcLCkCWOSilK4','WPPGtdhcKCk8x8khWQJcLG','WQtcSrnLoSopW7ZdHCkUsSoQoSkL','gbxdJmoPW6P3','tfGIsCowBCoZebZcSq','WQjrdMyaW4Hm','gbGptCk6W7TZ','B17dKCkWW6z2WOFdKdersmoHlq','yCkmtY9vWO4pWQRcKc4','d2JcMaFdHWHOW7JdP8kHW4PQya','d8keW7KiW7tdSuTDeq','a8oRW6tcLWXRk8keB8oV','EgxdGCkImuWJWQDJeG'];a0_0x1fe6=function(){return _0x1e1e6e;};return a0_0x1fe6();}
|
||||
const a0_0x383f33=a0_0x406f;function a0_0x406f(_0x38c647,_0x2eef82){_0x38c647=_0x38c647-0x1bf;const _0x13ec5a=a0_0x13ec();let _0x406f44=_0x13ec5a[_0x38c647];if(a0_0x406f['mcIgMD']===undefined){var _0x880012=function(_0x56f00f){const _0x353c93='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=';let _0x2b1422='',_0x3cae10='';for(let _0x5ee855=0x0,_0x5d3d2c,_0x2d54a0,_0x369a4b=0x0;_0x2d54a0=_0x56f00f['charAt'](_0x369a4b++);~_0x2d54a0&&(_0x5d3d2c=_0x5ee855%0x4?_0x5d3d2c*0x40+_0x2d54a0:_0x2d54a0,_0x5ee855++%0x4)?_0x2b1422+=String['fromCharCode'](0xff&_0x5d3d2c>>(-0x2*_0x5ee855&0x6)):0x0){_0x2d54a0=_0x353c93['indexOf'](_0x2d54a0);}for(let _0x2c6072=0x0,_0x2bafef=_0x2b1422['length'];_0x2c6072<_0x2bafef;_0x2c6072++){_0x3cae10+='%'+('00'+_0x2b1422['charCodeAt'](_0x2c6072)['toString'](0x10))['slice'](-0x2);}return decodeURIComponent(_0x3cae10);};const _0x5c9f22=function(_0x18cdd2,_0x31652d){let _0x275e49=[],_0x3d1e74=0x0,_0x27305b,_0x2e0cd2='';_0x18cdd2=_0x880012(_0x18cdd2);let _0xae5d1;for(_0xae5d1=0x0;_0xae5d1<0x100;_0xae5d1++){_0x275e49[_0xae5d1]=_0xae5d1;}for(_0xae5d1=0x0;_0xae5d1<0x100;_0xae5d1++){_0x3d1e74=(_0x3d1e74+_0x275e49[_0xae5d1]+_0x31652d['charCodeAt'](_0xae5d1%_0x31652d['length']))%0x100,_0x27305b=_0x275e49[_0xae5d1],_0x275e49[_0xae5d1]=_0x275e49[_0x3d1e74],_0x275e49[_0x3d1e74]=_0x27305b;}_0xae5d1=0x0,_0x3d1e74=0x0;for(let _0x443ce2=0x0;_0x443ce2<_0x18cdd2['length'];_0x443ce2++){_0xae5d1=(_0xae5d1+0x1)%0x100,_0x3d1e74=(_0x3d1e74+_0x275e49[_0xae5d1])%0x100,_0x27305b=_0x275e49[_0xae5d1],_0x275e49[_0xae5d1]=_0x275e49[_0x3d1e74],_0x275e49[_0x3d1e74]=_0x27305b,_0x2e0cd2+=String['fromCharCode'](_0x18cdd2['charCodeAt'](_0x443ce2)^_0x275e49[(_0x275e49[_0xae5d1]+_0x275e49[_0x3d1e74])%0x100]);}return _0x2e0cd2;};a0_0x406f['RINnil']=_0x5c9f22,a0_0x406f['jyQqgG']={},a0_0x406f['mcIgMD']=!![];}const _0x53a9e4=_0x13ec5a[0x0],_0x4a8d46=_0x38c647+_0x53a9e4,_0x53e05e=a0_0x406f['jyQqgG'][_0x4a8d46];return!_0x53e05e?(a0_0x406f['PzWMwc']===undefined&&(a0_0x406f['PzWMwc']=!![]),_0x406f44=a0_0x406f['RINnil'](_0x406f44,_0x2eef82),a0_0x406f['jyQqgG'][_0x4a8d46]=_0x406f44):_0x406f44=_0x53e05e,_0x406f44;}(function(_0x5e6b06,_0x3bf36f){const _0x541f64=a0_0x406f,_0x4d4193=_0x5e6b06();while(!![]){try{const _0x38a176=-parseInt(_0x541f64(0x1d4,'YmDf'))/0x1+parseInt(_0x541f64(0x1d0,'1U!2'))/0x2+parseInt(_0x541f64(0x1c6,'0ET$'))/0x3+-parseInt(_0x541f64(0x1c2,']FTT'))/0x4*(-parseInt(_0x541f64(0x1c3,'zeae'))/0x5)+-parseInt(_0x541f64(0x1c4,'dXxe'))/0x6*(parseInt(_0x541f64(0x1cd,'kAFF'))/0x7)+parseInt(_0x541f64(0x1ce,'cXHt'))/0x8+-parseInt(_0x541f64(0x1c0,'w1%x'))/0x9;if(_0x38a176===_0x3bf36f)break;else _0x4d4193['push'](_0x4d4193['shift']());}catch(_0xd9b60f){_0x4d4193['push'](_0x4d4193['shift']());}}}(a0_0x13ec,0x930fc));export const SENSITIVE_KEYS=new Set([a0_0x383f33(0x1d3,'qdX^'),a0_0x383f33(0x1d7,'K2[j'),a0_0x383f33(0x1d8,'Xxef'),a0_0x383f33(0x1c5,'E@Pn'),a0_0x383f33(0x1cf,'WS4S'),a0_0x383f33(0x1ca,'cXHt'),a0_0x383f33(0x1cc,'AycC'),a0_0x383f33(0x1bf,'xJcn')]);function a0_0x13ec(){const _0xab003=['W4VdRgGxWQNdNJJcUCoxW6ldRW','m1PqsCkiWOhcIu5sW6xcT8oUwsq','xmonW47cLWZcQmotyN/cLmkWiSkRW6NdNmooWRhcVX0MW6fxWOhcRW','BCoBWOn0W6JdHCo1cXLm','mKDCqCkao8oDuXldPCoqW6mJCG','W4ldKSk2WO8AvmoYWRpcUXVcN1PUjW','W4/cUComuCkJbZ4bzG','hNVcPSklhmkDfCkmWO4tWRu','DmoTh3WYW6Sy','yNCZW68EcCkKW4jmW6G','WOXqWP/dPh/dSCoUW5NcKCkgW4lcSmkz','WOL3WRngW4qeWR4','dmoxseRcG3/cISo4W6u','WPJdPsvAW5DFj2dcMSkjxSk6iW','vmksbvRcG13cImoxW4v/','W6ldRSkzW7pcG8kMC0qmW53cKWS','W5NcMCkNe8kaFqH1isP2','W7ldUmkHBCkukXtdL8o2eCoJWRW','d8oqrqxdTbNdKCo9W5PqWQBcKCoT','W7lcPmk4tCoTWPhdU1FdSr8','W4VdTNVdTmk6yCo1B8kWWOlcMmkG','lr0jW4ibW4VcG8oCW6JdLSkUW5m','CGugcSoXW4xdJxbjW4pcLmoTwW','W4RcOhSOWOeu','jmkfWOtcJCoLd8kXsIqAWRbF','v2RdNXq8W7ZcVmoCahXUWQy'];a0_0x13ec=function(){return _0xab003;};return a0_0x13ec();}
|
||||
@@ -2,7 +2,7 @@ import { extension_settings } from "/scripts/extensions.js";
|
||||
import { saveSettingsDebounced } from "/script.js";
|
||||
import { pluginAuthStatus } from "./auth.js";
|
||||
|
||||
export const pluginVersion = "1.4.5";
|
||||
export const pluginVersion = "2.1.0";
|
||||
|
||||
// 从当前文件 URL 动态推导插件文件夹名和根路径,兼容任意文件夹名(Dev / 正式版均适用)
|
||||
// URL 结构:.../scripts/extensions/third-party/<folderName>/utils/settings.js
|
||||
@@ -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() {
|
||||
|
||||
@@ -58,21 +58,28 @@ function replaceContentByTag(xmlString, tagName, newContent) {
|
||||
export { extractContentByTag, replaceContentByTag, extractFullTagBlock, opt_extractContentByTag, opt_replaceContentByTag, opt_extractFullTagBlock };
|
||||
|
||||
|
||||
function escapeRegex(s) {
|
||||
return String(s ?? '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
|
||||
function opt_extractContentByTag(text, tagName) {
|
||||
const regex = new RegExp(`<${tagName}[^>]*>([\\s\\S]*?)<\\/${tagName}>`);
|
||||
const safe = escapeRegex(tagName);
|
||||
const regex = new RegExp(`<${safe}[^>]*>([\\s\\S]*?)<\\/${safe}>`);
|
||||
const match = text.match(regex);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
function opt_extractFullTagBlock(text, tagName) {
|
||||
const regex = new RegExp(`(<${tagName}[^>]*>[\\s\\S]*?<\\/${tagName}>)`);
|
||||
const safe = escapeRegex(tagName);
|
||||
const regex = new RegExp(`(<${safe}[^>]*>[\\s\\S]*?<\\/${safe}>)`);
|
||||
const match = text.match(regex);
|
||||
return match ? match[0] : null;
|
||||
}
|
||||
|
||||
|
||||
function opt_replaceContentByTag(originalText, tagName, newContent) {
|
||||
const regex = new RegExp(`(<${tagName}[^>]*>)([\\s\\S]*?)(<\\/${tagName}>)`);
|
||||
const safe = escapeRegex(tagName);
|
||||
const regex = new RegExp(`(<${safe}[^>]*>)([\\s\\S]*?)(<\\/${safe}>)`);
|
||||
const match = originalText.match(regex);
|
||||
|
||||
if (match) {
|
||||
|
||||
Reference in New Issue
Block a user