Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions api.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// @ts-nocheck
// api.js
// [重构] 核心API模块,支持“自定义API”和“酒馆预设”两种模式
import { getRequestHeaders } from '/script.js';
Expand Down Expand Up @@ -40,11 +41,7 @@ function normalizeApiResponse(responseData) {
/**
* 主API调用入口,根据设置选择不同的模式
*/
<<<<<<< HEAD
export async function callInterceptionApi(messages, apiSettings, abortSignal) {
=======
export async function callInterceptionApi(messages, apiSettings, abortSignal = null) {
>>>>>>> 9d9294a0040fefed67bebf2d6763acb7f1f2d288
// messages 已经在 index.js 中构建完成,直接使用
// apiSettings 包含API连接配置

Expand Down
134 changes: 12 additions & 122 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// @ts-nocheck
// 剧情规划大师 - 聊天优化插件
// 由Cline移植并重构

Expand All @@ -13,8 +14,6 @@ console.log('[剧情优化大师] v3.5.1 Loading... (Timestamp: ' + Date.now() +
const extension_name = 'quick-response-force';
let isProcessing = false;
let tempPlotToSave = null; // [架构重构] 用于在生成和消息创建之间临时存储plot
let currentAbortController_QRF = null; // [新增] 用于中止正在进行的AI请求(剧情规划)
let wasStoppedByUser_QRF = false; // [新增] 标记本次规划是否被用户手动终止

/**
* [新增] 从世界书中直接提取数据库生成的 OutlineTable(总结大纲/总体大纲)条目内容,作为 $5 的兜底来源。
Expand Down Expand Up @@ -668,56 +667,6 @@ async function runOptimizationLogic(userMessage) {
return null; // 插件未启用,直接返回
}

<<<<<<< HEAD
// 重置中止控制器与标志(参考数据库版本的终止按钮行为)
wasStoppedByUser_QRF = false;
currentAbortController_QRF = new AbortController();

// 创建带“终止”按钮的 Toast
const toastMsg = `
<div style="display:flex; align-items:center; justify-content:space-between; gap:10px;">
<span class="toastr-message">正在规划剧情...</span>
<button class="qrf-abort-btn" title="终止本次规划">
<i class="fa-solid fa-stop"></i>
<span>终止</span>
</button>
</div>
`;
$toast = toastr.info(toastMsg, '剧情规划大师', {
timeOut: 0,
extendedTimeOut: 0,
escapeHtml: false,
tapToDismiss: false,
closeButton: false,
progressBar: false,
});

// 绑定终止按钮(优先绑定当前 toast 内按钮,避免误绑到旧 toast)
setTimeout(() => {
const $abortBtn = ($toast && $toast.find) ? $toast.find('.qrf-abort-btn') : $('.qrf-abort-btn');
if ($abortBtn.length > 0) {
$abortBtn.off('click').on('click', function (e) {
e.preventDefault();
e.stopPropagation();

wasStoppedByUser_QRF = true;
if (currentAbortController_QRF) currentAbortController_QRF.abort();

try {
if ($toast) toastr.clear($toast);
const $toastDom = $(this).closest('.toast');
if ($toastDom && $toastDom.length) $toastDom.remove();
} catch (err) {}

// 强制释放锁,避免卡死
isProcessing = false;

// 用户主动终止属于正常流程,不弹“错误”
setTimeout(() => toastr.info('规划任务已被用户中止。', '提示', { timeOut: 1500 }), 300);
});
}
}, 0);
=======
// 重置中止控制器
abortController = new AbortController();

Expand All @@ -735,7 +684,6 @@ async function runOptimizationLogic(userMessage) {
escapeHtml: false,
tapToDismiss: false
});
>>>>>>> 9d9294a0040fefed67bebf2d6763acb7f1f2d288

const context = getContext();
const character = characters[this_chid];
Expand Down Expand Up @@ -989,7 +937,9 @@ async function runOptimizationLogic(userMessage) {
for (let i = 0; i < relayFlows.length; i++) {
const flow = relayFlows[i];
if (!flow.enabled) continue;
if (wasStoppedByUser_QRF) throw new Error('TaskAbortedByUser');
if (abortController && abortController.signal.aborted) {
throw new Error('TaskAbortedByUser');
}

$toast.find('.toastr-message').text(`正在规划剧情... (接力流程:${flow.injectKey})`);

Expand All @@ -1005,7 +955,7 @@ async function runOptimizationLogic(userMessage) {

const overrides = flow.apiProfileId ? getApiProfileOverrides(flow.apiProfileId) : null;
const flowApiSettings = { ...apiSettings, ...(overrides || {}), extractTags: '' };
const flowResult = await callInterceptionApi(flowMessages, flowApiSettings, currentAbortController_QRF?.signal);
const flowResult = await callInterceptionApi(flowMessages, flowApiSettings, abortController?.signal);
// null 表示中止或错误:不覆盖旧输出
if (flowResult !== null && flowResult !== undefined) {
// 每个流程可配置独立的标签摘取:注入/保存的是提取后的内容;留空则保存全量
Expand Down Expand Up @@ -1066,28 +1016,17 @@ async function runOptimizationLogic(userMessage) {

if (minLength > 0) {
for (let i = 0; i < maxRetries; i++) {
<<<<<<< HEAD
if (wasStoppedByUser_QRF) {
throw new Error('TaskAbortedByUser');
}
=======
checkAbort();
>>>>>>> 9d9294a0040fefed67bebf2d6763acb7f1f2d288
$toast.find('.toastr-message').text(`正在规划剧情... (尝试 ${i + 1}/${maxRetries})`);

if (willUseMainApiGenerateRaw) {
planningGuard.ignoreNextGenerationEndedCount++;
}

// 直接传递构建好的 messages 数组
<<<<<<< HEAD
const tempMessage = await callInterceptionApi(messages, finalApiSettings, currentAbortController_QRF?.signal);
=======
const tempMessage = await callInterceptionApi(messages, finalApiSettings, abortController.signal);

checkAbort(); // API 调用后再次检查

>>>>>>> 9d9294a0040fefed67bebf2d6763acb7f1f2d288
if (tempMessage && tempMessage.length >= minLength) {
processedMessage = tempMessage;
if ($toast) toastr.clear($toast);
Expand All @@ -1100,19 +1039,12 @@ async function runOptimizationLogic(userMessage) {
}
}
} else {
<<<<<<< HEAD
if (wasStoppedByUser_QRF) {
throw new Error('TaskAbortedByUser');
}
processedMessage = await callInterceptionApi(messages, finalApiSettings, currentAbortController_QRF?.signal);
=======
checkAbort();
if (willUseMainApiGenerateRaw) {
planningGuard.ignoreNextGenerationEndedCount++;
}
processedMessage = await callInterceptionApi(messages, finalApiSettings, abortController.signal);
checkAbort();
>>>>>>> 9d9294a0040fefed67bebf2d6763acb7f1f2d288
}

if (processedMessage) {
Expand All @@ -1129,49 +1061,11 @@ async function runOptimizationLogic(userMessage) {
.map(t => t.trim())
.filter(t => t);
if (tagNames.length > 0) {
<<<<<<< HEAD
const extracted = extractLastBlocksPerTag(processedMessage, tagNames);
if (extracted) {
messageForTavern = extracted;
console.log(`[${extension_name}] 成功按标签分别摘取最后一次匹配: ${tagNames.join(', ')}`);
toastr.info(`已成功按标签分别摘取最后一次匹配并注入。`, '标签摘取');
=======
const extractedParts = [];

// [健全性] 仅提取“最后一组”标签的内容(支持处理类似 <key>123<key>456</key> 的异常嵌套)
// 规则:找到最后一个 </tag>,再回溯找到它之前最近的 <tag>,只取两者之间的内容。
const extractLastTagContent = (text, rawTagName) => {
if (!text || !rawTagName) return null;
const tagName = String(rawTagName).trim();
if (!tagName) return null;

const lower = text.toLowerCase();
const open = `<${tagName.toLowerCase()}>`;
const close = `</${tagName.toLowerCase()}>`;

const closeIdx = lower.lastIndexOf(close);
if (closeIdx === -1) return null;

const openIdx = lower.lastIndexOf(open, closeIdx);
if (openIdx === -1) return null;

const contentStart = openIdx + open.length;
const content = text.slice(contentStart, closeIdx);
return content;
};

tagNames.forEach(tagName => {
const content = extractLastTagContent(processedMessage, tagName);
if (content !== null) {
extractedParts.push(`<${tagName}>${content}</${tagName}>`);
}
});

if (extractedParts.length > 0) {
messageForTavern = extractedParts.join('\n\n');
console.log(`[${extension_name}] 成功摘取标签: ${tagNames.join(', ')}`);
toastr.info(`已成功摘取 [${tagNames.join(', ')}] 标签内容并注入。`, '标签摘取');
>>>>>>> 9d9294a0040fefed67bebf2d6763acb7f1f2d288
} else {
console.log(`[${extension_name}] 在回复中未找到指定标签: ${tagNames.join(', ')}`);
}
Expand All @@ -1194,16 +1088,10 @@ async function runOptimizationLogic(userMessage) {
return null;
}
} catch (error) {
<<<<<<< HEAD
if (error?.message === 'TaskAbortedByUser') {
// 用户主动终止:正常流程,不提示“规划失败”
if ($toast) toastr.clear($toast);
return null;
=======
if (error.message === 'TaskAbortedByUser') {
// 用户中止,返回特殊标记对象
return { aborted: true };
>>>>>>> 9d9294a0040fefed67bebf2d6763acb7f1f2d288
// 用户中止,返回特殊标记对象
return { aborted: true };
}
console.error(`[${extension_name}] 在核心优化逻辑中发生错误:`, error);
if ($toast) toastr.clear($toast);
Expand Down Expand Up @@ -1394,9 +1282,11 @@ jQuery(async () => {
// 首次加载时,执行一次预设加载和数据清理
loadPresetAndCleanCharacterData();

const intervalId = setInterval(async () => {
// 确保UI和TavernHelper都已加载
if ($('#extensions_settings').length > 0 && window.TavernHelper) {
const intervalId = setInterval(async () => {
// 确保UI和TavernHelper都已加载(兼容不同布局)
const hasSettingsContainer =
$('#extensions_settings').length > 0 || $('#extensions_settings2').length > 0;
if (hasSettingsContainer && window.TavernHelper) {
clearInterval(intervalId);
try {
loadPluginStyles();
Expand Down
8 changes: 2 additions & 6 deletions manifest.json
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
{
"name": "剧情优化大师",
"display_name": "剧情优化大师",
<<<<<<< HEAD
"version": "2.1.2",
=======
"version": "3.5.1",
>>>>>>> 9d9294a0040fefed67bebf2d6763acb7f1f2d288
"version": "3.5.2",
"author": "Cline",
"description": "一个独立的聊天优化插件,通过在生成前预处理用户输入,增强上下文和关键词,从而提升故事叙述的质量。",
"minSillyTavernVersion": "1.10.0",
"requires": [],
"loading_order": 101,
"js": "index.js",
"js": "index.js?v=3.5.2",
"css": "style.css",
"persistent_files": [
"settings.json"
Expand Down
63 changes: 0 additions & 63 deletions settings.html
Original file line number Diff line number Diff line change
Expand Up @@ -163,8 +163,6 @@ <h4><i class="fas fa-magic"></i> 剧情优化大师 - 设置 <span style="font-s
<input id="qrf_frequency_penalty" type="number" class="text_pole" min="-2" max="2" step="0.1" />
</div>
</div>
<<<<<<< HEAD

<hr>
<div class="qrf_settings_block">
<label for="qrf_api_profile_select"><i class="fa-solid fa-layer-group"></i> API配置库</label>
Expand Down Expand Up @@ -251,14 +249,6 @@ <h4><i class="fas fa-magic"></i> 剧情优化大师 - 设置 <span style="font-s

<fieldset class="settings-group">
<legend><i class="fas fa-book-open"></i> 内容设置</legend>
=======
</div>
</fieldset>

<fieldset class="settings-group qrf-collapsible collapsed">
<legend><i class="fas fa-chevron-down toggle-icon"></i> <i class="fas fa-book-open"></i> 内容设置</legend>
<div class="qrf-collapsible-content">
>>>>>>> 9d9294a0040fefed67bebf2d6763acb7f1f2d288

<div class="qrf_settings_block_radio">
<label>世界书来源</label>
Expand Down Expand Up @@ -311,59 +301,6 @@ <h4><i class="fas fa-magic"></i> 剧情优化大师 - 设置 <span style="font-s
</div>
</fieldset>

<fieldset class="settings-group qrf-collapsible collapsed">
<legend><i class="fas fa-chevron-down toggle-icon"></i> <i class="fas fa-edit"></i> 提示词指令</legend>
<div class="qrf-collapsible-content">

<div class="qrf_settings_block">
<label for="qrf_prompt_preset_select">加载提示词预设</label>
<div class="qrf_preset_selector_wrapper">
<select id="qrf_prompt_preset_select" class="text_pole">
<option value="">-- 选择一个预设 --</option>
</select>
<button id="qrf_import_prompt_presets" class="menu_button" title="导入预设"><i class="fa-solid fa-upload"></i></button>
<button id="qrf_export_prompt_presets" class="menu_button" title="导出所有预设"><i class="fa-solid fa-download"></i></button>
<button id="qrf_save_prompt_preset" class="menu_button" title="覆盖保存当前预设"><i class="fa-solid fa-save"></i></button>
<button id="qrf_save_as_new_prompt_preset" class="menu_button" title="另存为新预设"><i class="fa-solid fa-file-export"></i></button>
<button id="qrf_delete_prompt_preset" class="menu_button" title="删除当前选中的预设" style="display: none;"><i class="fa-solid fa-trash-alt"></i></button>
<input type="file" id="qrf_preset_file_input" style="display: none;" accept=".json">
</div>
<small class="notes">导入/导出JSON格式的预设文件。保存当前提示词。加载或删除选中的预设。</small>
</div>

<fieldset class="settings-group">
<legend><i class="fa-solid fa-right-left"></i> 匹配替换</legend>
<small class="notes" style="display: block; margin-bottom: 10px;">
在发送前,插件会将下方设置的数值替换掉三个提示词指令中的占位符(sulv1, sulv2, sulv3, sulv4)。
</small>
<div class="qrf_settings_block">
<label for="qrf_rate_main">主线剧情推进速率 (sulv1)</label>
<input id="qrf_rate_main" type="number" class="text_pole" step="0.05" value="1.0">
</div>
<div class="qrf_settings_block">
<label for="qrf_rate_personal">个人线推进速率 (sulv2)</label>
<input id="qrf_rate_personal" type="number" class="text_pole" step="0.05" value="1.0">
</div>
<div class="qrf_settings_block">
<label for="qrf_rate_erotic">色情事件推进速率 (sulv3)</label>
<input id="qrf_rate_erotic" type="number" class="text_pole" step="0.05" value="1.0">
</div>
<div class="qrf_settings_block">
<label for="qrf_rate_cuckold">绿帽线推进速率 (sulv4)</label>
<input id="qrf_rate_cuckold" type="number" class="text_pole" step="0.05" value="1.0">
</div>
</fieldset>

<div id="qrf_prompts_container" class="qrf_prompts_list">
<!-- Prompts will be dynamically generated here -->
</div>

<div class="qrf_settings_block" style="text-align: center; margin-top: 10px;">
<button id="qrf_add_prompt_btn" class="menu_button"><i class="fa-solid fa-plus"></i> 添加提示词</button>
</div>
</div>
</fieldset>

<div class="qrf_footer">
<small class="notes">所有设置将在关闭此面板时自动保存。</small>
</div>
Expand Down
10 changes: 3 additions & 7 deletions ui/bindings.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// @ts-nocheck
// 剧情优化大师 - UI数据绑定模块
// 由Cline参照 '优化/' 插件的健壮性实践重构

Expand Down Expand Up @@ -465,7 +466,6 @@ export async function loadWorldbookEntries(panel) {
container.empty();
totalEntries = allEntries.length;

<<<<<<< HEAD
// [新功能] 迁移逻辑:首次加载时,将当前所有条目设为未选中(加入禁用列表),
// 从而实现“默认全不勾选,新增条目自动勾选”的效果。
if (this_chid !== -1 && characters[this_chid]) {
Expand Down Expand Up @@ -513,9 +513,6 @@ export async function loadWorldbookEntries(panel) {
}
}
}

=======
>>>>>>> 9d9294a0040fefed67bebf2d6763acb7f1f2d288
if (totalEntries === 0) {
container.html('<p class="notes">所选世界书没有条目。</p>');
countDisplay.text('0 条目.');
Expand Down Expand Up @@ -1908,11 +1905,10 @@ export function initializeBindings() {
panel.find('#qrf_extract_tags').val(presetData.extractTags);
panel.find('#qrf_min_length').val(presetData.minLength);
panel.find('#qrf_context_turn_count').val(presetData.contextTurnCount);
<<<<<<< HEAD
renderRelayFlows(panel, presetData.relayFlows);
=======
panel.find('#qrf_worldbook_char_limit').val(presetData.worldbookCharLimit);
>>>>>>> 9d9294a0040fefed67bebf2d6763acb7f1f2d288
panel.find('#qrf_worldbook_char_limit').val(presetData.worldbookCharLimit);
renderRelayFlows(panel, presetData.relayFlows);

// 2. 直接、同步地覆盖apiSettings中的内容
// saveSetting现在是异步的,我们需要等待它完成
Expand Down
Loading