mirror of
https://github.com/SilenceLurker/ST-Amily2-Chat-Optimisation.git
synced 2026-06-06 05:25:51 +00:00
Initial commit with CC BY-NC-ND 4.0 license
This commit is contained in:
BIN
core/auto-char-card/Amily.png
Normal file
BIN
core/auto-char-card/Amily.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
528
core/auto-char-card/agent-manager.js
Normal file
528
core/auto-char-card/agent-manager.js
Normal 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
171
core/auto-char-card/api.js
Normal 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;
|
||||
}
|
||||
}
|
||||
272
core/auto-char-card/char-api.js
Normal file
272
core/auto-char-card/char-api.js
Normal 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;
|
||||
}
|
||||
}
|
||||
128
core/auto-char-card/context-manager.js
Normal file
128
core/auto-char-card/context-manager.js
Normal 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();
|
||||
}
|
||||
}
|
||||
91
core/auto-char-card/memory-system.js
Normal file
91
core/auto-char-card/memory-system.js
Normal 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;
|
||||
}
|
||||
}
|
||||
109
core/auto-char-card/task-state.js
Normal file
109
core/auto-char-card/task-state.js
Normal 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;
|
||||
}
|
||||
}
|
||||
680
core/auto-char-card/tools.js
Normal file
680
core/auto-char-card/tools.js
Normal 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"]
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
1595
core/auto-char-card/ui-bindings.js
Normal file
1595
core/auto-char-card/ui-bindings.js
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user