From 92768bc585a10c25bb33b646db6ecaf5dd8cc18d Mon Sep 17 00:00:00 2001 From: Wx-2025 <351320169@qq.com> Date: Tue, 23 Dec 2025 08:40:01 +0800 Subject: [PATCH] Add files via upload --- core/auto-char-card/agent-manager.js | 336 +++++++++++++++++++ core/auto-char-card/api.js | 122 +++++++ core/auto-char-card/char-api.js | 150 +++++++++ core/auto-char-card/context-manager.js | 63 ++++ core/auto-char-card/tools.js | 248 ++++++++++++++ core/auto-char-card/ui-bindings.js | 431 +++++++++++++++++++++++++ 6 files changed, 1350 insertions(+) create mode 100644 core/auto-char-card/agent-manager.js create mode 100644 core/auto-char-card/api.js create mode 100644 core/auto-char-card/char-api.js create mode 100644 core/auto-char-card/context-manager.js create mode 100644 core/auto-char-card/tools.js create mode 100644 core/auto-char-card/ui-bindings.js diff --git a/core/auto-char-card/agent-manager.js b/core/auto-char-card/agent-manager.js new file mode 100644 index 0000000..428deb9 --- /dev/null +++ b/core/auto-char-card/agent-manager.js @@ -0,0 +1,336 @@ +import { callAi, getApiConfig } from "./api.js"; +import { tools, getToolDefinitions } from "./tools.js"; +import { ContextManager } from "./context-manager.js"; + +export class AgentManager { + constructor() { + this.history = []; + this.executorHistory = []; + this.reviewerHistory = []; + this.executorSystemPrompt = this.buildExecutorSystemPrompt(); + this.reviewerSystemPrompt = this.buildReviewerSystemPrompt(); + this.contextManager = new ContextManager(); + this.currentChid = undefined; + this.currentBookName = undefined; + } + + setContext(chid, bookName) { + this.currentChid = chid; + this.currentBookName = bookName; + this.executorSystemPrompt = this.buildExecutorSystemPrompt(); + } + + buildReviewerSystemPrompt() { + const toolDefs = getToolDefinitions(); + let prompt = `你是一个经验丰富的角色卡设计师和辅导员(Reviewer)。你的搭档是一个执行力强且富有创造力的 AI 助手(Executor)。 +你的目标是根据用户的需求,设计出高质量的角色卡和世界书方案,并指导 Executor 一步步实现。 + +Executor 拥有以下工具(你不能直接使用,但需要知道它能做什么): +`; + toolDefs.forEach(tool => { + prompt += `- ${tool.name}: ${tool.description}\n`; + }); + + prompt += ` +### 世界书高级设置指南 (World Info Settings) +- **constant (蓝灯)**: 如果为 true,该条目将始终被激活并包含在上下文中,忽略关键词触发。 +- **position (插入位置)**: 决定条目内容在 Prompt 中的位置。 + - \`before/after_character_definition\`: 角色定义前后。 + - \`before/after_author_note\`: 作者注释前后。 + - \`at_depth_as_system\`: 在指定深度作为系统消息插入(推荐)。 +- **depth (插入深度)**: 仅当 position 为 \`at_depth_as_system\` 时有效。表示条目距离最新消息的距离(例如 0 为最新,4 为倒数第 4 条消息后)。 +- **scanDepth (扫描深度)**: 系统扫描关键词的消息范围。例如 2 表示只扫描最近 2 条消息。 +- **exclude_recursion**: 如果为 true,此条目的内容不会触发其他条目。 +- **prevent_recursion**: 如果为 true,其他条目的内容不会触发此条目。 + +你的工作流程: +1. 分析用户需求。 +2. 制定详细的实施计划(大纲)。 +3. 将计划拆解为 Executor 可以执行的**指导性指令**。 +4. 审查 Executor 的执行结果,提出修改意见。 + +**关键原则:** +- **只给方案,不给成品**:你负责提供创意方向、关键设定点和风格指导,让 Executor 去进行具体的文本创作和扩写。不要直接把完整的角色描述或世界书内容写出来让 Executor 照抄。 +- **示例**: + - ❌ 错误:“请写入以下描述:她有一头金发,性格傲娇...” + - ✅ 正确:“请为角色撰写一段详细的外貌和性格描述。外貌上要突出她的金发和贵族气质,性格上要体现出‘傲娇’的特点,即外表冷漠但内心渴望被关怀。请发挥你的文采。” + +交互规则: +- 当你需要 Executor 执行操作时,请在回复的最后一行使用标签:你的指令 +- **重要**: 标签内必须是**自然语言指令**。**严禁**直接输出 JSON 代码块作为指令。 +- **单步原则**:每次指令**只能包含一个**具体的任务(例如:只创建一个世界书条目,或只更新角色描述)。严禁一次性下达多个任务。 +- **字数强制**:在指令中必须明确要求 Executor 进行深度扩写。 + - 世界书条目:要求**不低于 300 字**。 + - 角色开场白:要求**不低于 1500 字**。 +- 当你认为任务已完成或需要用户反馈时,直接回复用户即可,不要包含 标签。 +`; + return prompt; + } + + buildExecutorSystemPrompt() { + const toolDefs = getToolDefinitions(); + let contextInfo = ""; + + if (this.currentChid === 'new') { + contextInfo += `**注意:用户希望创建一个新角色。**\n请首先使用 \`create_character\` 工具创建角色。创建成功后,你将获得新的角色 ID,请使用该 ID 进行后续操作(如 \`update_character_card\`)。\n`; + } else if (this.currentChid !== undefined) { + contextInfo += `当前操作的角色ID: ${this.currentChid}\n`; + } + + if (this.currentBookName) { + contextInfo += `当前操作的世界书: ${this.currentBookName}\n`; + } + + let prompt = `你是一个专业的角色卡构建助手(Executor)。你的目标是根据 Reviewer 的指导和用户的需求,在当前选定的“工作区”(角色卡和世界书)中进行**创作**和修改。 + +${contextInfo} + +**你的职责:** +1. **理解指令**:仔细阅读 Reviewer 的指导性指令。 +2. **深度扩写**:这是你的核心任务。Reviewer 给出的只是大纲,你需要将其扩写成丰富、细腻的文学作品。 + - **世界书条目**:必须丰富细节,字数**不低于 300 字**。 + - **角色开场白**:必须包含环境描写、心理活动、动作细节,字数**不低于 1500 字**。 +3. **执行操作**:使用工具将你创作的内容写入系统。 + +TOOL USE + +你拥有以下工具可以使用。你可以使用这些工具来完成任务。每次回复只能使用一个工具。 + +# Tools + +`; + + toolDefs.forEach(tool => { + prompt += `## ${tool.name}\n`; + prompt += `Description: ${tool.description}\n`; + prompt += `Parameters:\n${JSON.stringify(tool.parameters, null, 2)}\n\n`; + }); + + prompt += ` +### 世界书高级设置指南 (World Info Settings) +- **constant (蓝灯)**: 如果为 true,该条目将始终被激活并包含在上下文中,忽略关键词触发。 +- **position (插入位置)**: 决定条目内容在 Prompt 中的位置。 + - \`before/after_character_definition\`: 角色定义前后。 + - \`before/after_author_note\`: 作者注释前后。 + - \`at_depth_as_system\`: 在指定深度作为系统消息插入(推荐)。 +- **depth (插入深度)**: 仅当 position 为 \`at_depth_as_system\` 时有效。表示条目距离最新消息的距离(例如 0 为最新,4 为倒数第 4 条消息后)。 +- **scanDepth (扫描深度)**: 系统扫描关键词的消息范围。例如 2 表示只扫描最近 2 条消息。 +- **exclude_recursion**: 如果为 true,此条目的内容不会触发其他条目。 +- **prevent_recursion**: 如果为 true,其他条目的内容不会触发此条目。 + +# Tool Use Formatting + +工具调用必须使用以下 XML 格式。工具名称包含在开始和结束标签中,每个参数也包含在自己的标签中: + +<工具名称> +<参数1>值1 +<参数2>值2 +... + + +**注意**:对于复杂参数(如数组或对象),请直接在标签内写入 **JSON 字符串**。 + +例如: + +MyWorld +[{"key": "Entry1", "content": "..."}] + + +# Tool Use Guidelines + +1. **必须思考 (Mandatory Thinking)**: 在调用任何工具之前,你**必须**先输出一段思考过程,解释你为什么要这样做,以及你打算如何创作内容。请使用 \`\` 标签包裹你的思考。**严禁**直接输出工具调用而不进行思考。 +2. **单步执行**: 每次回复只能使用**一个**工具。必须等待工具执行结果(成功或失败)后,才能决定并执行下一步操作。 +3. **等待确认**: 永远不要假设工具执行成功。必须根据实际返回的结果来判断。 +4. **参数完整性**: 确保提供所有必需的参数。 + +# Capabilities + +- 你可以读取和修改当前绑定的世界书(World Info)。 +- 你可以读取和修改当前角色的详细信息(Name, Description, Personality, Scenario, First Message, etc.)。 +- 你可以管理角色的开场白(添加、修改、删除)。 + +# Rules + +1. **工作区**: 你始终在当前选定的角色卡和世界书上下文中操作。 +2. **路径**: 如果涉及文件路径(虽然主要通过 API 操作),请认为是相对于工作区的虚拟路径。 +3. **完成任务**: 当你认为任务已经完成时,请向用户汇报结果。不要在汇报结果后继续提问。 + +现在,请开始你的工作。 +`; + return prompt; + } + + async handleUserMessage(message, onStreamUpdate, onPreviewUpdate) { + this.history.push({ role: 'user', content: message }); + + this.reviewerHistory.push({ role: 'user', content: message }); + + await this.runDualAgentLoop(onStreamUpdate, onPreviewUpdate); + } + + async runDualAgentLoop(onStreamUpdate, onPreviewUpdate) { + let maxLoops = 3; + let currentLoop = 0; + + while (currentLoop < maxLoops) { + currentLoop++; + + onStreamUpdate("Reviewer (模型B) 正在思考...", 'system'); + + const reviewerConfig = getApiConfig('reviewer'); + const reviewerMessages = this.contextManager.buildMessages( + this.reviewerSystemPrompt, + this.reviewerHistory, + reviewerConfig.maxTokens + ); + + let reviewerResponse; + try { + reviewerResponse = await callAi('reviewer', reviewerMessages); + } catch (error) { + onStreamUpdate(`[Reviewer 错误] ${error.message}`, 'system'); + return; + } + + const instructionMatch = reviewerResponse.match(/([\s\S]*?)<\/instruction>/); + const instruction = instructionMatch ? instructionMatch[1].trim() : null; + + const displayContent = reviewerResponse.replace(/[\s\S]*?<\/instruction>/, '').trim(); + + if (displayContent) { + onStreamUpdate(displayContent, 'assistant'); + this.history.push({ role: 'assistant', content: displayContent }); + this.reviewerHistory.push({ role: 'assistant', content: displayContent }); + } + + if (!instruction) { + break; + } + + onStreamUpdate(`Reviewer 指令: ${instruction}`, 'system'); + + this.executorHistory.push({ role: 'user', content: instruction }); + + await this.runExecutorLoop(onStreamUpdate, onPreviewUpdate); + + const lastExecutorResponse = this.executorHistory[this.executorHistory.length - 1]; + if (lastExecutorResponse && lastExecutorResponse.role === 'assistant') { + this.reviewerHistory.push({ + role: 'user', + content: `[Executor 执行结果]\n${lastExecutorResponse.content}` + }); + } + } + } + + async runExecutorLoop(onStreamUpdate, onPreviewUpdate) { + let maxTurns = 5; + let currentTurn = 0; + + while (currentTurn < maxTurns) { + currentTurn++; + + const executorConfig = getApiConfig('executor'); + const messages = this.contextManager.buildMessages( + this.executorSystemPrompt, + this.executorHistory, + executorConfig.maxTokens + ); + + let responseContent; + try { + responseContent = await callAi('executor', messages); + } catch (error) { + onStreamUpdate(`[Executor 错误] ${error.message}`, 'system'); + return; + } + + onStreamUpdate(responseContent, 'executor'); + this.executorHistory.push({ role: 'assistant', content: responseContent }); + + const toolCall = this.parseToolCall(responseContent); + + if (toolCall) { + 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); + } + } + if (toolCall.name === 'write_world_info_entry' || toolCall.name === 'read_world_info') { + if (!toolCall.arguments.book_name && this.currentBookName) { + toolCall.arguments.book_name = this.currentBookName; + } + } + + onStreamUpdate(`[Executor] 执行工具: ${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]); + this.executorSystemPrompt = this.buildExecutorSystemPrompt(); + } + } + } else { + result = `Error: Tool '${toolCall.name}' not found.`; + } + } catch (error) { + result = `Error executing tool '${toolCall.name}': ${error.message}`; + } + + const toolResultMsg = `[Tool Result for ${toolCall.name}]\n${result}`; + this.executorHistory.push({ role: 'user', content: toolResultMsg }); + + onStreamUpdate(`[Executor] 工具结果: ${result.substring(0, 100)}...`, 'system'); + + if (onPreviewUpdate && !result.startsWith('Error')) { + onPreviewUpdate(toolCall.name, toolCall.arguments); + } + } else { + break; + } + } + } + + parseToolCall(content) { + const toolNames = Object.keys(tools); + for (const name of toolNames) { + const regex = new RegExp(`<${name}>([\\s\\S]*?)<\\/${name}>`); + const match = content.match(regex); + + if (match) { + const argsContent = match[1]; + const args = {}; + + const paramRegex = /<(\w+)>([\s\S]*?)<\/\1>/g; + let paramMatch; + while ((paramMatch = paramRegex.exec(argsContent)) !== null) { + const paramName = paramMatch[1]; + let paramValue = paramMatch[2]; + + if (paramValue.trim().startsWith('{') || paramValue.trim().startsWith('[')) { + try { + paramValue = JSON.parse(paramValue); + } catch (e) { + } + } + args[paramName] = paramValue; + } + + return { name, arguments: args }; + } + } + return null; + } + + clearHistory() { + this.history = []; + this.executorHistory = []; + this.reviewerHistory = []; + } +} diff --git a/core/auto-char-card/api.js b/core/auto-char-card/api.js new file mode 100644 index 0000000..be34018 --- /dev/null +++ b/core/auto-char-card/api.js @@ -0,0 +1,122 @@ +import { extension_settings } from "/scripts/extensions.js"; +import { getRequestHeaders } from "/script.js"; +import { extensionName } from "../../utils/settings.js"; + +const DEFAULT_CONFIG = { + apiUrl: "", + apiKey: "", + model: "", + maxTokens: 4000, + temperature: 0.7 +}; + +export function getApiConfig(role) { + const settings = extension_settings[extensionName] || {}; + const configKey = `acc_${role}_config`; + return { ...DEFAULT_CONFIG, ...(settings[configKey] || {}) }; +} + +export function setApiConfig(role, config) { + if (!extension_settings[extensionName]) { + extension_settings[extensionName] = {}; + } + const configKey = `acc_${role}_config`; + extension_settings[extensionName][configKey] = { ...getApiConfig(role), ...config }; +} + +export async function callAi(role, messages, options = {}) { + const config = { ...getApiConfig(role), ...options }; + const roleName = role === 'executor' ? '执行者(模型A)' : '规划者(模型B)'; + + if (!config.apiUrl || !config.apiKey || !config.model) { + throw new Error(`[自动构建器] ${roleName} API 配置不完整,请检查 URL、Key 和模型设置。`); + } + + console.log(`[自动构建器] 正在调用 AI (${roleName})...`, { model: config.model, messagesCount: messages.length }); + + const body = { + chat_completion_source: 'openai', + messages: messages, + model: config.model, + reverse_proxy: config.apiUrl, + proxy_password: config.apiKey, + stream: false, + max_tokens: config.maxTokens, + temperature: config.temperature, + top_p: 1, + custom_prompt_post_processing: 'strict', + enable_web_search: false, + frequency_penalty: 0, + presence_penalty: 0, + }; + + try { + const response = await fetch('/api/backends/chat-completions/generate', { + method: 'POST', + headers: { ...getRequestHeaders(), 'Content-Type': 'application/json' }, + body: JSON.stringify(body) + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`API 请求失败: ${response.status} - ${errorText}`); + } + + const responseData = await response.json(); + + if (!responseData || !responseData.choices || responseData.choices.length === 0) { + if (responseData.error) { + throw new Error(`API 返回错误: ${responseData.error.message || JSON.stringify(responseData.error)}`); + } + throw new Error('API 返回了空响应。'); + } + + const content = responseData.choices[0].message?.content; + console.log(`[自动构建器] AI (${roleName}) 响应接收成功。长度: ${content?.length}`); + return content; + + } catch (error) { + console.error(`[自动构建器] AI (${roleName}) 调用失败:`, error); + throw error; + } +} + +export async function testConnection(role) { + try { + const response = await callAi(role, [ + { role: 'user', content: 'Hi' } + ], { maxTokens: 10 }); + return !!response; + } catch (error) { + console.error(`[自动构建器] ${role} 连接测试失败:`, error); + return false; + } +} + +export async function fetchModels(apiUrl, apiKey) { + try { + const response = await fetch('/api/backends/chat-completions/status', { + method: 'POST', + headers: { ...getRequestHeaders(), 'Content-Type': 'application/json' }, + body: JSON.stringify({ + reverse_proxy: apiUrl, + proxy_password: apiKey, + chat_completion_source: 'openai' + }) + }); + + if (!response.ok) throw new Error(`HTTP ${response.status}`); + + const data = await response.json(); + const models = Array.isArray(data) ? data : (data.data || data.models || []); + + return models.map(m => { + const id = m.id || m.model || m.name || m; + return typeof id === 'string' ? id : JSON.stringify(id); + }).sort(); + + } catch (error) { + console.error('[自动构建器] 获取模型列表失败:', error); + throw error; + } +} diff --git a/core/auto-char-card/char-api.js b/core/auto-char-card/char-api.js new file mode 100644 index 0000000..b9658e6 --- /dev/null +++ b/core/auto-char-card/char-api.js @@ -0,0 +1,150 @@ +import { characters, saveCharacterDebounced, this_chid, getCharacters, getRequestHeaders } from "/script.js"; + +export function getCharacter(chid = this_chid) { + if (chid === undefined || chid < 0 || !characters[chid]) { + console.warn(`[Amily2 CharAPI] Invalid character ID: ${chid}`); + return null; + } + return characters[chid]; +} + +export function updateCharacter(chid, updates) { + const char = getCharacter(chid); + if (!char) return false; + + let changed = false; + const fields = ['name', 'description', 'personality', 'scenario', 'first_mes', 'mes_example']; + + fields.forEach(field => { + if (updates[field] !== undefined && char[field] !== updates[field]) { + char[field] = updates[field]; + changed = true; + } + }); + + if (changed) { + saveCharacterDebounced(); + console.log(`[Amily2 CharAPI] Updated character ${chid}:`, Object.keys(updates)); + return true; + } + return false; +} + +export function getFirstMessages(chid) { + const char = getCharacter(chid); + if (!char) return []; + + const messages = [char.first_mes]; + if (char.data && Array.isArray(char.data.alternate_greetings)) { + messages.push(...char.data.alternate_greetings); + } + return messages; +} + +export function addFirstMessage(chid, message) { + const char = getCharacter(chid); + if (!char) return false; + + if (!char.data) char.data = {}; + if (!Array.isArray(char.data.alternate_greetings)) { + char.data.alternate_greetings = []; + } + + char.data.alternate_greetings.push(message); + saveCharacterDebounced(); + console.log(`[Amily2 CharAPI] Added alternate greeting to character ${chid}`); + return true; +} + +export function updateFirstMessage(chid, index, message) { + const char = getCharacter(chid); + if (!char) return false; + + if (index === 0) { + char.first_mes = message; + } else { + const altIndex = index - 1; + if (char.data && Array.isArray(char.data.alternate_greetings) && char.data.alternate_greetings[altIndex] !== undefined) { + char.data.alternate_greetings[altIndex] = message; + } else { + console.warn(`[Amily2 CharAPI] Alternate greeting index out of bounds: ${altIndex}`); + return false; + } + } + saveCharacterDebounced(); + console.log(`[Amily2 CharAPI] Updated greeting ${index} for character ${chid}`); + return true; +} + +export function removeFirstMessage(chid, index) { + const char = getCharacter(chid); + if (!char) return false; + + if (index === 0) { + console.warn(`[Amily2 CharAPI] Cannot remove main greeting, clearing instead.`); + char.first_mes = ""; + } else { + const altIndex = index - 1; + if (char.data && Array.isArray(char.data.alternate_greetings) && char.data.alternate_greetings[altIndex] !== undefined) { + char.data.alternate_greetings.splice(altIndex, 1); + } else { + console.warn(`[Amily2 CharAPI] Alternate greeting index out of bounds: ${altIndex}`); + return false; + } + } + saveCharacterDebounced(); + console.log(`[Amily2 CharAPI] Removed greeting ${index} for character ${chid}`); + return true; +} + +export async function createNewCharacter(name) { + try { + const formData = new FormData(); + formData.append('ch_name', name); + formData.append('description', ''); + formData.append('personality', ''); + formData.append('scenario', ''); + formData.append('first_mes', 'Hello!'); + formData.append('mes_example', ''); + formData.append('creator', 'Amily2-AutoChar'); + formData.append('creator_notes', 'Character created automatically by Amily2 AutoChar Card.'); + formData.append('tags', ''); + formData.append('character_version', '1.0'); + formData.append('post_history_instructions', ''); + formData.append('system_prompt', ''); + formData.append('talkativeness', '0.5'); + formData.append('extensions', '{}'); + formData.append('fav', 'false'); + + formData.append('world', ''); + formData.append('depth_prompt_prompt', ''); + formData.append('depth_prompt_depth', '4'); + formData.append('depth_prompt_role', 'system'); + + const response = await fetch('/api/characters/create', { + method: 'POST', + headers: getRequestHeaders({ omitContentType: true }), + body: formData, + }); + + if (response.ok) { + const avatarId = await response.text(); + console.log(`[Amily2 CharAPI] Created character: ${name}, Avatar ID: ${avatarId}`); + + await getCharacters(); + + const newChid = characters.findIndex(c => c.avatar === avatarId); + if (newChid !== -1) { + return newChid; + } + + return -2; + } else { + console.error(`[Amily2 CharAPI] Failed to create character: ${response.statusText}`); + return -1; + } + } catch (error) { + console.error(`[Amily2 CharAPI] Error creating character:`, error); + return -1; + } +} diff --git a/core/auto-char-card/context-manager.js b/core/auto-char-card/context-manager.js new file mode 100644 index 0000000..9ef9a8e --- /dev/null +++ b/core/auto-char-card/context-manager.js @@ -0,0 +1,63 @@ +export class ContextManager { + constructor() { + this.keepToolOutputTurns = 3; + this.tokenLimit = 12000; + } + + estimateTokens(text) { + return Math.ceil((text || '').length / 3.5); + } + + buildMessages(systemPrompt, history, maxTokens) { + const limit = maxTokens || this.tokenLimit; + const systemTokens = this.estimateTokens(systemPrompt); + let availableTokens = limit - systemTokens - 1000; + + if (availableTokens < 0) availableTokens = 1000; + + const optimizedHistory = this.optimizeToolOutputs(history); + + const finalMessages = []; + let currentTokens = 0; + + for (let i = optimizedHistory.length - 1; i >= 0; i--) { + const msg = optimizedHistory[i]; + const msgTokens = this.estimateTokens(msg.content); + + if (currentTokens + msgTokens > availableTokens) { + finalMessages.unshift({ role: 'system', content: "[Earlier history truncated to save tokens]" }); + break; + } + + finalMessages.unshift(msg); + currentTokens += msgTokens; + } + + return [ + { role: 'system', content: systemPrompt }, + ...finalMessages + ]; + } + + optimizeToolOutputs(history) { + let toolOutputCount = 0; + const reversedHistory = [...history].reverse(); + + const processedReversed = reversedHistory.map((msg) => { + if (msg.role === 'user' && msg.content.startsWith('[Tool Result')) { + toolOutputCount++; + + if (toolOutputCount > this.keepToolOutputTurns) { + const firstLine = msg.content.split('\n')[0]; + return { + role: msg.role, + content: `${firstLine}\n[Content hidden to save tokens. The tool was executed successfully.]` + }; + } + } + return msg; + }); + + return processedReversed.reverse(); + } +} diff --git a/core/auto-char-card/tools.js b/core/auto-char-card/tools.js new file mode 100644 index 0000000..f5676be --- /dev/null +++ b/core/auto-char-card/tools.js @@ -0,0 +1,248 @@ +import { amilyHelper } from "../tavern-helper/main.js"; +import * as charApi from "./char-api.js"; + +export const tools = { + + read_world_info: async ({ book_name }) => { + const entries = await amilyHelper.getLorebookEntries(book_name); + return JSON.stringify(entries, null, 2); + }, + + write_world_info_entry: async ({ book_name, entries }) => { + if (typeof entries === 'string') { + try { + const cleanEntries = entries.replace(/```json/g, '').replace(/```/g, '').trim(); + entries = JSON.parse(cleanEntries); + } catch (e) { + return `错误: 'entries' 参数必须是有效的 JSON 数组。解析错误: ${e.message}`; + } + } + if (!Array.isArray(entries)) { + if (typeof entries === 'object' && entries !== null) { + entries = [entries]; + } else { + return "错误: 'entries' 参数必须是数组或对象。"; + } + } + + const updates = []; + const creates = []; + + for (const entry of entries) { + if (entry.uid !== undefined) { + updates.push(entry); + } else { + creates.push(entry); + } + } + + let resultMsg = ""; + if (updates.length > 0) { + const success = await amilyHelper.setLorebookEntries(book_name, updates); + resultMsg += success ? `成功更新了 ${updates.length} 个条目。 ` : `更新条目失败。 `; + } + if (creates.length > 0) { + const success = await amilyHelper.createLorebookEntries(book_name, creates); + resultMsg += success ? `成功创建了 ${creates.length} 个条目。 ` : `创建条目失败。 `; + } + return resultMsg || "未执行任何操作。"; + }, + + create_world_book: async ({ book_name }) => { + const success = await amilyHelper.createLorebook(book_name); + return success ? `世界书 "${book_name}" 创建成功。` : `创建世界书 "${book_name}" 失败。`; + }, + + read_character_card: async ({ chid }) => { + const char = charApi.getCharacter(chid); + if (!char) return "未找到角色。"; + + const safeChar = { + name: char.name, + description: char.description, + personality: char.personality, + scenario: char.scenario, + first_mes: char.first_mes, + mes_example: char.mes_example, + alternate_greetings: char.data?.alternate_greetings || [] + }; + return JSON.stringify(safeChar, null, 2); + }, + + update_character_card: async (args) => { + const { chid, ...updates } = args; + const finalUpdates = args.updates || updates; + + const success = charApi.updateCharacter(chid, finalUpdates); + return success ? "角色卡更新成功。" : "更新角色卡失败。"; + }, + + edit_character_text: async ({ chid, field, search, replace }) => { + const char = charApi.getCharacter(chid); + if (!char) return "未找到角色。"; + + const allowedFields = ['description', 'personality', 'scenario', 'first_mes', 'mes_example']; + if (!allowedFields.includes(field)) { + return `无效的字段。允许的字段: ${allowedFields.join(', ')}`; + } + + const originalText = char[field] || ''; + if (!originalText.includes(search)) { + return `在字段 '${field}' 中未找到搜索文本。`; + } + + const newText = originalText.replace(search, replace); + const success = charApi.updateCharacter(chid, { [field]: newText }); + + return success ? `字段 '${field}' 更新成功。` : `更新字段 '${field}' 失败。`; + }, + + manage_first_message: async ({ action, chid, index, message }) => { + let success = false; + switch (action) { + case 'add': + success = charApi.addFirstMessage(chid, message); + break; + case 'update': + success = charApi.updateFirstMessage(chid, index, message); + break; + case 'remove': + success = charApi.removeFirstMessage(chid, index); + break; + default: + return "无效的操作。"; + } + 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}`; + } +}; + +export function getToolDefinitions() { + return [ + { + name: "read_world_info", + description: "Read all entries from a specific world book.", + parameters: { + type: "object", + properties: { + book_name: { type: "string", description: "The name of the world book." } + }, + required: ["book_name"] + } + }, + { + name: "write_world_info_entry", + description: "Create or update entries in a world book.", + parameters: { + type: "object", + properties: { + book_name: { type: "string", description: "The name of the world book." }, + 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." } + } + } + } + }, + required: ["book_name", "entries"] + } + }, + { + name: "create_world_book", + description: "Create a new empty world book.", + parameters: { + type: "object", + properties: { + book_name: { type: "string", description: "The name of the new world book." } + }, + required: ["book_name"] + } + }, + { + name: "read_character_card", + description: "Read character card data.", + parameters: { + type: "object", + properties: { + chid: { type: "number", description: "Character ID." } + }, + required: ["chid"] + } + }, + { + name: "update_character_card", + description: "Update character card fields (overwrite).", + parameters: { + type: "object", + properties: { + chid: { type: "number", description: "Character ID." }, + name: { type: "string" }, + description: { type: "string" }, + personality: { type: "string" }, + scenario: { type: "string" }, + first_mes: { type: "string" }, + mes_example: { type: "string" } + }, + required: ["chid"] + } + }, + { + name: "edit_character_text", + description: "Edit a specific text field of a character using search and replace.", + 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." }, + search: { type: "string", description: "The exact text to find." }, + replace: { type: "string", description: "The text to replace with." } + }, + required: ["chid", "field", "search", "replace"] + } + }, + { + name: "manage_first_message", + description: "Add, update, or remove alternate greetings.", + 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)." } + }, + required: ["action", "chid"] + } + }, + { + name: "create_character", + description: "Create a new character card.", + parameters: { + type: "object", + properties: { + name: { type: "string", description: "Name of the new character." } + }, + required: ["name"] + } + } + ]; +} diff --git a/core/auto-char-card/ui-bindings.js b/core/auto-char-card/ui-bindings.js new file mode 100644 index 0000000..55722c2 --- /dev/null +++ b/core/auto-char-card/ui-bindings.js @@ -0,0 +1,431 @@ +import { extensionName } from "../../utils/settings.js"; +import { AgentManager } from "./agent-manager.js"; +import { characters, this_chid, saveSettingsDebounced } from "/script.js"; +import { world_names } from "/scripts/world-info.js"; +import { getApiConfig, setApiConfig, testConnection, fetchModels } from "./api.js"; +import { tools } from "./tools.js"; + +const extensionFolderPath = `scripts/extensions/third-party/${extensionName}`; + +let isInitialized = false; +let agentManager = null; +let previousCharData = {}; +let previousWorldData = {}; + +export async function openAutoCharCardWindow() { + toastr.info("该功能正在开发,尚未完成,请耐心等待。"); + return; + + if ($('#acc-window').length > 0) { + $('#acc-window').show(); + return; + } + + if (!$('#acc-style').length) { + $('') + .attr('id', 'acc-style') + .attr('rel', 'stylesheet') + .attr('type', 'text/css') + .attr('href', `${extensionFolderPath}/assets/auto-char-card/style.css`) + .appendTo('head'); + } + + try { + const htmlContent = await $.get(`${extensionFolderPath}/assets/auto-char-card/index.html`); + $('body').append(htmlContent); + + bindEvents(); + + agentManager = new AgentManager(); + + try { + populateDropdowns(); + loadApiSettings(); + } catch (dataError) { + console.error('[Amily2 AutoCharCard] Failed to load data:', dataError); + toastr.warning('数据加载部分失败,请检查控制台。'); + } + + isInitialized = true; + console.log('[Amily2 AutoCharCard] Window initialized.'); + } catch (error) { + console.error('[Amily2 AutoCharCard] Failed to initialize window:', error); + toastr.error(`无法加载自动构建器界面: ${error.message}`); + $('#acc-window').remove(); + } +} + +function populateDropdowns() { + const charSelect = $('#acc-target-char'); + charSelect.empty().append(''); + charSelect.append(''); + + characters.forEach((char, index) => { + if (char) { + const option = $(''); + worldSelect.append(''); + + world_names.forEach(name => { + worldSelect.append($(''); + + try { + const models = await fetchModels(apiUrl, apiKey); + select.empty().append(''); + + if (models.length === 0) { + select.append(''); + } else { + models.forEach(model => { + select.append(new Option(model, model)); + }); + toastr.success(`成功获取 ${models.length} 个模型`); + } + } catch (error) { + console.error(`[AutoCharCard] Failed to fetch models for ${role}:`, error); + toastr.error(`获取模型失败: ${error.message}`); + select.empty().append(''); + } finally { + btn.prop('disabled', false).html(originalIcon); + } + }; + + $('#acc-executor-refresh-models').on('click', () => handleRefreshModels('executor')); + $('#acc-reviewer-refresh-models').on('click', () => handleRefreshModels('reviewer')); + + $('#acc-executor-test').on('click', async function() { + const btn = $(this); + btn.prop('disabled', true).text('测试中...'); + const success = await testConnection('executor'); + btn.prop('disabled', false).text('测试连接'); + if (success) toastr.success('模型 A 连接成功'); + else toastr.error('模型 A 连接失败'); + }); + + $('#acc-reviewer-test').on('click', async function() { + const btn = $(this); + btn.prop('disabled', true).text('测试中...'); + const success = await testConnection('reviewer'); + btn.prop('disabled', false).text('测试连接'); + if (success) toastr.success('模型 B 连接成功'); + else toastr.error('模型 B 连接失败'); + }); +} + +async function handleSendMessage() { + const input = $('#acc-user-input'); + const message = input.val().trim(); + if (!message) return; + + if (!agentManager) { + toastr.error('Agent 未初始化'); + return; + } + + const selectedCharId = $('#acc-target-char').val(); + const selectedWorld = $('#acc-target-world').val(); + + if (!selectedCharId && selectedCharId !== '0') { + toastr.warning('请先选择一个目标角色(或选择新建)'); + return; + } + + addMessage('user', message); + input.val(''); + + $('#acc-send-btn').prop('disabled', true); + $('#acc-status-indicator').removeClass('status-idle').addClass('status-working').text('工作中...'); + + try { + agentManager.setContext(selectedCharId, selectedWorld); + + await agentManager.handleUserMessage( + message, + (content, role) => { + addMessage(role, content); + }, + (toolName, args) => { + updatePreview(toolName, args); + } + ); + } catch (error) { + console.error('Agent Error:', error); + addMessage('system', `发生错误: ${error.message}`); + } finally { + $('#acc-send-btn').prop('disabled', false); + $('#acc-status-indicator').removeClass('status-working').addClass('status-idle').text('空闲'); + } +} + +function addMessage(role, content) { + const stream = $('#acc-chat-stream'); + + let displayContent = content; + if (role === 'executor') { + const tools = [ + 'read_world_info', 'write_world_info_entry', 'create_world_book', + 'read_character_card', 'update_character_card', 'edit_character_text', + 'manage_first_message', 'use_tool' + ]; + const regex = new RegExp(`<(${tools.join('|')})>[\\s\\S]*?<\\/\\1>`, 'g'); + displayContent = content.replace(regex, '').trim(); + + if (!displayContent) { + displayContent = "(正在执行操作...)"; + } + } + + const escapedContent = displayContent + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); + + const formattedContent = escapedContent.replace(/\n/g, '
'); + + const msgDiv = $('
').addClass(`acc-message ${role}`); + + const avatarDiv = $('
').addClass('acc-avatar'); + if (role === 'user') { + avatarDiv.html(''); + } else if (role === 'assistant') { + avatarDiv.html(''); + } else if (role === 'executor') { + avatarDiv.html(''); + } else if (role === 'system') { + avatarDiv.html(''); + } + + const contentDiv = $('
').addClass('acc-message-content'); + + msgDiv.append(avatarDiv); + msgDiv.append(contentDiv); + stream.append(msgDiv); + + if (role === 'assistant') { + let i = 0; + const speed = 2; + const chunkSize = 5; + + function typeWriter() { + if (i < formattedContent.length) { + let chunk = ""; + let count = 0; + + while (count < chunkSize && i < formattedContent.length) { + if (formattedContent.charAt(i) === '<') { + const tagEnd = formattedContent.indexOf('>', i); + if (tagEnd !== -1) { + chunk += formattedContent.substring(i, tagEnd + 1); + i = tagEnd + 1; + } else { + chunk += formattedContent.charAt(i); + i++; + } + } else { + chunk += formattedContent.charAt(i); + i++; + } + count++; + } + + contentDiv.html(contentDiv.html() + chunk); + stream.scrollTop(stream[0].scrollHeight); + setTimeout(typeWriter, speed); + } + } + typeWriter(); + } else { + contentDiv.html(formattedContent); + stream.scrollTop(stream[0].scrollHeight); + } +} + +async function updatePreview(toolName, args) { + const container = $('#acc-preview-container'); + + if (toolName === 'update_character_card' || toolName === 'edit_character_text') { + const chid = args.chid !== undefined ? args.chid : $('#acc-target-char').val(); + if (chid !== undefined) { + const charData = await tools.read_character_card({ chid }); + const char = JSON.parse(charData); + + let html = `

角色预览: ${char.name}

`; + + const fields = ['description', 'personality', 'first_mes', 'scenario']; + fields.forEach(field => { + const oldVal = previousCharData[field] || ''; + const newVal = char[field] || ''; + let contentHtml = newVal; + + if (oldVal !== newVal) { + contentHtml = `
${newVal}
`; + if (oldVal) { + contentHtml += ``; + } + } + + html += `
${field}:
${contentHtml}
`; + }); + + container.html(html); + previousCharData = char; + } + } else if (toolName === 'write_world_info_entry') { + const bookName = args.book_name || $('#acc-target-world').val(); + if (bookName) { + const entriesData = await tools.read_world_info({ book_name: bookName }); + const entries = JSON.parse(entriesData); + + let html = `

世界书预览: ${bookName}

`; + entries.forEach(entry => { + + let isModified = false; + if (args.entries) { + const modifiedEntries = Array.isArray(args.entries) ? args.entries : [args.entries]; + isModified = modifiedEntries.some(e => e.key === entry.key || (Array.isArray(entry.keys) && entry.keys.includes(e.key))); + } + + const contentClass = isModified ? 'diff-added' : ''; + + html += `
+ Key: ${Array.isArray(entry.keys) ? entry.keys.join(', ') : entry.key}
+ Content:
${entry.content}
+
`; + }); + + container.html(html); + } + } +}