diff --git a/core/auto-char-card/agent-manager.js b/core/auto-char-card/agent-manager.js index ea76a36..e9def67 100644 --- a/core/auto-char-card/agent-manager.js +++ b/core/auto-char-card/agent-manager.js @@ -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); - 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`; + 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); } } diff --git a/core/auto-char-card/api.js b/core/auto-char-card/api.js index 29b4354..4ca6f50 100644 --- a/core/auto-char-card/api.js +++ b/core/auto-char-card/api.js @@ -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 }; diff --git a/core/auto-char-card/char-api.js b/core/auto-char-card/char-api.js index cffdd12..f0bc64c 100644 --- a/core/auto-char-card/char-api.js +++ b/core/auto-char-card/char-api.js @@ -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,9 +112,12 @@ export function updateCharacter(chid, updates) { }); if (changed) { - saveCharacterById(chid); - console.log(`[Amily2 CharAPI] Updated character ${chid}:`, Object.keys(updates)); - return true; + const success = await saveCharacterById(chid); + if (success) { + console.log(`[Amily2 CharAPI] Updated character ${chid}:`, Object.keys(updates)); + return true; + } + return false; } return false; } @@ -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); - console.log(`[Amily2 CharAPI] Added alternate greeting to character ${chid}`); - return true; + 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); - console.log(`[Amily2 CharAPI] Updated greeting ${index} for character ${chid}`); - return true; + 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); - console.log(`[Amily2 CharAPI] Removed greeting ${index} for character ${chid}`); - return true; + 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) { diff --git a/core/auto-char-card/memory-system.js b/core/auto-char-card/memory-system.js index 622199b..31609e4 100644 --- a/core/auto-char-card/memory-system.js +++ b/core/auto-char-card/memory-system.js @@ -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; } } diff --git a/core/auto-char-card/tools.js b/core/auto-char-card/tools.js index 8854817..cd04276 100644 --- a/core/auto-char-card/tools.js +++ b/core/auto-char-card/tools.js @@ -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,30 +196,52 @@ 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)) { - return JSON.stringify({ - status: "error", - code: "SEARCH_NOT_FOUND", - message: `在字段 '${field}' 中未找到搜索块。`, - suggestion: "请确保 SEARCH 块与现有内容完全匹配(包括空格)。" - }); + + if (content.includes(searchBlock)) { + content = content.replace(searchBlock, replaceBlock); + continue; } - content = content.replace(searchBlock, replaceBlock); + + 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: `在字段 '${field}' 中未找到搜索块。`, + suggestion: "请确保 SEARCH 块与现有内容完全匹配(包括空格)。" + }); } - 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: "添加、更新或删除候补开场白。", diff --git a/core/auto-char-card/ui-bindings.js b/core/auto-char-card/ui-bindings.js index 7f962ff..7fc41f1 100644 --- a/core/auto-char-card/ui-bindings.js +++ b/core/auto-char-card/ui-bindings.js @@ -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(''); charSelect.append(''); + let isPrevCharStillPresent = false; characters.forEach((char, index) => { if (char) { - const option = $(''); worldSelect.append(''); + let isPrevWorldStillPresent = false; world_names.forEach(name => { worldSelect.append($('