diff --git a/core/auto-char-card/Amily.png b/core/auto-char-card/Amily.png new file mode 100644 index 0000000..a0fa1ef Binary files /dev/null and b/core/auto-char-card/Amily.png differ diff --git a/core/auto-char-card/agent-manager.js b/core/auto-char-card/agent-manager.js index 775d9c2..ea76a36 100644 --- a/core/auto-char-card/agent-manager.js +++ b/core/auto-char-card/agent-manager.js @@ -12,7 +12,7 @@ export class AgentManager { this.memorySystem = new MemorySystem(); this.currentChid = undefined; this.currentBookName = undefined; - this.status = 'idle'; // idle, running, paused + this.status = 'idle'; this.approvalRequired = false; this.pendingToolCall = null; } @@ -23,7 +23,6 @@ export class AgentManager { if (bookName && bookName !== 'new') { try { - // Use return_full: true to get content for ContextManager (RAG) const bookData = await tools.read_world_info({ book_name: bookName, return_full: true }); const entries = JSON.parse(bookData); this.contextManager.setWorldInfo(entries); @@ -41,30 +40,28 @@ export class AgentManager { this.status = 'idle'; } - async resumeWithApproval(approved, feedback, onStreamUpdate, onPreviewUpdate, onApprovalRequest) { + async resumeWithApproval(approved, feedback, onStreamUpdate, onPreviewUpdate, onApprovalRequest, onContextUpdate) { if (this.status !== 'paused' || !this.pendingToolCall) return; if (approved) { this.status = 'running'; - await this.executePendingTool(onStreamUpdate, onPreviewUpdate); + await this.executePendingTool(onStreamUpdate, onPreviewUpdate, onContextUpdate); this.pendingToolCall = null; - await this.runTaskLoop(onStreamUpdate, onPreviewUpdate, onApprovalRequest); + await this.runTaskLoop(onStreamUpdate, onPreviewUpdate, onApprovalRequest, onContextUpdate); } else { this.status = 'running'; this.pendingToolCall = null; - // Add feedback as user message to guide correction this.history.push({ role: 'user', - content: `[Tool Execution Denied] User Feedback: ${feedback || "No reason provided."}` + content: `[工具执行被拒绝] 用户反馈: ${feedback || "未提供原因。"}` }); - await this.runTaskLoop(onStreamUpdate, onPreviewUpdate, onApprovalRequest); + await this.runTaskLoop(onStreamUpdate, onPreviewUpdate, onApprovalRequest, onContextUpdate); } } async buildSystemPrompt() { const toolDefs = getToolDefinitions(); - // 1. Role & Objective let prompt = `You are an expert Character Card Designer and World Builder. You accomplish a given task iteratively, breaking it down into clear steps and working through them methodically. @@ -98,8 +95,9 @@ ${this.taskState.getPromptContext()} prompt += `- **World Info Book**: ${this.currentBookName}\n`; } - // Dynamic Context Injection (Rules & World Info) - const contextText = this.getLastUserMessage() || ""; + const recentHistory = this.history.slice(-3).map(m => m.content).join('\n'); + const contextText = (recentHistory + "\n" + (this.getLastUserMessage() || "")).trim(); + const { rules, worldInfo } = this.contextManager.getRelevantContext(contextText); if (rules.length > 0) { @@ -116,7 +114,6 @@ ${this.taskState.getPromptContext()} }); } - // Environment Details Injection let envDetails = `\n\n`; envDetails += `# Current Time\n${new Date().toLocaleString()}\n\n`; @@ -136,7 +133,6 @@ ${this.taskState.getPromptContext()} if (this.currentBookName && this.currentBookName !== 'new') { try { - // Get the index for the system prompt const bookData = await tools.read_world_info({ book_name: this.currentBookName, return_full: false }); const result = JSON.parse(bookData); envDetails += `# Current World Book Index\n`; @@ -161,7 +157,6 @@ ${this.taskState.getPromptContext()} envDetails += `\n`; prompt += envDetails; - // 2. Tools prompt += `\n# Tools\n\n`; toolDefs.forEach(tool => { prompt += `## ${tool.name}\n`; @@ -169,7 +164,6 @@ ${this.taskState.getPromptContext()} prompt += `Parameters:\n${JSON.stringify(tool.parameters, null, 2)}\n\n`; }); - // 3. Tool Use Formatting prompt += ` # Tool Use Formatting @@ -191,7 +185,8 @@ Example: # Rules -- **Think First**: Before using any tool, you MUST output a \`\` block explaining your plan and reasoning. +- **Plan First**: Before using any tool, you MUST output a \`\` block listing the steps you intend to take. +- **Think**: After planning, output a \`\` block explaining your reasoning for the immediate next steps. - **One Tool Per Turn**: You can only use ONE tool per message. Wait for the result before proceeding. - **Verify Results**: Always check the [Tool Result] to ensure success. If a tool fails, analyze the error and try again. - **Detailed Writing**: When writing content (Description, First Message, World Info), be creative and detailed. @@ -212,90 +207,78 @@ Example: return null; } - async handleUserMessage(message, onStreamUpdate, onPreviewUpdate, onApprovalRequest) { - // Initialize task state if it's the first message or explicitly requested + async handleUserMessage(message, onStreamUpdate, onPreviewUpdate, onApprovalRequest, onContextUpdate) { 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); + await this.runTaskLoop(onStreamUpdate, onPreviewUpdate, onApprovalRequest, onContextUpdate); } - async runTaskLoop(onStreamUpdate, onPreviewUpdate, onApprovalRequest) { - let maxTurns = 20; // Safety limit + async runTaskLoop(onStreamUpdate, onPreviewUpdate, onApprovalRequest, onContextUpdate) { + let maxTurns = 20; let currentTurn = 0; while (this.status === 'running' && currentTurn < maxTurns) { currentTurn++; - - // 0. Check Memory/Context + const config = getApiConfig('executor'); const currentTokens = this.contextManager.estimateTokens(JSON.stringify(this.history)); if (this.memorySystem.shouldSummarize(this.history, currentTokens, config.maxTokens)) { - onStreamUpdate("Context limit approaching. Summarizing memory...", 'system'); + onStreamUpdate("上下文即将达到上限,正在总结记忆...", 'system'); const summary = await this.memorySystem.summarize(this.history, this.taskState); if (summary) { this.taskState.updateSummary(summary); - // Optional: Compress history here. For now, we just rely on the summary being injected. - // A simple compression strategy: Keep the last 5 messages, and replace the rest with a system note. if (this.history.length > 5) { const lastMessages = this.history.slice(-5); this.history = [ - { role: 'system', content: `[History Compressed] Previous conversation has been summarized in the "Memory & Task State" section. Resuming from recent messages.` }, + { role: 'system', content: `[历史记录已压缩] 之前的对话已总结在“记忆与任务状态”部分。从最近的消息继续。` }, ...lastMessages ]; } } } - // 1. Build System Prompt (Dynamic) const systemPrompt = await this.buildSystemPrompt(); - - // 2. Build Messages + const messages = this.contextManager.buildMessages( systemPrompt, this.history, config.maxTokens ); - // 3. Call AI let responseContent; let fullStreamedContent = ""; try { - onStreamUpdate("Thinking...", 'system'); + onStreamUpdate("思考中...", 'system'); responseContent = await callAi('executor', messages, {}, (chunk) => { onStreamUpdate(chunk, 'stream-assistant'); fullStreamedContent += chunk; - - // Try to parse partial tool call for real-time preview + if (onPreviewUpdate) { const partialTool = this.parsePartialToolCall(fullStreamedContent); if (partialTool) { - onPreviewUpdate(partialTool.name, partialTool.arguments, true); // true = isPartial + onPreviewUpdate(partialTool.name, partialTool.arguments, true); } } }); } catch (error) { - onStreamUpdate(`[Error] ${error.message}`, 'system'); - this.status = 'idle'; // Stop on API error + onStreamUpdate(`[错误] ${error.message}`, 'system'); + this.status = 'idle'; return; } - if (this.status !== 'running') return; // Check if stopped during await + if (this.status !== 'running') return; - // 4. Process Response - - // Check for truncation (Auto-Continue) const lastChar = responseContent.trim().slice(-1); const isTruncated = !['.', '!', '?', '"', "'", '}', ']', '>', '*'].includes(lastChar) && responseContent.length > 100; if (isTruncated && currentTurn < maxTurns) { console.log("检测到回复截断,正在自动继续..."); try { - // Append a continue message const continueMsg = { role: 'user', content: "Continue" }; const continueMessages = [...messages, { role: 'assistant', content: responseContent }, continueMsg]; @@ -316,33 +299,28 @@ Example: if (thinkingMatch) { onStreamUpdate(thinkingMatch[1].trim(), 'thought'); } - - // Clean up content for UI display + let cleanContent = responseContent .replace(/]*)?>[\s\S]*?<\/thinking>/gi, '') - .replace(/<\/thinking>/gi, ''); // Remove residual tags + .replace(/<\/thinking>/gi, ''); const toolNames = Object.keys(tools); const toolRegex = new RegExp(`<(${toolNames.join('|')})(?:\\s+[^>]*)?>[\\s\\S]*?<\\/\\1>`, 'gi'); cleanContent = cleanContent.replace(toolRegex, '').trim(); - // Update the UI with the final clean content (replacing the raw stream) if (cleanContent) { onStreamUpdate(cleanContent, 'assistant'); } - // 5. Parse Tool Call const toolCall = this.parseToolCall(responseContent); if (toolCall) { - // Check for duplicate tool calls to prevent loops if (this.isDuplicateToolCall(toolCall)) { - const warningMsg = `[System Warning] You have just executed this exact tool call (${toolCall.name}). Do not repeat the same action immediately. If you need to check the result again, look at the conversation history. If the previous result was unsatisfactory, try a different approach.`; + const warningMsg = `[系统警告] 你刚刚执行了完全相同的工具调用 (${toolCall.name})。请勿立即重复相同的操作。如果需要再次检查结果,请查看对话历史。如果之前的结果不满意,请尝试不同的方法。`; this.history.push({ role: 'user', content: warningMsg }); continue; } - // Inject Context if missing if (toolCall.name === 'update_character_card' || toolCall.name === 'read_character_card' || toolCall.name === 'edit_character_text' || toolCall.name === 'manage_first_message') { if (toolCall.arguments.chid === undefined && this.currentChid !== undefined) { toolCall.arguments.chid = parseInt(this.currentChid); @@ -361,9 +339,9 @@ Example: if (onApprovalRequest) { onApprovalRequest(toolCall.name, toolCall.arguments); } - return; // Exit loop, wait for resumeWithApproval + return; } else { - await this.executePendingTool(onStreamUpdate, onPreviewUpdate); + await this.executePendingTool(onStreamUpdate, onPreviewUpdate, onContextUpdate); this.pendingToolCall = null; } } else { @@ -372,51 +350,87 @@ Example: } } - async executePendingTool(onStreamUpdate, onPreviewUpdate) { + async executePendingTool(onStreamUpdate, onPreviewUpdate, onContextUpdate) { const toolCall = this.pendingToolCall; if (!toolCall) return; - onStreamUpdate(`Executing: ${toolCall.name}`, 'system'); + onStreamUpdate(`正在执行: ${toolCall.name}`, 'system'); let result; try { if (tools[toolCall.name]) { result = await tools[toolCall.name](toolCall.arguments); - - if (toolCall.name === 'create_character' && result.includes('ID:')) { - const match = result.match(/ID:\s*(\d+)/); - if (match) { - this.currentChid = parseInt(match[1]); + + try { + const jsonResult = JSON.parse(result); + + if (toolCall.name === 'create_character' && jsonResult.status === 'success' && jsonResult.data && jsonResult.data.id) { + this.currentChid = parseInt(jsonResult.data.id); + if (onContextUpdate) onContextUpdate('char', this.currentChid); + } + + if (toolCall.name === 'create_world_book' && jsonResult.status === 'success') { + this.currentBookName = toolCall.arguments.book_name; + if (onContextUpdate) onContextUpdate('world', this.currentBookName); + } + + if (jsonResult._action === 'update_task_state' && jsonResult._updates) { + if (jsonResult._updates.style_reference) { + this.taskState.setStyle(jsonResult._updates.style_reference); + } + } + + if (jsonResult._action === 'stop_and_wait') { + this.status = 'idle'; + } + } catch (e) { + if (toolCall.name === 'create_character' && result.includes('ID:')) { + const match = result.match(/ID:\s*(\d+)/); + if (match) { + this.currentChid = parseInt(match[1]); + if (onContextUpdate) onContextUpdate('char', this.currentChid); + } } } } else { - result = `Error: Tool '${toolCall.name}' not found.`; + result = JSON.stringify({ + status: "error", + code: "TOOL_NOT_FOUND", + message: `错误: 未找到工具 '${toolCall.name}'。` + }); } } catch (error) { - result = `Error executing tool '${toolCall.name}': ${error.message}`; + result = JSON.stringify({ + status: "error", + code: "EXECUTION_ERROR", + message: `执行工具 '${toolCall.name}' 时出错: ${error.message}` + }); } - const toolResultMsg = `[Tool Result for ${toolCall.name}]\n${result}`; + const toolResultMsg = `[工具 '${toolCall.name}' 的执行结果]\n${result}`; this.history.push({ role: 'user', content: toolResultMsg }); - - if (onPreviewUpdate && !result.startsWith('Error')) { + + let isError = false; + try { + const jsonResult = JSON.parse(result); + if (jsonResult.status === 'error') isError = true; + } catch (e) { + if (result.startsWith('Error')) isError = true; + } + + if (onPreviewUpdate && !isError) { onPreviewUpdate(toolCall.name, toolCall.arguments); } } isDuplicateToolCall(toolCall) { if (this.history.length < 3) return false; - - // History structure: - // ... - // Assistant: A (index -3) - // User: [Tool Result A] (index -2) - // Assistant: A (index -1, current) + const prevAssistantMsg = this.history[this.history.length - 3]; const prevUserMsg = this.history[this.history.length - 2]; - if (prevAssistantMsg.role === 'assistant' && prevUserMsg.role === 'user' && prevUserMsg.content.startsWith('[Tool Result')) { + if (prevAssistantMsg.role === 'assistant' && prevUserMsg.role === 'user' && prevUserMsg.content.startsWith('[工具')) { const prevToolCall = this.parseToolCall(prevAssistantMsg.content); if (prevToolCall && prevToolCall.name === toolCall.name && @@ -461,27 +475,22 @@ Example: parsePartialToolCall(content) { const toolNames = Object.keys(tools); for (const name of toolNames) { - // Look for the opening tag const openTagRegex = new RegExp(`<${name}>`); const openMatch = content.match(openTagRegex); if (openMatch) { - // We found a tool start. Now try to extract params, even if incomplete. const startIndex = openMatch.index + openMatch[0].length; const toolContent = content.slice(startIndex); const args = {}; - // Match complete tags or tags that are still open (at the end) - // value... OR value... + const paramRegex = /<(\w+)>([\s\S]*?)(?:<\/\1>|$)/g; let paramMatch; while ((paramMatch = paramRegex.exec(toolContent)) !== null) { const paramName = paramMatch[1]; let paramValue = paramMatch[2]; - - // Don't try to JSON parse partial content here, leave it as string - // The UI handler will deal with partial JSON + args[paramName] = paramValue; } diff --git a/core/auto-char-card/char-api.js b/core/auto-char-card/char-api.js index 1311b3e..cffdd12 100644 --- a/core/auto-char-card/char-api.js +++ b/core/auto-char-card/char-api.js @@ -1,4 +1,26 @@ import { characters, saveCharacterDebounced, this_chid, getCharacters, getRequestHeaders } from "/script.js"; +import { extensionName } from "../../utils/settings.js"; + +async function saveCharacterById(chid) { + const char = characters[chid]; + if (!char) return; + + try { + const response = await fetch('/api/characters/edit', { + method: 'POST', + headers: getRequestHeaders(), + body: JSON.stringify(char) + }); + + if (!response.ok) { + console.error(`[Amily2 CharAPI] Failed to save character ${chid}:`, response.statusText); + } else { + console.log(`[Amily2 CharAPI] Successfully saved character ${chid}`); + } + } catch (e) { + console.error(`[Amily2 CharAPI] Error saving character ${chid}:`, e); + } +} export function getCharacter(chid = this_chid) { if (chid === undefined || chid < 0 || !characters[chid]) { @@ -23,7 +45,7 @@ export function updateCharacter(chid, updates) { }); if (changed) { - saveCharacterDebounced(); + saveCharacterById(chid); console.log(`[Amily2 CharAPI] Updated character ${chid}:`, Object.keys(updates)); return true; } @@ -51,7 +73,7 @@ export function addFirstMessage(chid, message) { } char.data.alternate_greetings.push(message); - saveCharacterDebounced(); + saveCharacterById(chid); console.log(`[Amily2 CharAPI] Added alternate greeting to character ${chid}`); return true; } @@ -71,7 +93,7 @@ export function updateFirstMessage(chid, index, message) { return false; } } - saveCharacterDebounced(); + saveCharacterById(chid); console.log(`[Amily2 CharAPI] Updated greeting ${index} for character ${chid}`); return true; } @@ -92,7 +114,7 @@ export function removeFirstMessage(chid, index) { return false; } } - saveCharacterDebounced(); + saveCharacterById(chid); console.log(`[Amily2 CharAPI] Removed greeting ${index} for character ${chid}`); return true; } @@ -121,10 +143,26 @@ export async function createNewCharacter(name) { formData.append('depth_prompt_depth', '4'); formData.append('depth_prompt_role', 'system'); - const base64Png = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII="; - const res = await fetch(`data:image/png;base64,${base64Png}`); - const blob = await res.blob(); - formData.append('avatar', blob, 'default.png'); + try { + const res = await fetch(`scripts/extensions/third-party/${extensionName}/core/auto-char-card/Amily.png`); + if (res.ok) { + const blob = await res.blob(); + formData.append('avatar', blob, 'default.png'); + } else { + throw new Error('Failed to fetch default avatar'); + } + } catch (e) { + console.warn("[Amily2 CharAPI] Failed to load default avatar, using fallback 1x1 PNG.", e); + const base64Png = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="; + const byteCharacters = atob(base64Png); + const byteNumbers = new Array(byteCharacters.length); + for (let i = 0; i < byteCharacters.length; i++) { + byteNumbers[i] = byteCharacters.charCodeAt(i); + } + const byteArray = new Uint8Array(byteNumbers); + const blob = new Blob([byteArray], { type: 'image/png' }); + formData.append('avatar', blob, 'default.png'); + } const response = await fetch('/api/characters/create', { method: 'POST', diff --git a/core/auto-char-card/context-manager.js b/core/auto-char-card/context-manager.js index 21a4549..a80dad9 100644 --- a/core/auto-char-card/context-manager.js +++ b/core/auto-char-card/context-manager.js @@ -4,6 +4,8 @@ export class ContextManager { this.tokenLimit = 100000; this.rules = []; this.worldInfo = []; + this.activeWorldInfoCache = new Map(); + this.cacheDuration = 3; } addRule(rule) { @@ -40,12 +42,27 @@ export class ContextManager { return contextText.includes(rule.keyword); }); - const relevantWorldInfo = this.worldInfo.filter(entry => { + const currentMatches = this.worldInfo.filter(entry => { if (!entry.enabled) return false; if (!entry.keys || entry.keys.length === 0) return false; return entry.keys.some(key => contextText.includes(key)); }); + for (const [uid, data] of this.activeWorldInfoCache) { + data.turnsLeft--; + if (data.turnsLeft <= 0) { + this.activeWorldInfoCache.delete(uid); + } + } + + currentMatches.forEach(entry => { + this.activeWorldInfoCache.set(entry.id, { turnsLeft: this.cacheDuration }); + }); + + const allRelevantUIDs = new Set([...currentMatches.map(e => e.id), ...this.activeWorldInfoCache.keys()]); + + const relevantWorldInfo = this.worldInfo.filter(entry => allRelevantUIDs.has(entry.id)); + return { rules: relevantRules, worldInfo: relevantWorldInfo diff --git a/core/auto-char-card/memory-system.js b/core/auto-char-card/memory-system.js index 3dcc2f1..622199b 100644 --- a/core/auto-char-card/memory-system.js +++ b/core/auto-char-card/memory-system.js @@ -18,9 +18,46 @@ Format your response as a structured Markdown block. `; } + async extractKeyFacts(history) { + const extractionPrompt = ` +Analyze the recent conversation and extract "Key Facts" that should be remembered long-term. +Key Facts include: +- Specific decisions made (e.g., "Character has blue eyes", "Weapon is a sword"). +- User preferences stated (e.g., "User dislikes horror"). +- Completed milestones. + +Do NOT include temporary conversation details or planning steps. +Return the facts as a JSON array of strings. Example: ["Eyes: Blue", "Class: Mage"]. +Output ONLY valid JSON. +`; + const recentHistory = history.slice(-5); + const messages = [ + { role: 'system', content: extractionPrompt }, + ...recentHistory + ]; + + try { + const response = await callAi('executor', messages, { + max_tokens: 500, + temperature: 0.3 + }); + const cleanResponse = response.replace(/```json/g, '').replace(/```/g, '').trim(); + const facts = JSON.parse(cleanResponse); + return Array.isArray(facts) ? facts : []; + } catch (error) { + console.warn("Failed to extract key facts:", error); + return []; + } + } + async summarize(history, taskState) { const config = getApiConfig('executor'); + const newFacts = await this.extractKeyFacts(history); + if (newFacts.length > 0) { + taskState.addKeyFacts(newFacts); + } + const contextMsg = ` [System Note]: The following is the current Task State. Use this to inform your summary. ${taskState.getPromptContext()} diff --git a/core/auto-char-card/task-state.js b/core/auto-char-card/task-state.js index c743206..6074f47 100644 --- a/core/auto-char-card/task-state.js +++ b/core/auto-char-card/task-state.js @@ -6,10 +6,12 @@ export class TaskState { reset() { this.originalRequest = ""; this.currentGoal = ""; - this.completedSteps = []; // Array of strings - this.pendingSteps = []; // Array of strings - this.summary = ""; // The structured summary of the character/world so far - this.generatedData = {}; // Key-value pairs of generated attributes (e.g., name, personality) + this.completedSteps = []; + this.pendingSteps = []; + this.summary = ""; + this.generatedData = {}; + this.style_reference = ""; + this.keyFacts = []; this.lastSummaryTimestamp = 0; } @@ -41,11 +43,23 @@ export class TaskState { this.generatedData[key] = value; } + setStyle(style) { + this.style_reference = style; + } + + addKeyFacts(facts) { + this.keyFacts.push(...facts); + } + getPromptContext() { let context = `\n# Task State\n`; context += `- **Original Request**: ${this.originalRequest}\n`; context += `- **Current Goal**: ${this.currentGoal}\n`; + if (this.style_reference) { + context += `- **Style Reference**: ${this.style_reference}\n`; + } + if (this.completedSteps.length > 0) { context += `- **Completed Steps**:\n${this.completedSteps.map(s => ` - ${s}`).join('\n')}\n`; } @@ -54,8 +68,13 @@ export class TaskState { context += `- **Pending Steps**:\n${this.pendingSteps.map(s => ` - ${s}`).join('\n')}\n`; } + if (this.keyFacts.length > 0) { + context += `\n# Key Facts (Long Term Memory)\n`; + this.keyFacts.forEach(fact => context += `- ${fact}\n`); + } + if (this.summary) { - context += `\n# Memory & Context Summary\n${this.summary}\n`; + context += `\n# Recent Context Summary\n${this.summary}\n`; } return context; @@ -69,6 +88,8 @@ export class TaskState { pendingSteps: this.pendingSteps, summary: this.summary, generatedData: this.generatedData, + style_reference: this.style_reference, + keyFacts: this.keyFacts, lastSummaryTimestamp: this.lastSummaryTimestamp }; } @@ -81,6 +102,8 @@ export class TaskState { this.pendingSteps = json.pendingSteps || []; this.summary = json.summary || ""; this.generatedData = json.generatedData || {}; + this.style_reference = json.style_reference || ""; + this.keyFacts = json.keyFacts || []; this.lastSummaryTimestamp = json.lastSummaryTimestamp || 0; } } diff --git a/core/auto-char-card/tools.js b/core/auto-char-card/tools.js index b092305..8854817 100644 --- a/core/auto-char-card/tools.js +++ b/core/auto-char-card/tools.js @@ -1,5 +1,6 @@ import { amilyHelper } from "../tavern-helper/main.js"; import * as charApi from "./char-api.js"; +import { callAi } from "./api.js"; export const tools = { @@ -17,12 +18,12 @@ export const tools = { return { uid: e.uid, keys: keys, - comment: e.comment || keys || "Unnamed Entry", + comment: e.comment || keys || "未命名条目", }; }); return JSON.stringify({ - info: "Index of world book entries. Use 'read_world_entry' with 'uid' to read specific content.", + info: "世界书条目索引。请使用带有 'uid' 的 'read_world_entry' 来读取具体内容。", total_entries: entries.length, entries: summary }, null, 2); @@ -33,10 +34,18 @@ export const tools = { const entry = entries.find(e => String(e.uid) === String(uid)); if (!entry) { - return `Entry with UID ${uid} not found in world book "${book_name}".`; + return JSON.stringify({ + status: "error", + code: "ENTRY_NOT_FOUND", + message: `在世界书 "${book_name}" 中未找到 UID 为 ${uid} 的条目。`, + suggestion: "请使用 'read_world_info' 查看可用的 UID。" + }); } - return JSON.stringify(entry, null, 2); + return JSON.stringify({ + status: "success", + data: entry + }, null, 2); }, write_world_info_entry: async ({ book_name, entries }) => { @@ -45,14 +54,22 @@ export const tools = { const cleanEntries = entries.replace(/```json/g, '').replace(/```/g, '').trim(); entries = JSON.parse(cleanEntries); } catch (e) { - return `错误: 'entries' 参数必须是有效的 JSON 数组。解析错误: ${e.message}`; + return JSON.stringify({ + status: "error", + code: "INVALID_JSON", + message: `'entries' 参数必须是有效的 JSON 数组。解析错误: ${e.message}` + }); } } if (!Array.isArray(entries)) { if (typeof entries === 'object' && entries !== null) { entries = [entries]; } else { - return "错误: 'entries' 参数必须是数组或对象。"; + return JSON.stringify({ + status: "error", + code: "INVALID_TYPE", + message: "'entries' 参数必须是数组或对象。" + }); } } @@ -67,26 +84,61 @@ export const tools = { } } - let resultMsg = ""; + let updatedCount = 0; + let createdCount = 0; + let errors = []; + if (updates.length > 0) { const success = await amilyHelper.setLorebookEntries(book_name, updates); - resultMsg += success ? `成功更新了 ${updates.length} 个条目。 ` : `更新条目失败。 `; + if (success) updatedCount = updates.length; + else errors.push("更新条目失败。"); } if (creates.length > 0) { const success = await amilyHelper.createLorebookEntries(book_name, creates); - resultMsg += success ? `成功创建了 ${creates.length} 个条目。 ` : `创建条目失败。 `; + if (success) createdCount = creates.length; + else errors.push("创建条目失败。"); } - return resultMsg || "未执行任何操作。"; + + if (errors.length > 0 && updatedCount === 0 && createdCount === 0) { + return JSON.stringify({ + status: "error", + code: "WRITE_FAILED", + message: errors.join(" ") + }); + } + + return JSON.stringify({ + status: "success", + message: `成功更新了 ${updatedCount} 个条目,创建了 ${createdCount} 个条目。`, + data: { updated: updatedCount, created: createdCount } + }); }, create_world_book: async ({ book_name }) => { const success = await amilyHelper.createLorebook(book_name); - return success ? `世界书 "${book_name}" 创建成功。` : `创建世界书 "${book_name}" 失败。`; + if (success) { + return JSON.stringify({ + status: "success", + message: `世界书 "${book_name}" 创建成功。` + }); + } else { + return JSON.stringify({ + status: "error", + code: "CREATE_FAILED", + message: `创建世界书 "${book_name}" 失败。` + }); + } }, read_character_card: async ({ chid }) => { const char = charApi.getCharacter(chid); - if (!char) return "未找到角色。"; + if (!char) { + return JSON.stringify({ + status: "error", + code: "CHAR_NOT_FOUND", + message: "未找到角色。" + }); + } const safeChar = { name: char.name, @@ -97,7 +149,10 @@ export const tools = { mes_example: char.mes_example, alternate_greetings: char.data?.alternate_greetings || [] }; - return JSON.stringify(safeChar, null, 2); + return JSON.stringify({ + status: "success", + data: safeChar + }, null, 2); }, update_character_card: async (args) => { @@ -107,25 +162,42 @@ export const tools = { const success = charApi.updateCharacter(chid, finalUpdates); if (success) { const updatedFields = Object.keys(finalUpdates).join(', '); - return `角色卡更新成功 [ID: ${chid}]。已更新字段: ${updatedFields}。`; + return JSON.stringify({ + status: "success", + message: `角色卡更新成功 [ID: ${chid}]。`, + data: { updated_fields: updatedFields } + }); } else { - return "更新角色卡失败。"; + return JSON.stringify({ + status: "error", + code: "UPDATE_FAILED", + message: "更新角色卡失败。" + }); } }, edit_character_text: async ({ chid, field, diff }) => { const char = charApi.getCharacter(chid); - if (!char) return "未找到角色。"; + if (!char) { + return JSON.stringify({ + status: "error", + code: "CHAR_NOT_FOUND", + message: "未找到角色。" + }); + } const allowedFields = ['description', 'personality', 'scenario', 'first_mes', 'mes_example']; if (!allowedFields.includes(field)) { - return `无效的字段。允许的字段: ${allowedFields.join(', ')}`; + return JSON.stringify({ + status: "error", + code: "INVALID_FIELD", + message: `无效的字段。允许的字段: ${allowedFields.join(', ')}` + }); } let content = char[field] || ''; const changes = diff.split('------- SEARCH'); - - // Remove the first empty split if any + if (changes[0].trim() === '') changes.shift(); for (const change of changes) { @@ -136,14 +208,30 @@ export const tools = { const replaceBlock = parts[1].split('+++++++ REPLACE')[0].trim(); if (!content.includes(searchBlock)) { - return `错误: 在字段 '${field}' 中未找到以下搜索块:\n${searchBlock}`; + return JSON.stringify({ + status: "error", + code: "SEARCH_NOT_FOUND", + message: `在字段 '${field}' 中未找到搜索块。`, + suggestion: "请确保 SEARCH 块与现有内容完全匹配(包括空格)。" + }); } content = content.replace(searchBlock, replaceBlock); } const success = charApi.updateCharacter(chid, { [field]: content }); - return success ? `字段 '${field}' 更新成功。` : `更新字段 '${field}' 失败。`; + if (success) { + return JSON.stringify({ + status: "success", + message: `字段 '${field}' 更新成功。` + }); + } else { + return JSON.stringify({ + status: "error", + code: "UPDATE_FAILED", + message: `更新字段 '${field}' 失败。` + }); + } }, manage_first_message: async ({ action, chid, index, message }) => { @@ -159,16 +247,140 @@ export const tools = { success = charApi.removeFirstMessage(chid, index); break; default: - return "无效的操作。"; + return JSON.stringify({ + status: "error", + code: "INVALID_ACTION", + message: "无效的操作。" + }); + } + + if (success) { + return JSON.stringify({ + status: "success", + message: `开场白 ${action} 成功。` + }); + } else { + return JSON.stringify({ + status: "error", + code: "ACTION_FAILED", + message: `开场白 ${action} 失败。` + }); } - return success ? `开场白 ${action} 成功。` : `开场白 ${action} 失败。`; }, create_character: async ({ name }) => { const result = await charApi.createNewCharacter(name); - if (result === -1) return "创建角色失败。"; - if (result === -2) return "角色创建请求已发送。请手动刷新角色列表以查看新角色。"; - return `角色创建成功,ID: ${result}`; + if (result === -1) { + return JSON.stringify({ + status: "error", + code: "CREATE_FAILED", + message: "创建角色失败。" + }); + } + if (result === -2) { + return JSON.stringify({ + status: "warning", + code: "CREATE_PENDING", + message: "角色创建请求已发送。请手动刷新。" + }); + } + return JSON.stringify({ + status: "success", + message: `角色创建成功。`, + data: { id: result } + }); + }, + + simulate_chat: async ({ chid, message }) => { + const char = charApi.getCharacter(chid); + if (!char) { + return JSON.stringify({ + status: "error", + code: "CHAR_NOT_FOUND", + message: "未找到角色。" + }); + } + + const systemPrompt = `You are roleplaying as ${char.name}. +Description: ${char.description} +Personality: ${char.personality} +Scenario: ${char.scenario} + +Reply to the user's message in character. Stay in character.`; + + const messages = [ + { role: 'system', content: systemPrompt }, + { role: 'user', content: message } + ]; + + try { + const response = await callAi('executor', messages, { temperature: 0.9 }); + return JSON.stringify({ + status: "success", + data: { + character: char.name, + response: response + } + }); + } catch (error) { + return JSON.stringify({ + status: "error", + code: "SIMULATION_FAILED", + message: `模拟对话失败: ${error.message}` + }); + } + }, + + set_style_reference: async ({ style }) => { + + return JSON.stringify({ + status: "success", + message: `样式参考已设置为: ${style}`, + _action: "update_task_state", + _updates: { style_reference: style } + }); + }, + + analyze_entities: async ({ text }) => { + const systemPrompt = `You are an expert World Builder and Entity Extractor. +Analyze the provided text and identify key entities that should have their own World Info (Lorebook) entries. +Focus on: +- Proper Nouns (People, Places, Organizations, Artifacts) +- Unique Concepts (Magic systems, Historical events, Species) + +Return a JSON object with a "entities" array. Each entity should have: +- "name": The name of the entity. +- "type": The type (Person, Location, Organization, etc.). +- "description": A brief summary based on the text (1-2 sentences). +- "confidence": A score (0-1) of how important this entity seems. + +Output ONLY valid JSON.`; + + const messages = [ + { role: 'system', content: systemPrompt }, + { role: 'user', content: text } + ]; + + try { + const response = await callAi('executor', messages, { temperature: 0.1 }); + const cleanResponse = response.replace(/```json/g, '').replace(/```/g, '').trim(); + return cleanResponse; + } catch (error) { + return JSON.stringify({ + status: "error", + code: "ANALYSIS_FAILED", + message: `实体分析失败: ${error.message}` + }); + } + }, + + ask_user: async ({ question }) => { + return JSON.stringify({ + status: "success", + message: `已向用户提问: ${question}`, + _action: "stop_and_wait", + data: { question } + }); } }; @@ -176,50 +388,50 @@ export function getToolDefinitions() { return [ { name: "read_world_info", - description: "Read the index (list of entries with keys and comments) of a world book. Does NOT return full content.", + description: "读取世界书的索引(包含关键字和注释的条目列表)。不返回完整内容。", parameters: { type: "object", properties: { - book_name: { type: "string", description: "The name of the world book." } + book_name: { type: "string", description: "世界书名称。" } }, required: ["book_name"] } }, { name: "read_world_entry", - description: "Read the full content of a specific world book entry.", + description: "读取特定世界书条目的完整内容。", parameters: { type: "object", properties: { - book_name: { type: "string", description: "The name of the world book." }, - uid: { type: "number", description: "The UID of the entry to read." } + book_name: { type: "string", description: "世界书名称。" }, + uid: { type: "number", description: "要读取的条目 UID。" } }, required: ["book_name", "uid"] } }, { name: "write_world_info_entry", - description: "Create or update entries in a world book.", + description: "创建或更新世界书中的条目。", parameters: { type: "object", properties: { - book_name: { type: "string", description: "The name of the world book." }, + book_name: { type: "string", description: "世界书名称。" }, entries: { type: "array", items: { type: "object", properties: { - uid: { type: "number", description: "Entry ID (optional, for update)." }, - comment: { type: "string", description: "Entry title/comment." }, - content: { type: "string", description: "Entry content." }, - key: { type: "array", items: { type: "string" }, description: "Keywords." }, - enabled: { type: "boolean", description: "Is enabled." }, - constant: { type: "boolean", description: "Constant (Blue light)." }, - position: { type: "string", enum: ["before_character_definition", "after_character_definition", "before_author_note", "after_author_note", "at_depth_as_system"], description: "Insertion position." }, - depth: { type: "number", description: "Insertion depth." }, - scanDepth: { type: "number", description: "Scan depth." }, - exclude_recursion: { type: "boolean", description: "Exclude from recursion." }, - prevent_recursion: { type: "boolean", description: "Prevent recursion." } + uid: { type: "number", description: "条目 ID(可选,用于更新)。" }, + comment: { type: "string", description: "条目标题/注释。" }, + content: { type: "string", description: "条目内容。" }, + key: { type: "array", items: { type: "string" }, description: "关键字。" }, + enabled: { type: "boolean", description: "是否启用。" }, + constant: { type: "boolean", description: "常驻(蓝灯)。" }, + position: { type: "string", enum: ["before_character_definition", "after_character_definition", "before_author_note", "after_author_note", "at_depth_as_system"], description: "插入位置。" }, + depth: { type: "number", description: "插入深度。" }, + scanDepth: { type: "number", description: "扫描深度。" }, + exclude_recursion: { type: "boolean", description: "排除递归。" }, + prevent_recursion: { type: "boolean", description: "防止递归。" } } } } @@ -229,33 +441,33 @@ export function getToolDefinitions() { }, { name: "create_world_book", - description: "Create a new empty world book.", + description: "创建一个新的空世界书。", parameters: { type: "object", properties: { - book_name: { type: "string", description: "The name of the new world book." } + book_name: { type: "string", description: "新世界书的名称。" } }, required: ["book_name"] } }, { name: "read_character_card", - description: "Read character card data.", + description: "读取角色卡数据。", parameters: { type: "object", properties: { - chid: { type: "number", description: "Character ID." } + chid: { type: "number", description: "角色 ID。" } }, required: ["chid"] } }, { name: "update_character_card", - description: "Update character card fields (overwrite).", + description: "更新角色卡字段(覆盖)。", parameters: { type: "object", properties: { - chid: { type: "number", description: "Character ID." }, + chid: { type: "number", description: "角色 ID。" }, name: { type: "string" }, description: { type: "string" }, personality: { type: "string" }, @@ -268,15 +480,15 @@ export function getToolDefinitions() { }, { name: "edit_character_text", - description: "Edit a specific text field of a character using SEARCH/REPLACE blocks.", + description: "使用 搜索/替换 块编辑角色的特定文本字段。", parameters: { type: "object", properties: { - chid: { type: "number", description: "Character ID." }, - field: { type: "string", enum: ["description", "personality", "scenario", "first_mes", "mes_example"], description: "The field to edit." }, + chid: { type: "number", description: "角色 ID。" }, + field: { type: "string", enum: ["description", "personality", "scenario", "first_mes", "mes_example"], description: "要编辑的字段。" }, diff: { type: "string", - description: "One or more SEARCH/REPLACE blocks following this exact format:\n------- SEARCH\n[exact content to find]\n=======\n[new content to replace with]\n+++++++ REPLACE" + description: "一个或多个遵循此确切格式的 搜索/替换 块:\n------- SEARCH\n[exact content to find]\n=======\n[new content to replace with]\n+++++++ REPLACE" } }, required: ["chid", "field", "diff"] @@ -284,28 +496,73 @@ export function getToolDefinitions() { }, { name: "manage_first_message", - description: "Add, update, or remove alternate greetings.", + description: "添加、更新或删除候补开场白。", parameters: { type: "object", properties: { action: { type: "string", enum: ["add", "update", "remove"] }, - chid: { type: "number", description: "Character ID." }, - index: { type: "number", description: "Index of the greeting (required for update/remove)." }, - message: { type: "string", description: "Content of the greeting (required for add/update)." } + chid: { type: "number", description: "角色 ID。" }, + index: { type: "number", description: "开场白索引(更新/删除时必需)。" }, + message: { type: "string", description: "开场白内容(添加/更新时必需)。" } }, required: ["action", "chid"] } }, { name: "create_character", - description: "Create a new character card.", + description: "创建一个新角色卡。", parameters: { type: "object", properties: { - name: { type: "string", description: "Name of the new character." } + name: { type: "string", description: "新角色的名字。" } }, required: ["name"] } + }, + { + name: "simulate_chat", + description: "与角色模拟对话以测试其性格和设定。", + parameters: { + type: "object", + properties: { + chid: { type: "number", description: "角色 ID。" }, + message: { type: "string", description: "发送给角色的消息。" } + }, + required: ["chid", "message"] + } + }, + { + name: "set_style_reference", + description: "设置生成内容的风格参考或模板(例如:'黑暗奇幻风格','莎士比亚风格','JSON格式模板')。", + parameters: { + type: "object", + properties: { + style: { type: "string", description: "风格描述或模板内容。" } + }, + required: ["style"] + } + }, + { + name: "analyze_entities", + description: "分析文本并提取潜在的世界书条目(实体)。", + parameters: { + type: "object", + properties: { + text: { type: "string", description: "要分析的文本。" } + }, + required: ["text"] + } + }, + { + name: "ask_user", + description: "向用户提问以获取更多信息或确认。这将暂停自动执行并等待用户回复。", + parameters: { + type: "object", + properties: { + question: { type: "string", description: "要问的问题。" } + }, + required: ["question"] + } } ]; } diff --git a/core/auto-char-card/ui-bindings.js b/core/auto-char-card/ui-bindings.js index 15bc05e..7f962ff 100644 --- a/core/auto-char-card/ui-bindings.js +++ b/core/auto-char-card/ui-bindings.js @@ -1,6 +1,6 @@ import { extensionName } from "../../utils/settings.js"; import { AgentManager } from "./agent-manager.js"; -import { characters, this_chid, saveSettingsDebounced } from "/script.js"; +import { characters, this_chid, saveSettingsDebounced, getCharacters } from "/script.js"; import { world_names } from "/scripts/world-info.js"; import { getApiConfig, setApiConfig, testConnection, fetchModels } from "./api.js"; import { tools } from "./tools.js"; @@ -77,6 +77,22 @@ function populateDropdowns() { }); } +async function handleContextUpdate(type, value) { + console.log(`[Amily2 AutoCharCard] Context Update: ${type} -> ${value}`); + + if (type === 'char') { + await getCharacters(); // Force refresh character list + } + + populateDropdowns(); + + if (type === 'char') { + $('#acc-target-char').val(value); + } else if (type === 'world') { + $('#acc-target-world').val(value); + } +} + function renderRulesList() { const list = $('#acc-rules-list'); list.empty(); @@ -145,7 +161,6 @@ function bindEvents() { const chid = id; const field = subId; - // Check if already open const fileId = `char-${chid}-${field}`; if (openedFiles.has(fileId)) { activeFileId = fileId; @@ -153,32 +168,33 @@ function bindEvents() { return; } - // Fetch content if needed (we might have it cached in previousCharData or need to fetch) - // For simplicity, fetch again or use cache let content = ''; - if (previousCharData && previousCharData.name) { // Simple check if loaded - if (field.startsWith('greeting_')) { - const index = parseInt(field.split('_')[1]); - content = previousCharData.alternate_greetings[index]; - } else { - content = previousCharData[field]; - } - } else { - // Fetch - try { - const charData = await tools.read_character_card({ chid }); - const char = JSON.parse(charData); - previousCharData = char; - if (field.startsWith('greeting_')) { - const index = parseInt(field.split('_')[1]); - content = char.alternate_greetings[index]; - } else { - content = char[field]; - } - } catch (e) { - toastr.error('无法读取角色卡内容'); - return; + + + try { + console.log(`[AutoCharCard] Reading char ${chid}, field ${field}`); + const charData = await tools.read_character_card({ chid }); + const response = JSON.parse(charData); + + if (response.status !== 'success' || !response.data) { + throw new Error(response.message || 'Unknown error'); } + + const char = response.data; + previousCharData = char; + console.log(`[AutoCharCard] Char data:`, char); + + if (field.startsWith('greeting_')) { + const index = parseInt(field.split('_')[1]); + content = char.alternate_greetings[index]; + } else { + content = char[field]; + } + console.log(`[AutoCharCard] Content for ${field}:`, content); + } catch (e) { + console.error(e); + toastr.error('无法读取角色卡内容'); + return; } openedFiles.set(fileId, { @@ -203,7 +219,13 @@ function bindEvents() { try { const entryData = await tools.read_world_entry({ book_name: bookName, uid: uid }); - const entry = JSON.parse(entryData); + const response = JSON.parse(entryData); + + if (response.status !== 'success' || !response.data) { + throw new Error(response.message || 'Unknown error'); + } + + const entry = response.data; let keys = entry.key; if (Array.isArray(keys)) keys = keys.join(', '); @@ -217,6 +239,7 @@ function bindEvents() { activeFileId = fileId; renderEditor(); } catch (e) { + console.error(e); toastr.error('无法读取世界书条目'); } } @@ -269,9 +292,6 @@ function bindEvents() { } }); - // Removed old approval buttons handlers - - // Add Refresh Button to Preview Header if not exists const previewHeader = $('.acc-right-panel .acc-panel-header'); if (previewHeader.find('#acc-refresh-preview').length === 0) { const refreshBtn = $('