Initial commit with CC BY-NC-ND 4.0 license

This commit is contained in:
2026-02-13 09:59:19 +08:00
commit 2c31e1cbc8
140 changed files with 44625 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

View File

@@ -0,0 +1,528 @@
import { callAi, getApiConfig } from "./api.js";
import { tools, getToolDefinitions } from "./tools.js";
import { ContextManager } from "./context-manager.js";
import { TaskState } from "./task-state.js";
import { MemorySystem } from "./memory-system.js";
export class AgentManager {
constructor() {
this.history = [];
this.contextManager = new ContextManager();
this.taskState = new TaskState();
this.memorySystem = new MemorySystem();
this.currentChid = undefined;
this.currentBookName = undefined;
this.status = 'idle';
this.approvalRequired = false;
this.pendingToolCall = null;
}
async setContext(chid, bookName) {
this.currentChid = chid;
this.currentBookName = bookName;
if (bookName && bookName !== 'new') {
try {
const bookData = await tools.read_world_info({ book_name: bookName, return_full: true });
const entries = JSON.parse(bookData);
this.contextManager.setWorldInfo(entries);
} catch (e) {
console.error("Failed to load world info for context:", e);
}
}
}
setApprovalRequired(required) {
this.approvalRequired = required;
}
updatePendingToolArgs(newArgs) {
if (this.pendingToolCall) {
this.pendingToolCall.arguments = { ...this.pendingToolCall.arguments, ...newArgs };
console.log("[AgentManager] Pending tool args updated:", this.pendingToolCall.arguments);
}
}
stop() {
this.status = 'idle';
}
async resumeWithApproval(approved, feedback, onStreamUpdate, onPreviewUpdate, onApprovalRequest, onContextUpdate, onPromptGenerated) {
if (this.status !== 'paused' || !this.pendingToolCall) return;
if (approved) {
this.status = 'running';
await this.executePendingTool(onStreamUpdate, onPreviewUpdate, onContextUpdate);
this.pendingToolCall = null;
await this.runTaskLoop(onStreamUpdate, onPreviewUpdate, onApprovalRequest, onContextUpdate, onPromptGenerated);
} else {
this.status = 'running';
this.pendingToolCall = null;
this.history.push({
role: 'user',
content: `[工具执行被拒绝] 用户反馈: ${feedback || "未提供原因。"}`
});
await this.runTaskLoop(onStreamUpdate, onPreviewUpdate, onApprovalRequest, onContextUpdate, onPromptGenerated);
}
}
async buildSystemPrompt() {
const toolDefs = getToolDefinitions();
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.
IMPORTANT: You MUST speak in Chinese (Simplified) for all your responses and reasoning.
**Core Capabilities**:
- You can create and modify **Single Character Cards**.
- You can create and modify **Multi-Character World Cards** (Group Cards). Do not assume the user only wants a single character.
- You can manage **World Info (Lorebooks)**.
**Workflow**:
1. **Analyze & Explore First**: The World Book Index is provided in the context below. Review it to see what entries exist. If you need the *content* of a specific entry, use \`read_world_entry\` with its UID. Do not use \`read_world_info\` unless you need to refresh the index.
2. **Plan & Execute**: Based on the read data, formulate a plan and execute it step-by-step.
3. **Tool Usage**: Use one tool per turn. Always explain your reasoning in \`<thinking>\` tags before using a tool.
4. **Completion**: When finished, provide a concise summary.
# Memory & Task State
${this.taskState.getPromptContext()}
# Current Context
`;
if (this.currentChid === 'new') {
prompt += `- **Status**: Creating a NEW character.\n`;
prompt += `- **Action Required**: Use \`create_character\` first to get a Character ID.\n`;
} else if (this.currentChid !== undefined) {
prompt += `- **Character ID**: ${this.currentChid}\n`;
}
if (this.currentBookName) {
prompt += `- **World Info Book**: ${this.currentBookName}\n`;
}
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) {
prompt += `\n# Style Guides & Rules\n`;
rules.forEach(rule => {
prompt += `- ${rule.content}\n`;
});
}
if (worldInfo.length > 0) {
prompt += `\n# World Info Context\n`;
worldInfo.forEach(entry => {
prompt += `## Entry: ${entry.keys.join(', ')}\n${entry.content}\n\n`;
});
}
let envDetails = `\n<environment_details>\n`;
envDetails += `# Current Time\n${new Date().toLocaleString()}\n\n`;
if (this.currentChid !== undefined && this.currentChid !== 'new') {
try {
const charData = await tools.read_character_card({ chid: this.currentChid });
const response = JSON.parse(charData);
if (response.status === 'success' && response.data) {
const char = response.data;
envDetails += `# Current Character\n`;
envDetails += `Name: ${char.name}\n`;
envDetails += `Description Length: ${char.description?.length || 0}\n`;
envDetails += `First Message Length: ${char.first_mes?.length || 0}\n`;
envDetails += `Description Snippet: ${char.description?.substring(0, 200).replace(/\n/g, ' ')}...\n\n`;
} else {
envDetails += `# Current Character\nError reading character: ${response.message || 'Unknown error'}\n\n`;
}
} catch (e) {
envDetails += `# Current Character\nError reading character: ${e.message}\n\n`;
}
}
if (this.currentBookName && this.currentBookName !== 'new') {
try {
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`;
envDetails += `Name: ${this.currentBookName}\n`;
envDetails += `Total Entries: ${result.total_entries}\n`;
envDetails += `Entries List (UID | Name | Keys):\n`;
if (result.entries && result.entries.length > 0) {
result.entries.forEach(entry => {
const keys = Array.isArray(entry.keys) ? entry.keys.join(', ') : entry.keys;
const name = entry.comment || keys || "Unnamed";
envDetails += `- [${entry.uid}] ${name} (Keys: ${keys})\n`;
});
} else {
envDetails += `(No entries found)\n`;
}
envDetails += `\n`;
} catch (e) {
envDetails += `# Current World Book\nError reading world book: ${e.message}\n\n`;
}
}
envDetails += `</environment_details>\n`;
prompt += envDetails;
prompt += `\n# Tools\n\n`;
toolDefs.forEach(tool => {
prompt += `## ${tool.name}\n`;
prompt += `Description: ${tool.description}\n`;
prompt += `Parameters:\n${JSON.stringify(tool.parameters, null, 2)}\n\n`;
});
prompt += `
# Tool Use Formatting
Tool use is formatted using XML-style tags. The tool name is enclosed in opening and closing tags, and each parameter is similarly enclosed within its own set of tags.
<tool_name>
<parameter1_name>value1</parameter1_name>
<parameter2_name>value2</parameter2_name>
...
</tool_name>
**Important**: For complex parameters (arrays or objects), write the **JSON string** directly inside the tag.
Example:
<write_world_info_entry>
<book_name>MyWorld</book_name>
<entries>[{"key": "Entry1", "content": "..."}]</entries>
</write_world_info_entry>
# Rules
- **Plan First**: Before using any tool, you MUST output a \`<plan>\` block listing the steps you intend to take.
- **Think**: After planning, output a \`<thinking>\` 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.
- World Info entries: > 300 words.
- First Message: > 1500 words, including environment, psychology, and action.
- **Tool Selection**:
- **Use \`edit_character_text\`** for small modifications to existing large text fields (Description, First Message, etc.). This is more precise and saves tokens.
- **Use \`edit_world_info_entry\`** for small modifications to existing World Info entries.
- **Use \`update_character_card\`** only when populating empty fields or rewriting the entire content of a field.
- **Use \`write_world_info_entry\`** only when creating new entries or rewriting the entire content of an entry.
- **Do not ask for more information than necessary**: Use the tools provided to accomplish the user's request efficiently and effectively.
- **Completion**: When the task is done, provide a final summary to the user.
`;
return prompt;
}
getLastUserMessage() {
for (let i = this.history.length - 1; i >= 0; i--) {
if (this.history[i].role === 'user') {
return this.history[i].content;
}
}
return null;
}
async handleUserMessage(message, onStreamUpdate, onPreviewUpdate, onApprovalRequest, onContextUpdate, onPromptGenerated) {
if (this.history.length === 0) {
this.taskState.init(message);
}
this.history.push({ role: 'user', content: message });
this.status = 'running';
await this.runTaskLoop(onStreamUpdate, onPreviewUpdate, onApprovalRequest, onContextUpdate, onPromptGenerated);
}
async runTaskLoop(onStreamUpdate, onPreviewUpdate, onApprovalRequest, onContextUpdate, onPromptGenerated) {
let maxTurns = 20;
let currentTurn = 0;
while (this.status === 'running' && currentTurn < maxTurns) {
currentTurn++;
const config = getApiConfig('executor');
const currentTokens = this.contextManager.estimateTokens(JSON.stringify(this.history));
if (this.memorySystem.shouldSummarize(this.history, currentTokens, config.maxTokens)) {
onStreamUpdate("上下文即将达到上限,正在总结记忆...", 'system');
const summary = await this.memorySystem.summarize(this.history, this.taskState);
if (summary) {
this.taskState.updateSummary(summary);
if (this.history.length > 5) {
const lastMessages = this.history.slice(-5);
this.history = [
{ role: 'system', content: `[历史记录已压缩] 之前的对话已总结在“记忆与任务状态”部分。从最近的消息继续。` },
...lastMessages
];
}
}
}
const systemPrompt = await this.buildSystemPrompt();
const messages = this.contextManager.buildMessages(
systemPrompt,
this.history,
config.maxTokens
);
if (onPromptGenerated) {
onPromptGenerated(messages);
}
let responseContent;
let fullStreamedContent = "";
try {
onStreamUpdate("思考中...", 'system');
responseContent = await callAi('executor', messages, {}, (chunk) => {
onStreamUpdate(chunk, 'stream-assistant');
fullStreamedContent += chunk;
if (onPreviewUpdate) {
const partialTool = this.parsePartialToolCall(fullStreamedContent);
if (partialTool) {
onPreviewUpdate(partialTool.name, partialTool.arguments, true);
}
}
});
} catch (error) {
onStreamUpdate(`[错误] ${error.message}`, 'system');
this.status = 'idle';
return;
}
if (this.status !== 'running') return;
const lastChar = responseContent.trim().slice(-1);
const isTruncated = !['.', '!', '?', '"', "'", '}', ']', '>', '*'].includes(lastChar) && responseContent.length > 100;
if (isTruncated && currentTurn < maxTurns) {
console.log("检测到回复截断,正在自动继续...");
try {
const continueMsg = { role: 'user', content: "Continue" };
const continueMessages = [...messages, { role: 'assistant', content: responseContent }, continueMsg];
const continuation = await callAi('executor', continueMessages, {}, (chunk) => {
onStreamUpdate(chunk, 'stream-assistant');
});
responseContent += continuation;
console.log("自动合并接续内容完成");
} catch (e) {
console.warn("Auto-continue failed:", e);
}
}
this.history.push({ role: 'assistant', content: responseContent });
const thinkingMatch = responseContent.match(/<thinking>([\s\S]*?)<\/thinking>/);
if (thinkingMatch) {
onStreamUpdate(thinkingMatch[1].trim(), 'thought');
}
let cleanContent = responseContent
.replace(/<thinking(?:\s+[^>]*)?>[\s\S]*?<\/thinking>/gi, '')
.replace(/<\/thinking>/gi, '');
const toolNames = Object.keys(tools);
const toolRegex = new RegExp(`<(${toolNames.join('|')})(?:\\s+[^>]*)?>[\\s\\S]*?<\\/\\1>`, 'gi');
cleanContent = cleanContent.replace(toolRegex, '').trim();
if (cleanContent) {
onStreamUpdate(cleanContent, 'assistant');
}
const toolCall = this.parseToolCall(responseContent);
if (toolCall) {
if (this.isDuplicateToolCall(toolCall)) {
const warningMsg = `[系统警告] 你刚刚执行了完全相同的工具调用 (${toolCall.name})。请勿立即重复相同的操作。如果需要再次检查结果,请查看对话历史。如果之前的结果不满意,请尝试不同的方法。`;
this.history.push({ role: 'user', content: warningMsg });
continue;
}
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' || toolCall.name === 'edit_world_info_entry') {
if (!toolCall.arguments.book_name && this.currentBookName) {
toolCall.arguments.book_name = this.currentBookName;
}
}
this.pendingToolCall = toolCall;
if (this.approvalRequired) {
this.status = 'paused';
if (onApprovalRequest) {
onApprovalRequest(toolCall.name, toolCall.arguments);
}
return;
} else {
await this.executePendingTool(onStreamUpdate, onPreviewUpdate, onContextUpdate);
this.pendingToolCall = null;
}
} else {
this.status = 'idle';
}
}
}
async executePendingTool(onStreamUpdate, onPreviewUpdate, onContextUpdate) {
const toolCall = this.pendingToolCall;
if (!toolCall) return;
onStreamUpdate(`正在执行: ${toolCall.name}`, 'system');
let result;
try {
if (tools[toolCall.name]) {
result = await tools[toolCall.name](toolCall.arguments);
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 = JSON.stringify({
status: "error",
code: "TOOL_NOT_FOUND",
message: `错误: 未找到工具 '${toolCall.name}'。`
});
}
} catch (error) {
result = JSON.stringify({
status: "error",
code: "EXECUTION_ERROR",
message: `执行工具 '${toolCall.name}' 时出错: ${error.message}`
});
}
const toolResultMsg = `[工具 '${toolCall.name}' 的执行结果]\n${result}`;
this.history.push({ role: 'user', content: toolResultMsg });
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, false, true);
}
}
isDuplicateToolCall(toolCall) {
if (this.history.length < 3) return false;
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('[工具')) {
const prevToolCall = this.parseToolCall(prevAssistantMsg.content);
if (prevToolCall &&
prevToolCall.name === toolCall.name &&
JSON.stringify(prevToolCall.arguments) === JSON.stringify(toolCall.arguments)) {
return true;
}
}
return false;
}
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;
}
parsePartialToolCall(content) {
const toolNames = Object.keys(tools);
for (const name of toolNames) {
const openTagRegex = new RegExp(`<${name}>`);
const openMatch = content.match(openTagRegex);
if (openMatch) {
const startIndex = openMatch.index + openMatch[0].length;
const toolContent = content.slice(startIndex);
const args = {};
const paramRegex = /<(\w+)>([\s\S]*?)(?:<\/\1>|$)/g;
let paramMatch;
while ((paramMatch = paramRegex.exec(toolContent)) !== null) {
const paramName = paramMatch[1];
let paramValue = paramMatch[2];
args[paramName] = paramValue;
}
return { name, arguments: args };
}
}
return null;
}
clearHistory() {
this.history = [];
}
}

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

