Files
Jenkins CI 2c3072a3d8 release: v2.2.2 [2026-05-27 11:10:55]
### 新功能
- **Function Call 填表模式**:在填表设置中新增独立开关,启用后支持通过 OpenAI 兼容接口(DeepSeek / OpenRouter / 各类中转等)直接返回结构化操作列表,绕过 `<Amily2Edit>` 文本解析路径,填表更稳定
  - 遇到不支持 `tool_choice` 的接口时自动降级重试
  - 对思考模型注入强制调用指令,防止绕过工具直接输出文本
  - 全部走 ST 后端代理,修复 CSP 拦截直连外部 URL 的问题
- **主界面新增提示词链编辑器入口**,同时调换了记忆管理与角色世界书的按钮位置
- **规则中心**新增"自动排除用户楼层"选项
### 修复
- 提示词链按钮点击无响应(改为事件委托方式绑定)
- 拖拽组件微抖误触发(加 5px 移动阈值过滤)
- 填表检查窗若干问题修复;翰林院(批量回填)修复;防抖逻辑落地
- 角色世界书入口添加使用警告弹窗(强制 10 秒倒计时),提示该功能长期未维护
- ApiProfile `fakeStream` 字段保存丢失问题
- 正文优化默认改为关闭状态
- NGMS / NCCS API 配置槽位标签修正(NGMS→总结,NCCS→填表)
- API Profile 面板选择逻辑统一重构,修复多处旧字段覆盖新配置的问题
- 世界书控制参数兼容性修复(排除递归、插入位置、扫描深度等,适配 ST 1.17.0+)
2026-05-27 11:10:55 +08:00

