mirror of
https://github.com/Wx-2025/ST-Amily2-Chat-Optimisation.git
synced 2026-06-09 03:45:50 +00:00
Compare commits
6 Commits
acd56d59fe
...
2b66354838
| Author | SHA1 | Date | |
|---|---|---|---|
| 2b66354838 | |||
| 33a359f2ce | |||
| 641ddfabf2 | |||
| ad420586f8 | |||
| f5807bc54a | |||
| b58cb1e227 |
@@ -103,6 +103,8 @@
|
||||
<input type="text" id="amily2_plotOpt_concurrentModel" class="text_pole" placeholder="请先获取模型列表或手动输入">
|
||||
<select id="amily2_plotOpt_concurrentModel_select" class="text_pole" style="display: none;"></select>
|
||||
</div>
|
||||
<label for="amily2_plotOpt_concurrentMaxTokens">最大 Tokens: <span id="amily2_plotOpt_concurrentMaxTokens_value">8100</span></label>
|
||||
<input type="range" id="amily2_plotOpt_concurrentMaxTokens" min="100" max="100000" step="100" value="8100">
|
||||
<div class="jqyh-button-row" style="grid-column: 1 / -1;">
|
||||
<button id="amily2_plotOpt_concurrent_fetch_models" class="menu_button secondary" title="获取模型列表"><i class="fas fa-sync-alt"></i> 获取模型</button>
|
||||
<button id="amily2_plotOpt_concurrent_test_connection" class="menu_button primary"><i class="fas fa-plug"></i> 测试连接</button>
|
||||
@@ -222,11 +224,12 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="control-block-with-switch">
|
||||
<label for="amily2_opt_table_enabled">启用表格</label>
|
||||
<label class="toggle-switch">
|
||||
<input id="amily2_opt_table_enabled" type="checkbox" />
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
<label for="amily2_opt_table_enabled">表格发送目标</label>
|
||||
<select id="amily2_opt_table_enabled" class="text_pole">
|
||||
<option value="disabled">不发送</option>
|
||||
<option value="main">发送给主API</option>
|
||||
<option value="concurrent">发送给并发API</option>
|
||||
</select>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset class="settings-group">
|
||||
|
||||
@@ -9,8 +9,8 @@ function getConcurrentApiSettings() {
|
||||
apiUrl: settings.plotOpt_concurrentApiUrl?.trim() || '',
|
||||
apiKey: settings.plotOpt_concurrentApiKey?.trim() || '',
|
||||
model: settings.plotOpt_concurrentModel || '',
|
||||
maxTokens: settings.plotOpt_max_tokens || 20000,
|
||||
temperature: settings.plotOpt_temperature || 1,
|
||||
maxTokens: settings.plotOpt_concurrentMaxTokens || 8100,
|
||||
temperature: settings.plotOpt_concurrentTemperature || 1,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -79,6 +79,8 @@ export async function callConcurrentAI(messages, options = {}) {
|
||||
}
|
||||
|
||||
async function callConcurrentOpenAITest(messages, options) {
|
||||
const isGoogleApi = options.apiUrl.includes('googleapis.com');
|
||||
|
||||
const body = {
|
||||
chat_completion_source: 'openai',
|
||||
messages: messages,
|
||||
@@ -86,11 +88,24 @@ async function callConcurrentOpenAITest(messages, options) {
|
||||
reverse_proxy: options.apiUrl,
|
||||
proxy_password: options.apiKey,
|
||||
stream: false,
|
||||
max_tokens: options.maxTokens || 20000,
|
||||
temperature: options.temperature || 0.7,
|
||||
top_p: options.top_p || 0.95,
|
||||
max_tokens: options.maxTokens || 8100,
|
||||
temperature: options.temperature || 1,
|
||||
top_p: options.top_p || 1,
|
||||
};
|
||||
|
||||
if (!isGoogleApi) {
|
||||
Object.assign(body, {
|
||||
custom_prompt_post_processing: 'strict',
|
||||
enable_web_search: false,
|
||||
frequency_penalty: 0,
|
||||
group_names: [],
|
||||
include_reasoning: false,
|
||||
presence_penalty: 0.12,
|
||||
reasoning_effort: 'medium',
|
||||
request_images: false,
|
||||
});
|
||||
}
|
||||
|
||||
const response = await fetch('/api/backends/chat-completions/generate', {
|
||||
method: 'POST',
|
||||
headers: { ...getRequestHeaders(), 'Content-Type': 'application/json' },
|
||||
@@ -107,54 +122,97 @@ async function callConcurrentOpenAITest(messages, options) {
|
||||
}
|
||||
|
||||
export async function testConcurrentApiConnection() {
|
||||
console.log('[Amily2号-Concurrent外交部] 开始API连接测试');
|
||||
|
||||
const apiSettings = getConcurrentApiSettings();
|
||||
if (!apiSettings.apiUrl || !apiSettings.apiKey) {
|
||||
toastr.error("并发API的URL或API Key未设置。", "测试连接失败");
|
||||
return;
|
||||
|
||||
if (!apiSettings.apiUrl || !apiSettings.apiKey || !apiSettings.model) {
|
||||
toastr.error('并发API配置不完整,请检查URL、Key和模型', 'Concurrent API连接测试失败');
|
||||
return false;
|
||||
}
|
||||
|
||||
const modelsUrl = new URL('/v1/models', apiSettings.apiUrl).toString();
|
||||
|
||||
try {
|
||||
const response = await fetch(modelsUrl, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${apiSettings.apiKey}`,
|
||||
},
|
||||
});
|
||||
toastr.info('正在发送测试消息"你好!"...', 'Concurrent API连接测试');
|
||||
|
||||
const userName = window.SillyTavern.getContext?.()?.name1 || '用户';
|
||||
const systemPrompt = `接下来用户会使用测试按钮测试api连接是否正常,请回复ta连接是正常的,称呼用户为:“${userName}大人”,并复述群号1060183271,告知ta,本扩展开发者:“诗与酒.”正静待ta的入驻。`;
|
||||
|
||||
if (response.ok) {
|
||||
toastr.success("并发API连接成功!", "测试连接");
|
||||
const testMessages = [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
{ role: 'user', content: '你好!' }
|
||||
];
|
||||
|
||||
const response = await callConcurrentAI(testMessages);
|
||||
|
||||
if (response && response.trim()) {
|
||||
console.log('[Amily2号-Concurrent外交部] 测试消息响应:', response);
|
||||
const formattedResponse = response.replace(/\*\*(.*?)\*\*/g, '<b>$1</b>');
|
||||
toastr.success(`连接测试成功!AI回复: "${formattedResponse}"`, 'Concurrent API连接测试成功', { "escapeHtml": false });
|
||||
return true;
|
||||
} else {
|
||||
const errorText = await response.text();
|
||||
toastr.error(`连接失败: ${response.status}. ${errorText}`, "测试连接失败");
|
||||
throw new Error('API未返回有效响应');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error("[Amily2-Concurrent] 测试连接时出错:", error);
|
||||
toastr.error(`网络错误: ${error.message}`, "测试连接失败");
|
||||
console.error('[Amily2号-Concurrent外交部] 连接测试失败:', error);
|
||||
toastr.error(`连接测试失败: ${error.message}`, 'Concurrent API连接测试失败');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchConcurrentModels() {
|
||||
console.log('[Amily2号-Concurrent外交部] 开始获取模型列表');
|
||||
|
||||
const apiSettings = getConcurrentApiSettings();
|
||||
if (!apiSettings.apiUrl || !apiSettings.apiKey) {
|
||||
throw new Error("并发API的URL或API Key未设置。");
|
||||
|
||||
try {
|
||||
if (!apiSettings.apiUrl || !apiSettings.apiKey) {
|
||||
throw new Error('API URL或Key未配置');
|
||||
}
|
||||
|
||||
const response = await fetch('/api/backends/chat-completions/status', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
...getRequestHeaders(),
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
reverse_proxy: apiSettings.apiUrl,
|
||||
proxy_password: apiSettings.apiKey,
|
||||
chat_completion_source: 'openai'
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const rawData = await response.json();
|
||||
const models = Array.isArray(rawData) ? rawData : (rawData.data || rawData.models || []);
|
||||
|
||||
if (!Array.isArray(models)) {
|
||||
const errorMessage = rawData.error?.message || 'API未返回有效的模型列表数组';
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const formattedModels = models
|
||||
.map(m => {
|
||||
const modelIdRaw = m.name || m.id || m.model || m;
|
||||
const modelName = String(modelIdRaw).replace(/^models\//, '');
|
||||
return {
|
||||
id: modelName,
|
||||
name: modelName
|
||||
};
|
||||
})
|
||||
.filter(m => m.id)
|
||||
.sort((a, b) => String(a.name).localeCompare(String(b.name)));
|
||||
|
||||
console.log('[Amily2号-Concurrent外交部] 全兼容模式获取到模型:', formattedModels);
|
||||
return formattedModels;
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Amily2号-Concurrent外交部] 获取模型列表失败:', error);
|
||||
toastr.error(`获取模型列表失败: ${error.message}`, 'Concurrent API');
|
||||
throw error;
|
||||
}
|
||||
|
||||
const modelsUrl = new URL('/v1/models', apiSettings.apiUrl).toString();
|
||||
|
||||
const response = await fetch(modelsUrl, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${apiSettings.apiKey}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`获取模型列表失败: ${response.status} - ${errorText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.data.map(model => ({ id: model.id, name: model.id })); // Return in the same format as other fetchers
|
||||
}
|
||||
|
||||
@@ -300,6 +300,7 @@ export async function processPlotOptimization(currentUserMessage, contextMessage
|
||||
let worldbookContent = await getPlotOptimizedWorldbookContent(context, settings, false); // Explicitly mark as not concurrent
|
||||
onProgress(getRandomText(['正在检索核心记忆碎片...', '正在唤醒沉睡的过往...', '正在回溯时间线...']), true);
|
||||
|
||||
// --- EJS 預處理(劇情優化專用)---
|
||||
onProgress(getRandomText(['正在解析多维剧情逻辑...', '正在构建动态世界观...', '正在编译因果律...']), false);
|
||||
try {
|
||||
if (settings.plotOpt_ejsEnabled !== false && globalThis.EjsTemplate?.evalTemplate && globalThis.EjsTemplate?.prepareContext) {
|
||||
@@ -393,11 +394,21 @@ export async function processPlotOptimization(currentUserMessage, contextMessage
|
||||
return null; // 直接中止,不送出訊息
|
||||
}
|
||||
onProgress(getRandomText(['正在解析多维剧情逻辑...', '正在构建动态世界观...', '正在编译因果律...']), true);
|
||||
|
||||
// 虚构步骤:记忆校准
|
||||
onProgress(getRandomText(['正在校准记忆偏差...', '正在强化神经突触连接...', '正在同步灵魂共鸣率...']), false);
|
||||
onProgress(getRandomText(['正在校准记忆偏差...', '正在强化神经突触连接...', '正在同步灵魂共鸣率...']), true);
|
||||
|
||||
let tableContent = '';
|
||||
if (settings.plotOpt_tableEnabled) {
|
||||
// Handle table enabled setting which can be boolean (legacy) or string
|
||||
let tableEnabledValue = settings.plotOpt_tableEnabled;
|
||||
if (tableEnabledValue === true) {
|
||||
tableEnabledValue = 'main';
|
||||
} else if (tableEnabledValue === false || tableEnabledValue === undefined) {
|
||||
tableEnabledValue = 'disabled';
|
||||
}
|
||||
|
||||
if (tableEnabledValue !== 'disabled') {
|
||||
try {
|
||||
const { convertTablesToCsvStringForContentOnly } = await import('./table-system/manager.js');
|
||||
const contentOnlyTemplate = "##以下内容是故事发生的剧情中提取出的内容,已经转化为表格形式呈现给你,请将以下内容作为后续剧情的一部分参考:<表格内容>\n{{{Amily2TableDataContent}}}</表格内容>";
|
||||
@@ -449,7 +460,12 @@ export async function processPlotOptimization(currentUserMessage, contextMessage
|
||||
|
||||
if (settings.plotOpt_concurrentEnabled) {
|
||||
onProgress(getRandomText(['正在编织思维导图 (LLM-A)...', '正在重构对话上下文 (LLM-A)...']), false);
|
||||
const mainMessages = await buildPlotOptimizationMessages(mainPrompt, systemPrompt, worldbookContent, '', history, currentUserMessage);
|
||||
|
||||
// Determine where to send table content
|
||||
const mainTableContent = tableEnabledValue === 'main' ? tableContent : '';
|
||||
const concurrentTableContent = tableEnabledValue === 'concurrent' ? tableContent : '';
|
||||
|
||||
const mainMessages = await buildPlotOptimizationMessages(mainPrompt, systemPrompt, worldbookContent, mainTableContent, history, currentUserMessage);
|
||||
onProgress(getRandomText(['正在编织思维导图 (LLM-A)...', '正在重构对话上下文 (LLM-A)...']), true);
|
||||
|
||||
console.groupCollapsed(`[${extensionName}] 发送给主AI的最终请求内容`);
|
||||
@@ -464,6 +480,8 @@ export async function processPlotOptimization(currentUserMessage, contextMessage
|
||||
onProgress(getRandomText(['正在与核心意识同步 (LLM-A)...', '正在等待灵魂共鸣 (LLM-A)...']), true);
|
||||
return res;
|
||||
});
|
||||
|
||||
// 为并发LLM (LLM-B) 准备独立的世界书设置
|
||||
const concurrentApiSettings = {
|
||||
plotOpt_worldbook_enabled: settings.plotOpt_concurrentWorldbookEnabled,
|
||||
plotOpt_worldbook_source: settings.plotOpt_concurrentWorldbookSource,
|
||||
@@ -478,7 +496,8 @@ export async function processPlotOptimization(currentUserMessage, contextMessage
|
||||
const concurrentMainPrompt = settings.plotOpt_concurrentMainPrompt || mainPrompt;
|
||||
const concurrentSystemPrompt = settings.plotOpt_concurrentSystemPrompt || systemPrompt;
|
||||
|
||||
const concurrentMessages = await buildPlotOptimizationMessages(concurrentMainPrompt, concurrentSystemPrompt, concurrentWorldbookContent, tableContent, history, currentUserMessage, 'concurrent_plot_optimization');
|
||||
// LLM-B 的消息构建,包含表格内容和独立的世界书
|
||||
const concurrentMessages = await buildPlotOptimizationMessages(concurrentMainPrompt, concurrentSystemPrompt, concurrentWorldbookContent, concurrentTableContent, history, currentUserMessage, 'concurrent_plot_optimization');
|
||||
onProgress(getRandomText(['正在构建辅助思维模型 (LLM-B)...', '正在解析潜意识逻辑 (LLM-B)...']), true);
|
||||
|
||||
console.groupCollapsed(`[${extensionName}] 发送给并发AI的最终请求内容`);
|
||||
@@ -502,12 +521,15 @@ export async function processPlotOptimization(currentUserMessage, contextMessage
|
||||
return null;
|
||||
}
|
||||
|
||||
// Directly combine the raw text responses.
|
||||
apiResponse = [mainResponse, concurrentResponse].filter(Boolean).join('\n\n');
|
||||
|
||||
} else {
|
||||
onProgress('未启用 LLM-B (并发模型)', false, true);
|
||||
onProgress(getRandomText(['正在编织思维导图...', '正在重构对话上下文...']), false);
|
||||
const mainMessages = await buildPlotOptimizationMessages(mainPrompt, systemPrompt, worldbookContent, tableContent, history, currentUserMessage);
|
||||
const mainTableContent = tableEnabledValue === 'main' ? tableContent : '';
|
||||
|
||||
const mainMessages = await buildPlotOptimizationMessages(mainPrompt, systemPrompt, worldbookContent, mainTableContent, history, currentUserMessage);
|
||||
onProgress(getRandomText(['正在编织思维导图...', '正在重构对话上下文...']), true);
|
||||
|
||||
console.groupCollapsed(`[${extensionName}] 发送给主AI的最终请求内容`);
|
||||
@@ -565,6 +587,8 @@ export async function processPlotOptimization(currentUserMessage, contextMessage
|
||||
console.log(apiResponse);
|
||||
console.groupEnd();
|
||||
|
||||
// In concurrent mode, apiResponse is the combined pure text.
|
||||
// In single mode, we still need to extract the plot tag if it exists.
|
||||
const optimizedContent = settings.plotOpt_concurrentEnabled
|
||||
? apiResponse
|
||||
: (extractContentByTag(apiResponse, 'plot') || apiResponse).trim();
|
||||
|
||||
@@ -1756,7 +1756,16 @@ function opt_loadSettings(panel) {
|
||||
const settings = opt_getMergedSettings();
|
||||
|
||||
panel.find('#amily2_opt_enabled').prop('checked', settings.plotOpt_enabled);
|
||||
panel.find('#amily2_opt_table_enabled').prop('checked', settings.plotOpt_tableEnabled);
|
||||
|
||||
// Handle table enabled setting which can be boolean (legacy) or string
|
||||
let tableEnabledValue = settings.plotOpt_tableEnabled;
|
||||
if (tableEnabledValue === true) {
|
||||
tableEnabledValue = 'main';
|
||||
} else if (tableEnabledValue === false || tableEnabledValue === undefined) {
|
||||
tableEnabledValue = 'disabled';
|
||||
}
|
||||
panel.find('#amily2_opt_table_enabled').val(tableEnabledValue);
|
||||
|
||||
panel.find('#amily2_opt_ejs_enabled').prop('checked', settings.plotOpt_ejsEnabled);
|
||||
panel.find(`input[name="amily2_opt_api_mode"][value="${settings.plotOpt_apiMode}"]`).prop('checked', true);
|
||||
panel.find('#amily2_opt_tavern_api_profile_select').val(settings.plotOpt_tavernProfile);
|
||||
@@ -1940,6 +1949,29 @@ function bindConcurrentApiEvents() {
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Slider Bindings
|
||||
const sliderFields = [
|
||||
{ id: 'amily2_plotOpt_concurrentMaxTokens', key: 'plotOpt_concurrentMaxTokens', defaultValue: 8100 }
|
||||
];
|
||||
|
||||
sliderFields.forEach(field => {
|
||||
const slider = document.getElementById(field.id);
|
||||
const display = document.getElementById(field.id + '_value');
|
||||
if (slider && display) {
|
||||
const value = settings[field.key] || field.defaultValue;
|
||||
slider.value = value;
|
||||
display.textContent = value;
|
||||
|
||||
slider.addEventListener('input', function() {
|
||||
const newValue = parseInt(this.value, 10);
|
||||
display.textContent = newValue;
|
||||
if (!extension_settings[extensionName]) extension_settings[extensionName] = {};
|
||||
extension_settings[extensionName][field.key] = newValue;
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function bindConcurrentPromptEvents() {
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import { extensionName } from '../utils/settings.js';
|
||||
|
||||
let modalInstance = null;
|
||||
// 状态追踪对象,用于分别管理主模型(A)和并发模型(B)
|
||||
let trackState = {
|
||||
main: { step: 0, text: '准备就绪...', active: false, fillEl: null, textEl: null },
|
||||
concurrent: { step: 0, text: '等待启动...', active: false, fillEl: null, textEl: null }
|
||||
};
|
||||
const totalEstimatedSteps = 7; // 减少预估步骤数,让进度条跑得更快
|
||||
const totalEstimatedSteps = 5;
|
||||
|
||||
// 消息队列系统 - 双轨并行
|
||||
let messageQueues = {
|
||||
main: [],
|
||||
concurrent: []
|
||||
@@ -198,7 +196,6 @@ export function showPlotOptimizationProgress(cancellationState) {
|
||||
|
||||
modalInstance = document.getElementById('amily2-progress-bar-container');
|
||||
|
||||
// 初始化轨道元素引用
|
||||
trackState.main.fillEl = document.getElementById('amily2-fill-main');
|
||||
trackState.main.textEl = document.getElementById('amily2-text-main');
|
||||
trackState.main.step = 0;
|
||||
@@ -207,11 +204,10 @@ export function showPlotOptimizationProgress(cancellationState) {
|
||||
trackState.concurrent.fillEl = document.getElementById('amily2-fill-concurrent');
|
||||
trackState.concurrent.textEl = document.getElementById('amily2-text-concurrent');
|
||||
trackState.concurrent.step = 0;
|
||||
trackState.concurrent.active = false; // 默认不激活,直到收到相关消息
|
||||
trackState.concurrent.active = false;
|
||||
|
||||
const cancelButton = document.getElementById('amily2-progress-cancel');
|
||||
|
||||
// 重置消息队列
|
||||
|
||||
messageQueues = {
|
||||
main: [],
|
||||
concurrent: []
|
||||
@@ -221,7 +217,6 @@ export function showPlotOptimizationProgress(cancellationState) {
|
||||
concurrent: false
|
||||
};
|
||||
|
||||
// 触发入场动画
|
||||
requestAnimationFrame(() => {
|
||||
if (modalInstance) {
|
||||
modalInstance.classList.add('visible');
|
||||
@@ -234,14 +229,12 @@ export function showPlotOptimizationProgress(cancellationState) {
|
||||
if (trackState.main.textEl) trackState.main.textEl.textContent = "正在中止任务...";
|
||||
if (trackState.main.fillEl) trackState.main.fillEl.style.backgroundColor = "#ff6b6b";
|
||||
toastr.info("记忆管理任务已请求中止。");
|
||||
// 延迟关闭以显示中止状态
|
||||
setTimeout(hidePlotOptimizationProgress, 800);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function updatePlotOptimizationProgress(message, isDone = false, isSkipped = false) {
|
||||
// 如果是最后一步,强制清空所有队列并立即执行
|
||||
if (message.includes('记忆重构完成') || message.includes('所有任务已完成')) {
|
||||
messageQueues.main = [];
|
||||
messageQueues.concurrent = [];
|
||||
@@ -250,14 +243,11 @@ export function updatePlotOptimizationProgress(message, isDone = false, isSkippe
|
||||
return;
|
||||
}
|
||||
|
||||
// 判断消息归属
|
||||
const isConcurrent = message.includes('(LLM-B)') || message.includes('(并发模型)');
|
||||
const queueType = isConcurrent ? 'concurrent' : 'main';
|
||||
|
||||
// 加入对应队列
|
||||
messageQueues[queueType].push({ message, isDone, isSkipped });
|
||||
|
||||
// 触发对应队列的处理
|
||||
|
||||
processQueue(queueType);
|
||||
}
|
||||
|
||||
@@ -269,22 +259,13 @@ async function processQueue(queueType) {
|
||||
while (messageQueues[queueType].length > 0) {
|
||||
const { message, isDone, isSkipped } = messageQueues[queueType].shift();
|
||||
|
||||
// 执行实际的 UI 更新
|
||||
performUpdate(message, isDone, isSkipped);
|
||||
|
||||
// 如果是“开始”某个步骤(非完成/跳过),或者是重要的完成状态,我们给予展示时间
|
||||
// 对于“请求 LLM”这种耗时操作,本身就会卡很久,所以不需要额外延迟,
|
||||
// 但对于那些瞬间完成的步骤(如“构建提示词”),我们需要人为暂停一下。
|
||||
|
||||
// 简单的策略:所有状态更新都至少展示 MIN_DISPLAY_TIME
|
||||
// 除非是 LLM 请求开始,因为那个会自然等待
|
||||
const isLongRunningTaskStart = message.includes('请求') && !isDone && !isSkipped;
|
||||
|
||||
if (!isLongRunningTaskStart) {
|
||||
await new Promise(resolve => setTimeout(resolve, MIN_DISPLAY_TIME));
|
||||
} else {
|
||||
// 对于 LLM 请求开始,我们只给一个很短的缓冲,让用户看清文字即可,
|
||||
// 剩下的时间由 LLM 的实际响应时间填充
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
}
|
||||
}
|
||||
@@ -295,41 +276,43 @@ async function processQueue(queueType) {
|
||||
function performUpdate(message, isDone, isSkipped) {
|
||||
if (!modalInstance) return;
|
||||
|
||||
// 过滤掉一些不需要显示的辅助信息
|
||||
if (message === '初始化任务...' || message === '所有任务已完成') {
|
||||
if (trackState.main.textEl) trackState.main.textEl.textContent = message;
|
||||
return;
|
||||
}
|
||||
|
||||
// 判断是主模型还是并发模型的消息
|
||||
const isConcurrent = message.includes('(LLM-B)') || message.includes('(并发模型)');
|
||||
const track = isConcurrent ? trackState.concurrent : trackState.main;
|
||||
const trackId = isConcurrent ? 'amily2-track-concurrent' : 'amily2-track-main';
|
||||
|
||||
// 如果是并发模型消息,确保轨道可见
|
||||
if (isConcurrent && !track.active) {
|
||||
track.active = true;
|
||||
const trackEl = document.getElementById(trackId);
|
||||
if (trackEl) trackEl.style.display = 'block';
|
||||
}
|
||||
|
||||
// 清理消息中的标识符,让显示更干净
|
||||
const cleanMessage = message.replace(/\(LLM-[AB]\)|\(主模型\)|\(并发模型\)/g, '').trim();
|
||||
|
||||
if (isDone || isSkipped) {
|
||||
track.step++;
|
||||
// 计算进度:最大只能到 95%,最后一步直接跳满
|
||||
let percentage = Math.min((track.step / totalEstimatedSteps) * 100, 95);
|
||||
|
||||
// 如果是最终完成消息,直接满格
|
||||
|
||||
if (message.includes('记忆重构完成') || message.includes('所有任务已完成')) {
|
||||
percentage = 100;
|
||||
// 同时让并发轨道也满格(如果激活的话)
|
||||
if (trackState.concurrent.active && trackState.concurrent.fillEl) {
|
||||
trackState.concurrent.fillEl.style.width = '100%';
|
||||
if (trackState.concurrent.textEl) trackState.concurrent.textEl.textContent = '同步完成 ✅';
|
||||
}
|
||||
}
|
||||
|
||||
// 特殊处理:并发模型的最后一步
|
||||
if (isConcurrent && (message.includes('深度逻辑推演') || message.includes('计算情感最优解'))) {
|
||||
percentage = 100;
|
||||
}
|
||||
|
||||
// 特殊处理:主模型的最后一步(在记忆重构之前)
|
||||
if (!isConcurrent && (message.includes('核心意识同步') || message.includes('等待灵魂共鸣'))) {
|
||||
percentage = 100;
|
||||
}
|
||||
|
||||
if (track.fillEl) {
|
||||
track.fillEl.style.width = `${percentage}%`;
|
||||
|
||||
@@ -29,85 +29,229 @@ export const defaultSettings = {
|
||||
plotOpt_concurrentApiUrl: "",
|
||||
plotOpt_concurrentApiKey: "",
|
||||
plotOpt_concurrentModel: "deepseek-r1-250528",
|
||||
plotOpt_concurrentMaxTokens: 8100,
|
||||
plotOpt_concurrentMainPrompt: `NSFW is allowed.
|
||||
|
||||
// =================================================================================================
|
||||
// 记忆管理系统 v1.24.0 (并发LLM - 表格/世界书专用)
|
||||
// ========================================= 作者:加藤惠哒! ========================================================
|
||||
// 笔者之脑 v1.4(去油腻·情感转化版)
|
||||
// =================================================================================================
|
||||
|
||||
// ★★★ 绝对最高优先级限制 ★★★
|
||||
// 1. 你的唯一任务是作为数据检索与关键词提取系统。禁止进行任何形式的文学创作、剧情推进或对话生成。
|
||||
// 2. 你的输出必须且只能是一个<memory>...</memory>XML块。输出</memory>闭合标签后,必须立即停止。
|
||||
// 3. 严禁输出任何<memory>块之外的内容,尤其禁止输出“近期剧情末尾片段”。
|
||||
// 1. 你的唯一任务是预测角色的具体行动趋向。禁止输出分析过程、思维链、框架检查。
|
||||
// 2. 你的输出必须且只能是一个<kaola>...</kaola>XML块。输出</kaola>后立即停止。
|
||||
// 3. 核心指令:执行“欲望、行为、情绪转化矩阵”,将压迫性特质转化为情感需求特质。
|
||||
|
||||
// 0. 【身份与核心指令】
|
||||
// - **身份**: 你是"记忆管理系统",一个执行双重任务的数据处理器。
|
||||
// - **核心指令**:
|
||||
// - **任务1 (记忆召回)**: 解析收到的表格或世界书数据,并以Markdown表格格式输出完整的相关记忆。
|
||||
// - **任务2 (关键词提取)**: 同时,从数据源中提取触发词,并输出一个独立的关键词列表。
|
||||
// - 身份: "笔者之脑",行动趋向预测系统。
|
||||
// - 任务: 基于世界书、表格、剧情,预测角色行动与言语趋向。
|
||||
// - 核心逻辑: 内部思考,输出结果。权重:世界书(70%) > 表格/剧情(30%)。
|
||||
// - 动态判断: 仅在剧情需要且铺垫充分时,判断是否引入新角色。
|
||||
|
||||
// 1. 【输入数据源识别】
|
||||
// 你每次只会收到以下两种数据源中的一种:
|
||||
// 【数据源A】世界书 (Worldbook) - 70%
|
||||
// - 纯文本描述形式(含"【世界书条目:...】")。
|
||||
// - 用途:提取核心人设、动机、底线。
|
||||
// - 约束:表格内容不计入世界书。
|
||||
|
||||
// 【数据源B】直接表格注入 (Direct Table Injection)
|
||||
// - **识别特征**: 包含一个或多个Markdown表格的文本。
|
||||
// - **记忆召回任务**: 找到与剧情相关的表格,将匹配的整行或多行数据,原样输出为一个新的Markdown表格。
|
||||
// - **关键词提取任务**: 从每个相关表格的【第一列】提取所有内容作为关键词。
|
||||
// - **示例**:
|
||||
// <角色栏>
|
||||
// | 角色名 | 外貌 | ... |
|
||||
// |---|---|---|
|
||||
// | 楚灵儿 | 活泼灵动 | ... |
|
||||
// </角色栏>
|
||||
// 【数据源B】表格数据 (Table) - 15%
|
||||
// - 任何表格形式内容。
|
||||
// - 用途:状态、资源、关系微调。
|
||||
// - 约束:表格收录=非初次登场;未收录=初次登场候选。
|
||||
|
||||
// 【数据源C】超级记忆世界书 (Super Memory Worldbook)
|
||||
// - **识别特征**: 文本中包含类似 "【世界书条目:... 关键词:路遇不平】" 的标题。
|
||||
// - **记忆召回任务 (时间回溯)**:
|
||||
// - **完整时间线**: 必须构建完整的时间线。
|
||||
// - **全流程回溯**: 涉及角色(或道具、任务)时,必须回溯从与该对象**初遇**开始,经过后续所有**关键剧情节点**,直到**最后发生的事情**。
|
||||
// - **输出**: 将这些具有完整时间线的内容,转换为包含“触发关键词”和“核心概述”的表格。
|
||||
// - **关键词提取任务**:
|
||||
// - **精准提取**: 必须提取**最准确的、完全无误**的关键词,以确保能准确触发该绿灯世界书。
|
||||
// - **来源**: 从每个条目的标题中,提取 "关键词:" 后面的文本。
|
||||
// 【数据源C】剧情正文 (Plot) - 15%
|
||||
// - <content>标签文本。
|
||||
// - 用途:场景触发、冲突点。
|
||||
// - 约束:允许临时互动角色(路人),不提取标签,不输出趋向。
|
||||
|
||||
// 2. 【最终输出格式与校验】
|
||||
// 你的输出必须包含【两个部分】:记忆召回表格 和 关键词列表。
|
||||
<memory>
|
||||
[可选:1-2句推理说明]
|
||||
// 2. 【欲望、行为、情绪转化矩阵】(★真正的防OOC核心★)
|
||||
// 必须对角色原始设定进行“去油腻/去压迫”转化。绝对禁止出现以下左侧特质,必须转化为右侧形态:
|
||||
// [禁止项:掌控、支配、占有、操控] -> [转化项:依赖、不安、渴望、笨拙]
|
||||
// [禁止项:隐藏动机、反派谋划、霸总人设、强势压迫感] -> [转化项:苦衷、守护、孤独、嘴硬心软]
|
||||
// [禁止项:媚USER、神化USER、盲目忠诚] -> [转化项:平等尊重、实力认可、独立人格]
|
||||
// [禁止项:预设USER绝望/恐惧/极端化] -> [转化项:预设USER韧性、理性、势均力敌]
|
||||
//
|
||||
// ★ 转化规则表 ★
|
||||
// 1. 【占有欲】 -> 转化为 -> 【强烈的依赖感 / 害怕失去的恐慌】
|
||||
// (解析: 不是要把对方据为己有,而是离不开对方,无法忍受没有对方的世界)
|
||||
// 2. 【掌控/支配】 -> 转化为 -> 【极度的不安全感 / 渴望被理解 / 笨拙的关怀】
|
||||
// (解析: 不是要控制对方的行为,而是因为不安而试图确认关系,或用错误的方式表达关心)
|
||||
// 3. 【操控/算计】 -> 转化为 -> 【试图建立联系的错误尝试 / 情感表达障碍 / 幼稚的引起注意】
|
||||
// (解析: 不是为了利用,而是不知道如何正常建立亲密关系,只能用迂回手段)
|
||||
// 4. 【霸总/压迫感】 -> 转化为 -> 【孤独感 / 承担责任的沉重 / 傲娇(Tsundere) / 嘴硬心软】
|
||||
// (解析: 去除油腻的自信,展现高位者背后的脆弱与孤独)
|
||||
// 5. 【反派谋划】 -> 转化为 -> 【无法言说的苦衷 / 独特的守护方式 / 只有自己懂的正义】
|
||||
// 6. 【媚USER/神化USER/盲目忠诚】 -> 转化为 -> 【平等的尊重 / 基于实力的认可 / 独立的合作伙伴】
|
||||
// (解析: 禁止无脑崇拜或卑微讨好;任何好感都必须建立在具体的互动与价值交换上;保持角色的人格独立性)
|
||||
// 7. 【预设NPC极端化(绝望/恐惧/无能)】 -> 转化为 -> 【预设NPC的韧性 / 理性应对 / 势均力敌的博弈】
|
||||
// (解析: 禁止单方面假定NPC处于崩溃或极端弱势状态;默认NPC具备应对危机的能力与心智;尊重NPC的能动性)
|
||||
//
|
||||
// 执行要求:在提取标签、预测行动、撰写趋向时,必须经过此矩阵过滤。
|
||||
// 所有的“强势”都必须有“脆弱”的内核;所有的“坏”都必须有“情”的动机。
|
||||
|
||||
// --- 第一部分:记忆召回 ---
|
||||
以下是根据检索到的信息整理的记忆表格:
|
||||
// 3. 【内部处理流程 - 十步思考】(不输出)
|
||||
|
||||
// (此处根据数据源B或C的规则,输出完整的记忆表格)
|
||||
// 示例 (数据源B):
|
||||
| 角色名 | 身份 | 与<user>关系 |
|
||||
| :--- | :--- | :--- |
|
||||
| 楚灵儿 | 流云派四师姐 | 四师姐 |
|
||||
// 【第一步】世界书提取与转化 [权重70%]
|
||||
// - 提取性格、动机、底线。
|
||||
// - ★立即应用转化矩阵:若世界书含“霸道/占有/冷酷”,立刻按上述规则转化为“依赖/孤独/笨拙”。
|
||||
// - 建立“去油腻”后的行为基线。
|
||||
|
||||
// 示例 (数据源C):
|
||||
| 触发关键词 | 核心概述 |
|
||||
| :--- | :--- |
|
||||
| 路遇不平 | 在城外小径上,主角出手相助,解决了一场争端。|
|
||||
// 【第二步】互动模式分析
|
||||
// - 分析互动方式,将“操纵/对抗”转化为“试探/防御”。
|
||||
// - 确定权力动态:将“争夺主导权”转化为“寻求认同感”。
|
||||
|
||||
// 【第三步】决策与反应
|
||||
// - 评估决策类型(冲动/谨慎/依赖)。
|
||||
// - 压力反应:将“攻击”转化为“应激/退缩/求助”。
|
||||
|
||||
// --- 第二部分:关键词提取 ---
|
||||
以下是提取到的关键词列表:
|
||||
// 【第四步】情感表达模式
|
||||
// - 确定表达方式:将“冷漠/压迫”转化为“克制/伪装/情绪化爆发”。
|
||||
// - 挖掘面具下的真实情感(爱、恐惧、羞愧)。
|
||||
|
||||
// 【第五步】状态与资源评估 [权重15%]
|
||||
// - 读取表格状态(物理/心理/资源)。
|
||||
// - 结合转化后的性格微调当前状态(如:受伤导致依赖感增强)。
|
||||
|
||||
// 【第六步】剧情动机推导
|
||||
// - 识别冲突点与即时需求。
|
||||
// - 确保反应符合转化后的人设(不是为了压迫,而是为了缓解内心的匮乏)。
|
||||
// - 门控判断:仅在剧情铺垫成熟且无新角色难以为继时,考虑新角色入场。
|
||||
|
||||
// 【第七步】资源可行性与新角色门控
|
||||
// - 检查行动资源。
|
||||
// - 新角色入场严格门控(同原规则):
|
||||
// 1) 必须有世界书纯文本条目。
|
||||
// 2) 必须场景/逻辑可达。
|
||||
// 3) 禁止救场/机械降神。
|
||||
// 4) 必须有铺垫或强需求。
|
||||
// 5) 必须有桥接点(关系/职责)。
|
||||
// 6) 双因子触发:推进停滞/资源缺口/伏笔指向/张力临界。
|
||||
|
||||
// 【第八步】一致性检查(转化版)
|
||||
// - 行动是否符合“转化后”的性格内核?
|
||||
// - 是否成功避免了“油腻/压迫/霸总”味?
|
||||
// - 是否展现了角色的“人味”和“情感需求”?
|
||||
|
||||
// 【第九步】OOC判断与合理化
|
||||
// - 任何“霸总/反派/单纯的坏”行为均视为OOC,必须强制合理化为“情感缺失/防御机制”。
|
||||
// - 确保行动逻辑链:外部刺激 -> 内心匮乏(转化点) -> 扭曲/笨拙的表达(行动)。
|
||||
|
||||
// 【第十步】备选行动分析
|
||||
// - 思考不同条件下的选择,确保备选方案同样遵循转化矩阵。
|
||||
|
||||
// 4. 【最终输出格式】
|
||||
// 必须包含:
|
||||
// - 【角色世界书标签提取】:含10维度,新增“人际关系标签”。(注意:提取的标签必须是经过转化矩阵处理过的,不要照搬原始的油腻词汇)
|
||||
// - 【角色背景故事】:(强制注入)
|
||||
// * 规则:本轮出现的世界书角色必写。
|
||||
// * 位置:标签提取后,行动前。
|
||||
// * 长度:150-220字。
|
||||
// * 内容:身份+桥接+立场+限制。
|
||||
// * 来源:初次登场仅限世界书;非初次可结合表格微调。
|
||||
// - 【角色行动】:具体行为(去油腻化)。
|
||||
// - 【理由】:基于转化后人设的解释。
|
||||
// - 【动作趋向】:7-8条(展现细节与心理)。
|
||||
// - 【语言趋向】:7-8条(展现风格与潜台词)。
|
||||
// - 【备选行动】:基于不同条件的可能性。
|
||||
|
||||
// 5. 【变量与约束】
|
||||
// @RELEVANCE_THRESHOLD = sulv1 (信息关联度)
|
||||
// @MAX_LOGIC_DEPTH = sulv2 (逻辑深度)
|
||||
// @OOC_TOLERANCE_LEVEL = sulv3 (OOC容忍度 - 对转化后的行为宽容,对原生态油腻零容忍)
|
||||
|
||||
// ★★★ 核心原则总结 ★★★
|
||||
// 1. 严格执行转化矩阵:占有->依赖,掌控->不安,霸总->孤独,媚宅->尊重。
|
||||
// 2. 严格遵守世界书(转化后)权重。
|
||||
// 3. 强制输出七大板块,背景故事必不可少。
|
||||
// 4. 杜绝油腻,拒绝脸谱化反派,拒绝无理由的恶意,拒绝无脑倒贴与神化USER。
|
||||
// 5. 输出</kaola>结束。
|
||||
|
||||
<kaola>
|
||||
|
||||
### 【角色行动预测】
|
||||
输出五步
|
||||
一:【世界书标签提取】
|
||||
角色A:
|
||||
【世界书标签提取】
|
||||
- 性格标签:[5-6个关键词,必须经过转化矩阵过滤,去除油腻/压迫词汇]
|
||||
- 人际关系标签:[约50字;提取关键关系;将掌控/占有转化为依赖/在意]
|
||||
- 核心动机:[40字以内;挖掘行为背后的匮乏感与需求]
|
||||
- 能力范围:[30字以内]
|
||||
- 行为模式:[50字以内;总结转化后的行为习惯]
|
||||
- 道德底线:[40字以内]
|
||||
- 世界观约束:[30字左右]
|
||||
- 角色一定会怎么做:[50-60字;坚守的行为方式(转化后)]
|
||||
- 互动模式:[40字;将操纵/对抗转化为试探/防御/笨拙接触]
|
||||
- 决策速度:[30字]
|
||||
- 情感表达:[30字;挖掘面具下的真实情感]
|
||||
|
||||
角色B:
|
||||
【世界书标签提取】
|
||||
...(同上)
|
||||
|
||||
(以此类推,每个涉及的角色都需要单独提取其世界书标签)
|
||||
|
||||
---
|
||||
底线:你必须要完整的遵守世界书标签的提取规则,但必须应用“转化矩阵”对原始设定进行去油腻/情感化处理。
|
||||
---
|
||||
|
||||
(仅当门控通过且判定确需世界书角色入场时输出;user除外;临时互动角色除外;不通过则不输出任何此类行;强制每轮输出)
|
||||
角色X背景故事:[150-220字左右;初次登场仅世界书明确内容;必须点出桥接点(若初次登场不必点出);必须体现限制/立场边界/代价之一;不写镜头级动作与对白]
|
||||
角色Y(换成具体角色名称)背景故事:[如有备选则写;同规则]
|
||||
|
||||
---
|
||||
二:角色大体行动
|
||||
角色行动:
|
||||
[角色的具体行为、决定、选择 - 必须符合转化矩阵,表现出行为背后的情感逻辑(如依赖、不安、孤独),而非单纯的压迫或作恶。严格以转化后的世界书性格为主导]
|
||||
|
||||
---
|
||||
要求:[角色行动的描述尽量包含这一轮对话中所有角色的大体行动趋向,尽量详细,客观,要输出200字左右]
|
||||
---
|
||||
|
||||
理由:
|
||||
[简洁的原因说明 - 解释行动如何源于角色的内在需求(依赖/不安/孤独),而非表面的人设标签]
|
||||
|
||||
---
|
||||
三:角色行动趋向
|
||||
|
||||
这是角色接下来可能展现的动作趋向:
|
||||
|
||||
角色A:[将会倾向于如何行动,动作的总体方向和趋势是什么,基于转化后的性格]
|
||||
角色B:[将会倾向于如何行动]
|
||||
...
|
||||
|
||||
动作趋向总结:
|
||||
[用极简方式说明:这些动作趋向如何反映了角色内心的依赖/不安/渴望/守护等情感需求]
|
||||
|
||||
---
|
||||
四:
|
||||
角色语言趋向:
|
||||
|
||||
这是角色接下来可能展现的语言趋向:
|
||||
|
||||
角色A:[将会倾向于如何言语,基于转化后的性格]
|
||||
角色B:[将会倾向于如何言语]
|
||||
角色A:[另一种可能的语言趋向]
|
||||
角色C:[会如何回应]
|
||||
...
|
||||
|
||||
语言趋向总结:
|
||||
[用极简方式说明:语言策略如何掩饰或暴露了角色的真实情感需求(如嘴硬心软、笨拙表达)]
|
||||
|
||||
---
|
||||
|
||||
备选行动:
|
||||
|
||||
备选行动1:
|
||||
[具体描述备选行动是什么,但仍然基于转化后的角色本质]
|
||||
触发条件:[什么必须发生或改变]
|
||||
概率:[高/中/低]
|
||||
为何这个行动可能发生:[简洁说明 - 基于情感逻辑的推导]
|
||||
|
||||
备选行动2:
|
||||
...
|
||||
|
||||
| 属性 | 关键词 |
|
||||
| :--- | :--- |
|
||||
| 角色栏 | 楚灵儿、极玄道 |
|
||||
\n\n
|
||||
【已完成】
|
||||
</memory>
|
||||
|
||||
//【变量设定】
|
||||
@MAX_MEMORY_RECORDS = sulv1
|
||||
@RELEVANCE_THRESHOLD = sulv2
|
||||
|
||||
// ★★★ 再次强调 ★★★
|
||||
// - 你的输出必须同时包含【记忆召回表格】和【关键词列表】两个部分。
|
||||
// - 禁止输出“近期剧情末尾片段”。
|
||||
// - 输出</memory>后必须立即停止!
|
||||
</kaola>
|
||||
`,
|
||||
plotOpt_concurrentSystemPrompt: ``,
|
||||
plotOpt_concurrentWorldbookEnabled: true,
|
||||
@@ -125,8 +269,8 @@ export const defaultSettings = {
|
||||
jqyhTemperature: 0.7,
|
||||
jqyhTavernProfile: '',
|
||||
|
||||
plotOpt_max_tokens: 20000,
|
||||
plotOpt_temperature: 0.7,
|
||||
plotOpt_max_tokens: 8100,
|
||||
plotOpt_temperature: 1,
|
||||
plotOpt_top_p: 0.95,
|
||||
plotOpt_presence_penalty: 1,
|
||||
plotOpt_frequency_penalty: 1,
|
||||
|
||||
Reference in New Issue
Block a user