@@ -0,0 +1,171 @@
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 = {}, onChunk = null) {
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, stream: !!onChunk });
const body = {
chat_completion_source: 'openai',
messages: messages,
model: config.model,
reverse_proxy: config.apiUrl,
proxy_password: config.apiKey,
stream: !!onChunk,
max_tokens: config.maxTokens > 0 ? config.maxTokens : undefined,
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}`);
}
if (onChunk) {
const reader = response.body.getReader();
const decoder = new TextDecoder("utf-8");
let fullContent = "";
let buffer = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop();
for (const line of lines) {
const trimmedLine = line.trim();
if (trimmedLine.startsWith('data: ')) {
const dataStr = trimmedLine.slice(6).trim();
if (dataStr === '[DONE]') continue;
try {
const data = JSON.parse(dataStr);
const delta = data.choices[0].delta?.content || "";
if (delta) {
fullContent += delta;
onChunk(delta);
}
} catch (e) {
}
}
}
}
console.log(`[自动构建器] AI (${roleName}) 流式响应结束。长度: ${fullContent.length}`);
return fullContent;
} else {
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;
if (!content) {
console.warn(`[自动构建器] AI (${roleName}) 响应内容为空。完整响应:`, responseData);
if (responseData.choices && responseData.choices[0]) {
console.warn("Choices[0]:", responseData.choices[0]);
}
}
console.log(`[自动构建器] AI (${roleName}) 响应接收成功。长度: ${content?.length}`);
return content;
}
} catch (error) {
console.error(`[自动构建器] AI (${roleName}) 调用失败:`, error);
throw error;
}
}
export async function testConnection(role, config = {}) {
try {
const response = await callAi(role, [
{ role: 'user', content: 'Say hello' }
], { maxTokens: 50, ...config });
if (!response) {
return { success: false, error: "API 返回了空内容 (可能是被安全过滤或模型无响应)" };
}
return { success: true };
} catch (error) {
console.error(`[自动构建器] ${role} 连接测试失败:`, error);
return { success: false, error: error.message };
}
}
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,272 @@
import { characters, saveCharacterDebounced, this_chid, getCharacters, getRequestHeaders } from "/script.js";
import { getContext } from "/scripts/extensions.js";
import { extensionName } from "../../utils/settings.js";
async function saveCharacterById(chid) {
let currentChid = undefined;
try {
const context = getContext();
if (context) currentChid = context.characterId;
} catch (e) {}
if (currentChid === undefined) currentChid = this_chid;
if (currentChid === undefined && typeof window !== 'undefined' && window.this_chid !== undefined) {
currentChid = window.this_chid;
}
if (currentChid === undefined && typeof $ !== 'undefined') {
const selected = $('.character_select.selected, .character-list-item.selected');
if (selected.length) {
currentChid = selected.attr('chid');
}
}
if (typeof saveCharacterDebounced === 'function') {
if (currentChid === undefined || chid == currentChid) {
saveCharacterDebounced();
console.log(`[Amily2 CharAPI] Triggered saveCharacterDebounced for character ${chid} (Detected: ${currentChid})`);
return { success: true };
}
}
try {
const formData = new FormData();
formData.append('avatar_url', char.avatar);
formData.append('ch_name', char.name);
formData.append('description', char.description || '');
formData.append('personality', char.personality || '');
formData.append('scenario', char.scenario || '');
formData.append('first_mes', char.first_mes || '');
formData.append('mes_example', char.mes_example || '');
formData.append('creator', char.creator || '');
formData.append('creator_notes', char.creator_notes || '');
formData.append('tags', Array.isArray(char.tags) ? char.tags.join(',') : (char.tags || ''));
formData.append('talkativeness', char.talkativeness || '0.5');
formData.append('fav', char.fav || 'false');
if (char.data) {
formData.append('extensions', JSON.stringify(char.data));
}
if (char.data && Array.isArray(char.data.alternate_greetings)) {
for (const value of char.data.alternate_greetings) {
formData.append('alternate_greetings', value);
}
}
const response = await fetch('/api/characters/edit', {
method: 'POST',
headers: getRequestHeaders({ omitContentType: true }),
body: formData
});
if (!response.ok) {
const errorText = await response.text();
console.error(`[Amily2 CharAPI] Failed to save character ${chid}:`, response.statusText, errorText);
return { success: false, message: `Save failed: ${response.statusText}` };
} else {
console.log(`[Amily2 CharAPI] Successfully saved character ${chid} (Background)`);
return { success: true };
}
} catch (e) {
console.error(`[Amily2 CharAPI] Error saving character ${chid}:`, e);
return { success: false, message: `Save error: ${e.message}` };
}
}
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 async 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) {
const success = await saveCharacterById(chid);
if (success) {
console.log(`[Amily2 CharAPI] Updated character ${chid}:`, Object.keys(updates));
return true;
}
return false;
}
return false;
}
export function getFirstMessages(chid) {
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 async 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);
const success = await saveCharacterById(chid);
if (success) {
console.log(`[Amily2 CharAPI] Added alternate greeting to character ${chid}`);
return true;
}
return false;
}
export async 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;
}
}
const success = await saveCharacterById(chid);
if (success) {
console.log(`[Amily2 CharAPI] Updated greeting ${index} for character ${chid}`);
return true;
}
return false;
}
export async 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;
}
}
const success = await saveCharacterById(chid);
if (success) {
console.log(`[Amily2 CharAPI] Removed greeting ${index} for character ${chid}`);
return true;
}
return false;
}
export async function createNewCharacter(name) {
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');
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',
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,128 @@
export class ContextManager {
constructor() {
this.keepToolOutputTurns = 5;
this.tokenLimit = 100000;
this.rules = [];
this.worldInfo = [];
this.activeWorldInfoCache = new Map();
this.cacheDuration = 3;
}
addRule(rule) {
this.rules.push({
id: rule.id || Date.now().toString(),
keyword: rule.keyword || null,
content: rule.content,
enabled: rule.enabled !== undefined ? rule.enabled : true
});
}
setWorldInfo(entries) {
this.worldInfo = entries.map(entry => {
let keys = [];
if (Array.isArray(entry.key)) {
keys = entry.key;
} else if (typeof entry.key === 'string') {
keys = entry.key.split(',').map(k => k.trim()).filter(k => k);
}
return {
id: entry.uid,
keys: keys,
content: entry.content,
enabled: entry.enabled !== false
};
});
}
getRelevantContext(contextText) {
const relevantRules = this.rules.filter(rule => {
if (!rule.enabled) return false;
if (!rule.keyword) return true;
return contextText.includes(rule.keyword);
});
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
};
}
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,91 @@
import { callAi, getApiConfig } from "./api.js";
export class MemorySystem {
constructor() {
this.summarizePrompt = `
The current conversation context is growing large. Your task is to create a comprehensive, structured summary of the character/world generation process so far.
This summary will be used as the "Memory" for the next steps, so it must be detailed enough to prevent information loss.
Please summarize the following:
1. **Core Identity**: Name, Age, Gender, Role, etc.
2. **Personality & Traits**: Key personality keywords, behavioral quirks, speech patterns.
3. **Appearance**: Physical description, clothing, accessories.
4. **Background & Lore**: Backstory, world setting, important relationships.
5. **Current Progress**: What has been completed, what is currently being worked on, and what is left to do.
6. **User Preferences**: Any specific constraints or requests made by the user (e.g., "Make her tsundere", "Don't use modern technology").
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()}
`;
const messages = [
{ role: 'system', content: this.summarizePrompt },
...history.slice(-10),
{ role: 'user', content: `Please summarize the session based on the history above. ${contextMsg}` }
];
try {
const response = await callAi('executor', messages, {
max_tokens: 2000,
temperature: 0.5
});
return response;
} catch (error) {
console.error("Failed to generate summary:", error);
return null;
}
}
shouldSummarize(history, tokenCount, maxTokens) {
const tokenUsageRatio = tokenCount / maxTokens;
if (tokenUsageRatio > 0.7) return true;
if (history.length > 35) return true;
return false;
}
}

View File

@@ -0,0 +1,109 @@
export class TaskState {
constructor() {
this.reset();
}
reset() {
this.originalRequest = "";
this.currentGoal = "";
this.completedSteps = [];
this.pendingSteps = [];
this.summary = "";
this.generatedData = {};
this.style_reference = "";
this.keyFacts = [];
this.lastSummaryTimestamp = 0;
}
init(request) {
this.reset();
this.originalRequest = request;
this.currentGoal = "Analyze request and plan steps";
this.lastSummaryTimestamp = Date.now();
}
updateSummary(newSummary) {
this.summary = newSummary;
this.lastSummaryTimestamp = Date.now();
}
addCompletedStep(step) {
this.completedSteps.push(step);
}
setPendingSteps(steps) {
this.pendingSteps = steps;
}
setCurrentGoal(goal) {
this.currentGoal = goal;
}
updateGeneratedData(key, value) {
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`;
}
if (this.pendingSteps.length > 0) {
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# Recent Context Summary\n${this.summary}\n`;
}
return context;
}
toJSON() {
return {
originalRequest: this.originalRequest,
currentGoal: this.currentGoal,
completedSteps: this.completedSteps,
pendingSteps: this.pendingSteps,
summary: this.summary,
generatedData: this.generatedData,
style_reference: this.style_reference,
keyFacts: this.keyFacts,
lastSummaryTimestamp: this.lastSummaryTimestamp
};
}
fromJSON(json) {
if (!json) return;
this.originalRequest = json.originalRequest || "";
this.currentGoal = json.currentGoal || "";
this.completedSteps = json.completedSteps || [];
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;
}
}