644 lines
28 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { getContext, extension_settings } from '/scripts/extensions.js';
import { characters } from '/script.js';
import { loadWorldInfo } from '/scripts/world-info.js';
import { log } from './logger.js';
import { updateTableFromText } from './manager.js';
import { extensionName } from '../../utils/settings.js';
import { renderTables } from '../../ui/table-bindings.js';
import { getPresetPrompts, getMixedOrder } from '../../PresetSettings/index.js';
import { callAI, callAIForTools, generateRandomSeed } from '../api.js';
import { callNccsAI } from '../api/NccsApi.js';
import { TABLE_FILL_TOOL, parseToolCallArgs } from './formatters/tool-call.js';
import { updateTableFromOps } from './manager.js';
import { extractBlocksByTags, applyExclusionRules } from '../utils/rag-tag-extractor.js';
import { resolveTableRuleConfig } from '../../utils/config/RuleProfileManager.js';
import { showTableFillReviewModal } from '../../ui/page-window.js';
import { getBatchFillerRuleTemplate, getBatchFillerFlowTemplate, convertTablesToCsvString } from './manager.js';
const CONTINUE_PROMPT = '上一条回复不完整或缺少 <Amily2Edit> 指令块。请直接从中断处继续生成剩余内容,不要重复已输出的文本,也不要添加任何解释或寒暄,确保最终输出中包含完整的 <Amily2Edit>...</Amily2Edit> 指令块。';
async function requestContinuation(baseMessages, partialResponse) {
const continueMessages = [
...baseMessages,
{ role: 'assistant', content: partialResponse || '' },
{ role: 'user', content: CONTINUE_PROMPT },
];
const continued = await callTableModel(continueMessages);
if (!continued) return null;
return `${partialResponse || ''}${continued}`;
}
let isFilling = false;
let manualStopRequested = false;
let currentBatch = 0;
let totalBatches = 0;
let chatHistoryLength = 0;
let threshold = 30;
const MAX_RETRIES = 2;
async function getWorldBookContext() {
const settings = extension_settings[extensionName] || {};
if (!settings.table_worldbook_enabled) {
return '';
}
const context = getContext();
let bookNames = [];
let content = '';
if (settings.table_worldbook_source === 'character') {
const characterId = context.characterId;
const character = characters[characterId];
const characterBook = character?.data?.extensions?.world;
if (characterBook) {
bookNames.push(characterBook);
}
} else {
bookNames = settings.table_selected_worldbooks || [];
}
if (bookNames.length === 0) {
return '';
}
const selectedEntriesConfig = settings.table_selected_entries || {};
for (const bookName of bookNames) {
try {
const bookData = await loadWorldInfo(bookName);
if (!bookData || !bookData.entries) continue;
const entriesToInclude = settings.table_worldbook_source === 'manual'
? (selectedEntriesConfig[bookName] || []).map(uid => String(uid))
: Object.values(bookData.entries).map(entry => String(entry.uid));
for (const entry of Object.values(bookData.entries)) {
if (entriesToInclude.includes(String(entry.uid))) {
content += `[来源:世界书,条目名字:${entry.comment || '无标题条目'}]\n${entry.content}\n\n`;
}
}
} catch (error) {
log(`加载世界书 "${bookName}" 失败: ${error.message}`, 'error');
}
}
if (content.length > settings.table_worldbook_char_limit) {
content = content.substring(0, settings.table_worldbook_char_limit);
}
return content.trim() ? `<世界书>\n${content.trim()}\n</世界书>` : '';
}
const fillButton = () => document.getElementById('fill-table-now-btn');
function updateButtonState(state, batchNum = 0, attemptNum = 0) {
const button = fillButton();
if (!button) return;
switch (state) {
case 'processing':
let attemptText = attemptNum > 0 ? ` (尝试 ${attemptNum + 1})` : '';
button.textContent = `点击停止 (${batchNum}/${totalBatches})${attemptText}`;
button.disabled = false;
isFilling = true;
break;
case 'stopping':
button.textContent = '正在停止...';
button.disabled = true;
break;
case 'paused':
button.textContent = '继续填表';
button.disabled = false;
isFilling = true;
break;
case 'error':
button.textContent = '继续填表 (出错)';
button.disabled = false;
isFilling = true;
break;
case 'idle':
default:
button.textContent = '立即填表';
button.disabled = false;
isFilling = false;
currentBatch = 0;
manualStopRequested = false;
break;
}
}
async function callTableModel(messages) {
try {
const settings = extension_settings[extensionName] || {};
if (settings.nccsEnabled) {
log('使用 Nccs API 进行表格填充...', 'info');
const result = await callNccsAI(messages);
if (!result) {
throw new Error('Nccs API返回内容为空。');
}
return result;
} else {
log('使用 tableFilling slot 进行表格填充...', 'info');
const result = await callAI(messages, { slot: 'tableFilling' });
if (!result) {
throw new Error('API返回内容为空。');
}
return result;
}
} catch (error) {
log(`与模型通讯时发生异常: ${error.message}`, "error");
toastr.error(`与模型通讯时发生异常: ${error.message}`, "通讯异常");
return null;
}
}
function getRawMessagesForSummary(startFloor, endFloor) {
const context = getContext();
const chat = context.chat;
const settings = extension_settings[extensionName] || {};
const historySlice = chat.slice(startFloor - 1, endFloor);
if (historySlice.length === 0) return null;
const userName = context.name1 || '用户';
const characterName = context.name2 || '角色';
let tagsToExtract = [];
let exclusionRules = [];
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) => {
let content = msg.mes;
if (tagsToExtract.length > 0) {
const blocks = extractBlocksByTags(content, tagsToExtract);
content = blocks.length > 0 ? blocks.join('\n\n') : '';
}
if (content) {
content = applyExclusionRules(content, exclusionRules);
}
if (!content.trim()) return null;
return {
floor: startFloor + index,
author: msg.is_user ? userName : characterName,
authorType: msg.is_user ? 'user' : 'char',
content: content.trim()
};
}).filter(Boolean);
return messages;
}
async function runBatchAttempt(batchNum, attemptNum) {
try {
if (manualStopRequested) {
log(`任务已在批次 ${batchNum} 开始前手动暂停。`, 'warn');
updateButtonState('paused');
return;
}
updateButtonState('processing', batchNum, attemptNum);
const startFloor = (batchNum - 1) * threshold + 1;
const endFloor = Math.min(startFloor + threshold - 1, chatHistoryLength);
log(`正在处理批次 ${batchNum}/${totalBatches} (楼层 ${startFloor}-${endFloor}, 尝试 ${attemptNum + 1}/${MAX_RETRIES + 1})`, 'info');
const purifiedMessages = getRawMessagesForSummary(startFloor, endFloor);
if (!purifiedMessages || purifiedMessages.length === 0) {
throw new Error('净化后无有效内容可处理。');
}
const batchContent = purifiedMessages.map(m => `【第 ${m.floor} 楼】 ${m.author}: ${m.content}`).join('\n');
const ruleTemplate = getBatchFillerRuleTemplate();
const flowTemplate = getBatchFillerFlowTemplate();
const currentTableDataString = convertTablesToCsvString();
const finalFlowPrompt = flowTemplate.replace('{{{Amily2TableData}}}', currentTableDataString);
let mixedOrder;
try {
const savedOrder = localStorage.getItem('amily2_prompt_presets_v2_mixed_order');
if (savedOrder) {
mixedOrder = JSON.parse(savedOrder);
}
} catch (e) {
console.error("[批量填表] 加载混合顺序失败:", e);
}
const order = getMixedOrder('batch_filler') || [];
const presetPrompts = await getPresetPrompts('batch_filler');
const worldBookContext = await getWorldBookContext();
const messages = [
{ role: 'system', content: generateRandomSeed() }
];
let promptCounter = 0;
for (const item of order) {
if (item.type === 'prompt') {
if (presetPrompts && presetPrompts[promptCounter]) {
messages.push(presetPrompts[promptCounter]);
promptCounter++;
}
} else if (item.type === 'conditional') {
switch (item.id) {
case 'worldbook':
if (worldBookContext) {
messages.push({ role: 'system', content: worldBookContext });
}
break;
case 'ruleTemplate':
messages.push({ role: "system", content: ruleTemplate });
break;
case 'flowTemplate':
messages.push({ role: "system", content: finalFlowPrompt });
break;
case 'coreContent':
messages.push({ role: 'user', content: `请严格根据以下"对话记录"中的内容进行填写表格,并按照指定的格式输出,不要添加任何额外信息。\n\n<对话记录>\n${batchContent}\n</对话记录>` });
break;
}
}
}
if (!presetPrompts || presetPrompts.length === 0) {
const defaultPrompts = [
{ role: 'system', content: generateRandomSeed() }
];
messages.splice(1, 0, ...defaultPrompts);
}
console.groupCollapsed(`[Amily2 立即远征] 批次 ${batchNum}/${totalBatches} - 即将发送至 API 的内容`);
console.dir(messages);
console.groupEnd();
const batchSettings = extension_settings[extensionName] || {};
if (batchSettings.tableFillFunctionCall) {
// Function Call 路径:结构化输出,无需检查 <Amily2Edit>
const argsString = await callAIForTools(messages, TABLE_FILL_TOOL, { slot: 'tableFilling' });
if (!argsString) throw new Error('Function Call 返回为空。');
const ops = parseToolCallArgs(argsString);
if (ops.length === 0) {
log(`批次 ${batchNum} 的 Function Call 返回操作列表为空AI 判断此批次无需变更。`, 'warn');
toastr.info('AI 判断此批次无需修改。', `批次 ${batchNum}`);
} else {
await updateTableFromOps(ops, { immediateDelete: true });
renderTables();
log(`批次 ${batchNum} Function Call 处理成功(${ops.length} 条操作)。`, 'success');
}
} else {
// Legacy 文本路径
const resultText = await callTableModel(messages);
console.log(`[Amily2 立即远征] 批次 ${batchNum}/${totalBatches} - 收到 API 原始回复:`, resultText);
if (!resultText) throw new Error('API返回内容为空。');
if (!resultText.includes('<Amily2Edit>')) {
log(`批次 ${batchNum} 的响应未包含 <Amily2Edit> 指令块,弹出检查窗口等待用户处理。`, 'warn');
updateButtonState('paused');
showTableFillReviewModal(resultText, {
title: `填表响应检查 - 批次 ${batchNum}/${totalBatches}`,
subtitle: `批次 ${batchNum}/${totalBatches}(楼层 ${startFloor}-${endFloor})的 AI 响应未包含有效的 <Amily2Edit> 指令块。请检查原始响应并选择处理方式。`,
onContinue: async (currentText) => {
const merged = await requestContinuation(messages, currentText);
if (!merged) { toastr.error('补全请求失败或返回为空。', '继续补全'); return null; }
if (!merged.includes('<Amily2Edit>')) {
toastr.warning('补全后仍未包含 <Amily2Edit> 指令块,可继续补全、手动应用或重新填表。', '继续补全');
} else {
toastr.success('已获得包含指令块的补全内容,可点击”手动应用”写入。', '继续补全');
}
return merged;
},
onApply: (editedText) => {
if (!editedText || !editedText.includes('<Amily2Edit>')) {
toastr.warning('应用的文本中未检测到 <Amily2Edit> 指令块,已按原文尝试写入。', '手动应用');
}
try {
updateTableFromText(editedText, { immediateDelete: true });
renderTables();
log(`批次 ${batchNum} 已由用户手动处理完成。`, 'success');
} catch (err) {
log(`批次 ${batchNum} 手动应用失败: ${err.message}`, 'error');
toastr.error(`手动应用失败: ${err.message}`, '写入异常');
currentBatch = batchNum - 1;
updateButtonState('error');
return;
}
currentBatch = batchNum;
setTimeout(processNextBatch, 500);
},
onRetry: () => {
log(`用户选择重新填表,批次 ${batchNum} 将重新执行。`, 'warn');
setTimeout(() => runBatchAttempt(batchNum, 0), 300);
},
onCancel: () => {
log(`用户取消了批次 ${batchNum} 的处理,任务已暂停。`, 'warn');
currentBatch = batchNum - 1;
updateButtonState('error');
},
});
return;
}
updateTableFromText(resultText, { immediateDelete: true });
renderTables();
log(`批次 ${batchNum} 处理成功。`, 'success');
}
currentBatch = batchNum;
setTimeout(processNextBatch, 1000);
} catch (error) {
log(`批次 ${batchNum} 尝试 ${attemptNum + 1} 失败: ${error.message}`, 'error');
if (attemptNum >= MAX_RETRIES) {
log(`批次 ${batchNum} 已达到最大重试次数,任务暂停。`, 'error');
toastr.error(`批次 ${batchNum} 多次失败请检查网络或API设置后手动继续。`, '任务暂停');
currentBatch = batchNum - 1;
updateButtonState('error');
} else {
log(`将在3秒后自动重试批次 ${batchNum}...`, 'warn');
setTimeout(() => runBatchAttempt(batchNum, attemptNum + 1), 3000);
}
}
}
async function processNextBatch() {
if (manualStopRequested) {
log(`任务已在批次 ${currentBatch + 1} 开始前手动暂停。`, 'warn');
updateButtonState('paused');
return;
}
if (currentBatch >= totalBatches) {
log('所有批次处理完毕!', 'success');
updateButtonState('idle');
return;
}
runBatchAttempt(currentBatch + 1, 0);
}
export function startBatchFilling() {
const button = fillButton();
if (!button) return;
const settings = extension_settings[extensionName] || {};
const tableSystemEnabled = settings.table_system_enabled !== false;
if (!tableSystemEnabled) {
log('表格系统总开关已关闭,跳过批量填表。', 'info');
toastr.info('表格系统总开关已关闭,无法执行批量填表。');
return;
}
if (isFilling) {
if (button.textContent.startsWith('点击停止')) {
manualStopRequested = true;
updateButtonState('stopping');
log('停战敕令已下达!将在当前批次完成后暂停。', 'warn');
} else if (button.textContent.startsWith('继续填表')) {
manualStopRequested = false;
log('从上次暂停处继续处理...', 'info');
processNextBatch();
}
return;
}
manualStopRequested = false;
const context = getContext();
chatHistoryLength = context.chat.length;
threshold = parseInt(document.getElementById('batch-filling-threshold')?.value, 10) || 30;
const ruleTemplate = getBatchFillerRuleTemplate();
const flowTemplate = getBatchFillerFlowTemplate();
if (!ruleTemplate || !flowTemplate) {
log('规则或流程提示词为空,无法开始填表。', 'error');
toastr.error('请确保"规则提示词"和"流程提示词"都已填写。', '无法开始');
return;
}
if (chatHistoryLength === 0) {
log('聊天记录为空,无需填表。', 'info');
return;
}
totalBatches = Math.ceil(chatHistoryLength / threshold);
currentBatch = 0;
const startFloorInput = document.getElementById('floor-start-input');
console.log('[Amily2 Debug] startFloorInput found:', !!startFloorInput);
if (startFloorInput) {
console.log('[Amily2 Debug] startFloorInput value:', startFloorInput.value);
const val = parseInt(startFloorInput.value, 10);
console.log('[Amily2 Debug] Parsed val:', val, 'Threshold:', threshold);
if (!isNaN(val) && val > 1) {
const startBatch = Math.ceil(val / threshold);
console.log('[Amily2 Debug] Calculated startBatch:', startBatch);
currentBatch = startBatch - 1;
log(`根据设定,将从第 ${startBatch} 批次(包含楼层 ${val})开始执行。`, 'info');
} else {
console.log('[Amily2 Debug] Value is NaN or <= 1');
}
} else {
console.log('[Amily2 Debug] startFloorInput element not found');
}
log(`准备开始批量填表任务,共 ${totalBatches} 个批次。`, 'info');
processNextBatch();
}
export async function startFloorRangeFilling(startFloor, endFloor) {
const settings = extension_settings[extensionName] || {};
const tableSystemEnabled = settings.table_system_enabled !== false;
if (!tableSystemEnabled) {
log('表格系统总开关已关闭,跳过楼层填表。', 'info');
toastr.info('表格系统总开关已关闭,无法执行楼层填表。');
return;
}
const context = getContext();
const currentChatLength = context.chat.length;
if (endFloor > currentChatLength) {
toastr.warning(`结束楼层 ${endFloor} 超出了当前聊天记录长度 ${currentChatLength}`);
return;
}
const ruleTemplate = getBatchFillerRuleTemplate();
const flowTemplate = getBatchFillerFlowTemplate();
if (!ruleTemplate || !flowTemplate) {
log('规则或流程提示词为空,无法开始楼层填表。', 'error');
toastr.error('请确保"规则提示词"和"流程提示词"都已填写。', '无法开始');
return;
}
try {
log(`开始处理楼层 ${startFloor}-${endFloor} 的内容...`, 'info');
const purifiedMessages = getRawMessagesForSummary(startFloor, endFloor);
if (!purifiedMessages || purifiedMessages.length === 0) {
toastr.warning('指定楼层范围内没有有效内容可处理。');
return;
}
const batchContent = purifiedMessages.map(m => `【第 ${m.floor} 楼】 ${m.author}: ${m.content}`).join('\n');
const currentTableDataString = convertTablesToCsvString();
const finalFlowPrompt = flowTemplate.replace('{{{Amily2TableData}}}', currentTableDataString);
let mixedOrder;
try {
const savedOrder = localStorage.getItem('amily2_prompt_presets_v2_mixed_order');
if (savedOrder) {
mixedOrder = JSON.parse(savedOrder);
}
} catch (e) {
console.error("[楼层填表] 加载混合顺序失败:", e);
}
const order = getMixedOrder('batch_filler') || [];
const presetPrompts = await getPresetPrompts('batch_filler');
const worldBookContext = await getWorldBookContext();
const messages = [
{ role: 'system', content: generateRandomSeed() }
];
let promptCounter = 0;
for (const item of order) {
if (item.type === 'prompt') {
if (presetPrompts && presetPrompts[promptCounter]) {
messages.push(presetPrompts[promptCounter]);
promptCounter++;
}
} else if (item.type === 'conditional') {
switch (item.id) {
case 'worldbook':
if (worldBookContext) {
messages.push({ role: 'system', content: worldBookContext });
}
break;
case 'ruleTemplate':
messages.push({ role: "system", content: ruleTemplate });
break;
case 'flowTemplate':
messages.push({ role: "system", content: finalFlowPrompt });
break;
case 'coreContent':
messages.push({ role: 'user', content: `请严格根据以下"对话记录"中的内容进行填写表格,并按照指定的格式输出,不要添加任何额外信息。\n\n<对话记录>\n${batchContent}\n</对话记录>` });
break;
}
}
}
if (!presetPrompts || presetPrompts.length === 0) {
const defaultPrompts = [
{ role: 'system', content: generateRandomSeed() }
];
messages.splice(1, 0, ...defaultPrompts);
}
console.groupCollapsed(`[Amily2 楼层填表] 楼层 ${startFloor}-${endFloor} - 即将发送至 API 的内容`);
console.dir(messages);
console.groupEnd();
const floorSettings = extension_settings[extensionName] || {};
if (floorSettings.tableFillFunctionCall) {
const argsString = await callAIForTools(messages, TABLE_FILL_TOOL, { slot: 'tableFilling' });
if (!argsString) throw new Error('Function Call 返回为空。');
const ops = parseToolCallArgs(argsString);
if (ops.length === 0) {
log(`楼层 ${startFloor}-${endFloor} Function Call 返回操作列表为空,无需变更。`, 'warn');
toastr.info('AI 判断此楼层范围无需修改。', `楼层 ${startFloor}-${endFloor}`);
} else {
await updateTableFromOps(ops, { immediateDelete: true });
renderTables();
toastr.success(`楼层 ${startFloor}-${endFloor} 填表完成!`);
log(`楼层 ${startFloor}-${endFloor} Function Call 处理成功(${ops.length} 条操作)。`, 'success');
}
} else {
const resultText = await callTableModel(messages);
console.log(`[Amily2 楼层填表] 楼层 ${startFloor}-${endFloor} - 收到 API 原始回复:`, resultText);
if (!resultText) throw new Error('API返回内容为空。');
if (!resultText.includes('<Amily2Edit>')) {
log(`楼层 ${startFloor}-${endFloor} 的响应未包含 <Amily2Edit> 指令块,弹出检查窗口等待用户处理。`, 'warn');
showTableFillReviewModal(resultText, {
title: `填表响应检查 - 楼层 ${startFloor}-${endFloor}`,
subtitle: `楼层 ${startFloor}-${endFloor} 的 AI 响应未包含有效的 <Amily2Edit> 指令块。请检查原始响应并选择处理方式。`,
onContinue: async (currentText) => {
const merged = await requestContinuation(messages, currentText);
if (!merged) { toastr.error('补全请求失败或返回为空。', '继续补全'); return null; }
if (!merged.includes('<Amily2Edit>')) {
toastr.warning('补全后仍未包含 <Amily2Edit> 指令块,可继续补全、手动应用或重新填表。', '继续补全');
} else {
toastr.success('已获得包含指令块的补全内容,可点击”手动应用”写入。', '继续补全');
}
return merged;
},
onApply: (editedText) => {
if (!editedText || !editedText.includes('<Amily2Edit>')) {
toastr.warning('应用的文本中未检测到 <Amily2Edit> 指令块,已按原文尝试写入。', '手动应用');
}
try {
updateTableFromText(editedText, { immediateDelete: true });
renderTables();
toastr.success(`楼层 ${startFloor}-${endFloor} 填表完成!`);
log(`楼层 ${startFloor}-${endFloor} 填表由用户手动处理完成。`, 'success');
} catch (err) {
log(`楼层 ${startFloor}-${endFloor} 手动应用失败: ${err.message}`, 'error');
toastr.error(`手动应用失败: ${err.message}`, '写入异常');
}
},
onRetry: () => {
log(`用户请求重新填写楼层 ${startFloor}-${endFloor}`, 'warn');
setTimeout(() => startFloorRangeFilling(startFloor, endFloor), 300);
},
onCancel: () => {
log(`用户取消了楼层 ${startFloor}-${endFloor} 的填表。`, 'warn');
toastr.info(`已取消楼层 ${startFloor}-${endFloor} 的填表。`);
},
});
return;
}
updateTableFromText(resultText, { immediateDelete: true });
renderTables();
toastr.success(`楼层 ${startFloor}-${endFloor} 填表完成!`);
log(`楼层 ${startFloor}-${endFloor} 填表处理完成。`, 'success');
}
} catch (error) {
log(`楼层 ${startFloor}-${endFloor} 填表失败: ${error.message}`, 'error');
toastr.error(`楼层填表失败: ${error.message}`, '处理失败');
}
}
export async function startCurrentFloorFilling() {
const context = getContext();
const currentFloor = context.chat.length;
if (currentFloor === 0) {
toastr.info('当前没有聊天记录。');
return;
}
log(`准备填写当前楼层(第 ${currentFloor} 楼)...`, 'info');
await startFloorRangeFilling(currentFloor, currentFloor);
}