mirror of
https://github.com/Cola-Echo/memory-manager-concurrent.git
synced 2026-06-06 03:05:51 +00:00
1. 将 window.extension_settings 替换为 getExtensionSettings() - 新版 SillyTavern 已废弃 window.extension_settings 全局变量 - 改用 SillyTavern.getContext().extensionSettings(通过已有的 getExtensionSettings helper) - 影响文件: table-filler.js, mode-manager.js, service-interceptor.js, independent-template-modal.js 2. 修复总结世界书未配置时自动拆分导致的渲染崩溃 - renderSummaryPartsUI 中 getSummaryConfig() 在未配置时会 throw - 导致整个世界书列表渲染失败,自动拆分开关也无法显示 - 改为安全读取 config?.summaryConfigs?.[book.name],未配置时显示"未配置"状态
1164 lines
40 KiB
JavaScript
1164 lines
40 KiB
JavaScript
/**
|
||
* Amily表格并发 UI 组件
|
||
* @module ui/components/table-filler
|
||
*/
|
||
|
||
import Logger from "@core/logger";
|
||
import { getExtensionSettings } from "@core/sillytavern-api";
|
||
import {
|
||
getTableFillerConfig,
|
||
isTableFillerEnabled,
|
||
updateTableFillerConfig,
|
||
setTableFillerEnabled,
|
||
setTableApiConfig,
|
||
deleteTableApiConfig,
|
||
getGlobalSettings,
|
||
getIndependentTagName,
|
||
setIndependentTagName,
|
||
} from "@config/config-manager";
|
||
import {
|
||
CallMode,
|
||
getModeStatus,
|
||
isSecondaryApiMode,
|
||
getAmily2FillingModeName,
|
||
} from "@table-filler/mode-manager";
|
||
import { disableTableFiller, reinitTableFiller } from "@table-filler/index";
|
||
import { APIAdapter } from "@api/adapter";
|
||
import { bindIndependentTemplateEvents } from "@ui/modals/independent-template-modal";
|
||
|
||
const log = Logger.createModuleLogger('Amily表格并发');
|
||
|
||
// 当前编辑的表格名称(null表示默认配置)
|
||
let currentEditingTable = null;
|
||
|
||
// 缓存的表格名称列表
|
||
let cachedTableNames = [];
|
||
|
||
// 缓存的 Amily2 设置键名
|
||
let cachedAmily2ExtKey = null;
|
||
|
||
/**
|
||
* 从 Amily2 获取当前表格名称列表
|
||
* 使用多种方法尝试获取,确保兼容性
|
||
* @returns {Promise<string[]>} 表格名称数组
|
||
*/
|
||
async function getAmily2TableNames() {
|
||
try {
|
||
// 方法1:直接从 extension_settings 获取预设中的表格定义(最可靠)
|
||
const amilyExtName = "ST-Amily2-Chat-Optimisation";
|
||
const settings = getExtensionSettings()?.[amilyExtName];
|
||
|
||
if (settings) {
|
||
log.debug("找到 Amily2 扩展设置,检查表格定义...");
|
||
log.debug("设置中的键:", Object.keys(settings));
|
||
|
||
// 优先使用 global_table_preset.tables(全局预设)
|
||
if (settings.global_table_preset?.tables) {
|
||
const tables = settings.global_table_preset.tables;
|
||
if (Array.isArray(tables) && tables.length > 0) {
|
||
const names = tables.map(t => t.name).filter(Boolean);
|
||
if (names.length > 0) {
|
||
log.debug("从 global_table_preset 获取表格名称:", names);
|
||
return names;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 尝试直接的 tables 字段
|
||
if (settings.tables && Array.isArray(settings.tables)) {
|
||
const names = settings.tables.map(t => t.name).filter(Boolean);
|
||
if (names.length > 0) {
|
||
log.debug("从 settings.tables 获取表格名称:", names);
|
||
return names;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 方法2:尝试从 Amily2Bus 查询 TableManager
|
||
if (window.Amily2Bus) {
|
||
log.debug("尝试通过 Amily2Bus 获取表格...");
|
||
try {
|
||
// 尝试查询 Amily2 模块
|
||
const amily2Module = window.Amily2Bus.query("Amily2");
|
||
if (amily2Module) {
|
||
log.debug("Amily2 模块暴露的接口:", Object.keys(amily2Module));
|
||
}
|
||
|
||
// 尝试获取 TableManager
|
||
const busTableManager = window.Amily2Bus.query("TableManager");
|
||
if (busTableManager) {
|
||
log.debug("找到 TableManager,尝试获取表格...");
|
||
if (typeof busTableManager.getMemoryState === 'function') {
|
||
const tables = busTableManager.getMemoryState();
|
||
if (Array.isArray(tables) && tables.length > 0) {
|
||
const names = tables.map(t => t.name).filter(Boolean);
|
||
if (names.length > 0) {
|
||
log.debug("从 Amily2Bus.TableManager 获取表格名称:", names);
|
||
return names;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
} catch (e) {
|
||
log.warn("Amily2Bus 查询失败:", e.message);
|
||
}
|
||
}
|
||
|
||
// 方法3:检查 DOM 中已渲染的表格标签(Amily2 使用标签式 UI)
|
||
const tableTabsContainer = document.querySelector('.amily2-table-tabs');
|
||
if (tableTabsContainer) {
|
||
const tabButtons = tableTabsContainer.querySelectorAll('button.menu_button');
|
||
const names = [];
|
||
tabButtons.forEach(btn => {
|
||
// 排除 "+" 按钮
|
||
if (!btn.querySelector('.fa-plus')) {
|
||
const name = btn.textContent?.trim().replace(/•$/, '').trim();
|
||
if (name) names.push(name);
|
||
}
|
||
});
|
||
if (names.length > 0) {
|
||
log.debug("从 DOM 标签获取表格名称:", names);
|
||
return names;
|
||
}
|
||
}
|
||
|
||
// 方法4:从聊天消息的 extra 数据中获取表格
|
||
const context = typeof SillyTavern !== 'undefined' ? SillyTavern.getContext?.() : null;
|
||
if (context?.chat) {
|
||
// 从最新消息往前找,找到包含表格数据的消息
|
||
for (let i = context.chat.length - 1; i >= 0 && i >= context.chat.length - 10; i--) {
|
||
const msg = context.chat[i];
|
||
const tablesData = msg?.extra?.amily2_tables_data;
|
||
if (tablesData && Array.isArray(tablesData) && tablesData.length > 0) {
|
||
const names = tablesData.map(t => t.name).filter(Boolean);
|
||
if (names.length > 0) {
|
||
log.debug("从聊天消息 extra 获取表格名称:", names);
|
||
return names;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 方法5:遍历所有 extension_settings 找 Amily2 相关
|
||
log.debug("遍历所有扩展设置查找 Amily2...");
|
||
for (const key in getExtensionSettings() || {}) {
|
||
if (key.toLowerCase().includes('amily')) {
|
||
const extSettings = getExtensionSettings()[key];
|
||
log.debug(`检查扩展 ${key}:`, Object.keys(extSettings || {}));
|
||
|
||
// 深度搜索 tables
|
||
const tableNames = findTablesInObject(extSettings, key);
|
||
if (tableNames.length > 0) {
|
||
return tableNames;
|
||
}
|
||
}
|
||
}
|
||
|
||
log.warn("未能获取 Amily2 表格名称,请确保:");
|
||
log.warn("1. Amily2 插件已安装并启用");
|
||
log.warn("2. 已导入表格预设");
|
||
log.warn("3. 当前有打开的聊天会话");
|
||
return [];
|
||
} catch (e) {
|
||
log.error("获取 Amily2 表格名称失败:", e);
|
||
return [];
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 递归查找对象中的 tables 数组
|
||
* @param {object} obj - 要搜索的对象
|
||
* @param {string} path - 当前路径(用于日志)
|
||
* @param {number} depth - 当前深度
|
||
* @returns {string[]} 表格名称数组
|
||
*/
|
||
function findTablesInObject(obj, path = '', depth = 0) {
|
||
if (!obj || typeof obj !== 'object' || depth > 5) return [];
|
||
|
||
// 检查是否有 tables 数组
|
||
if (Array.isArray(obj.tables) && obj.tables.length > 0) {
|
||
const firstItem = obj.tables[0];
|
||
// 确保是表格定义对象(有 name 字段)
|
||
if (firstItem && typeof firstItem === 'object' && 'name' in firstItem) {
|
||
const names = obj.tables.map(t => t?.name).filter(Boolean);
|
||
if (names.length > 0) {
|
||
log.debug(`在 ${path}.tables 找到表格名称:`, names);
|
||
return names;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 递归搜索子对象(跳过某些不相关的大型对象)
|
||
const skipKeys = ['chat', 'messages', 'characters', 'world_info', 'lorebook'];
|
||
for (const key of Object.keys(obj)) {
|
||
if (key === 'tables' || skipKeys.includes(key)) continue;
|
||
const value = obj[key];
|
||
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
||
const result = findTablesInObject(value, `${path}.${key}`, depth + 1);
|
||
if (result.length > 0) return result;
|
||
}
|
||
}
|
||
|
||
return [];
|
||
}
|
||
|
||
/**
|
||
* 刷新表格名称缓存(异步)
|
||
* @returns {Promise<string[]>} 表格名称数组
|
||
*/
|
||
async function refreshTableNames() {
|
||
cachedTableNames = await getAmily2TableNames();
|
||
return cachedTableNames;
|
||
}
|
||
|
||
/**
|
||
* 获取缓存的表格名称
|
||
* @returns {string[]} 表格名称数组
|
||
*/
|
||
function getTableNames() {
|
||
return cachedTableNames;
|
||
}
|
||
|
||
/**
|
||
* 初始化表格填表 UI
|
||
*/
|
||
export function initTableFillerUI() {
|
||
const config = getTableFillerConfig();
|
||
|
||
// 设置启用状态
|
||
const enableCheckbox = document.getElementById("mm-table-filler-enabled");
|
||
if (enableCheckbox) {
|
||
enableCheckbox.checked = config.enabled || false;
|
||
}
|
||
|
||
// 设置调用模式
|
||
const callModeSelect = document.getElementById("mm-table-filler-call-mode");
|
||
if (callModeSelect) {
|
||
callModeSelect.value = config.callMode || CallMode.AUTO;
|
||
}
|
||
|
||
// 设置重试次数
|
||
const retryCountInput = document.getElementById("mm-table-filler-retry-count");
|
||
const retryCountValue = document.getElementById("mm-table-filler-retry-count-value");
|
||
if (retryCountInput) {
|
||
const retryCount = config.retryCount ?? 2;
|
||
retryCountInput.value = retryCount;
|
||
if (retryCountValue) {
|
||
retryCountValue.textContent = retryCount;
|
||
}
|
||
}
|
||
|
||
// 设置重试延迟
|
||
const retryDelayInput = document.getElementById("mm-table-filler-retry-delay");
|
||
const retryDelayValue = document.getElementById("mm-table-filler-retry-delay-value");
|
||
if (retryDelayInput) {
|
||
const retryDelay = config.retryDelay ?? 2000;
|
||
retryDelayInput.value = retryDelay;
|
||
if (retryDelayValue) {
|
||
retryDelayValue.textContent = retryDelay;
|
||
}
|
||
}
|
||
|
||
// 设置提示词模式按钮状态
|
||
updatePromptModeButtons(config.promptMode || "shared");
|
||
|
||
// 设置调试模式开关
|
||
const debugModeCheckbox = document.getElementById("mm-table-filler-debug-mode");
|
||
if (debugModeCheckbox) {
|
||
debugModeCheckbox.checked = config.debugMode || false;
|
||
}
|
||
|
||
// 更新模式状态显示
|
||
updateModeStatusDisplay();
|
||
|
||
// 更新徽章状态
|
||
updateTableFillerBadge(config.enabled || false);
|
||
|
||
// 渲染表格 API 列表
|
||
renderTableApiList();
|
||
|
||
// 更新配置计数
|
||
updateConfigCount();
|
||
|
||
// 更新预设信息显示
|
||
updatePresetInfoDisplay();
|
||
|
||
// 检查 Amily2 填表模式兼容性
|
||
checkFillingModeCompatibility();
|
||
|
||
// 初始化标签名输入框(只显示非默认值)
|
||
const tagNameInput = /** @type {HTMLInputElement|null} */ (document.getElementById("mm-table-filler-tag-name"));
|
||
if (tagNameInput) {
|
||
const currentTagName = getIndependentTagName();
|
||
const defaultTagName = "Instructions for filling out the form";
|
||
tagNameInput.value = currentTagName !== defaultTagName ? currentTagName : "";
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 绑定表格填表事件
|
||
*/
|
||
export function bindTableFillerEvents() {
|
||
// 折叠卡片切换
|
||
document
|
||
.getElementById("mm-table-filler-toggle")
|
||
?.addEventListener("click", () => {
|
||
const card = document.getElementById("mm-table-filler-card");
|
||
if (card) {
|
||
card.classList.toggle("expanded");
|
||
if (card.classList.contains("expanded")) {
|
||
// 展开时刷新状态
|
||
updateModeStatusDisplay();
|
||
renderTableApiList();
|
||
checkFillingModeCompatibility();
|
||
}
|
||
}
|
||
});
|
||
|
||
// 高级设置折叠
|
||
document
|
||
.getElementById("mm-table-filler-advanced-toggle")
|
||
?.addEventListener("click", () => {
|
||
const body = document.getElementById("mm-table-filler-advanced-body");
|
||
const toggle = document.getElementById("mm-table-filler-advanced-toggle");
|
||
if (body && toggle) {
|
||
const isVisible = body.style.display !== "none";
|
||
body.style.display = isVisible ? "none" : "block";
|
||
toggle.classList.toggle("expanded", !isVisible);
|
||
}
|
||
});
|
||
|
||
// 启用开关
|
||
document
|
||
.getElementById("mm-table-filler-enabled")
|
||
?.addEventListener("change", async (e) => {
|
||
const checked = e.target.checked;
|
||
|
||
// 检查填表模式(仅作为警告,不阻止启用)
|
||
if (checked && !isSecondaryApiMode()) {
|
||
const modeName = getAmily2FillingModeName();
|
||
if (typeof toastr !== "undefined") {
|
||
toastr.warning(
|
||
`建议使用「分步填表」模式,当前为「${modeName}」`,
|
||
"Amily表格并发",
|
||
{ timeOut: 5000 },
|
||
);
|
||
}
|
||
}
|
||
|
||
setTableFillerEnabled(checked);
|
||
updateTableFillerBadge(checked);
|
||
|
||
// 初始化或禁用模块
|
||
if (checked) {
|
||
await reinitTableFiller();
|
||
if (typeof toastr !== "undefined") {
|
||
toastr.success("Amily表格并发已启用", "Amily表格并发");
|
||
}
|
||
} else {
|
||
disableTableFiller();
|
||
if (typeof toastr !== "undefined") {
|
||
toastr.info("Amily表格并发已禁用", "Amily表格并发");
|
||
}
|
||
}
|
||
});
|
||
|
||
// 调用模式选择
|
||
document
|
||
.getElementById("mm-table-filler-call-mode")
|
||
?.addEventListener("change", async (e) => {
|
||
const value = e.target.value;
|
||
updateTableFillerConfig({ callMode: value });
|
||
|
||
// 如果已启用,重新初始化
|
||
if (isTableFillerEnabled()) {
|
||
await reinitTableFiller();
|
||
}
|
||
|
||
updateModeStatusDisplay();
|
||
|
||
if (typeof toastr !== "undefined") {
|
||
const modeNames = {
|
||
auto: "自动选择",
|
||
intercept_only: "仅拦截模式",
|
||
bus_only: "仅Bus联动",
|
||
};
|
||
toastr.success(
|
||
`调用模式已切换为「${modeNames[value] || value}」`,
|
||
"Amily表格并发",
|
||
);
|
||
}
|
||
});
|
||
|
||
// 重试次数滑块
|
||
document
|
||
.getElementById("mm-table-filler-retry-count")
|
||
?.addEventListener("input", (e) => {
|
||
const value = parseInt(e.target.value) || 0;
|
||
const valueEl = document.getElementById("mm-table-filler-retry-count-value");
|
||
if (valueEl) {
|
||
valueEl.textContent = value;
|
||
}
|
||
updateTableFillerConfig({ retryCount: value });
|
||
});
|
||
|
||
// 重试延迟滑块
|
||
document
|
||
.getElementById("mm-table-filler-retry-delay")
|
||
?.addEventListener("input", (e) => {
|
||
const value = parseInt(e.target.value) || 2000;
|
||
const valueEl = document.getElementById("mm-table-filler-retry-delay-value");
|
||
if (valueEl) {
|
||
valueEl.textContent = value;
|
||
}
|
||
updateTableFillerConfig({ retryDelay: value });
|
||
});
|
||
|
||
// 提示词模式切换 - 使用 radio 按钮的 change 事件
|
||
document
|
||
.getElementById("mm-prompt-mode-shared")
|
||
?.addEventListener("change", (e) => {
|
||
const radio = /** @type {HTMLInputElement} */ (e.target);
|
||
if (radio.checked) {
|
||
updateTableFillerConfig({ promptMode: "shared" });
|
||
updatePromptModeButtons("shared");
|
||
|
||
if (typeof toastr !== "undefined") {
|
||
toastr.success("已切换为共享模式", "Amily表格并发");
|
||
}
|
||
}
|
||
});
|
||
|
||
// 提示词模式切换 - 独立模式
|
||
document
|
||
.getElementById("mm-prompt-mode-independent")
|
||
?.addEventListener("change", (e) => {
|
||
const radio = /** @type {HTMLInputElement} */ (e.target);
|
||
if (radio.checked) {
|
||
updateTableFillerConfig({ promptMode: "independent" });
|
||
updatePromptModeButtons("independent");
|
||
|
||
if (typeof toastr !== "undefined") {
|
||
toastr.success("已切换为独立模式", "Amily表格并发");
|
||
}
|
||
}
|
||
});
|
||
|
||
// 调试模式开关
|
||
document
|
||
.getElementById("mm-table-filler-debug-mode")
|
||
?.addEventListener("change", (e) => {
|
||
const checkbox = /** @type {HTMLInputElement} */ (e.target);
|
||
updateTableFillerConfig({ debugMode: checkbox.checked });
|
||
|
||
if (typeof toastr !== "undefined") {
|
||
if (checkbox.checked) {
|
||
toastr.info("已启用调试模式,发送前和合并后将显示检查弹窗", "调试模式");
|
||
} else {
|
||
toastr.info("已关闭调试模式", "调试模式");
|
||
}
|
||
}
|
||
});
|
||
|
||
// 标签名输入框(独立模式)
|
||
document
|
||
.getElementById("mm-table-filler-tag-name")
|
||
?.addEventListener("change", (e) => {
|
||
const input = /** @type {HTMLInputElement} */ (e.target);
|
||
const value = input.value.trim();
|
||
// 只有填入内容才更改,留空则恢复默认值
|
||
if (value) {
|
||
setIndependentTagName(value);
|
||
} else {
|
||
setIndependentTagName("Instructions for filling out the form");
|
||
}
|
||
});
|
||
|
||
// 刷新表格列表按钮
|
||
document
|
||
.getElementById("mm-table-filler-add-table-api")
|
||
?.addEventListener("click", () => {
|
||
renderAllTablesWithStatus();
|
||
});
|
||
|
||
// 绑定API配置弹窗事件
|
||
bindApiModalEvents();
|
||
|
||
// 绑定表格选择弹窗事件
|
||
bindTableSelectModalEvents();
|
||
|
||
// 绑定独立模式模板弹窗事件
|
||
bindIndependentTemplateEvents();
|
||
}
|
||
|
||
/**
|
||
* 绑定API配置弹窗事件
|
||
*/
|
||
function bindApiModalEvents() {
|
||
const modal = document.getElementById("mm-table-filler-api-modal");
|
||
if (!modal) return;
|
||
|
||
// 关闭按钮
|
||
modal.querySelector(".mm-modal-close")?.addEventListener("click", hideApiModal);
|
||
document.getElementById("mm-table-filler-api-cancel")?.addEventListener("click", hideApiModal);
|
||
|
||
// 保存按钮
|
||
document.getElementById("mm-table-filler-api-save")?.addEventListener("click", handleApiSave);
|
||
|
||
// 获取模型按钮
|
||
document.getElementById("mm-table-filler-fetch-models")?.addEventListener("click", handleFetchModels);
|
||
|
||
// 测试连接按钮
|
||
document.getElementById("mm-table-filler-test-connection")?.addEventListener("click", handleTestConnection);
|
||
|
||
// Temperature 滑块
|
||
document.getElementById("mm-table-filler-api-temperature")?.addEventListener("input", (e) => {
|
||
const valueEl = document.getElementById("mm-table-filler-api-temperature-value");
|
||
if (valueEl) valueEl.textContent = e.target.value;
|
||
});
|
||
|
||
// API格式切换
|
||
document.querySelectorAll('input[name="mm-table-filler-api-format"]').forEach(radio => {
|
||
radio.addEventListener("change", (e) => {
|
||
const customOptions = document.getElementById("mm-table-filler-custom-options");
|
||
if (customOptions) {
|
||
customOptions.classList.toggle("mm-hidden", e.target.value !== "custom");
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 绑定表格选择弹窗事件
|
||
*/
|
||
function bindTableSelectModalEvents() {
|
||
const modal = document.getElementById("mm-table-filler-select-modal");
|
||
if (!modal) return;
|
||
|
||
modal.querySelector(".mm-modal-close")?.addEventListener("click", hideTableSelectModal);
|
||
document.getElementById("mm-table-filler-select-cancel")?.addEventListener("click", hideTableSelectModal);
|
||
}
|
||
|
||
/**
|
||
* 显示API配置弹窗
|
||
* @param {string|null} tableName - 表格名称,null表示默认配置
|
||
*/
|
||
function showTableFillerApiModal(tableName) {
|
||
currentEditingTable = tableName;
|
||
|
||
const modal = document.getElementById("mm-table-filler-api-modal");
|
||
if (!modal) {
|
||
Logger.error("找不到API配置弹窗元素");
|
||
if (typeof toastr !== "undefined") {
|
||
toastr.error("弹窗加载失败,请刷新页面重试", "Amily表格并发");
|
||
}
|
||
return;
|
||
}
|
||
|
||
const config = getTableFillerConfig();
|
||
let apiConfig;
|
||
|
||
if (tableName) {
|
||
apiConfig = config.tableApiConfigs?.[tableName] || {};
|
||
} else {
|
||
apiConfig = config.defaultApi || {};
|
||
}
|
||
|
||
// 设置标题
|
||
const titleEl = document.getElementById("mm-table-filler-api-title");
|
||
if (titleEl) {
|
||
titleEl.textContent = tableName ? `配置「${tableName}」API` : "配置默认API";
|
||
}
|
||
|
||
// 填充表单
|
||
fillApiForm(apiConfig);
|
||
|
||
// 应用主题
|
||
const settings = getGlobalSettings();
|
||
const theme = settings.theme || "default";
|
||
if (theme !== "default") {
|
||
modal.setAttribute("data-mm-theme", theme);
|
||
} else {
|
||
modal.removeAttribute("data-mm-theme");
|
||
}
|
||
|
||
// 显示弹窗
|
||
modal.classList.add("mm-modal-visible");
|
||
console.log("[Amily表格并发] 弹窗已添加 mm-modal-visible 类");
|
||
}
|
||
|
||
/**
|
||
* 隐藏API配置弹窗
|
||
*/
|
||
function hideApiModal() {
|
||
const modal = document.getElementById("mm-table-filler-api-modal");
|
||
if (modal) {
|
||
modal.classList.remove("mm-modal-visible");
|
||
}
|
||
currentEditingTable = null;
|
||
}
|
||
|
||
/**
|
||
* 填充API表单
|
||
* @param {object} apiConfig - API配置
|
||
*/
|
||
function fillApiForm(apiConfig) {
|
||
// API格式
|
||
const formatRadio = document.querySelector(`input[name="mm-table-filler-api-format"][value="${apiConfig.apiFormat || 'openai'}"]`);
|
||
if (formatRadio) formatRadio.checked = true;
|
||
|
||
// 显示/隐藏自定义选项
|
||
const customOptions = document.getElementById("mm-table-filler-custom-options");
|
||
if (customOptions) {
|
||
customOptions.classList.toggle("mm-hidden", apiConfig.apiFormat !== "custom");
|
||
}
|
||
|
||
// URL
|
||
const urlInput = document.getElementById("mm-table-filler-api-url");
|
||
if (urlInput) urlInput.value = apiConfig.apiUrl || "";
|
||
|
||
// Key
|
||
const keyInput = document.getElementById("mm-table-filler-api-key");
|
||
if (keyInput) keyInput.value = apiConfig.apiKey || "";
|
||
|
||
// Model
|
||
const modelSelect = document.getElementById("mm-table-filler-api-model");
|
||
if (modelSelect) {
|
||
if (apiConfig.model) {
|
||
modelSelect.innerHTML = `<option value="${escapeHtml(apiConfig.model)}" selected>${escapeHtml(apiConfig.model)}</option>`;
|
||
} else {
|
||
modelSelect.innerHTML = '<option value="" disabled selected>--- 请获取模型 ---</option>';
|
||
}
|
||
}
|
||
|
||
// Max Tokens
|
||
const maxTokensInput = document.getElementById("mm-table-filler-api-max-tokens");
|
||
if (maxTokensInput) maxTokensInput.value = apiConfig.maxTokens || 4096;
|
||
|
||
// Temperature
|
||
const temperatureInput = document.getElementById("mm-table-filler-api-temperature");
|
||
const temperatureValue = document.getElementById("mm-table-filler-api-temperature-value");
|
||
if (temperatureInput) temperatureInput.value = apiConfig.temperature || 0.7;
|
||
if (temperatureValue) temperatureValue.textContent = apiConfig.temperature || 0.7;
|
||
|
||
// 自定义模板
|
||
const customTemplate = document.getElementById("mm-table-filler-custom-template");
|
||
if (customTemplate) customTemplate.value = apiConfig.customTemplate || "";
|
||
|
||
// 响应路径
|
||
const responsePath = document.getElementById("mm-table-filler-response-path");
|
||
if (responsePath) responsePath.value = apiConfig.responsePath || "choices.0.message.content";
|
||
|
||
// 清空测试结果
|
||
const testResult = document.getElementById("mm-table-filler-test-result");
|
||
if (testResult) {
|
||
testResult.textContent = "";
|
||
testResult.className = "mm-test-result";
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 处理保存API配置
|
||
*/
|
||
function handleApiSave() {
|
||
const apiConfig = collectApiFormData();
|
||
if (!apiConfig) return;
|
||
|
||
if (!currentEditingTable) {
|
||
if (typeof toastr !== "undefined") {
|
||
toastr.warning("请选择要配置的表格", "Amily表格并发");
|
||
}
|
||
return;
|
||
}
|
||
|
||
setTableApiConfig(currentEditingTable, apiConfig);
|
||
renderTableApiList();
|
||
updateConfigCount();
|
||
if (typeof toastr !== "undefined") {
|
||
toastr.success(`「${currentEditingTable}」API配置已保存`, "Amily表格并发");
|
||
}
|
||
|
||
hideApiModal();
|
||
}
|
||
|
||
/**
|
||
* 收集API表单数据
|
||
* @returns {object|null}
|
||
*/
|
||
function collectApiFormData() {
|
||
const apiUrl = document.getElementById("mm-table-filler-api-url")?.value.trim();
|
||
const model = document.getElementById("mm-table-filler-api-model")?.value;
|
||
|
||
if (!apiUrl) {
|
||
if (typeof toastr !== "undefined") {
|
||
toastr.warning("请填写API URL", "Amily表格并发");
|
||
}
|
||
return null;
|
||
}
|
||
|
||
if (!model) {
|
||
if (typeof toastr !== "undefined") {
|
||
toastr.warning("请选择模型", "Amily表格并发");
|
||
}
|
||
return null;
|
||
}
|
||
|
||
const format = document.querySelector('input[name="mm-table-filler-api-format"]:checked')?.value || "openai";
|
||
|
||
return {
|
||
apiFormat: format,
|
||
apiUrl,
|
||
apiKey: document.getElementById("mm-table-filler-api-key")?.value.trim() || "",
|
||
model,
|
||
maxTokens: parseInt(document.getElementById("mm-table-filler-api-max-tokens")?.value) || 4096,
|
||
temperature: parseFloat(document.getElementById("mm-table-filler-api-temperature")?.value) || 0.7,
|
||
customTemplate: document.getElementById("mm-table-filler-custom-template")?.value.trim() || "",
|
||
responsePath: document.getElementById("mm-table-filler-response-path")?.value.trim() || "choices.0.message.content",
|
||
useDefault: false,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 处理获取模型
|
||
*/
|
||
async function handleFetchModels() {
|
||
const apiUrl = document.getElementById("mm-table-filler-api-url")?.value.trim();
|
||
const apiKey = document.getElementById("mm-table-filler-api-key")?.value.trim();
|
||
const format = document.querySelector('input[name="mm-table-filler-api-format"]:checked')?.value || "openai";
|
||
|
||
if (!apiUrl) {
|
||
if (typeof toastr !== "undefined") {
|
||
toastr.warning("请先填写API URL", "Amily表格并发");
|
||
}
|
||
return;
|
||
}
|
||
|
||
const fetchBtn = document.getElementById("mm-table-filler-fetch-models");
|
||
const modelSelect = document.getElementById("mm-table-filler-api-model");
|
||
|
||
if (fetchBtn) {
|
||
fetchBtn.disabled = true;
|
||
fetchBtn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i>';
|
||
}
|
||
|
||
try {
|
||
const models = await fetchModelsFromApi(apiUrl, apiKey, format);
|
||
|
||
if (modelSelect) {
|
||
modelSelect.innerHTML = "";
|
||
if (models.length === 0) {
|
||
modelSelect.innerHTML = '<option value="" disabled selected>--- 未获取到模型 ---</option>';
|
||
} else {
|
||
models.forEach(model => {
|
||
const option = document.createElement("option");
|
||
option.value = model;
|
||
option.textContent = model;
|
||
modelSelect.appendChild(option);
|
||
});
|
||
}
|
||
}
|
||
|
||
if (typeof toastr !== "undefined") {
|
||
toastr.success(`获取到 ${models.length} 个模型`, "Amily表格并发");
|
||
}
|
||
} catch (error) {
|
||
log.error("获取模型失败:", error);
|
||
if (modelSelect) {
|
||
modelSelect.innerHTML = '<option value="" disabled selected>--- 获取失败 ---</option>';
|
||
}
|
||
if (typeof toastr !== "undefined") {
|
||
toastr.error(`获取模型失败: ${error.message}`, "Amily表格并发");
|
||
}
|
||
} finally {
|
||
if (fetchBtn) {
|
||
fetchBtn.disabled = false;
|
||
fetchBtn.innerHTML = '<i class="fa-solid fa-download"></i> 获取';
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 从API获取模型列表
|
||
*/
|
||
async function fetchModelsFromApi(apiUrl, apiKey, format) {
|
||
let modelsUrl = apiUrl;
|
||
|
||
if (format === "openai") {
|
||
if (apiUrl.endsWith("/v1") || apiUrl.endsWith("/v1/")) {
|
||
modelsUrl = apiUrl.replace(/\/v1\/?$/, "/v1/models");
|
||
} else if (!apiUrl.includes("/models")) {
|
||
modelsUrl = apiUrl.replace(/\/?$/, "/models");
|
||
}
|
||
} else {
|
||
throw new Error("此API格式不支持获取模型列表");
|
||
}
|
||
|
||
const headers = { "Content-Type": "application/json" };
|
||
if (apiKey) {
|
||
headers["Authorization"] = `Bearer ${apiKey}`;
|
||
}
|
||
|
||
const response = await fetch(modelsUrl, { headers });
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP ${response.status}`);
|
||
}
|
||
|
||
const data = await response.json();
|
||
const models = data.data || data.models || [];
|
||
return models.map(m => m.id || m.name || m).filter(Boolean).sort();
|
||
}
|
||
|
||
/**
|
||
* 处理测试连接
|
||
*/
|
||
async function handleTestConnection() {
|
||
const testResult = document.getElementById("mm-table-filler-test-result");
|
||
if (testResult) {
|
||
testResult.textContent = "测试中...";
|
||
testResult.className = "mm-test-result";
|
||
}
|
||
|
||
const config = collectApiFormData();
|
||
if (!config) {
|
||
if (testResult) {
|
||
testResult.textContent = "请填写必要字段";
|
||
testResult.className = "mm-test-result mm-test-error";
|
||
}
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const result = await APIAdapter.testConnection(config);
|
||
if (result.success) {
|
||
if (testResult) {
|
||
testResult.textContent = `连接成功 (${result.latency}ms)`;
|
||
testResult.className = "mm-test-result mm-test-success";
|
||
}
|
||
} else {
|
||
if (testResult) {
|
||
testResult.textContent = `失败: ${result.message}`;
|
||
testResult.className = "mm-test-result mm-test-error";
|
||
}
|
||
}
|
||
} catch (error) {
|
||
if (testResult) {
|
||
testResult.textContent = `失败: ${error.message}`;
|
||
testResult.className = "mm-test-result mm-test-error";
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 显示表格选择弹窗(已弃用,改为直接渲染表格列表)
|
||
* @deprecated 使用 renderAllTablesWithStatus 代替
|
||
*/
|
||
async function showTableSelectModal() {
|
||
// 此函数已弃用,改为直接在界面中显示所有表格
|
||
await renderAllTablesWithStatus();
|
||
}
|
||
|
||
/**
|
||
* 渲染所有检测到的表格及其配置状态
|
||
* 直接显示在界面中,无需弹窗
|
||
*/
|
||
async function renderAllTablesWithStatus() {
|
||
const listEl = document.getElementById("mm-table-filler-api-list");
|
||
const emptyEl = document.getElementById("mm-table-filler-api-empty");
|
||
const countEl = document.getElementById("mm-table-filler-config-count");
|
||
|
||
if (!listEl) return;
|
||
|
||
// 异步刷新表格名称
|
||
const tableNames = await refreshTableNames();
|
||
const config = getTableFillerConfig();
|
||
const tableApis = config.tableApiConfigs || {};
|
||
|
||
// 更新计数
|
||
const configuredCount = tableNames.filter(name =>
|
||
tableApis[name] && !tableApis[name].useDefault && tableApis[name].model
|
||
).length;
|
||
|
||
if (countEl) {
|
||
if (tableNames.length > 0) {
|
||
countEl.textContent = `已配置 ${configuredCount}/${tableNames.length}`;
|
||
} else {
|
||
countEl.textContent = `未检测到表格`;
|
||
}
|
||
countEl.classList.toggle("configured", configuredCount > 0);
|
||
}
|
||
|
||
listEl.innerHTML = "";
|
||
|
||
if (tableNames.length === 0) {
|
||
if (emptyEl) {
|
||
emptyEl.style.display = "flex";
|
||
emptyEl.innerHTML = `
|
||
<i class="fa-solid fa-triangle-exclamation"></i>
|
||
<span>未检测到 Amily2 表格<br><small>请确保已加载表格预设并开启聊天</small></span>
|
||
`;
|
||
}
|
||
return;
|
||
}
|
||
|
||
if (emptyEl) emptyEl.style.display = "none";
|
||
|
||
// 渲染所有检测到的表格
|
||
tableNames.forEach((tableName) => {
|
||
const tableConfig = tableApis[tableName];
|
||
const isConfigured = tableConfig && !tableConfig.useDefault && tableConfig.model;
|
||
|
||
const item = document.createElement("div");
|
||
item.className = "mm-multi-ai-provider-item";
|
||
item.dataset.tableName = tableName;
|
||
|
||
if (isConfigured) {
|
||
// 已配置的表格
|
||
item.innerHTML = `
|
||
<div class="mm-multi-ai-provider-info">
|
||
<span class="mm-multi-ai-provider-name">
|
||
<i class="fa-solid fa-check-circle mm-configured-icon"></i>
|
||
${escapeHtml(tableName)}
|
||
</span>
|
||
<span class="mm-multi-ai-provider-details">
|
||
${escapeHtml(tableConfig.model || "未配置")}
|
||
</span>
|
||
</div>
|
||
<div class="mm-multi-ai-provider-actions">
|
||
<button type="button" class="mm-btn mm-btn-icon mm-table-api-edit" title="编辑">
|
||
<i class="fa-solid fa-pen"></i>
|
||
</button>
|
||
<button type="button" class="mm-btn mm-btn-icon mm-btn-danger mm-table-api-delete" title="删除配置">
|
||
<i class="fa-solid fa-trash"></i>
|
||
</button>
|
||
</div>
|
||
`;
|
||
|
||
// 绑定编辑按钮
|
||
item.querySelector(".mm-table-api-edit")?.addEventListener("click", () => {
|
||
showTableFillerApiModal(tableName);
|
||
});
|
||
|
||
// 绑定删除按钮
|
||
item.querySelector(".mm-table-api-delete")?.addEventListener("click", () => {
|
||
if (confirm(`确定删除「${tableName}」的API配置吗?`)) {
|
||
deleteTableApiConfig(tableName);
|
||
renderAllTablesWithStatus();
|
||
}
|
||
});
|
||
|
||
// 为已配置表格也绑定行点击事件(点击非按钮区域打开编辑弹窗)
|
||
item.addEventListener("click", (e) => {
|
||
const target = e.target;
|
||
if (target instanceof Element && !target.closest(".mm-btn")) {
|
||
showTableFillerApiModal(tableName);
|
||
}
|
||
});
|
||
} else {
|
||
// 未配置的表格
|
||
item.classList.add("mm-table-unconfigured");
|
||
item.innerHTML = `
|
||
<div class="mm-multi-ai-provider-info">
|
||
<span class="mm-multi-ai-provider-name">
|
||
<i class="fa-solid fa-circle mm-unconfigured-icon"></i>
|
||
${escapeHtml(tableName)}
|
||
</span>
|
||
<span class="mm-multi-ai-provider-details mm-text-muted">
|
||
点击配置API
|
||
</span>
|
||
</div>
|
||
<div class="mm-multi-ai-provider-actions">
|
||
<button type="button" class="mm-btn mm-btn-icon mm-btn-primary mm-table-api-add" title="配置API">
|
||
<i class="fa-solid fa-plus"></i>
|
||
</button>
|
||
</div>
|
||
`;
|
||
|
||
// 点击整行或加号按钮都可配置
|
||
item.addEventListener("click", (e) => {
|
||
const target = e.target;
|
||
if (target instanceof Element && !target.closest(".mm-btn")) {
|
||
showTableFillerApiModal(tableName);
|
||
}
|
||
});
|
||
item.querySelector(".mm-table-api-add")?.addEventListener("click", () => {
|
||
showTableFillerApiModal(tableName);
|
||
});
|
||
}
|
||
|
||
listEl.appendChild(item);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 隐藏表格选择弹窗
|
||
*/
|
||
function hideTableSelectModal() {
|
||
const modal = document.getElementById("mm-table-filler-select-modal");
|
||
if (modal) {
|
||
modal.classList.remove("mm-modal-visible");
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 更新提示词模式按钮状态
|
||
* @param {string} mode - 'shared' 或 'independent'
|
||
*/
|
||
function updatePromptModeButtons(mode) {
|
||
const sharedRadio = /** @type {HTMLInputElement} */ (document.getElementById("mm-prompt-mode-shared"));
|
||
const independentRadio = /** @type {HTMLInputElement} */ (document.getElementById("mm-prompt-mode-independent"));
|
||
|
||
if (sharedRadio) {
|
||
sharedRadio.checked = mode === "shared";
|
||
}
|
||
if (independentRadio) {
|
||
independentRadio.checked = mode === "independent";
|
||
}
|
||
|
||
// 显示/隐藏独立模式区域
|
||
const independentSection = document.getElementById(
|
||
"mm-table-filler-independent-section",
|
||
);
|
||
if (independentSection) {
|
||
independentSection.style.display =
|
||
mode === "independent" ? "block" : "none";
|
||
}
|
||
|
||
// 更新高级设置 header 中的提示词模式徽章
|
||
const badge = document.getElementById("mm-prompt-mode-badge");
|
||
if (badge) {
|
||
badge.textContent = mode === "shared" ? "共享模式" : "独立模式";
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 更新模式状态显示
|
||
*/
|
||
function updateModeStatusDisplay() {
|
||
const status = getModeStatus();
|
||
|
||
const busStatusEl = document.getElementById("mm-table-filler-bus-status");
|
||
const interceptStatusEl = document.getElementById("mm-table-filler-intercept-status");
|
||
|
||
if (busStatusEl) {
|
||
if (status.busAvailable) {
|
||
busStatusEl.textContent = "Bus: 已连接";
|
||
busStatusEl.className = "mm-status-badge mm-status-success";
|
||
} else if (status.busRegistered) {
|
||
busStatusEl.textContent = "Bus: 已注册";
|
||
busStatusEl.className = "mm-status-badge mm-status-warning";
|
||
} else {
|
||
busStatusEl.textContent = "Bus: 不可用";
|
||
busStatusEl.className = "mm-status-badge mm-status-inactive";
|
||
}
|
||
}
|
||
|
||
if (interceptStatusEl) {
|
||
// 检查全局钩子是否存在
|
||
const hookInstalled = !!window._tableFillerInterceptor;
|
||
|
||
if (status.interceptorInstalled || hookInstalled) {
|
||
interceptStatusEl.textContent = "拦截: 已就绪";
|
||
interceptStatusEl.className = "mm-status-badge mm-status-success";
|
||
} else if (status.interceptAvailable) {
|
||
interceptStatusEl.textContent = "拦截: 可用";
|
||
interceptStatusEl.className = "mm-status-badge mm-status-warning";
|
||
} else {
|
||
interceptStatusEl.textContent = "拦截: 未安装";
|
||
interceptStatusEl.className = "mm-status-badge mm-status-inactive";
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 更新表格填表徽章状态
|
||
* @param {boolean} enabled - 是否启用
|
||
*/
|
||
export function updateTableFillerBadge(enabled) {
|
||
const badge = document.getElementById("mm-table-filler-badge");
|
||
if (badge) {
|
||
if (enabled) {
|
||
badge.textContent = "开启";
|
||
badge.classList.add("active");
|
||
} else {
|
||
badge.textContent = "关闭";
|
||
badge.classList.remove("active");
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 更新配置计数显示
|
||
* @deprecated 由 renderAllTablesWithStatus 统一处理
|
||
*/
|
||
function updateConfigCount() {
|
||
// 计数更新已由 renderAllTablesWithStatus 处理
|
||
// 此函数保留以兼容旧调用
|
||
}
|
||
|
||
/**
|
||
* 更新预设信息显示
|
||
*/
|
||
function updatePresetInfoDisplay() {
|
||
const config = getTableFillerConfig();
|
||
const preset = config.importedPreset;
|
||
const versionEl = document.getElementById("mm-table-filler-preset-version");
|
||
const clearBtn = document.getElementById("mm-table-filler-clear-preset");
|
||
|
||
if (versionEl) {
|
||
if (preset?.version) {
|
||
versionEl.textContent = preset.version;
|
||
} else {
|
||
versionEl.textContent = "未加载";
|
||
}
|
||
}
|
||
|
||
if (clearBtn) {
|
||
clearBtn.style.display = preset ? "inline-flex" : "none";
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 检查 Amily2 填表模式兼容性
|
||
*/
|
||
function checkFillingModeCompatibility() {
|
||
const warningEl = document.getElementById("mm-table-filler-mode-warning");
|
||
const currentModeEl = document.getElementById("mm-table-filler-current-mode");
|
||
|
||
if (!warningEl) return;
|
||
|
||
if (isSecondaryApiMode()) {
|
||
warningEl.style.display = "none";
|
||
} else {
|
||
warningEl.style.display = "flex";
|
||
if (currentModeEl) {
|
||
currentModeEl.textContent = getAmily2FillingModeName();
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 渲染表格 API 配置列表
|
||
* @deprecated 使用 renderAllTablesWithStatus 代替
|
||
*/
|
||
function renderTableApiList() {
|
||
// 调用新的异步渲染函数
|
||
renderAllTablesWithStatus();
|
||
}
|
||
|
||
/**
|
||
* 转义 HTML
|
||
* @param {string} text - 文本
|
||
* @returns {string} 转义后的文本
|
||
*/
|
||
function escapeHtml(text) {
|
||
const div = document.createElement("div");
|
||
div.textContent = text;
|
||
return div.innerHTML;
|
||
}
|
||
|
||
export default {
|
||
initTableFillerUI,
|
||
bindTableFillerEvents,
|
||
updateTableFillerBadge,
|
||
};
|