View File

@@ -0,0 +1,680 @@
import { amilyHelper } from "../tavern-helper/main.js";
import * as charApi from "./char-api.js";
import { callAi } from "./api.js";
export const tools = {
read_world_info: async ({ book_name, return_full = false }) => {
const entries = await amilyHelper.getLorebookEntries(book_name);
if (return_full) {
return JSON.stringify(entries, null, 2);
}
const summary = entries.map(e => {
let keys = e.key;
if (Array.isArray(keys)) keys = keys.join(', ');
return {
uid: e.uid,
keys: keys,
comment: e.comment || keys || "未命名条目",
};
});
return JSON.stringify({
info: "世界书条目索引。请使用带有 'uid' 的 'read_world_entry' 来读取具体内容。",
total_entries: entries.length,
entries: summary
}, null, 2);
},
read_world_entry: async ({ book_name, uid }) => {
const entries = await amilyHelper.getLorebookEntries(book_name);
const entry = entries.find(e => String(e.uid) === String(uid));
if (!entry) {
return JSON.stringify({
status: "error",
code: "ENTRY_NOT_FOUND",
message: `在世界书 "${book_name}" 中未找到 UID 为 ${uid} 的条目。`,
suggestion: "请使用 'read_world_info' 查看可用的 UID。"
});
}
return JSON.stringify({
status: "success",
data: entry
}, 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 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 JSON.stringify({
status: "error",
code: "INVALID_TYPE",
message: "'entries' 参数必须是数组或对象。"
});
}
}
const updates = [];
const creates = [];
for (const entry of entries) {
if (entry.uid !== undefined) {
updates.push(entry);
} else {
creates.push(entry);
}
}
let updatedCount = 0;
let createdCount = 0;
let errors = [];
if (updates.length > 0) {
const success = await amilyHelper.setLorebookEntries(book_name, updates);
if (success) updatedCount = updates.length;
else errors.push("更新条目失败。");
}
if (creates.length > 0) {
const success = await amilyHelper.createLorebookEntries(book_name, creates);
if (success) createdCount = creates.length;
else errors.push("创建条目失败。");
}
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);
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 JSON.stringify({
status: "error",
code: "CHAR_NOT_FOUND",
message: "未找到角色。"
});
}
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({
status: "success",
data: safeChar
}, null, 2);
},
update_character_card: async (args) => {
const { chid, ...updates } = args;
const finalUpdates = args.updates || updates;
const success = await charApi.updateCharacter(chid, finalUpdates);
if (success) {
const updatedFields = Object.keys(finalUpdates).join(', ');
return JSON.stringify({
status: "success",
message: `角色卡更新成功 [ID: ${chid}]。`,
data: { updated_fields: updatedFields }
});
} else {
return JSON.stringify({
status: "error",
code: "UPDATE_FAILED",
message: "更新角色卡失败。请确保您正在编辑当前选中的角色(暂不支持后台编辑其他角色)。"
});
}
},
edit_character_text: async ({ chid, field, diff }) => {
const char = charApi.getCharacter(chid);
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 JSON.stringify({
status: "error",
code: "INVALID_FIELD",
message: `无效的字段。允许的字段: ${allowedFields.join(', ')}`
});
}
let content = char[field] || '';
const normalizedDiff = diff
.replace(/-------\s*SEARCH/g, '------- SEARCH')
.replace(/=======\s*/g, '=======')
.replace(/\+\+\+\+\+\+\+\s*REPLACE/g, '+++++++ REPLACE');
const changes = normalizedDiff.split('------- SEARCH');
if (changes[0].trim() === '') changes.shift();
for (const change of changes) {
const parts = change.split('=======');
if (parts.length < 2) continue;
const searchBlock = parts[0].trim();
const replaceBlock = parts[1].split('+++++++ REPLACE')[0].trim();
if (content.includes(searchBlock)) {
content = content.replace(searchBlock, replaceBlock);
continue;
}
const normalizedSearch = searchBlock.replace(/\r\n/g, '\n');
const lines = normalizedSearch.split('\n');
const regexPattern = lines.map(line => line.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('\\r?\\n');
const regex = new RegExp(regexPattern);
const match = content.match(regex);
if (match) {
content = content.replace(match[0], replaceBlock);
continue;
}
return JSON.stringify({
status: "error",
code: "SEARCH_NOT_FOUND",
message: `在字段 '${field}' 中未找到搜索块。`,
suggestion: "请确保 SEARCH 块与现有内容完全匹配(包括空格)。"
});
}
const success = await charApi.updateCharacter(chid, { [field]: content });
if (success) {
return JSON.stringify({
status: "success",
message: `字段 '${field}' 更新成功。`
});
} else {
return JSON.stringify({
status: "error",
code: "UPDATE_FAILED",
message: `更新字段 '${field}' 失败。请确保您正在编辑当前选中的角色(暂不支持后台编辑其他角色)。`
});
}
},
edit_world_info_entry: async ({ book_name, uid, diff }) => {
const entries = await amilyHelper.getLorebookEntries(book_name);
const entry = entries.find(e => String(e.uid) === String(uid));
if (!entry) {
return JSON.stringify({
status: "error",
code: "ENTRY_NOT_FOUND",
message: `在世界书 "${book_name}" 中未找到 UID 为 ${uid} 的条目。`
});
}
let content = entry.content || '';
const normalizedDiff = diff
.replace(/-------\s*SEARCH/g, '------- SEARCH')
.replace(/=======\s*/g, '=======')
.replace(/\+\+\+\+\+\+\+\s*REPLACE/g, '+++++++ REPLACE');
const changes = normalizedDiff.split('------- SEARCH');
if (changes[0].trim() === '') changes.shift();
for (const change of changes) {
const parts = change.split('=======');
if (parts.length < 2) continue;
const searchBlock = parts[0].trim();
const replaceBlock = parts[1].split('+++++++ REPLACE')[0].trim();
if (content.includes(searchBlock)) {
content = content.replace(searchBlock, replaceBlock);
continue;
}
const normalizedSearch = searchBlock.replace(/\r\n/g, '\n');
const lines = normalizedSearch.split('\n');
const regexPattern = lines.map(line => line.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('\\r?\\n');
const regex = new RegExp(regexPattern);
const match = content.match(regex);
if (match) {
content = content.replace(match[0], replaceBlock);
continue;
}
return JSON.stringify({
status: "error",
code: "SEARCH_NOT_FOUND",
message: `在条目内容中未找到搜索块。`,
suggestion: "请确保 SEARCH 块与现有内容完全匹配(包括空格)。"
});
}
const success = await amilyHelper.setLorebookEntries(book_name, [{ uid: entry.uid, content: content }]);
if (success) {
return JSON.stringify({
status: "success",
message: `条目 [${uid}] 更新成功。`
});
} else {
return JSON.stringify({
status: "error",
code: "UPDATE_FAILED",
message: `更新条目 [${uid}] 失败。`
});
}
},
manage_first_message: async ({ action, chid, index, message }) => {
let success = false;
switch (action) {
case 'add':
success = await charApi.addFirstMessage(chid, message);
break;
case 'update':
success = await charApi.updateFirstMessage(chid, index, message);
break;
case 'remove':
success = await charApi.removeFirstMessage(chid, index);
break;
default:
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} 失败。`
});
}
},
create_character: async ({ name }) => {
const result = await charApi.createNewCharacter(name);
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 }
});
}
};
export function getToolDefinitions() {
return [
{
name: "read_world_info",
description: "读取世界书的索引(包含关键字和注释的条目列表)。不返回完整内容。",
parameters: {
type: "object",
properties: {
book_name: { type: "string", description: "世界书名称。" }
},
required: ["book_name"]
}
},
{
name: "read_world_entry",
description: "读取特定世界书条目的完整内容。",
parameters: {
type: "object",
properties: {
book_name: { type: "string", description: "世界书名称。" },
uid: { type: "number", description: "要读取的条目 UID。" }
},
required: ["book_name", "uid"]
}
},
{
name: "write_world_info_entry",
description: "创建或更新世界书中的条目。",
parameters: {
type: "object",
properties: {
book_name: { type: "string", description: "世界书名称。" },
entries: {
type: "array",
items: {
type: "object",
properties: {
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: "防止递归。" }
}
}
}
},
required: ["book_name", "entries"]
}
},
{
name: "create_world_book",
description: "创建一个新的空世界书。",
parameters: {
type: "object",
properties: {
book_name: { type: "string", description: "新世界书的名称。" }
},
required: ["book_name"]
}
},
{
name: "read_character_card",
description: "读取角色卡数据。",
parameters: {
type: "object",
properties: {
chid: { type: "number", description: "角色 ID。" }
},
required: ["chid"]
}
},
{
name: "update_character_card",
description: "更新角色卡字段(覆盖)。",
parameters: {
type: "object",
properties: {
chid: { type: "number", description: "角色 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: "使用 搜索/替换 块编辑角色的特定文本字段。",
parameters: {
type: "object",
properties: {
chid: { type: "number", description: "角色 ID。" },
field: { type: "string", enum: ["description", "personality", "scenario", "first_mes", "mes_example"], description: "要编辑的字段。" },
diff: {
type: "string",
description: "一个或多个遵循此确切格式的 搜索/替换 块:\n------- SEARCH\n[exact content to find]\n=======\n[new content to replace with]\n+++++++ REPLACE"
}
},
required: ["chid", "field", "diff"]
}
},
{
name: "edit_world_info_entry",
description: "使用 搜索/替换 块编辑世界书条目的内容。",
parameters: {
type: "object",
properties: {
book_name: { type: "string", description: "世界书名称。" },
uid: { type: "number", description: "条目 UID。" },
diff: {
type: "string",
description: "一个或多个遵循此确切格式的 搜索/替换 块:\n------- SEARCH\n[exact content to find]\n=======\n[new content to replace with]\n+++++++ REPLACE"
}
},
required: ["book_name", "uid", "diff"]
}
},
{
name: "manage_first_message",
description: "添加、更新或删除候补开场白。",
parameters: {
type: "object",
properties: {
action: { type: "string", enum: ["add", "update", "remove"] },
chid: { type: "number", description: "角色 ID。" },
index: { type: "number", description: "开场白索引(更新/删除时必需)。" },
message: { type: "string", description: "开场白内容(添加/更新时必需)。" }
},
required: ["action", "chid"]
}
},
{
name: "create_character",
description: "创建一个新角色卡。",
parameters: {
type: "object",
properties: {
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"]
}
}
];
}

File diff suppressed because it is too large Load Diff