Add files via upload

This commit is contained in:
2026-01-02 09:10:13 +08:00
committed by GitHub
parent 2f5aea445f
commit c65a607dc4
6 changed files with 894 additions and 156 deletions

View File

@@ -36,18 +36,25 @@ export class AgentManager {
this.approvalRequired = required;
}
updatePendingToolArgs(newArgs) {
if (this.pendingToolCall) {
this.pendingToolCall.arguments = { ...this.pendingToolCall.arguments, ...newArgs };
console.log("[AgentManager] Pending tool args updated:", this.pendingToolCall.arguments);
}
}
stop() {
this.status = 'idle';
}
async resumeWithApproval(approved, feedback, onStreamUpdate, onPreviewUpdate, onApprovalRequest, onContextUpdate) {
async resumeWithApproval(approved, feedback, onStreamUpdate, onPreviewUpdate, onApprovalRequest, onContextUpdate, onPromptGenerated) {
if (this.status !== 'paused' || !this.pendingToolCall) return;
if (approved) {
this.status = 'running';
await this.executePendingTool(onStreamUpdate, onPreviewUpdate, onContextUpdate);
this.pendingToolCall = null;
await this.runTaskLoop(onStreamUpdate, onPreviewUpdate, onApprovalRequest, onContextUpdate);
await this.runTaskLoop(onStreamUpdate, onPreviewUpdate, onApprovalRequest, onContextUpdate, onPromptGenerated);
} else {
this.status = 'running';
this.pendingToolCall = null;
@@ -55,7 +62,7 @@ export class AgentManager {
role: 'user',
content: `[工具执行被拒绝] 用户反馈: ${feedback || "未提供原因。"}`
});
await this.runTaskLoop(onStreamUpdate, onPreviewUpdate, onApprovalRequest, onContextUpdate);
await this.runTaskLoop(onStreamUpdate, onPreviewUpdate, onApprovalRequest, onContextUpdate, onPromptGenerated);
}
}
@@ -120,12 +127,18 @@ ${this.taskState.getPromptContext()}
if (this.currentChid !== undefined && this.currentChid !== 'new') {
try {
const charData = await tools.read_character_card({ chid: this.currentChid });
const char = JSON.parse(charData);
const response = JSON.parse(charData);
if (response.status === 'success' && response.data) {
const char = response.data;
envDetails += `# Current Character\n`;
envDetails += `Name: ${char.name}\n`;
envDetails += `Description Length: ${char.description?.length || 0}\n`;
envDetails += `First Message Length: ${char.first_mes?.length || 0}\n`;
envDetails += `Description Snippet: ${char.description?.substring(0, 200).replace(/\n/g, ' ')}...\n\n`;
} else {
envDetails += `# Current Character\nError reading character: ${response.message || 'Unknown error'}\n\n`;
}
} catch (e) {
envDetails += `# Current Character\nError reading character: ${e.message}\n\n`;
}
@@ -192,6 +205,11 @@ Example:
- **Detailed Writing**: When writing content (Description, First Message, World Info), be creative and detailed.
- World Info entries: > 300 words.
- First Message: > 1500 words, including environment, psychology, and action.
- **Tool Selection**:
- **Use \`edit_character_text\`** for small modifications to existing large text fields (Description, First Message, etc.). This is more precise and saves tokens.
- **Use \`edit_world_info_entry\`** for small modifications to existing World Info entries.
- **Use \`update_character_card\`** only when populating empty fields or rewriting the entire content of a field.
- **Use \`write_world_info_entry\`** only when creating new entries or rewriting the entire content of an entry.
- **Do not ask for more information than necessary**: Use the tools provided to accomplish the user's request efficiently and effectively.
- **Completion**: When the task is done, provide a final summary to the user.
`;
@@ -207,17 +225,17 @@ Example:
return null;
}
async handleUserMessage(message, onStreamUpdate, onPreviewUpdate, onApprovalRequest, onContextUpdate) {
async handleUserMessage(message, onStreamUpdate, onPreviewUpdate, onApprovalRequest, onContextUpdate, onPromptGenerated) {
if (this.history.length === 0) {
this.taskState.init(message);
}
this.history.push({ role: 'user', content: message });
this.status = 'running';
await this.runTaskLoop(onStreamUpdate, onPreviewUpdate, onApprovalRequest, onContextUpdate);
await this.runTaskLoop(onStreamUpdate, onPreviewUpdate, onApprovalRequest, onContextUpdate, onPromptGenerated);
}
async runTaskLoop(onStreamUpdate, onPreviewUpdate, onApprovalRequest, onContextUpdate) {
async runTaskLoop(onStreamUpdate, onPreviewUpdate, onApprovalRequest, onContextUpdate, onPromptGenerated) {
let maxTurns = 20;
let currentTurn = 0;
@@ -250,6 +268,10 @@ Example:
config.maxTokens
);
if (onPromptGenerated) {
onPromptGenerated(messages);
}
let responseContent;
let fullStreamedContent = "";
try {
@@ -326,7 +348,7 @@ Example:
toolCall.arguments.chid = parseInt(this.currentChid);
}
}
if (toolCall.name === 'write_world_info_entry' || toolCall.name === 'read_world_info') {
if (toolCall.name === 'write_world_info_entry' || toolCall.name === 'read_world_info' || toolCall.name === 'edit_world_info_entry') {
if (!toolCall.arguments.book_name && this.currentBookName) {
toolCall.arguments.book_name = this.currentBookName;
}
@@ -419,7 +441,7 @@ Example:
}
if (onPreviewUpdate && !isError) {
onPreviewUpdate(toolCall.name, toolCall.arguments);
onPreviewUpdate(toolCall.name, toolCall.arguments, false, true);
}
}

View File

@@ -74,7 +74,7 @@ export async function callAi(role, messages, options = {}, onChunk = null) {
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop(); // Keep incomplete line in buffer
buffer = lines.pop();
for (const line of lines) {
const trimmedLine = line.trim();
@@ -89,7 +89,7 @@ export async function callAi(role, messages, options = {}, onChunk = null) {
onChunk(delta);
}
} catch (e) {
// Ignore parse errors for partial chunks
}
}
}
@@ -107,6 +107,14 @@ export async function callAi(role, messages, options = {}, onChunk = null) {
}
const content = responseData.choices[0].message?.content;
if (!content) {
console.warn(`[自动构建器] AI (${roleName}) 响应内容为空。完整响应:`, responseData);
if (responseData.choices && responseData.choices[0]) {
console.warn("Choices[0]:", responseData.choices[0]);
}
}
console.log(`[自动构建器] AI (${roleName}) 响应接收成功。长度: ${content?.length}`);
return content;
}
@@ -117,12 +125,17 @@ export async function callAi(role, messages, options = {}, onChunk = null) {
}
}
export async function testConnection(role) {
export async function testConnection(role, config = {}) {
try {
const response = await callAi(role, [
{ role: 'user', content: 'Hi' }
], { maxTokens: 10 });
return { success: !!response };
{ role: 'user', content: 'Say hello' }
], { maxTokens: 50, ...config });
if (!response) {
return { success: false, error: "API 返回了空内容 (可能是被安全过滤或模型无响应)" };
}
return { success: true };
} catch (error) {
console.error(`[自动构建器] ${role} 连接测试失败:`, error);
return { success: false, error: error.message };

View File

@@ -1,24 +1,91 @@
import { characters, saveCharacterDebounced, this_chid, getCharacters, getRequestHeaders } from "/script.js";
import { getContext } from "/scripts/extensions.js";
import { extensionName } from "../../utils/settings.js";
async function saveCharacterById(chid) {
const char = characters[chid];
if (!char) return;
let currentChid = undefined;
try {
const context = getContext();
if (context) currentChid = context.characterId;
} catch (e) {}
if (currentChid === undefined) currentChid = this_chid;
if (currentChid === undefined && typeof window !== 'undefined' && window.this_chid !== undefined) {
currentChid = window.this_chid;
}
if (currentChid === undefined && typeof $ !== 'undefined') {
const selected = $('.character_select.selected, .character-list-item.selected');
if (selected.length) {
currentChid = selected.attr('chid');
}
}
if (typeof saveCharacterDebounced === 'function') {
if (currentChid === undefined || chid == currentChid) {
saveCharacterDebounced();
console.log(`[Amily2 CharAPI] Triggered saveCharacterDebounced for character ${chid} (Detected: ${currentChid})`);
return { success: true };
}
}
try {
const formData = new FormData();
formData.append('avatar_url', char.avatar);
formData.append('ch_name', char.name);
formData.append('description', char.description || '');
formData.append('personality', char.personality || '');
formData.append('scenario', char.scenario || '');
formData.append('first_mes', char.first_mes || '');
formData.append('mes_example', char.mes_example || '');
formData.append('creator', char.creator || '');
formData.append('creator_notes', char.creator_notes || '');
formData.append('tags', Array.isArray(char.tags) ? char.tags.join(',') : (char.tags || ''));
formData.append('talkativeness', char.talkativeness || '0.5');
formData.append('fav', char.fav || 'false');
if (char.data) {
formData.append('extensions', JSON.stringify(char.data));
}
if (char.data && Array.isArray(char.data.alternate_greetings)) {
for (const value of char.data.alternate_greetings) {
formData.append('alternate_greetings', value);
}
}
const response = await fetch('/api/characters/edit', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify(char)
headers: getRequestHeaders({ omitContentType: true }),
body: formData
});
if (!response.ok) {
console.error(`[Amily2 CharAPI] Failed to save character ${chid}:`, response.statusText);
const errorText = await response.text();
console.error(`[Amily2 CharAPI] Failed to save character ${chid}:`, response.statusText, errorText);
return { success: false, message: `Save failed: ${response.statusText}` };
} else {
console.log(`[Amily2 CharAPI] Successfully saved character ${chid}`);
console.log(`[Amily2 CharAPI] Successfully saved character ${chid} (Background)`);
return { success: true };
}
} catch (e) {
console.error(`[Amily2 CharAPI] Error saving character ${chid}:`, e);
return { success: false, message: `Save error: ${e.message}` };
}
}
@@ -30,7 +97,7 @@ export function getCharacter(chid = this_chid) {
return characters[chid];
}
export function updateCharacter(chid, updates) {
export async function updateCharacter(chid, updates) {
const char = getCharacter(chid);
if (!char) return false;
@@ -45,11 +112,14 @@ export function updateCharacter(chid, updates) {
});
if (changed) {
saveCharacterById(chid);
const success = await saveCharacterById(chid);
if (success) {
console.log(`[Amily2 CharAPI] Updated character ${chid}:`, Object.keys(updates));
return true;
}
return false;
}
return false;
}
export function getFirstMessages(chid) {
@@ -63,7 +133,7 @@ export function getFirstMessages(chid) {
return messages;
}
export function addFirstMessage(chid, message) {
export async function addFirstMessage(chid, message) {
const char = getCharacter(chid);
if (!char) return false;
@@ -73,12 +143,15 @@ export function addFirstMessage(chid, message) {
}
char.data.alternate_greetings.push(message);
saveCharacterById(chid);
const success = await saveCharacterById(chid);
if (success) {
console.log(`[Amily2 CharAPI] Added alternate greeting to character ${chid}`);
return true;
}
return false;
}
export function updateFirstMessage(chid, index, message) {
export async function updateFirstMessage(chid, index, message) {
const char = getCharacter(chid);
if (!char) return false;
@@ -93,12 +166,15 @@ export function updateFirstMessage(chid, index, message) {
return false;
}
}
saveCharacterById(chid);
const success = await saveCharacterById(chid);
if (success) {
console.log(`[Amily2 CharAPI] Updated greeting ${index} for character ${chid}`);
return true;
}
return false;
}
export function removeFirstMessage(chid, index) {
export async function removeFirstMessage(chid, index) {
const char = getCharacter(chid);
if (!char) return false;
@@ -114,9 +190,12 @@ export function removeFirstMessage(chid, index) {
return false;
}
}
saveCharacterById(chid);
const success = await saveCharacterById(chid);
if (success) {
console.log(`[Amily2 CharAPI] Removed greeting ${index} for character ${chid}`);
return true;
}
return false;
}
export async function createNewCharacter(name) {

View File

@@ -85,7 +85,7 @@ ${taskState.getPromptContext()}
shouldSummarize(history, tokenCount, maxTokens) {
const tokenUsageRatio = tokenCount / maxTokens;
if (tokenUsageRatio > 0.7) return true;
if (history.length > 15) return true;
if (history.length > 35) return true;
return false;
}
}

View File

@@ -159,7 +159,7 @@ export const tools = {
const { chid, ...updates } = args;
const finalUpdates = args.updates || updates;
const success = charApi.updateCharacter(chid, finalUpdates);
const success = await charApi.updateCharacter(chid, finalUpdates);
if (success) {
const updatedFields = Object.keys(finalUpdates).join(', ');
return JSON.stringify({
@@ -171,7 +171,7 @@ export const tools = {
return JSON.stringify({
status: "error",
code: "UPDATE_FAILED",
message: "更新角色卡失败。"
message: "更新角色卡失败。请确保您正在编辑当前选中的角色(暂不支持后台编辑其他角色)。"
});
}
},
@@ -196,18 +196,43 @@ export const tools = {
}
let content = char[field] || '';
const changes = diff.split('------- SEARCH');
const normalizedDiff = diff
.replace(/-------\s*SEARCH/g, '------- SEARCH')
.replace(/=======\s*/g, '=======')
.replace(/\+\+\+\+\+\+\+\s*REPLACE/g, '+++++++ REPLACE');
const changes = normalizedDiff.split('------- SEARCH');
if (changes[0].trim() === '') changes.shift();
for (const change of changes) {
const parts = change.split('=======');
if (parts.length !== 2) continue;
if (parts.length < 2) continue;
const searchBlock = parts[0].trim();
const replaceBlock = parts[1].split('+++++++ REPLACE')[0].trim();
if (!content.includes(searchBlock)) {
if (content.includes(searchBlock)) {
content = content.replace(searchBlock, replaceBlock);
continue;
}
const normalizedSearch = searchBlock.replace(/\r\n/g, '\n');
const lines = normalizedSearch.split('\n');
const regexPattern = lines.map(line => line.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('\\r?\\n');
const regex = new RegExp(regexPattern);
const match = content.match(regex);
if (match) {
content = content.replace(match[0], replaceBlock);
continue;
}
return JSON.stringify({
status: "error",
code: "SEARCH_NOT_FOUND",
@@ -216,10 +241,7 @@ export const tools = {
});
}
content = content.replace(searchBlock, replaceBlock);
}
const success = charApi.updateCharacter(chid, { [field]: content });
const success = await charApi.updateCharacter(chid, { [field]: content });
if (success) {
return JSON.stringify({
status: "success",
@@ -229,7 +251,81 @@ export const tools = {
return JSON.stringify({
status: "error",
code: "UPDATE_FAILED",
message: `更新字段 '${field}' 失败。`
message: `更新字段 '${field}' 失败。请确保您正在编辑当前选中的角色(暂不支持后台编辑其他角色)。`
});
}
},
edit_world_info_entry: async ({ book_name, uid, diff }) => {
const entries = await amilyHelper.getLorebookEntries(book_name);
const entry = entries.find(e => String(e.uid) === String(uid));
if (!entry) {
return JSON.stringify({
status: "error",
code: "ENTRY_NOT_FOUND",
message: `在世界书 "${book_name}" 中未找到 UID 为 ${uid} 的条目。`
});
}
let content = entry.content || '';
const normalizedDiff = diff
.replace(/-------\s*SEARCH/g, '------- SEARCH')
.replace(/=======\s*/g, '=======')
.replace(/\+\+\+\+\+\+\+\s*REPLACE/g, '+++++++ REPLACE');
const changes = normalizedDiff.split('------- SEARCH');
if (changes[0].trim() === '') changes.shift();
for (const change of changes) {
const parts = change.split('=======');
if (parts.length < 2) continue;
const searchBlock = parts[0].trim();
const replaceBlock = parts[1].split('+++++++ REPLACE')[0].trim();
if (content.includes(searchBlock)) {
content = content.replace(searchBlock, replaceBlock);
continue;
}
const normalizedSearch = searchBlock.replace(/\r\n/g, '\n');
const lines = normalizedSearch.split('\n');
const regexPattern = lines.map(line => line.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('\\r?\\n');
const regex = new RegExp(regexPattern);
const match = content.match(regex);
if (match) {
content = content.replace(match[0], replaceBlock);
continue;
}
return JSON.stringify({
status: "error",
code: "SEARCH_NOT_FOUND",
message: `在条目内容中未找到搜索块。`,
suggestion: "请确保 SEARCH 块与现有内容完全匹配(包括空格)。"
});
}
const success = await amilyHelper.setLorebookEntries(book_name, [{ uid: entry.uid, content: content }]);
if (success) {
return JSON.stringify({
status: "success",
message: `条目 [${uid}] 更新成功。`
});
} else {
return JSON.stringify({
status: "error",
code: "UPDATE_FAILED",
message: `更新条目 [${uid}] 失败。`
});
}
},
@@ -238,13 +334,13 @@ export const tools = {
let success = false;
switch (action) {
case 'add':
success = charApi.addFirstMessage(chid, message);
success = await charApi.addFirstMessage(chid, message);
break;
case 'update':
success = charApi.updateFirstMessage(chid, index, message);
success = await charApi.updateFirstMessage(chid, index, message);
break;
case 'remove':
success = charApi.removeFirstMessage(chid, index);
success = await charApi.removeFirstMessage(chid, index);
break;
default:
return JSON.stringify({
@@ -494,6 +590,22 @@ export function getToolDefinitions() {
required: ["chid", "field", "diff"]
}
},
{
name: "edit_world_info_entry",
description: "使用 搜索/替换 块编辑世界书条目的内容。",
parameters: {
type: "object",
properties: {
book_name: { type: "string", description: "世界书名称。" },
uid: { type: "number", description: "条目 UID。" },
diff: {
type: "string",
description: "一个或多个遵循此确切格式的 搜索/替换 块:\n------- SEARCH\n[exact content to find]\n=======\n[new content to replace with]\n+++++++ REPLACE"
}
},
required: ["book_name", "uid", "diff"]
}
},
{
name: "manage_first_message",
description: "添加、更新或删除候补开场白。",

View File

@@ -12,8 +12,9 @@ let agentManager = null;
let previousCharData = {};
let previousWorldData = {};
let isWaitingForApproval = false;
let openedFiles = new Map(); // Key: string ID, Value: { title, content, type, metadata }
let openedFiles = new Map();
let activeFileId = null;
let promptLogContent = "=== Prompt Log ===\n\n";
export async function openAutoCharCardWindow() {
if ($('#acc-window').length > 0) {
@@ -57,31 +58,51 @@ export async function openAutoCharCardWindow() {
function populateDropdowns() {
const charSelect = $('#acc-target-char');
const prevCharId = charSelect.val();
charSelect.empty().append('<option value="">-- 请选择 --</option>');
charSelect.append('<option value="new">新建角色卡</option>');
let isPrevCharStillPresent = false;
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);
charSelect.append($('<option>').val(index).text(char.name));
if (String(index) === prevCharId) {
isPrevCharStillPresent = true;
}
}
});
if (isPrevCharStillPresent) {
charSelect.val(prevCharId);
} else if (this_chid !== undefined) {
charSelect.val(this_chid);
}
const worldSelect = $('#acc-target-world');
const prevWorldName = worldSelect.val();
worldSelect.empty().append('<option value="">-- 请选择 --</option>');
worldSelect.append('<option value="new">新建世界书</option>');
let isPrevWorldStillPresent = false;
world_names.forEach(name => {
worldSelect.append($('<option>').val(name).text(name));
if (name === prevWorldName) {
isPrevWorldStillPresent = true;
}
});
if (isPrevWorldStillPresent) {
worldSelect.val(prevWorldName);
}
}
async function handleContextUpdate(type, value) {
console.log(`[Amily2 AutoCharCard] Context Update: ${type} -> ${value}`);
if (type === 'char') {
await getCharacters(); // Force refresh character list
await getCharacters();
}
populateDropdowns();
@@ -93,6 +114,29 @@ async function handleContextUpdate(type, value) {
}
}
function handlePromptLog(messages) {
const userType = localStorage.getItem("plugin_user_type");
if (userType !== "3") return;
const timestamp = new Date().toLocaleTimeString();
let logEntry = `\n\n--- [${timestamp}] New Request ---\n`;
messages.forEach(msg => {
logEntry += `\n[${msg.role.toUpperCase()}]\n${msg.content}\n`;
});
promptLogContent += logEntry;
if (openedFiles.has('debug-prompt-log')) {
const file = openedFiles.get('debug-prompt-log');
file.content = promptLogContent;
if (activeFileId === 'debug-prompt-log') {
renderEditor();
}
}
}
function renderRulesList() {
const list = $('#acc-rules-list');
list.empty();
@@ -157,10 +201,32 @@ function bindEvents() {
const [type, id, subId] = val.split('|');
if (type === 'debug' && id === 'log') {
const userType = localStorage.getItem("plugin_user_type");
if (userType !== "3") {
toastr.warning('权限不足:仅开发者可查看调试日志。');
$(this).val('');
return;
}
const fileId = 'debug-prompt-log';
openedFiles.set(fileId, {
title: 'Prompt Log',
content: promptLogContent,
type: 'log',
metadata: null
});
activeFileId = fileId;
renderEditor();
$(this).val('');
return;
}
if (type === 'char') {
const chid = id;
const field = subId;
const fileId = `char-${chid}-${field}`;
if (openedFiles.has(fileId)) {
activeFileId = fileId;
@@ -168,9 +234,13 @@ function bindEvents() {
return;
}
let content = '';
try {
console.log(`[AutoCharCard] Reading char ${chid}, field ${field}`);
const charData = await tools.read_character_card({ chid });
@@ -244,7 +314,7 @@ function bindEvents() {
}
}
// Reset selector
$(this).val('');
});
@@ -292,6 +362,9 @@ function bindEvents() {
}
});
const previewHeader = $('.acc-right-panel .acc-panel-header');
if (previewHeader.find('#acc-refresh-preview').length === 0) {
const refreshBtn = $('<button>')
@@ -423,12 +496,15 @@ function bindEvents() {
}
});
$('.acc-nav-btn').on('click', function() {
const targetClass = $(this).data('target');
$('.acc-nav-btn').removeClass('active');
$(this).addClass('active');
$('.acc-column').removeClass('mobile-active');
$(`.${targetClass}`).addClass('mobile-active');
});
@@ -458,23 +534,27 @@ async function handleSendMessage() {
input.val('');
if (message) {
addMessage('user', message);
await agentManager.resumeWithApproval(
false,
message,
(content, role) => addMessage(role, content),
(toolName, args) => updatePreview(toolName, args),
updatePreview,
showApprovalRequest,
handleContextUpdate
handleContextUpdate,
handlePromptLog
);
} else {
await agentManager.resumeWithApproval(
true,
null,
(content, role) => addMessage(role, content),
(toolName, args) => updatePreview(toolName, args),
updatePreview,
showApprovalRequest,
handleContextUpdate
handleContextUpdate,
handlePromptLog
);
}
return;
@@ -511,11 +591,10 @@ async function handleSendMessage() {
(content, role) => {
addMessage(role, content);
},
(toolName, args) => {
updatePreview(toolName, args);
},
updatePreview,
showApprovalRequest,
handleContextUpdate
handleContextUpdate,
handlePromptLog
);
} catch (error) {
console.error('Agent Error:', error);
@@ -530,12 +609,17 @@ async function handleSendMessage() {
function showApprovalRequest(toolName, args) {
isWaitingForApproval = true;
updatePreview(toolName, args, false);
const btn = $('#acc-send-btn');
btn.html('<i class="fas fa-check"></i>');
btn.prop('title', '批准执行');
btn.addClass('acc-btn-success');
$('#acc-user-input').attr('placeholder', '输入反馈以修改,或点击 √ 批准,点击 X 拒绝...');
if ($('#acc-reject-btn').length === 0) {
const rejectBtn = $('<button>')
.attr('id', 'acc-reject-btn')
@@ -563,6 +647,7 @@ function showApprovalRequest(toolName, args) {
const input = $('#acc-user-input');
const message = input.val().trim();
btn.html('<i class="fas fa-paper-plane"></i>');
btn.prop('title', '发送');
btn.removeClass('acc-btn-success');
@@ -570,6 +655,9 @@ function showApprovalRequest(toolName, args) {
input.attr('placeholder', '描述您的需求...');
input.val('');
updatePreview(toolName, args, false, true);
const feedback = message || "用户拒绝了操作。";
addMessage('user', `[拒绝] ${feedback}`);
@@ -577,13 +665,15 @@ function showApprovalRequest(toolName, args) {
false,
feedback,
(content, role) => addMessage(role, content),
(toolName, args) => updatePreview(toolName, args),
updatePreview,
showApprovalRequest,
handleContextUpdate
handleContextUpdate,
handlePromptLog
);
});
}
const toolDisplay = `
<div class="acc-tool-request">
<details>
@@ -603,7 +693,10 @@ function addMessage(role, content) {
if (role === 'stream-assistant') {
let lastMsg = stream.children().last();
if (!lastMsg.hasClass('assistant') || !lastMsg.hasClass('acc-streaming')) {
const msgDiv = $('<div>').addClass('acc-message assistant acc-streaming');
const avatarDiv = $('<div>').addClass('acc-avatar').html('<i class="fas fa-robot" style="color: #4caf50;"></i>');
const contentDiv = $('<div>').addClass('acc-message-content');
@@ -614,6 +707,7 @@ function addMessage(role, content) {
const contentDiv = lastMsg.find('.acc-message-content');
const escapedContent = content
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
@@ -631,6 +725,7 @@ function addMessage(role, content) {
let displayContent = content;
if (role === 'executor' || role === 'assistant') {
displayContent = displayContent
.replace(/<thinking(?:\s+[^>]*)?>[\s\S]*?<\/thinking>/gi, '')
.replace(/<\/thinking>/gi, '')
@@ -640,8 +735,10 @@ function addMessage(role, content) {
const tools = [
'read_world_info', 'read_world_entry', 'write_world_info_entry', 'create_world_book',
'read_character_card', 'update_character_card', 'edit_character_text',
'edit_world_info_entry',
'manage_first_message', 'use_tool'
];
const regex = new RegExp(`<(${tools.join('|')})(?:\\s+[^>]*)?>[\\s\\S]*?<\\/\\1>`, 'gi');
displayContent = displayContent.replace(regex, '').trim();
@@ -649,6 +746,7 @@ function addMessage(role, content) {
displayContent = "<i>(正在执行操作...)</i>";
}
if (role === 'assistant') {
stream.find('.acc-streaming').remove();
}
@@ -656,6 +754,7 @@ function addMessage(role, content) {
let formattedContent;
if (displayContent.trim().startsWith('<div class="acc-tool-request"')) {
formattedContent = displayContent;
} else {
@@ -699,28 +798,46 @@ function addMessage(role, content) {
function parseMarkdown(text) {
if (!text) return '';
let html = text
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">");
html = html.replace(/```([\s\S]*?)```/g, '<pre><code>$1</code></pre>');
html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
html = html.replace(/^#### (.*$)/gm, '<h4>$1</h4>');
html = html.replace(/^### (.*$)/gm, '<h3>$1</h3>');
html = html.replace(/^## (.*$)/gm, '<h2>$1</h2>');
html = html.replace(/^# (.*$)/gm, '<h1>$1</h1>');
html = html.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
html = html.replace(/\*(.*?)\*/g, '<em>$1</em>');
html = html.replace(/~~(.*?)~~/g, '<del>$1</del>');
html = html.replace(/^[\*\-]{3,}$/gm, '<hr>');
html = html.replace(/^\s*[\-\*]\s+(.*$)/gm, '<li>$1</li>');
html = html.replace(/\n/g, '<br>');
return html;
@@ -731,6 +848,9 @@ function renderEditor() {
const tabsContainer = $('.acc-preview-tabs');
container.empty();
tabsContainer.empty();
@@ -739,6 +859,7 @@ function renderEditor() {
return;
}
if (!activeFileId || !openedFiles.has(activeFileId)) {
activeFileId = openedFiles.keys().next().value;
}
@@ -757,6 +878,7 @@ function renderEditor() {
const icon = $('<i class="fas fa-file-alt"></i>');
const titleSpan = $('<span>').addClass('acc-tab-title').text(file.title);
const closeBtn = $('<span>')
.html('&times;')
.addClass('acc-tab-close')
@@ -777,7 +899,7 @@ function renderEditor() {
.css('flex-direction', 'column')
.css('height', '100%');
// Toolbar
const toolbar = $('<div>').addClass('acc-editor-toolbar').css({
'padding': '5px',
'border-bottom': '1px solid #444',
@@ -792,9 +914,121 @@ function renderEditor() {
.on('click', () => saveFile(id));
toolbar.append(saveBtn);
contentDiv.append(toolbar);
// Textarea
if (file.type === 'diff-view') {
const editorDiv = $('<div>')
.addClass('acc-editor-diff-view')
.css({
'flex': '1',
'width': '100%',
'background': '#1e1e1e',
'color': '#d4d4d4',
'padding': '10px',
'font-family': 'monospace',
'overflow-y': 'auto',
'white-space': 'pre-wrap'
});
if (file.segments) {
file.segments.forEach((segment) => {
if (segment.type === 'text') {
editorDiv.append($('<span>').text(segment.content));
} else if (segment.type === 'change') {
const container = $('<div>').addClass('acc-diff-container').css({
'display': 'block',
'margin': '10px 0',
'border': '1px solid #444',
'padding': '5px',
'border-radius': '4px'
});
const renderChange = () => {
container.empty();
if (segment.active) {
const removed = $('<div>')
.text(segment.original)
.css({
'background-color': 'rgba(255, 0, 0, 0.2)',
'cursor': 'pointer',
'padding': '5px',
'margin-bottom': '2px',
'white-space': 'pre-wrap',
'color': '#d4d4d4'
})
.attr('title', '点击恢复 (Click to restore)');
const added = $('<div>')
.attr('contenteditable', 'true')
.css({
'background-color': 'rgba(0, 255, 0, 0.2)',
'cursor': 'text',
'padding': '5px',
'white-space': 'pre-wrap',
'color': '#d4d4d4',
'outline': 'none'
})
.attr('title', '点击编辑');
const toggle = () => {
segment.active = false;
renderChange();
if (agentManager) {
const newDiff = reconstructDiff(file.segments);
agentManager.updatePendingToolArgs({ diff: newDiff });
}
};
removed.on('click', toggle);
added.on('input', function() {
segment.new = $(this).text();
if (agentManager) {
const newDiff = reconstructDiff(file.segments);
agentManager.updatePendingToolArgs({ diff: newDiff });
}
});
container.append(removed).append(added);
} else {
const restored = $('<div>')
.text(segment.original)
.css({
'cursor': 'pointer',
'border-left': '3px solid #666',
'padding': '5px',
'white-space': 'pre-wrap',
'opacity': '0.7'
})
.attr('title', '点击重新应用修改 (Click to re-apply change)');
restored.on('click', () => {
segment.active = true;
renderChange();
if (agentManager) {
const newDiff = reconstructDiff(file.segments);
agentManager.updatePendingToolArgs({ diff: newDiff });
}
});
container.append(restored);
}
};
renderChange();
editorDiv.append(container);
}
});
} else {
editorDiv.text('Error: No segments found for diff view.');
}
contentDiv.append(editorDiv);
} else {
const textarea = $('<textarea>')
.addClass('acc-editor-textarea')
.val(file.content)
@@ -814,6 +1048,8 @@ function renderEditor() {
});
contentDiv.append(textarea);
}
container.append(contentDiv);
}
});
@@ -829,42 +1065,61 @@ async function saveFile(id) {
return;
}
let contentToSave = file.content;
if (file.type === 'diff-view' && file.segments) {
contentToSave = file.segments.map(seg => {
if (seg.type === 'text') return seg.content;
if (seg.type === 'change') {
return seg.active ? seg.new : seg.original;
}
return '';
}).join('');
}
try {
let result;
if (meta.type === 'char') {
if (meta.field.startsWith('greeting_')) {
const index = parseInt(meta.field.split('_')[1]);
result = await tools.manage_first_message({
action: 'update',
chid: meta.chid,
index: index + 1,
message: file.content
message: contentToSave
});
} else {
result = await tools.update_character_card({
chid: meta.chid,
[meta.field]: file.content
[meta.field]: contentToSave
});
}
} else if (meta.type === 'wi') {
if (meta.uid !== undefined) {
result = await tools.write_world_info_entry({
book_name: meta.bookName,
entries: [{ uid: meta.uid, content: file.content }]
entries: [{ uid: meta.uid, content: contentToSave }]
});
} else {
try {
const entry = JSON.parse(file.content);
const entry = JSON.parse(contentToSave);
result = await tools.write_world_info_entry({
book_name: meta.bookName,
entries: [entry]
});
} catch (e) {
toastr.error('保存失败: 内容必须是有效的 JSON (针对新建条目) 或包含 UID');
return;
}
@@ -873,6 +1128,15 @@ async function saveFile(id) {
if (result && !result.startsWith('Error') && !result.includes('失败')) {
toastr.success('保存成功');
if (file.type === 'diff-view') {
file.type = 'normal';
delete file.segments;
file.content = contentToSave;
renderEditor();
}
} else {
toastr.error(result || '保存失败');
}
@@ -901,6 +1165,7 @@ async function loadContextToEditor() {
const char = response.data;
previousCharData = char;
const charGroup = $('<optgroup label="角色卡字段">');
const fields = ['description', 'personality', 'first_mes', 'scenario', 'mes_example'];
fields.forEach(field => {
@@ -914,6 +1179,13 @@ async function loadContextToEditor() {
}
selector.append(charGroup);
const userType = localStorage.getItem("plugin_user_type");
if (userType === "3") {
selector.append('<option value="debug|log">Debug: Prompt Log</option>');
}
if (openedFiles.size === 0 && char.description) {
const id = `char-${chid}-description`;
openedFiles.set(id, {
@@ -932,6 +1204,7 @@ async function loadContextToEditor() {
if (bookName && bookName !== 'new') {
try {
const indexData = await tools.read_world_info({ book_name: bookName, return_full: false });
const index = JSON.parse(indexData);
@@ -952,9 +1225,18 @@ async function loadContextToEditor() {
renderEditor();
}
async function updatePreview(toolName, args, isPartial = false) {
const chid = args.chid !== undefined ? args.chid : $('#acc-target-char').val();
const bookName = args.book_name !== undefined ? args.book_name : $('#acc-target-world').val();
async function updatePreview(toolName, args, isPartial = false, isExecuted = false) {
let chid = args.chid;
if (chid === undefined || chid === null || chid === '') {
chid = $('#acc-target-char').val();
}
chid = String(chid);
let bookName = args.book_name;
if (bookName === undefined || bookName === null || bookName === '') {
bookName = $('#acc-target-world').val();
}
bookName = String(bookName);
if (toolName === 'update_character_card') {
const fields = ['description', 'personality', 'first_mes', 'scenario', 'mes_example'];
@@ -976,11 +1258,30 @@ async function updatePreview(toolName, args, isPartial = false) {
} else if (toolName === 'edit_character_text') {
const field = args.field || 'Unknown Field';
if (field !== 'Unknown Field') {
const unknownDiffId = `diff-${chid}-Unknown Field`;
if (openedFiles.has(unknownDiffId)) {
openedFiles.delete(unknownDiffId);
}
const unknownId = `char-${chid}-Unknown Field`;
if (openedFiles.has(unknownId)) {
const file = openedFiles.get(unknownId);
openedFiles.delete(unknownId);
file.title = field;
file.metadata.field = field;
openedFiles.set(id, file);
if (activeFileId === unknownId) activeFileId = id;
}
}
const diff = args.diff || '';
const id = `char-${chid}-${field}`;
if (isPartial) {
const diffId = `diff-${chid}-${field}`;
openedFiles.set(diffId, {
title: `Diff: ${field}`,
@@ -989,6 +1290,91 @@ async function updatePreview(toolName, args, isPartial = false) {
metadata: null
});
activeFileId = diffId;
} else if (isExecuted) {
let success = false;
let content = '';
try {
const charData = await tools.read_character_card({ chid });
const response = JSON.parse(charData);
if (response.status === 'success' && response.data) {
const char = response.data;
if (field.startsWith('greeting_')) {
const index = parseInt(field.split('_')[1]);
content = char.alternate_greetings[index];
} else {
content = char[field];
}
success = true;
}
} catch (e) {
console.error("Failed to refresh content after edit", e);
}
openedFiles.delete(`diff-${chid}-${field}`);
if (!openedFiles.has(id)) {
for (const [key, val] of openedFiles) {
if (val.metadata && val.metadata.chid == chid && val.metadata.field == field) {
id = key;
break;
}
}
}
let foundAndFixed = false;
openedFiles.forEach((file, fileId) => {
if (file.metadata && file.metadata.chid == chid && file.metadata.field == field) {
if (file.type === 'diff-view') {
if (file.segments) {
const newContent = file.segments.map(s => s.type === 'change' ? s.new : s.content).join('');
file.content = newContent;
}
file.type = 'normal';
delete file.segments;
if (fileId !== id) {
openedFiles.delete(fileId);
openedFiles.set(id, file);
if (activeFileId === fileId) activeFileId = id;
}
foundAndFixed = true;
}
}
});
if (success) {
openedFiles.set(id, {
title: field,
content: content,
type: 'normal',
metadata: { type: 'char', chid, field }
});
activeFileId = id;
} else if (!foundAndFixed) {
if (openedFiles.has(id)) {
const file = openedFiles.get(id);
file.type = 'normal';
activeFileId = id;
}
}
renderEditor();
} else {
let originalContent = '';
@@ -996,52 +1382,121 @@ async function updatePreview(toolName, args, isPartial = false) {
originalContent = openedFiles.get(id).content;
} else {
try {
const charData = await tools.read_character_card({ chid });
const response = JSON.parse(charData);
if (response.status === 'success' && response.data) {
const char = response.data;
if (field.startsWith('greeting_')) {
const index = parseInt(field.split('_')[1]);
originalContent = char.alternate_greetings[index];
} else {
originalContent = char[field];
}
}
} catch (e) {
console.error("Failed to fetch original content for diff view", e);
}
}
if (originalContent) {
// Apply diff logic
const changes = diff.split('------- SEARCH');
if (changes[0].trim() === '') changes.shift();
let newContent = originalContent;
let applied = true;
for (const change of changes) {
const parts = change.split('=======');
if (parts.length === 2) {
const searchBlock = parts[0].trim();
const replaceBlock = parts[1].split('+++++++ REPLACE')[0].trim();
if (newContent.includes(searchBlock)) {
newContent = newContent.replace(searchBlock, replaceBlock);
} else {
applied = false;
}
}
}
if (applied) {
const segments = parseDiff(originalContent, diff);
openedFiles.set(id, {
title: field,
content: newContent,
type: 'normal',
content: originalContent,
segments: segments,
type: 'diff-view',
metadata: { type: 'char', chid, field }
});
activeFileId = id;
openedFiles.delete(`diff-${chid}-${field}`);
} else {
const diffId = `diff-${chid}-${field}`;
openedFiles.set(diffId, {
title: `Diff: ${field} (Failed to Apply)`,
title: `Diff: ${field}`,
content: diff,
type: 'diff',
metadata: null
});
activeFileId = diffId;
}
} else {
const diffId = `diff-${chid}-${field}`;
}
} else if (toolName === 'edit_world_info_entry') {
const uid = args.uid;
if (uid !== undefined) {
const unknownDiffId = `diff-wi-${bookName}-undefined`;
if (openedFiles.has(unknownDiffId)) {
openedFiles.delete(unknownDiffId);
}
}
const diff = args.diff || '';
const id = `wi-${bookName}-${uid}`;
if (isPartial) {
const diffId = `diff-wi-${bookName}-${uid}`;
openedFiles.set(diffId, {
title: `Diff: ${field}`,
title: `Diff: WI ${uid}`,
content: diff,
type: 'diff',
metadata: null
});
activeFileId = diffId;
} else if (isExecuted) {
try {
const entryData = await tools.read_world_entry({ book_name: bookName, uid: uid });
const response = JSON.parse(entryData);
if (response.status === 'success' && response.data) {
openedFiles.delete(`diff-wi-${bookName}-${uid}`);
openedFiles.delete(id);
openedFiles.set(id, {
title: `WI: ${uid}`,
content: response.data.content,
type: 'normal',
metadata: { type: 'wi', bookName, uid }
});
activeFileId = id;
}
} catch (e) {
console.error("Failed to refresh WI content after edit", e);
}
} else {
let originalContent = '';
if (openedFiles.has(id)) {
originalContent = openedFiles.get(id).content;
} else {
try {
const entryData = await tools.read_world_entry({ book_name: bookName, uid: uid });
const response = JSON.parse(entryData);
if (response.status === 'success' && response.data) {
originalContent = response.data.content;
}
} catch (e) {
console.error("Failed to fetch original content for WI diff view", e);
}
}
if (originalContent) {
const segments = parseDiff(originalContent, diff);
openedFiles.set(id, {
title: `WI: ${uid}`,
content: originalContent,
segments: segments,
type: 'diff-view',
metadata: { type: 'wi', bookName, uid }
});
activeFileId = id;
openedFiles.delete(`diff-wi-${bookName}-${uid}`);
} else {
const diffId = `diff-wi-${bookName}-${uid}`;
openedFiles.set(diffId, {
title: `Diff: WI ${uid}`,
content: diff,
type: 'diff',
metadata: null
@@ -1054,6 +1509,7 @@ async function updatePreview(toolName, args, isPartial = false) {
let entries = args.entries;
if (isPartial && typeof entries === 'string') {
const id = `wi-raw-partial`;
openedFiles.set(id, {
title: 'WI Entry (Generating...)',
@@ -1081,9 +1537,65 @@ async function updatePreview(toolName, args, isPartial = false) {
});
activeFileId = id;
});
openedFiles.delete(`wi-raw-partial`);
}
}
renderEditor();
}
function parseDiff(originalContent, diff) {
const segments = [];
let currentIndex = 0;
const parts = diff.split('------- SEARCH');
for (let i = 1; i < parts.length; i++) {
const part = parts[i];
const split1 = part.split('=======');
if (split1.length < 2) continue;
const searchContent = split1[0].trim();
const split2 = split1[1].split('+++++++ REPLACE');
if (split2.length < 1) continue;
const replaceContent = split2[0].trim();
const foundIndex = originalContent.indexOf(searchContent, currentIndex);
if (foundIndex !== -1) {
if (foundIndex > currentIndex) {
segments.push({
type: 'text',
content: originalContent.substring(currentIndex, foundIndex)
});
}
segments.push({
type: 'change',
original: searchContent,
new: replaceContent,
active: true
});
currentIndex = foundIndex + searchContent.length;
}
}
if (currentIndex < originalContent.length) {
segments.push({
type: 'text',
content: originalContent.substring(currentIndex)
});
}
return segments;
}
function reconstructDiff(segments) {
return segments
.filter(s => s.type === 'change' && s.active)
.map(s => `------- SEARCH\n${s.original}\n=======\n${s.new}\n+++++++ REPLACE`)
.join('\n');
}