Add files via upload

This commit is contained in:
2025-12-23 08:40:01 +08:00
committed by GitHub
parent c77aaca07e
commit 92768bc585
6 changed files with 1350 additions and 0 deletions

View File

@@ -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 执行操作时,请在回复的最后一行使用标签:<instruction>你的指令</instruction>
- **重要**<instruction> 标签内必须是**自然语言指令**。**严禁**直接输出 JSON 代码块作为指令。
- **单步原则**:每次指令**只能包含一个**具体的任务(例如:只创建一个世界书条目,或只更新角色描述)。严禁一次性下达多个任务。
- **字数强制**:在指令中必须明确要求 Executor 进行深度扩写。
- 世界书条目:要求**不低于 300 字**。
- 角色开场白:要求**不低于 1500 字**。
- 当你认为任务已完成或需要用户反馈时,直接回复用户即可,不要包含 <instruction> 标签。
`;
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</参数1>
<参数2>值2</参数2>
...
</工具名称>
**注意**:对于复杂参数(如数组或对象),请直接在标签内写入 **JSON 字符串**。
例如:
<write_world_info_entry>
<book_name>MyWorld</book_name>
<entries>[{"key": "Entry1", "content": "..."}]</entries>
</write_world_info_entry>
# Tool Use Guidelines
1. **必须思考 (Mandatory Thinking)**: 在调用任何工具之前,你**必须**先输出一段思考过程,解释你为什么要这样做,以及你打算如何创作内容。请使用 \`<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(/<instruction>([\s\S]*?)<\/instruction>/);
const instruction = instructionMatch ? instructionMatch[1].trim() : null;
const displayContent = reviewerResponse.replace(/<instruction>[\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 = [];
}
}

122
core/auto-char-card/api.js Normal file
View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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"]
}
}
];
}

View File

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