mirror of
https://github.com/Wx-2025/ST-Amily2-Chat-Optimisation.git
synced 2026-06-06 06:55:51 +00:00
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+)
This commit is contained in:
@@ -6,13 +6,29 @@ 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, generateRandomSeed } from '../api.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;
|
||||
@@ -268,24 +284,80 @@ async function runBatchAttempt(batchNum, attemptNum) {
|
||||
console.dir(messages);
|
||||
console.groupEnd();
|
||||
|
||||
const resultText = await callTableModel(messages);
|
||||
console.log(`[Amily2 立即远征] 批次 ${batchNum}/${totalBatches} - 收到 API 原始回复:`, resultText);
|
||||
if (!resultText) {
|
||||
throw new Error('API返回内容为空。');
|
||||
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');
|
||||
}
|
||||
|
||||
// 【修复】检查 AI 是否返回了有效的指令块,防止 AI 偷懒或格式错误被误判为成功
|
||||
if (!resultText.includes('<Amily2Edit>')) {
|
||||
throw new Error('AI未返回有效的 <Amily2Edit> 指令块,可能格式错误或未产生实质性变更。');
|
||||
}
|
||||
|
||||
// 【V155.0】批量填表时,启用立即删除模式,避免红色待删除行残留
|
||||
updateTableFromText(resultText, { immediateDelete: true });
|
||||
renderTables();
|
||||
log(`批次 ${batchNum} 处理成功。`, 'success');
|
||||
|
||||
currentBatch = batchNum;
|
||||
setTimeout(processNextBatch, 1000);
|
||||
currentBatch = batchNum;
|
||||
setTimeout(processNextBatch, 1000);
|
||||
|
||||
} catch (error) {
|
||||
log(`批次 ${batchNum} 尝试 ${attemptNum + 1} 失败: ${error.message}`, 'error');
|
||||
@@ -484,24 +556,72 @@ export async function startFloorRangeFilling(startFloor, endFloor) {
|
||||
console.dir(messages);
|
||||
console.groupEnd();
|
||||
|
||||
const resultText = await callTableModel(messages);
|
||||
console.log(`[Amily2 楼层填表] 楼层 ${startFloor}-${endFloor} - 收到 API 原始回复:`, resultText);
|
||||
|
||||
if (!resultText) {
|
||||
throw new Error('API返回内容为空。');
|
||||
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');
|
||||
}
|
||||
|
||||
// 【修复】检查 AI 是否返回了有效的指令块
|
||||
if (!resultText.includes('<Amily2Edit>')) {
|
||||
throw new Error('AI未返回有效的 <Amily2Edit> 指令块,可能格式错误或未产生实质性变更。');
|
||||
}
|
||||
|
||||
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}`, '处理失败');
|
||||
|
||||
91
core/table-system/formatters/tool-call.js
Normal file
91
core/table-system/formatters/tool-call.js
Normal file
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* @file formatters/tool-call.js — Function Call 填表格式器
|
||||
*
|
||||
* 职责:
|
||||
* - 导出 TABLE_FILL_TOOL:发给模型的 tools 定义(单工具 + operations 数组)
|
||||
* - 导出 parseToolCallArgs:把 tool_calls[0].function.arguments 解析为 Operation[]
|
||||
*
|
||||
* 与 executor.js(legacy formatter)并列;下游 applyOperations 不感知来源。
|
||||
*
|
||||
* @typedef {import('../dto/Operation.js').Operation} Operation
|
||||
*/
|
||||
|
||||
/**
|
||||
* 填表工具 schema。使用 operations 数组而非多工具并发,兼容所有支持 function calling 的提供商。
|
||||
*
|
||||
* data 的 key 为列索引字符串("0"、"1"...),与 executor.js legacy 格式保持一致,
|
||||
* 提示词中会给出列索引与列名的对应关系。
|
||||
*/
|
||||
export const TABLE_FILL_TOOL = {
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'apply_table_edits',
|
||||
description: '将一批表格编辑操作应用到记忆表格中。',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
operations: {
|
||||
type: 'array',
|
||||
description: '按顺序执行的操作列表。',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
op: {
|
||||
type: 'string',
|
||||
enum: ['insertRow', 'updateRow', 'deleteRow'],
|
||||
description: 'insertRow=新增行,updateRow=更新已有行,deleteRow=删除行'
|
||||
},
|
||||
tableIndex: {
|
||||
type: 'integer',
|
||||
description: '目标表格的 0-based 索引'
|
||||
},
|
||||
rowIndex: {
|
||||
type: 'integer',
|
||||
description: 'updateRow / deleteRow 时必填,目标行的 0-based 索引'
|
||||
},
|
||||
data: {
|
||||
type: 'object',
|
||||
description: 'insertRow / updateRow 时必填,key 为列索引字符串("0"/"1"...),value 为单元格内容',
|
||||
additionalProperties: { type: 'string' }
|
||||
}
|
||||
},
|
||||
required: ['op', 'tableIndex']
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['operations']
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 解析 tool_calls[0].function.arguments 字符串为 Operation[]。
|
||||
* 结构校验失败的单条操作会被静默跳过,不中断整体解析。
|
||||
*
|
||||
* @param {string} argsString - JSON 字符串
|
||||
* @returns {Operation[]}
|
||||
*/
|
||||
export function parseToolCallArgs(argsString) {
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(argsString);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
|
||||
const rawOps = parsed?.operations;
|
||||
if (!Array.isArray(rawOps)) return [];
|
||||
|
||||
/** @type {Operation[]} */
|
||||
const ops = [];
|
||||
for (const raw of rawOps) {
|
||||
if (raw.op === 'insertRow' && Number.isInteger(raw.tableIndex) && raw.data && typeof raw.data === 'object') {
|
||||
ops.push({ op: 'insertRow', tableIndex: raw.tableIndex, data: raw.data });
|
||||
} else if (raw.op === 'updateRow' && Number.isInteger(raw.tableIndex) && Number.isInteger(raw.rowIndex) && raw.data && typeof raw.data === 'object') {
|
||||
ops.push({ op: 'updateRow', tableIndex: raw.tableIndex, rowIndex: raw.rowIndex, data: raw.data });
|
||||
} else if (raw.op === 'deleteRow' && Number.isInteger(raw.tableIndex) && Number.isInteger(raw.rowIndex)) {
|
||||
ops.push({ op: 'deleteRow', tableIndex: raw.tableIndex, rowIndex: raw.rowIndex });
|
||||
}
|
||||
}
|
||||
return ops;
|
||||
}
|
||||
@@ -29,6 +29,7 @@ import { extensionName } from '../../utils/settings.js';
|
||||
|
||||
import { log } from './logger.js';
|
||||
import { executeCommands } from './executor.js';
|
||||
import { applyOperations } from './actions/applyOperations.js';
|
||||
import { fillWithSecondaryApi } from './secondary-filler.js';
|
||||
import { renderTables } from '../../ui/table-bindings.js';
|
||||
import { updateOrInsertTableInChat } from '../../ui/message-table-renderer.js';
|
||||
@@ -874,6 +875,65 @@ export async function updateTableFromText(textContent, options = {}) {
|
||||
document.dispatchEvent(new CustomEvent('amily2-force-ui-reload'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 直接从 Operation[] 应用变更(Function Call 路径),跳过文本解析。
|
||||
* 后续流程与 updateTableFromText 完全一致。
|
||||
*
|
||||
* @param {import('./dto/Operation.js').Operation[]} ops
|
||||
* @param {Object} options - 同 updateTableFromText 的 options
|
||||
*/
|
||||
export async function updateTableFromOps(ops, options = {}) {
|
||||
const settings = extension_settings[extensionName] || {};
|
||||
if (settings.table_system_enabled === false) return;
|
||||
|
||||
if (!Array.isArray(ops) || ops.length === 0) {
|
||||
log('Function Call 返回操作列表为空,无需更新表格。', 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
const { state, changes } = applyOperations(getState(), ops);
|
||||
|
||||
if (changes.length === 0) {
|
||||
log('Function Call 操作未产生任何实质性变更。', 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
setState(state);
|
||||
|
||||
if (options.immediateDelete) {
|
||||
commitPendingDeletions();
|
||||
}
|
||||
|
||||
changes.forEach(change => {
|
||||
markTableUpdated(change.tableIndex);
|
||||
if (change.type === 'update' || change.type === 'insert') {
|
||||
if (change.rowIndex !== undefined && change.colIndex !== undefined) {
|
||||
addHighlight(change.tableIndex, change.rowIndex, change.colIndex);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
log(`Function Call 成功执行了 ${changes.length} 处变更。`, 'success');
|
||||
|
||||
const affectedTables = [...new Set(changes.map(c => c.tableIndex))];
|
||||
affectedTables.forEach(tableIndex => dispatchTableUpdate(tableIndex));
|
||||
|
||||
const context = getContext();
|
||||
if (context.chat && context.chat.length > 0) {
|
||||
const lastMessage = context.chat[context.chat.length - 1];
|
||||
if (_persistSaveStateToMessage(getState(), lastMessage)) {
|
||||
await saveChat();
|
||||
toastr.success('已根据AI的指示成功更新表格!', '填表完成');
|
||||
document.dispatchEvent(new CustomEvent('amily2-force-ui-reload'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
saveChatDebounced();
|
||||
toastr.success('已根据AI的指示成功更新表格!', '填表完成');
|
||||
document.dispatchEvent(new CustomEvent('amily2-force-ui-reload'));
|
||||
}
|
||||
|
||||
// ── 预设(re-export 或 wrapper) ─────────────────────────────────────────
|
||||
|
||||
export const exportPreset = _presetExportPreset;
|
||||
|
||||
@@ -4,13 +4,58 @@ import { saveChat } from "/script.js";
|
||||
import { renderTables } from '../../ui/table-bindings.js';
|
||||
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 { updateTableFromText, updateTableFromOps, getBatchFillerRuleTemplate, getBatchFillerFlowTemplate, convertTablesToCsvString, saveStateToMessage, getMemoryState, clearHighlights } from './manager.js';
|
||||
import { getPresetPrompts, getMixedOrder } from '../../PresetSettings/index.js';
|
||||
import { callAI, generateRandomSeed } from '../api.js';
|
||||
import { callAI, callAIForTools, generateRandomSeed } from '../api.js';
|
||||
import { TABLE_FILL_TOOL, parseToolCallArgs } from './formatters/tool-call.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';
|
||||
import { log } from './logger.js';
|
||||
import { showTableFillReviewModal } from '../../ui/page-window.js';
|
||||
|
||||
const CONTINUE_PROMPT_SECONDARY = '上一条回复不完整或缺少 <Amily2Edit> 指令块。请直接从中断处继续生成剩余内容,不要重复已输出的文本,也不要添加任何解释或寒暄,确保最终输出中包含完整的 <Amily2Edit>...</Amily2Edit> 指令块。';
|
||||
|
||||
let secondaryFillerDebounceTimer = null;
|
||||
|
||||
async function callSecondaryModel(messages) {
|
||||
const settings = extension_settings[extensionName] || {};
|
||||
if (settings.nccsEnabled) {
|
||||
return await callNccsAI(messages);
|
||||
}
|
||||
return await callAI(messages);
|
||||
}
|
||||
|
||||
async function requestSecondaryContinuation(baseMessages, partialResponse) {
|
||||
const continueMessages = [
|
||||
...baseMessages,
|
||||
{ role: 'assistant', content: partialResponse || '' },
|
||||
{ role: 'user', content: CONTINUE_PROMPT_SECONDARY },
|
||||
];
|
||||
const continued = await callSecondaryModel(continueMessages);
|
||||
if (!continued) return null;
|
||||
return `${partialResponse || ''}${continued}`;
|
||||
}
|
||||
|
||||
function commitSecondaryFillResult(rawContent, targetMessages) {
|
||||
updateTableFromText(rawContent);
|
||||
|
||||
const memoryState = getMemoryState();
|
||||
const lastProcessedMsg = targetMessages[targetMessages.length - 1].msg;
|
||||
|
||||
for (const target of targetMessages) {
|
||||
if (!target.msg.metadata) target.msg.metadata = {};
|
||||
target.msg.metadata.Amily2_Process_Hash = target.hash;
|
||||
}
|
||||
|
||||
if (saveStateToMessage(memoryState, lastProcessedMsg)) {
|
||||
renderTables();
|
||||
updateOrInsertTableInChat();
|
||||
}
|
||||
|
||||
saveChat();
|
||||
}
|
||||
|
||||
|
||||
async function getWorldBookContext() {
|
||||
@@ -66,10 +111,29 @@ async function getWorldBookContext() {
|
||||
}
|
||||
|
||||
export async function fillWithSecondaryApi(latestMessage, forceRun = false) {
|
||||
clearHighlights();
|
||||
|
||||
const settings = extension_settings[extensionName] || {};
|
||||
|
||||
// 【V2.1.1】分步填表触发延迟 / 防抖:自动触发时若配置了延迟,则延后执行,
|
||||
// 延迟期内再次到来的事件会重置计时器,避免消息连续到达时重复拉起填表。
|
||||
const delay = Math.max(0, parseInt(settings.secondary_filler_delay || 0, 10));
|
||||
if (!forceRun && delay > 0) {
|
||||
if (secondaryFillerDebounceTimer) {
|
||||
clearTimeout(secondaryFillerDebounceTimer);
|
||||
}
|
||||
secondaryFillerDebounceTimer = setTimeout(() => {
|
||||
secondaryFillerDebounceTimer = null;
|
||||
fillWithSecondaryApi(latestMessage, forceRun);
|
||||
}, delay);
|
||||
console.log(`[Amily2-副API] 分步填表已按防抖延迟 ${delay}ms 调度。`);
|
||||
return;
|
||||
}
|
||||
if (secondaryFillerDebounceTimer) {
|
||||
clearTimeout(secondaryFillerDebounceTimer);
|
||||
secondaryFillerDebounceTimer = null;
|
||||
}
|
||||
|
||||
clearHighlights();
|
||||
|
||||
// 总开关关闭时,分步填表同样禁用
|
||||
if (settings.table_system_enabled === false) {
|
||||
log('【分步填表】表格系统总开关已关闭,跳过。', 'info');
|
||||
@@ -272,44 +336,87 @@ export async function fillWithSecondaryApi(latestMessage, forceRun = false) {
|
||||
console.dir(messages);
|
||||
console.groupEnd();
|
||||
|
||||
let rawContent;
|
||||
if (settings.nccsEnabled) {
|
||||
console.log('[Amily2-副API] 使用 Nccs API 进行分步填表...');
|
||||
rawContent = await callNccsAI(messages);
|
||||
if (settings.tableFillFunctionCall) {
|
||||
// Function Call 路径
|
||||
const argsString = await callAIForTools(messages, TABLE_FILL_TOOL, { slot: 'tableFilling' });
|
||||
if (!argsString) {
|
||||
console.error('[Amily2-副API] Function Call 返回为空。');
|
||||
return;
|
||||
}
|
||||
const ops = parseToolCallArgs(argsString);
|
||||
if (ops.length === 0) {
|
||||
console.warn('[Amily2-副API] Function Call 返回操作列表为空,无需变更。');
|
||||
toastr.info('AI 判断此范围无需修改。', 'Amily2-分步填表');
|
||||
} else {
|
||||
await updateTableFromOps(ops);
|
||||
toastr.success('分步填表(Function Call)执行完毕。', 'Amily2-分步填表');
|
||||
}
|
||||
} else {
|
||||
console.log('[Amily2-副API] 使用 tableFilling slot 进行分步填表...');
|
||||
rawContent = await callAI(messages, { slot: 'tableFilling' });
|
||||
// Legacy 文本路径
|
||||
let rawContent;
|
||||
if (settings.nccsEnabled) {
|
||||
console.log('[Amily2-副API] 使用 Nccs API 进行分步填表...');
|
||||
rawContent = await callNccsAI(messages);
|
||||
} else {
|
||||
console.log('[Amily2-副API] 使用 tableFilling slot 进行分步填表...');
|
||||
rawContent = await callAI(messages, { slot: 'tableFilling' });
|
||||
}
|
||||
|
||||
if (!rawContent) {
|
||||
console.error('[Amily2-副API] 未能获取AI响应内容。');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[Amily2号-副API-原始回复]:', rawContent);
|
||||
|
||||
if (!rawContent.includes('<Amily2Edit>')) {
|
||||
const rangeLabel = `${targetMessages[0].index + 1} - ${targetMessages[targetMessages.length - 1].index + 1}`;
|
||||
console.warn(`[Amily2-副API] 响应未包含 <Amily2Edit> 指令块(楼层 ${rangeLabel}),弹出检查窗口等待用户处理。`);
|
||||
toastr.warning(`分步填表(楼层 ${rangeLabel})的响应缺少 <Amily2Edit> 指令块,请在弹窗中处理。`, 'Amily2-分步填表');
|
||||
if (latestMessage && latestMessage.metadata) {
|
||||
delete latestMessage.metadata.Amily2_Retry_Count;
|
||||
}
|
||||
showTableFillReviewModal(rawContent, {
|
||||
title: `分步填表响应检查 - 楼层 ${rangeLabel}`,
|
||||
subtitle: `分步填表(楼层 ${rangeLabel})的 AI 响应未包含有效的 <Amily2Edit> 指令块。请检查原始响应并选择处理方式。`,
|
||||
onContinue: async (currentText) => {
|
||||
const merged = await requestSecondaryContinuation(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 {
|
||||
commitSecondaryFillResult(editedText, targetMessages);
|
||||
toastr.success('分步填表已由用户手动处理完成。', 'Amily2-分步填表');
|
||||
} catch (err) {
|
||||
console.error('[Amily2-副API] 手动应用失败:', err);
|
||||
toastr.error(`手动应用失败: ${err.message}`, '写入异常');
|
||||
}
|
||||
},
|
||||
onRetry: () => {
|
||||
if (latestMessage && latestMessage.metadata) {
|
||||
delete latestMessage.metadata.Amily2_Retry_Count;
|
||||
}
|
||||
toastr.info('将重新执行分步填表...', 'Amily2-分步填表');
|
||||
setTimeout(() => fillWithSecondaryApi(latestMessage, forceRun), 300);
|
||||
},
|
||||
onCancel: () => {
|
||||
toastr.info('已取消本次分步填表。', 'Amily2-分步填表');
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
commitSecondaryFillResult(rawContent, targetMessages);
|
||||
}
|
||||
|
||||
if (!rawContent) {
|
||||
console.error('[Amily2-副API] 未能获取AI响应内容。');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("[Amily2号-副API-原始回复]:", rawContent);
|
||||
|
||||
// 【修复】检查 AI 是否返回了有效的指令块,防止 AI 偷懒或格式错误被误判为成功
|
||||
if (!rawContent.includes('<Amily2Edit>')) {
|
||||
throw new Error('AI未返回有效的 <Amily2Edit> 指令块,可能格式错误或未产生实质性变更。');
|
||||
}
|
||||
|
||||
updateTableFromText(rawContent);
|
||||
|
||||
const memoryState = getMemoryState();
|
||||
|
||||
const lastProcessedMsg = targetMessages[targetMessages.length - 1].msg;
|
||||
|
||||
for (const target of targetMessages) {
|
||||
if (!target.msg.metadata) target.msg.metadata = {};
|
||||
target.msg.metadata.Amily2_Process_Hash = target.hash;
|
||||
}
|
||||
|
||||
if (saveStateToMessage(memoryState, lastProcessedMsg)) {
|
||||
renderTables();
|
||||
updateOrInsertTableInChat();
|
||||
}
|
||||
|
||||
saveChat();
|
||||
toastr.success("分步填表执行完毕。", "Amily2-分步填表");
|
||||
|
||||
} catch (error) {
|
||||
|
||||
@@ -158,4 +158,7 @@ export const tableSystemDefaultSettings = {
|
||||
// Nccs API 设置
|
||||
nccsEnabled: false,
|
||||
nccsFakeStreamEnabled: false,
|
||||
|
||||
// Function Call 填表
|
||||
tableFillFunctionCall: false,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user