mirror of
https://github.com/Wx-2025/ST-Amily2-Chat-Optimisation.git
synced 2026-06-06 16:15:50 +00:00
Add files via upload
This commit is contained in:
431
core/auto-char-card/ui-bindings.js
Normal file
431
core/auto-char-card/ui-bindings.js
Normal file
@@ -0,0 +1,431 @@
|
||||
import { extensionName } from "../../utils/settings.js";
|
||||
import { AgentManager } from "./agent-manager.js";
|
||||
import { characters, this_chid, saveSettingsDebounced } from "/script.js";
|
||||
import { world_names } from "/scripts/world-info.js";
|
||||
import { getApiConfig, setApiConfig, testConnection, fetchModels } from "./api.js";
|
||||
import { tools } from "./tools.js";
|
||||
|
||||
const extensionFolderPath = `scripts/extensions/third-party/${extensionName}`;
|
||||
|
||||
let isInitialized = false;
|
||||
let agentManager = null;
|
||||
let previousCharData = {};
|
||||
let previousWorldData = {};
|
||||
|
||||
export async function openAutoCharCardWindow() {
|
||||
toastr.info("该功能正在开发,尚未完成,请耐心等待。");
|
||||
return;
|
||||
|
||||
if ($('#acc-window').length > 0) {
|
||||
$('#acc-window').show();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$('#acc-style').length) {
|
||||
$('<link>')
|
||||
.attr('id', 'acc-style')
|
||||
.attr('rel', 'stylesheet')
|
||||
.attr('type', 'text/css')
|
||||
.attr('href', `${extensionFolderPath}/assets/auto-char-card/style.css`)
|
||||
.appendTo('head');
|
||||
}
|
||||
|
||||
try {
|
||||
const htmlContent = await $.get(`${extensionFolderPath}/assets/auto-char-card/index.html`);
|
||||
$('body').append(htmlContent);
|
||||
|
||||
bindEvents();
|
||||
|
||||
agentManager = new AgentManager();
|
||||
|
||||
try {
|
||||
populateDropdowns();
|
||||
loadApiSettings();
|
||||
} catch (dataError) {
|
||||
console.error('[Amily2 AutoCharCard] Failed to load data:', dataError);
|
||||
toastr.warning('数据加载部分失败,请检查控制台。');
|
||||
}
|
||||
|
||||
isInitialized = true;
|
||||
console.log('[Amily2 AutoCharCard] Window initialized.');
|
||||
} catch (error) {
|
||||
console.error('[Amily2 AutoCharCard] Failed to initialize window:', error);
|
||||
toastr.error(`无法加载自动构建器界面: ${error.message}`);
|
||||
$('#acc-window').remove();
|
||||
}
|
||||
}
|
||||
|
||||
function populateDropdowns() {
|
||||
const charSelect = $('#acc-target-char');
|
||||
charSelect.empty().append('<option value="">-- 请选择 --</option>');
|
||||
charSelect.append('<option value="new">新建角色卡</option>');
|
||||
|
||||
characters.forEach((char, index) => {
|
||||
if (char) {
|
||||
const option = $('<option>').val(index).text(char.name);
|
||||
if (index === this_chid) option.prop('selected', true);
|
||||
charSelect.append(option);
|
||||
}
|
||||
});
|
||||
|
||||
const worldSelect = $('#acc-target-world');
|
||||
worldSelect.empty().append('<option value="">-- 请选择 --</option>');
|
||||
worldSelect.append('<option value="new">新建世界书</option>');
|
||||
|
||||
world_names.forEach(name => {
|
||||
worldSelect.append($('<option>').val(name).text(name));
|
||||
});
|
||||
}
|
||||
|
||||
function loadApiSettings() {
|
||||
const executorConfig = getApiConfig('executor');
|
||||
$('#acc-executor-url').val(executorConfig.apiUrl);
|
||||
$('#acc-executor-key').val(executorConfig.apiKey);
|
||||
|
||||
const executorModelSelect = $('#acc-executor-model');
|
||||
if (executorConfig.model) {
|
||||
if (executorModelSelect.find(`option[value="${executorConfig.model}"]`).length === 0) {
|
||||
executorModelSelect.append(new Option(executorConfig.model, executorConfig.model));
|
||||
}
|
||||
executorModelSelect.val(executorConfig.model);
|
||||
}
|
||||
|
||||
const reviewerConfig = getApiConfig('reviewer');
|
||||
$('#acc-reviewer-url').val(reviewerConfig.apiUrl);
|
||||
$('#acc-reviewer-key').val(reviewerConfig.apiKey);
|
||||
|
||||
const reviewerModelSelect = $('#acc-reviewer-model');
|
||||
if (reviewerConfig.model) {
|
||||
if (reviewerModelSelect.find(`option[value="${reviewerConfig.model}"]`).length === 0) {
|
||||
reviewerModelSelect.append(new Option(reviewerConfig.model, reviewerConfig.model));
|
||||
}
|
||||
reviewerModelSelect.val(reviewerConfig.model);
|
||||
}
|
||||
}
|
||||
|
||||
function bindEvents() {
|
||||
const windowEl = $('#acc-window');
|
||||
const minIcon = $('#acc-minimized-icon');
|
||||
|
||||
$('#acc-close-btn').on('click', () => {
|
||||
if (confirm('确定要关闭自动构建器吗?当前任务可能会丢失。')) {
|
||||
windowEl.remove();
|
||||
minIcon.hide();
|
||||
isInitialized = false;
|
||||
agentManager = null;
|
||||
}
|
||||
});
|
||||
|
||||
$('#acc-minimize-btn').on('click', () => {
|
||||
windowEl.hide();
|
||||
minIcon.show();
|
||||
});
|
||||
|
||||
minIcon.on('click', () => {
|
||||
minIcon.hide();
|
||||
windowEl.show();
|
||||
minIcon.find('.acc-notification-dot').hide();
|
||||
});
|
||||
|
||||
$('#acc-send-btn').on('click', handleSendMessage);
|
||||
$('#acc-user-input').on('keypress', (e) => {
|
||||
if (e.which === 13 && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSendMessage();
|
||||
}
|
||||
});
|
||||
|
||||
$('.acc-preview-tabs .acc-tab-btn').on('click', function() {
|
||||
$('.acc-preview-tabs .acc-tab-btn').removeClass('active');
|
||||
$(this).addClass('active');
|
||||
const tab = $(this).data('tab');
|
||||
console.log('Switch preview tab to:', tab);
|
||||
});
|
||||
|
||||
$('#acc-api-settings-toggle').on('click', function() {
|
||||
const content = $('#acc-api-settings-content');
|
||||
const icon = $(this).find('.fa-chevron-down, .fa-chevron-up');
|
||||
if (content.is(':visible')) {
|
||||
content.slideUp();
|
||||
icon.removeClass('fa-chevron-up').addClass('fa-chevron-down');
|
||||
} else {
|
||||
content.slideDown();
|
||||
icon.removeClass('fa-chevron-down').addClass('fa-chevron-up');
|
||||
}
|
||||
});
|
||||
|
||||
$('#acc-api-settings-content .acc-tab-btn').on('click', function() {
|
||||
const target = $(this).data('target');
|
||||
$('#acc-api-settings-content .acc-tab-btn').removeClass('active');
|
||||
$(this).addClass('active');
|
||||
|
||||
$('.acc-api-group').hide();
|
||||
$(`#acc-api-${target}`).show();
|
||||
});
|
||||
|
||||
$('#acc-save-api').on('click', () => {
|
||||
const executorConfig = {
|
||||
apiUrl: $('#acc-executor-url').val().trim(),
|
||||
apiKey: $('#acc-executor-key').val().trim(),
|
||||
model: $('#acc-executor-model').val() || ''
|
||||
};
|
||||
const reviewerConfig = {
|
||||
apiUrl: $('#acc-reviewer-url').val().trim(),
|
||||
apiKey: $('#acc-reviewer-key').val().trim(),
|
||||
model: $('#acc-reviewer-model').val() || ''
|
||||
};
|
||||
|
||||
setApiConfig('executor', executorConfig);
|
||||
setApiConfig('reviewer', reviewerConfig);
|
||||
saveSettingsDebounced();
|
||||
toastr.success('API 配置已保存');
|
||||
});
|
||||
|
||||
const handleRefreshModels = async (role) => {
|
||||
const urlInput = $(`#acc-${role}-url`);
|
||||
const keyInput = $(`#acc-${role}-key`);
|
||||
const select = $(`#acc-${role}-model`);
|
||||
const btn = $(`#acc-${role}-refresh-models`);
|
||||
|
||||
const apiUrl = urlInput.val().trim();
|
||||
const apiKey = keyInput.val().trim();
|
||||
|
||||
if (!apiUrl) {
|
||||
toastr.warning('请先输入 API URL');
|
||||
return;
|
||||
}
|
||||
|
||||
const originalIcon = btn.html();
|
||||
btn.prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i>');
|
||||
select.empty().append('<option value="">加载中...</option>');
|
||||
|
||||
try {
|
||||
const models = await fetchModels(apiUrl, apiKey);
|
||||
select.empty().append('<option value="">-- 请选择模型 --</option>');
|
||||
|
||||
if (models.length === 0) {
|
||||
select.append('<option value="" disabled>未找到模型</option>');
|
||||
} else {
|
||||
models.forEach(model => {
|
||||
select.append(new Option(model, model));
|
||||
});
|
||||
toastr.success(`成功获取 ${models.length} 个模型`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[AutoCharCard] Failed to fetch models for ${role}:`, error);
|
||||
toastr.error(`获取模型失败: ${error.message}`);
|
||||
select.empty().append('<option value="">获取失败</option>');
|
||||
} finally {
|
||||
btn.prop('disabled', false).html(originalIcon);
|
||||
}
|
||||
};
|
||||
|
||||
$('#acc-executor-refresh-models').on('click', () => handleRefreshModels('executor'));
|
||||
$('#acc-reviewer-refresh-models').on('click', () => handleRefreshModels('reviewer'));
|
||||
|
||||
$('#acc-executor-test').on('click', async function() {
|
||||
const btn = $(this);
|
||||
btn.prop('disabled', true).text('测试中...');
|
||||
const success = await testConnection('executor');
|
||||
btn.prop('disabled', false).text('测试连接');
|
||||
if (success) toastr.success('模型 A 连接成功');
|
||||
else toastr.error('模型 A 连接失败');
|
||||
});
|
||||
|
||||
$('#acc-reviewer-test').on('click', async function() {
|
||||
const btn = $(this);
|
||||
btn.prop('disabled', true).text('测试中...');
|
||||
const success = await testConnection('reviewer');
|
||||
btn.prop('disabled', false).text('测试连接');
|
||||
if (success) toastr.success('模型 B 连接成功');
|
||||
else toastr.error('模型 B 连接失败');
|
||||
});
|
||||
}
|
||||
|
||||
async function handleSendMessage() {
|
||||
const input = $('#acc-user-input');
|
||||
const message = input.val().trim();
|
||||
if (!message) return;
|
||||
|
||||
if (!agentManager) {
|
||||
toastr.error('Agent 未初始化');
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedCharId = $('#acc-target-char').val();
|
||||
const selectedWorld = $('#acc-target-world').val();
|
||||
|
||||
if (!selectedCharId && selectedCharId !== '0') {
|
||||
toastr.warning('请先选择一个目标角色(或选择新建)');
|
||||
return;
|
||||
}
|
||||
|
||||
addMessage('user', message);
|
||||
input.val('');
|
||||
|
||||
$('#acc-send-btn').prop('disabled', true);
|
||||
$('#acc-status-indicator').removeClass('status-idle').addClass('status-working').text('工作中...');
|
||||
|
||||
try {
|
||||
agentManager.setContext(selectedCharId, selectedWorld);
|
||||
|
||||
await agentManager.handleUserMessage(
|
||||
message,
|
||||
(content, role) => {
|
||||
addMessage(role, content);
|
||||
},
|
||||
(toolName, args) => {
|
||||
updatePreview(toolName, args);
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Agent Error:', error);
|
||||
addMessage('system', `发生错误: ${error.message}`);
|
||||
} finally {
|
||||
$('#acc-send-btn').prop('disabled', false);
|
||||
$('#acc-status-indicator').removeClass('status-working').addClass('status-idle').text('空闲');
|
||||
}
|
||||
}
|
||||
|
||||
function addMessage(role, content) {
|
||||
const stream = $('#acc-chat-stream');
|
||||
|
||||
let displayContent = content;
|
||||
if (role === 'executor') {
|
||||
const tools = [
|
||||
'read_world_info', 'write_world_info_entry', 'create_world_book',
|
||||
'read_character_card', 'update_character_card', 'edit_character_text',
|
||||
'manage_first_message', 'use_tool'
|
||||
];
|
||||
const regex = new RegExp(`<(${tools.join('|')})>[\\s\\S]*?<\\/\\1>`, 'g');
|
||||
displayContent = content.replace(regex, '').trim();
|
||||
|
||||
if (!displayContent) {
|
||||
displayContent = "<i>(正在执行操作...)</i>";
|
||||
}
|
||||
}
|
||||
|
||||
const escapedContent = displayContent
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
|
||||
const formattedContent = escapedContent.replace(/\n/g, '<br>');
|
||||
|
||||
const msgDiv = $('<div>').addClass(`acc-message ${role}`);
|
||||
|
||||
const avatarDiv = $('<div>').addClass('acc-avatar');
|
||||
if (role === 'user') {
|
||||
avatarDiv.html('<i class="fas fa-user"></i>');
|
||||
} else if (role === 'assistant') {
|
||||
avatarDiv.html('<i class="fas fa-brain" style="color: #ff9800;"></i>');
|
||||
} else if (role === 'executor') {
|
||||
avatarDiv.html('<i class="fas fa-robot" style="color: #4caf50;"></i>');
|
||||
} else if (role === 'system') {
|
||||
avatarDiv.html('<i class="fas fa-info-circle"></i>');
|
||||
}
|
||||
|
||||
const contentDiv = $('<div>').addClass('acc-message-content');
|
||||
|
||||
msgDiv.append(avatarDiv);
|
||||
msgDiv.append(contentDiv);
|
||||
stream.append(msgDiv);
|
||||
|
||||
if (role === 'assistant') {
|
||||
let i = 0;
|
||||
const speed = 2;
|
||||
const chunkSize = 5;
|
||||
|
||||
function typeWriter() {
|
||||
if (i < formattedContent.length) {
|
||||
let chunk = "";
|
||||
let count = 0;
|
||||
|
||||
while (count < chunkSize && i < formattedContent.length) {
|
||||
if (formattedContent.charAt(i) === '<') {
|
||||
const tagEnd = formattedContent.indexOf('>', i);
|
||||
if (tagEnd !== -1) {
|
||||
chunk += formattedContent.substring(i, tagEnd + 1);
|
||||
i = tagEnd + 1;
|
||||
} else {
|
||||
chunk += formattedContent.charAt(i);
|
||||
i++;
|
||||
}
|
||||
} else {
|
||||
chunk += formattedContent.charAt(i);
|
||||
i++;
|
||||
}
|
||||
count++;
|
||||
}
|
||||
|
||||
contentDiv.html(contentDiv.html() + chunk);
|
||||
stream.scrollTop(stream[0].scrollHeight);
|
||||
setTimeout(typeWriter, speed);
|
||||
}
|
||||
}
|
||||
typeWriter();
|
||||
} else {
|
||||
contentDiv.html(formattedContent);
|
||||
stream.scrollTop(stream[0].scrollHeight);
|
||||
}
|
||||
}
|
||||
|
||||
async function updatePreview(toolName, args) {
|
||||
const container = $('#acc-preview-container');
|
||||
|
||||
if (toolName === 'update_character_card' || toolName === 'edit_character_text') {
|
||||
const chid = args.chid !== undefined ? args.chid : $('#acc-target-char').val();
|
||||
if (chid !== undefined) {
|
||||
const charData = await tools.read_character_card({ chid });
|
||||
const char = JSON.parse(charData);
|
||||
|
||||
let html = `<h3>角色预览: ${char.name}</h3>`;
|
||||
|
||||
const fields = ['description', 'personality', 'first_mes', 'scenario'];
|
||||
fields.forEach(field => {
|
||||
const oldVal = previousCharData[field] || '';
|
||||
const newVal = char[field] || '';
|
||||
let contentHtml = newVal;
|
||||
|
||||
if (oldVal !== newVal) {
|
||||
contentHtml = `<div class="diff-added">${newVal}</div>`;
|
||||
if (oldVal) {
|
||||
contentHtml += `<div class="diff-removed" style="display:none;">${oldVal}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
html += `<div class="acc-preview-item"><strong>${field}:</strong><pre>${contentHtml}</pre></div>`;
|
||||
});
|
||||
|
||||
container.html(html);
|
||||
previousCharData = char;
|
||||
}
|
||||
} else if (toolName === 'write_world_info_entry') {
|
||||
const bookName = args.book_name || $('#acc-target-world').val();
|
||||
if (bookName) {
|
||||
const entriesData = await tools.read_world_info({ book_name: bookName });
|
||||
const entries = JSON.parse(entriesData);
|
||||
|
||||
let html = `<h3>世界书预览: ${bookName}</h3>`;
|
||||
entries.forEach(entry => {
|
||||
|
||||
let isModified = false;
|
||||
if (args.entries) {
|
||||
const modifiedEntries = Array.isArray(args.entries) ? args.entries : [args.entries];
|
||||
isModified = modifiedEntries.some(e => e.key === entry.key || (Array.isArray(entry.keys) && entry.keys.includes(e.key)));
|
||||
}
|
||||
|
||||
const contentClass = isModified ? 'diff-added' : '';
|
||||
|
||||
html += `<div class="acc-preview-item ${contentClass}">
|
||||
<strong>Key:</strong> ${Array.isArray(entry.keys) ? entry.keys.join(', ') : entry.key}<br>
|
||||
<strong>Content:</strong><pre>${entry.content}</pre>
|
||||
</div>`;
|
||||
});
|
||||
|
||||
container.html(html);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user