mirror of
https://github.com/Wx-2025/ST-Amily2-Chat-Optimisation.git
synced 2026-06-06 08:05:49 +00:00
Compare commits
2 Commits
5df9c5ebd3
...
2f5aea445f
| Author | SHA1 | Date | |
|---|---|---|---|
| 2f5aea445f | |||
| f44395120b |
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 |
@@ -12,7 +12,7 @@ export class AgentManager {
|
||||
this.memorySystem = new MemorySystem();
|
||||
this.currentChid = undefined;
|
||||
this.currentBookName = undefined;
|
||||
this.status = 'idle'; // idle, running, paused
|
||||
this.status = 'idle';
|
||||
this.approvalRequired = false;
|
||||
this.pendingToolCall = null;
|
||||
}
|
||||
@@ -23,7 +23,6 @@ export class AgentManager {
|
||||
|
||||
if (bookName && bookName !== 'new') {
|
||||
try {
|
||||
// Use return_full: true to get content for ContextManager (RAG)
|
||||
const bookData = await tools.read_world_info({ book_name: bookName, return_full: true });
|
||||
const entries = JSON.parse(bookData);
|
||||
this.contextManager.setWorldInfo(entries);
|
||||
@@ -41,30 +40,28 @@ export class AgentManager {
|
||||
this.status = 'idle';
|
||||
}
|
||||
|
||||
async resumeWithApproval(approved, feedback, onStreamUpdate, onPreviewUpdate, onApprovalRequest) {
|
||||
async resumeWithApproval(approved, feedback, onStreamUpdate, onPreviewUpdate, onApprovalRequest, onContextUpdate) {
|
||||
if (this.status !== 'paused' || !this.pendingToolCall) return;
|
||||
|
||||
if (approved) {
|
||||
this.status = 'running';
|
||||
await this.executePendingTool(onStreamUpdate, onPreviewUpdate);
|
||||
await this.executePendingTool(onStreamUpdate, onPreviewUpdate, onContextUpdate);
|
||||
this.pendingToolCall = null;
|
||||
await this.runTaskLoop(onStreamUpdate, onPreviewUpdate, onApprovalRequest);
|
||||
await this.runTaskLoop(onStreamUpdate, onPreviewUpdate, onApprovalRequest, onContextUpdate);
|
||||
} else {
|
||||
this.status = 'running';
|
||||
this.pendingToolCall = null;
|
||||
// Add feedback as user message to guide correction
|
||||
this.history.push({
|
||||
role: 'user',
|
||||
content: `[Tool Execution Denied] User Feedback: ${feedback || "No reason provided."}`
|
||||
content: `[工具执行被拒绝] 用户反馈: ${feedback || "未提供原因。"}`
|
||||
});
|
||||
await this.runTaskLoop(onStreamUpdate, onPreviewUpdate, onApprovalRequest);
|
||||
await this.runTaskLoop(onStreamUpdate, onPreviewUpdate, onApprovalRequest, onContextUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
async buildSystemPrompt() {
|
||||
const toolDefs = getToolDefinitions();
|
||||
|
||||
// 1. Role & Objective
|
||||
let prompt = `You are an expert Character Card Designer and World Builder.
|
||||
You accomplish a given task iteratively, breaking it down into clear steps and working through them methodically.
|
||||
|
||||
@@ -98,8 +95,9 @@ ${this.taskState.getPromptContext()}
|
||||
prompt += `- **World Info Book**: ${this.currentBookName}\n`;
|
||||
}
|
||||
|
||||
// Dynamic Context Injection (Rules & World Info)
|
||||
const contextText = this.getLastUserMessage() || "";
|
||||
const recentHistory = this.history.slice(-3).map(m => m.content).join('\n');
|
||||
const contextText = (recentHistory + "\n" + (this.getLastUserMessage() || "")).trim();
|
||||
|
||||
const { rules, worldInfo } = this.contextManager.getRelevantContext(contextText);
|
||||
|
||||
if (rules.length > 0) {
|
||||
@@ -116,7 +114,6 @@ ${this.taskState.getPromptContext()}
|
||||
});
|
||||
}
|
||||
|
||||
// Environment Details Injection
|
||||
let envDetails = `\n<environment_details>\n`;
|
||||
envDetails += `# Current Time\n${new Date().toLocaleString()}\n\n`;
|
||||
|
||||
@@ -136,7 +133,6 @@ ${this.taskState.getPromptContext()}
|
||||
|
||||
if (this.currentBookName && this.currentBookName !== 'new') {
|
||||
try {
|
||||
// Get the index for the system prompt
|
||||
const bookData = await tools.read_world_info({ book_name: this.currentBookName, return_full: false });
|
||||
const result = JSON.parse(bookData);
|
||||
envDetails += `# Current World Book Index\n`;
|
||||
@@ -161,7 +157,6 @@ ${this.taskState.getPromptContext()}
|
||||
envDetails += `</environment_details>\n`;
|
||||
prompt += envDetails;
|
||||
|
||||
// 2. Tools
|
||||
prompt += `\n# Tools\n\n`;
|
||||
toolDefs.forEach(tool => {
|
||||
prompt += `## ${tool.name}\n`;
|
||||
@@ -169,7 +164,6 @@ ${this.taskState.getPromptContext()}
|
||||
prompt += `Parameters:\n${JSON.stringify(tool.parameters, null, 2)}\n\n`;
|
||||
});
|
||||
|
||||
// 3. Tool Use Formatting
|
||||
prompt += `
|
||||
# Tool Use Formatting
|
||||
|
||||
@@ -191,7 +185,8 @@ Example:
|
||||
|
||||
# Rules
|
||||
|
||||
- **Think First**: Before using any tool, you MUST output a \`<thinking>\` block explaining your plan and reasoning.
|
||||
- **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.
|
||||
@@ -212,90 +207,78 @@ Example:
|
||||
return null;
|
||||
}
|
||||
|
||||
async handleUserMessage(message, onStreamUpdate, onPreviewUpdate, onApprovalRequest) {
|
||||
// Initialize task state if it's the first message or explicitly requested
|
||||
async handleUserMessage(message, onStreamUpdate, onPreviewUpdate, onApprovalRequest, onContextUpdate) {
|
||||
if (this.history.length === 0) {
|
||||
this.taskState.init(message);
|
||||
}
|
||||
|
||||
this.history.push({ role: 'user', content: message });
|
||||
this.status = 'running';
|
||||
await this.runTaskLoop(onStreamUpdate, onPreviewUpdate, onApprovalRequest);
|
||||
await this.runTaskLoop(onStreamUpdate, onPreviewUpdate, onApprovalRequest, onContextUpdate);
|
||||
}
|
||||
|
||||
async runTaskLoop(onStreamUpdate, onPreviewUpdate, onApprovalRequest) {
|
||||
let maxTurns = 20; // Safety limit
|
||||
async runTaskLoop(onStreamUpdate, onPreviewUpdate, onApprovalRequest, onContextUpdate) {
|
||||
let maxTurns = 20;
|
||||
let currentTurn = 0;
|
||||
|
||||
while (this.status === 'running' && currentTurn < maxTurns) {
|
||||
currentTurn++;
|
||||
|
||||
// 0. Check Memory/Context
|
||||
|
||||
const config = getApiConfig('executor');
|
||||
const currentTokens = this.contextManager.estimateTokens(JSON.stringify(this.history));
|
||||
|
||||
if (this.memorySystem.shouldSummarize(this.history, currentTokens, config.maxTokens)) {
|
||||
onStreamUpdate("Context limit approaching. Summarizing memory...", 'system');
|
||||
onStreamUpdate("上下文即将达到上限,正在总结记忆...", 'system');
|
||||
const summary = await this.memorySystem.summarize(this.history, this.taskState);
|
||||
if (summary) {
|
||||
this.taskState.updateSummary(summary);
|
||||
// Optional: Compress history here. For now, we just rely on the summary being injected.
|
||||
// A simple compression strategy: Keep the last 5 messages, and replace the rest with a system note.
|
||||
if (this.history.length > 5) {
|
||||
const lastMessages = this.history.slice(-5);
|
||||
this.history = [
|
||||
{ role: 'system', content: `[History Compressed] Previous conversation has been summarized in the "Memory & Task State" section. Resuming from recent messages.` },
|
||||
{ role: 'system', content: `[历史记录已压缩] 之前的对话已总结在“记忆与任务状态”部分。从最近的消息继续。` },
|
||||
...lastMessages
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 1. Build System Prompt (Dynamic)
|
||||
const systemPrompt = await this.buildSystemPrompt();
|
||||
|
||||
// 2. Build Messages
|
||||
|
||||
const messages = this.contextManager.buildMessages(
|
||||
systemPrompt,
|
||||
this.history,
|
||||
config.maxTokens
|
||||
);
|
||||
|
||||
// 3. Call AI
|
||||
let responseContent;
|
||||
let fullStreamedContent = "";
|
||||
try {
|
||||
onStreamUpdate("Thinking...", 'system');
|
||||
onStreamUpdate("思考中...", 'system');
|
||||
responseContent = await callAi('executor', messages, {}, (chunk) => {
|
||||
onStreamUpdate(chunk, 'stream-assistant');
|
||||
fullStreamedContent += chunk;
|
||||
|
||||
// Try to parse partial tool call for real-time preview
|
||||
|
||||
if (onPreviewUpdate) {
|
||||
const partialTool = this.parsePartialToolCall(fullStreamedContent);
|
||||
if (partialTool) {
|
||||
onPreviewUpdate(partialTool.name, partialTool.arguments, true); // true = isPartial
|
||||
onPreviewUpdate(partialTool.name, partialTool.arguments, true);
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
onStreamUpdate(`[Error] ${error.message}`, 'system');
|
||||
this.status = 'idle'; // Stop on API error
|
||||
onStreamUpdate(`[错误] ${error.message}`, 'system');
|
||||
this.status = 'idle';
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.status !== 'running') return; // Check if stopped during await
|
||||
if (this.status !== 'running') return;
|
||||
|
||||
// 4. Process Response
|
||||
|
||||
// Check for truncation (Auto-Continue)
|
||||
const lastChar = responseContent.trim().slice(-1);
|
||||
const isTruncated = !['.', '!', '?', '"', "'", '}', ']', '>', '*'].includes(lastChar) && responseContent.length > 100;
|
||||
|
||||
if (isTruncated && currentTurn < maxTurns) {
|
||||
console.log("检测到回复截断,正在自动继续...");
|
||||
try {
|
||||
// Append a continue message
|
||||
const continueMsg = { role: 'user', content: "Continue" };
|
||||
const continueMessages = [...messages, { role: 'assistant', content: responseContent }, continueMsg];
|
||||
|
||||
@@ -316,33 +299,28 @@ Example:
|
||||
if (thinkingMatch) {
|
||||
onStreamUpdate(thinkingMatch[1].trim(), 'thought');
|
||||
}
|
||||
|
||||
// Clean up content for UI display
|
||||
|
||||
let cleanContent = responseContent
|
||||
.replace(/<thinking(?:\s+[^>]*)?>[\s\S]*?<\/thinking>/gi, '')
|
||||
.replace(/<\/thinking>/gi, ''); // Remove residual tags
|
||||
.replace(/<\/thinking>/gi, '');
|
||||
|
||||
const toolNames = Object.keys(tools);
|
||||
const toolRegex = new RegExp(`<(${toolNames.join('|')})(?:\\s+[^>]*)?>[\\s\\S]*?<\\/\\1>`, 'gi');
|
||||
cleanContent = cleanContent.replace(toolRegex, '').trim();
|
||||
|
||||
// Update the UI with the final clean content (replacing the raw stream)
|
||||
if (cleanContent) {
|
||||
onStreamUpdate(cleanContent, 'assistant');
|
||||
}
|
||||
|
||||
// 5. Parse Tool Call
|
||||
const toolCall = this.parseToolCall(responseContent);
|
||||
|
||||
if (toolCall) {
|
||||
// Check for duplicate tool calls to prevent loops
|
||||
if (this.isDuplicateToolCall(toolCall)) {
|
||||
const warningMsg = `[System Warning] You have just executed this exact tool call (${toolCall.name}). Do not repeat the same action immediately. If you need to check the result again, look at the conversation history. If the previous result was unsatisfactory, try a different approach.`;
|
||||
const warningMsg = `[系统警告] 你刚刚执行了完全相同的工具调用 (${toolCall.name})。请勿立即重复相同的操作。如果需要再次检查结果,请查看对话历史。如果之前的结果不满意,请尝试不同的方法。`;
|
||||
this.history.push({ role: 'user', content: warningMsg });
|
||||
continue;
|
||||
}
|
||||
|
||||
// Inject Context if missing
|
||||
if (toolCall.name === 'update_character_card' || toolCall.name === 'read_character_card' || toolCall.name === 'edit_character_text' || toolCall.name === 'manage_first_message') {
|
||||
if (toolCall.arguments.chid === undefined && this.currentChid !== undefined) {
|
||||
toolCall.arguments.chid = parseInt(this.currentChid);
|
||||
@@ -361,9 +339,9 @@ Example:
|
||||
if (onApprovalRequest) {
|
||||
onApprovalRequest(toolCall.name, toolCall.arguments);
|
||||
}
|
||||
return; // Exit loop, wait for resumeWithApproval
|
||||
return;
|
||||
} else {
|
||||
await this.executePendingTool(onStreamUpdate, onPreviewUpdate);
|
||||
await this.executePendingTool(onStreamUpdate, onPreviewUpdate, onContextUpdate);
|
||||
this.pendingToolCall = null;
|
||||
}
|
||||
} else {
|
||||
@@ -372,51 +350,87 @@ Example:
|
||||
}
|
||||
}
|
||||
|
||||
async executePendingTool(onStreamUpdate, onPreviewUpdate) {
|
||||
async executePendingTool(onStreamUpdate, onPreviewUpdate, onContextUpdate) {
|
||||
const toolCall = this.pendingToolCall;
|
||||
if (!toolCall) return;
|
||||
|
||||
onStreamUpdate(`Executing: ${toolCall.name}`, 'system');
|
||||
onStreamUpdate(`正在执行: ${toolCall.name}`, 'system');
|
||||
|
||||
let result;
|
||||
try {
|
||||
if (tools[toolCall.name]) {
|
||||
result = await tools[toolCall.name](toolCall.arguments);
|
||||
|
||||
if (toolCall.name === 'create_character' && result.includes('ID:')) {
|
||||
const match = result.match(/ID:\s*(\d+)/);
|
||||
if (match) {
|
||||
this.currentChid = parseInt(match[1]);
|
||||
|
||||
try {
|
||||
const jsonResult = JSON.parse(result);
|
||||
|
||||
if (toolCall.name === 'create_character' && jsonResult.status === 'success' && jsonResult.data && jsonResult.data.id) {
|
||||
this.currentChid = parseInt(jsonResult.data.id);
|
||||
if (onContextUpdate) onContextUpdate('char', this.currentChid);
|
||||
}
|
||||
|
||||
if (toolCall.name === 'create_world_book' && jsonResult.status === 'success') {
|
||||
this.currentBookName = toolCall.arguments.book_name;
|
||||
if (onContextUpdate) onContextUpdate('world', this.currentBookName);
|
||||
}
|
||||
|
||||
if (jsonResult._action === 'update_task_state' && jsonResult._updates) {
|
||||
if (jsonResult._updates.style_reference) {
|
||||
this.taskState.setStyle(jsonResult._updates.style_reference);
|
||||
}
|
||||
}
|
||||
|
||||
if (jsonResult._action === 'stop_and_wait') {
|
||||
this.status = 'idle';
|
||||
}
|
||||
} catch (e) {
|
||||
if (toolCall.name === 'create_character' && result.includes('ID:')) {
|
||||
const match = result.match(/ID:\s*(\d+)/);
|
||||
if (match) {
|
||||
this.currentChid = parseInt(match[1]);
|
||||
if (onContextUpdate) onContextUpdate('char', this.currentChid);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
result = `Error: Tool '${toolCall.name}' not found.`;
|
||||
result = JSON.stringify({
|
||||
status: "error",
|
||||
code: "TOOL_NOT_FOUND",
|
||||
message: `错误: 未找到工具 '${toolCall.name}'。`
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
result = `Error executing tool '${toolCall.name}': ${error.message}`;
|
||||
result = JSON.stringify({
|
||||
status: "error",
|
||||
code: "EXECUTION_ERROR",
|
||||
message: `执行工具 '${toolCall.name}' 时出错: ${error.message}`
|
||||
});
|
||||
}
|
||||
|
||||
const toolResultMsg = `[Tool Result for ${toolCall.name}]\n${result}`;
|
||||
const toolResultMsg = `[工具 '${toolCall.name}' 的执行结果]\n${result}`;
|
||||
this.history.push({ role: 'user', content: toolResultMsg });
|
||||
|
||||
if (onPreviewUpdate && !result.startsWith('Error')) {
|
||||
|
||||
let isError = false;
|
||||
try {
|
||||
const jsonResult = JSON.parse(result);
|
||||
if (jsonResult.status === 'error') isError = true;
|
||||
} catch (e) {
|
||||
if (result.startsWith('Error')) isError = true;
|
||||
}
|
||||
|
||||
if (onPreviewUpdate && !isError) {
|
||||
onPreviewUpdate(toolCall.name, toolCall.arguments);
|
||||
}
|
||||
}
|
||||
|
||||
isDuplicateToolCall(toolCall) {
|
||||
if (this.history.length < 3) return false;
|
||||
|
||||
// History structure:
|
||||
// ...
|
||||
// Assistant: <tool>A</tool> (index -3)
|
||||
// User: [Tool Result A] (index -2)
|
||||
// Assistant: <tool>A</tool> (index -1, current)
|
||||
|
||||
|
||||
const prevAssistantMsg = this.history[this.history.length - 3];
|
||||
const prevUserMsg = this.history[this.history.length - 2];
|
||||
|
||||
if (prevAssistantMsg.role === 'assistant' && prevUserMsg.role === 'user' && prevUserMsg.content.startsWith('[Tool Result')) {
|
||||
if (prevAssistantMsg.role === 'assistant' && prevUserMsg.role === 'user' && prevUserMsg.content.startsWith('[工具')) {
|
||||
const prevToolCall = this.parseToolCall(prevAssistantMsg.content);
|
||||
if (prevToolCall &&
|
||||
prevToolCall.name === toolCall.name &&
|
||||
@@ -461,27 +475,22 @@ Example:
|
||||
parsePartialToolCall(content) {
|
||||
const toolNames = Object.keys(tools);
|
||||
for (const name of toolNames) {
|
||||
// Look for the opening tag
|
||||
const openTagRegex = new RegExp(`<${name}>`);
|
||||
const openMatch = content.match(openTagRegex);
|
||||
|
||||
if (openMatch) {
|
||||
// We found a tool start. Now try to extract params, even if incomplete.
|
||||
const startIndex = openMatch.index + openMatch[0].length;
|
||||
const toolContent = content.slice(startIndex);
|
||||
|
||||
const args = {};
|
||||
// Match complete tags or tags that are still open (at the end)
|
||||
// <param>value...</param> OR <param>value...
|
||||
|
||||
const paramRegex = /<(\w+)>([\s\S]*?)(?:<\/\1>|$)/g;
|
||||
|
||||
let paramMatch;
|
||||
while ((paramMatch = paramRegex.exec(toolContent)) !== null) {
|
||||
const paramName = paramMatch[1];
|
||||
let paramValue = paramMatch[2];
|
||||
|
||||
// Don't try to JSON parse partial content here, leave it as string
|
||||
// The UI handler will deal with partial JSON
|
||||
|
||||
args[paramName] = paramValue;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,26 @@
|
||||
import { characters, saveCharacterDebounced, this_chid, getCharacters, getRequestHeaders } from "/script.js";
|
||||
import { extensionName } from "../../utils/settings.js";
|
||||
|
||||
async function saveCharacterById(chid) {
|
||||
const char = characters[chid];
|
||||
if (!char) return;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/characters/edit', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
body: JSON.stringify(char)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error(`[Amily2 CharAPI] Failed to save character ${chid}:`, response.statusText);
|
||||
} else {
|
||||
console.log(`[Amily2 CharAPI] Successfully saved character ${chid}`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`[Amily2 CharAPI] Error saving character ${chid}:`, e);
|
||||
}
|
||||
}
|
||||
|
||||
export function getCharacter(chid = this_chid) {
|
||||
if (chid === undefined || chid < 0 || !characters[chid]) {
|
||||
@@ -23,7 +45,7 @@ export function updateCharacter(chid, updates) {
|
||||
});
|
||||
|
||||
if (changed) {
|
||||
saveCharacterDebounced();
|
||||
saveCharacterById(chid);
|
||||
console.log(`[Amily2 CharAPI] Updated character ${chid}:`, Object.keys(updates));
|
||||
return true;
|
||||
}
|
||||
@@ -51,7 +73,7 @@ export function addFirstMessage(chid, message) {
|
||||
}
|
||||
|
||||
char.data.alternate_greetings.push(message);
|
||||
saveCharacterDebounced();
|
||||
saveCharacterById(chid);
|
||||
console.log(`[Amily2 CharAPI] Added alternate greeting to character ${chid}`);
|
||||
return true;
|
||||
}
|
||||
@@ -71,7 +93,7 @@ export function updateFirstMessage(chid, index, message) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
saveCharacterDebounced();
|
||||
saveCharacterById(chid);
|
||||
console.log(`[Amily2 CharAPI] Updated greeting ${index} for character ${chid}`);
|
||||
return true;
|
||||
}
|
||||
@@ -92,7 +114,7 @@ export function removeFirstMessage(chid, index) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
saveCharacterDebounced();
|
||||
saveCharacterById(chid);
|
||||
console.log(`[Amily2 CharAPI] Removed greeting ${index} for character ${chid}`);
|
||||
return true;
|
||||
}
|
||||
@@ -121,10 +143,26 @@ export async function createNewCharacter(name) {
|
||||
formData.append('depth_prompt_depth', '4');
|
||||
formData.append('depth_prompt_role', 'system');
|
||||
|
||||
const base64Png = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=";
|
||||
const res = await fetch(`data:image/png;base64,${base64Png}`);
|
||||
const blob = await res.blob();
|
||||
formData.append('avatar', blob, 'default.png');
|
||||
try {
|
||||
const res = await fetch(`scripts/extensions/third-party/${extensionName}/core/auto-char-card/Amily.png`);
|
||||
if (res.ok) {
|
||||
const blob = await res.blob();
|
||||
formData.append('avatar', blob, 'default.png');
|
||||
} else {
|
||||
throw new Error('Failed to fetch default avatar');
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("[Amily2 CharAPI] Failed to load default avatar, using fallback 1x1 PNG.", e);
|
||||
const base64Png = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==";
|
||||
const byteCharacters = atob(base64Png);
|
||||
const byteNumbers = new Array(byteCharacters.length);
|
||||
for (let i = 0; i < byteCharacters.length; i++) {
|
||||
byteNumbers[i] = byteCharacters.charCodeAt(i);
|
||||
}
|
||||
const byteArray = new Uint8Array(byteNumbers);
|
||||
const blob = new Blob([byteArray], { type: 'image/png' });
|
||||
formData.append('avatar', blob, 'default.png');
|
||||
}
|
||||
|
||||
const response = await fetch('/api/characters/create', {
|
||||
method: 'POST',
|
||||
|
||||
@@ -4,6 +4,8 @@ export class ContextManager {
|
||||
this.tokenLimit = 100000;
|
||||
this.rules = [];
|
||||
this.worldInfo = [];
|
||||
this.activeWorldInfoCache = new Map();
|
||||
this.cacheDuration = 3;
|
||||
}
|
||||
|
||||
addRule(rule) {
|
||||
@@ -40,12 +42,27 @@ export class ContextManager {
|
||||
return contextText.includes(rule.keyword);
|
||||
});
|
||||
|
||||
const relevantWorldInfo = this.worldInfo.filter(entry => {
|
||||
const currentMatches = this.worldInfo.filter(entry => {
|
||||
if (!entry.enabled) return false;
|
||||
if (!entry.keys || entry.keys.length === 0) return false;
|
||||
return entry.keys.some(key => contextText.includes(key));
|
||||
});
|
||||
|
||||
for (const [uid, data] of this.activeWorldInfoCache) {
|
||||
data.turnsLeft--;
|
||||
if (data.turnsLeft <= 0) {
|
||||
this.activeWorldInfoCache.delete(uid);
|
||||
}
|
||||
}
|
||||
|
||||
currentMatches.forEach(entry => {
|
||||
this.activeWorldInfoCache.set(entry.id, { turnsLeft: this.cacheDuration });
|
||||
});
|
||||
|
||||
const allRelevantUIDs = new Set([...currentMatches.map(e => e.id), ...this.activeWorldInfoCache.keys()]);
|
||||
|
||||
const relevantWorldInfo = this.worldInfo.filter(entry => allRelevantUIDs.has(entry.id));
|
||||
|
||||
return {
|
||||
rules: relevantRules,
|
||||
worldInfo: relevantWorldInfo
|
||||
|
||||
@@ -18,9 +18,46 @@ Format your response as a structured Markdown block.
|
||||
`;
|
||||
}
|
||||
|
||||
async extractKeyFacts(history) {
|
||||
const extractionPrompt = `
|
||||
Analyze the recent conversation and extract "Key Facts" that should be remembered long-term.
|
||||
Key Facts include:
|
||||
- Specific decisions made (e.g., "Character has blue eyes", "Weapon is a sword").
|
||||
- User preferences stated (e.g., "User dislikes horror").
|
||||
- Completed milestones.
|
||||
|
||||
Do NOT include temporary conversation details or planning steps.
|
||||
Return the facts as a JSON array of strings. Example: ["Eyes: Blue", "Class: Mage"].
|
||||
Output ONLY valid JSON.
|
||||
`;
|
||||
const recentHistory = history.slice(-5);
|
||||
const messages = [
|
||||
{ role: 'system', content: extractionPrompt },
|
||||
...recentHistory
|
||||
];
|
||||
|
||||
try {
|
||||
const response = await callAi('executor', messages, {
|
||||
max_tokens: 500,
|
||||
temperature: 0.3
|
||||
});
|
||||
const cleanResponse = response.replace(/```json/g, '').replace(/```/g, '').trim();
|
||||
const facts = JSON.parse(cleanResponse);
|
||||
return Array.isArray(facts) ? facts : [];
|
||||
} catch (error) {
|
||||
console.warn("Failed to extract key facts:", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async summarize(history, taskState) {
|
||||
const config = getApiConfig('executor');
|
||||
|
||||
const newFacts = await this.extractKeyFacts(history);
|
||||
if (newFacts.length > 0) {
|
||||
taskState.addKeyFacts(newFacts);
|
||||
}
|
||||
|
||||
const contextMsg = `
|
||||
[System Note]: The following is the current Task State. Use this to inform your summary.
|
||||
${taskState.getPromptContext()}
|
||||
|
||||
@@ -6,10 +6,12 @@ export class TaskState {
|
||||
reset() {
|
||||
this.originalRequest = "";
|
||||
this.currentGoal = "";
|
||||
this.completedSteps = []; // Array of strings
|
||||
this.pendingSteps = []; // Array of strings
|
||||
this.summary = ""; // The structured summary of the character/world so far
|
||||
this.generatedData = {}; // Key-value pairs of generated attributes (e.g., name, personality)
|
||||
this.completedSteps = [];
|
||||
this.pendingSteps = [];
|
||||
this.summary = "";
|
||||
this.generatedData = {};
|
||||
this.style_reference = "";
|
||||
this.keyFacts = [];
|
||||
this.lastSummaryTimestamp = 0;
|
||||
}
|
||||
|
||||
@@ -41,11 +43,23 @@ export class TaskState {
|
||||
this.generatedData[key] = value;
|
||||
}
|
||||
|
||||
setStyle(style) {
|
||||
this.style_reference = style;
|
||||
}
|
||||
|
||||
addKeyFacts(facts) {
|
||||
this.keyFacts.push(...facts);
|
||||
}
|
||||
|
||||
getPromptContext() {
|
||||
let context = `\n# Task State\n`;
|
||||
context += `- **Original Request**: ${this.originalRequest}\n`;
|
||||
context += `- **Current Goal**: ${this.currentGoal}\n`;
|
||||
|
||||
if (this.style_reference) {
|
||||
context += `- **Style Reference**: ${this.style_reference}\n`;
|
||||
}
|
||||
|
||||
if (this.completedSteps.length > 0) {
|
||||
context += `- **Completed Steps**:\n${this.completedSteps.map(s => ` - ${s}`).join('\n')}\n`;
|
||||
}
|
||||
@@ -54,8 +68,13 @@ export class TaskState {
|
||||
context += `- **Pending Steps**:\n${this.pendingSteps.map(s => ` - ${s}`).join('\n')}\n`;
|
||||
}
|
||||
|
||||
if (this.keyFacts.length > 0) {
|
||||
context += `\n# Key Facts (Long Term Memory)\n`;
|
||||
this.keyFacts.forEach(fact => context += `- ${fact}\n`);
|
||||
}
|
||||
|
||||
if (this.summary) {
|
||||
context += `\n# Memory & Context Summary\n${this.summary}\n`;
|
||||
context += `\n# Recent Context Summary\n${this.summary}\n`;
|
||||
}
|
||||
|
||||
return context;
|
||||
@@ -69,6 +88,8 @@ export class TaskState {
|
||||
pendingSteps: this.pendingSteps,
|
||||
summary: this.summary,
|
||||
generatedData: this.generatedData,
|
||||
style_reference: this.style_reference,
|
||||
keyFacts: this.keyFacts,
|
||||
lastSummaryTimestamp: this.lastSummaryTimestamp
|
||||
};
|
||||
}
|
||||
@@ -81,6 +102,8 @@ export class TaskState {
|
||||
this.pendingSteps = json.pendingSteps || [];
|
||||
this.summary = json.summary || "";
|
||||
this.generatedData = json.generatedData || {};
|
||||
this.style_reference = json.style_reference || "";
|
||||
this.keyFacts = json.keyFacts || [];
|
||||
this.lastSummaryTimestamp = json.lastSummaryTimestamp || 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { amilyHelper } from "../tavern-helper/main.js";
|
||||
import * as charApi from "./char-api.js";
|
||||
import { callAi } from "./api.js";
|
||||
|
||||
export const tools = {
|
||||
|
||||
@@ -17,12 +18,12 @@ export const tools = {
|
||||
return {
|
||||
uid: e.uid,
|
||||
keys: keys,
|
||||
comment: e.comment || keys || "Unnamed Entry",
|
||||
comment: e.comment || keys || "未命名条目",
|
||||
};
|
||||
});
|
||||
|
||||
return JSON.stringify({
|
||||
info: "Index of world book entries. Use 'read_world_entry' with 'uid' to read specific content.",
|
||||
info: "世界书条目索引。请使用带有 'uid' 的 'read_world_entry' 来读取具体内容。",
|
||||
total_entries: entries.length,
|
||||
entries: summary
|
||||
}, null, 2);
|
||||
@@ -33,10 +34,18 @@ export const tools = {
|
||||
const entry = entries.find(e => String(e.uid) === String(uid));
|
||||
|
||||
if (!entry) {
|
||||
return `Entry with UID ${uid} not found in world book "${book_name}".`;
|
||||
return JSON.stringify({
|
||||
status: "error",
|
||||
code: "ENTRY_NOT_FOUND",
|
||||
message: `在世界书 "${book_name}" 中未找到 UID 为 ${uid} 的条目。`,
|
||||
suggestion: "请使用 'read_world_info' 查看可用的 UID。"
|
||||
});
|
||||
}
|
||||
|
||||
return JSON.stringify(entry, null, 2);
|
||||
return JSON.stringify({
|
||||
status: "success",
|
||||
data: entry
|
||||
}, null, 2);
|
||||
},
|
||||
|
||||
write_world_info_entry: async ({ book_name, entries }) => {
|
||||
@@ -45,14 +54,22 @@ export const tools = {
|
||||
const cleanEntries = entries.replace(/```json/g, '').replace(/```/g, '').trim();
|
||||
entries = JSON.parse(cleanEntries);
|
||||
} catch (e) {
|
||||
return `错误: 'entries' 参数必须是有效的 JSON 数组。解析错误: ${e.message}`;
|
||||
return JSON.stringify({
|
||||
status: "error",
|
||||
code: "INVALID_JSON",
|
||||
message: `'entries' 参数必须是有效的 JSON 数组。解析错误: ${e.message}`
|
||||
});
|
||||
}
|
||||
}
|
||||
if (!Array.isArray(entries)) {
|
||||
if (typeof entries === 'object' && entries !== null) {
|
||||
entries = [entries];
|
||||
} else {
|
||||
return "错误: 'entries' 参数必须是数组或对象。";
|
||||
return JSON.stringify({
|
||||
status: "error",
|
||||
code: "INVALID_TYPE",
|
||||
message: "'entries' 参数必须是数组或对象。"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,26 +84,61 @@ export const tools = {
|
||||
}
|
||||
}
|
||||
|
||||
let resultMsg = "";
|
||||
let updatedCount = 0;
|
||||
let createdCount = 0;
|
||||
let errors = [];
|
||||
|
||||
if (updates.length > 0) {
|
||||
const success = await amilyHelper.setLorebookEntries(book_name, updates);
|
||||
resultMsg += success ? `成功更新了 ${updates.length} 个条目。 ` : `更新条目失败。 `;
|
||||
if (success) updatedCount = updates.length;
|
||||
else errors.push("更新条目失败。");
|
||||
}
|
||||
if (creates.length > 0) {
|
||||
const success = await amilyHelper.createLorebookEntries(book_name, creates);
|
||||
resultMsg += success ? `成功创建了 ${creates.length} 个条目。 ` : `创建条目失败。 `;
|
||||
if (success) createdCount = creates.length;
|
||||
else errors.push("创建条目失败。");
|
||||
}
|
||||
return resultMsg || "未执行任何操作。";
|
||||
|
||||
if (errors.length > 0 && updatedCount === 0 && createdCount === 0) {
|
||||
return JSON.stringify({
|
||||
status: "error",
|
||||
code: "WRITE_FAILED",
|
||||
message: errors.join(" ")
|
||||
});
|
||||
}
|
||||
|
||||
return JSON.stringify({
|
||||
status: "success",
|
||||
message: `成功更新了 ${updatedCount} 个条目,创建了 ${createdCount} 个条目。`,
|
||||
data: { updated: updatedCount, created: createdCount }
|
||||
});
|
||||
},
|
||||
|
||||
create_world_book: async ({ book_name }) => {
|
||||
const success = await amilyHelper.createLorebook(book_name);
|
||||
return success ? `世界书 "${book_name}" 创建成功。` : `创建世界书 "${book_name}" 失败。`;
|
||||
if (success) {
|
||||
return JSON.stringify({
|
||||
status: "success",
|
||||
message: `世界书 "${book_name}" 创建成功。`
|
||||
});
|
||||
} else {
|
||||
return JSON.stringify({
|
||||
status: "error",
|
||||
code: "CREATE_FAILED",
|
||||
message: `创建世界书 "${book_name}" 失败。`
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
read_character_card: async ({ chid }) => {
|
||||
const char = charApi.getCharacter(chid);
|
||||
if (!char) return "未找到角色。";
|
||||
if (!char) {
|
||||
return JSON.stringify({
|
||||
status: "error",
|
||||
code: "CHAR_NOT_FOUND",
|
||||
message: "未找到角色。"
|
||||
});
|
||||
}
|
||||
|
||||
const safeChar = {
|
||||
name: char.name,
|
||||
@@ -97,7 +149,10 @@ export const tools = {
|
||||
mes_example: char.mes_example,
|
||||
alternate_greetings: char.data?.alternate_greetings || []
|
||||
};
|
||||
return JSON.stringify(safeChar, null, 2);
|
||||
return JSON.stringify({
|
||||
status: "success",
|
||||
data: safeChar
|
||||
}, null, 2);
|
||||
},
|
||||
|
||||
update_character_card: async (args) => {
|
||||
@@ -107,25 +162,42 @@ export const tools = {
|
||||
const success = charApi.updateCharacter(chid, finalUpdates);
|
||||
if (success) {
|
||||
const updatedFields = Object.keys(finalUpdates).join(', ');
|
||||
return `角色卡更新成功 [ID: ${chid}]。已更新字段: ${updatedFields}。`;
|
||||
return JSON.stringify({
|
||||
status: "success",
|
||||
message: `角色卡更新成功 [ID: ${chid}]。`,
|
||||
data: { updated_fields: updatedFields }
|
||||
});
|
||||
} else {
|
||||
return "更新角色卡失败。";
|
||||
return JSON.stringify({
|
||||
status: "error",
|
||||
code: "UPDATE_FAILED",
|
||||
message: "更新角色卡失败。"
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
edit_character_text: async ({ chid, field, diff }) => {
|
||||
const char = charApi.getCharacter(chid);
|
||||
if (!char) return "未找到角色。";
|
||||
if (!char) {
|
||||
return JSON.stringify({
|
||||
status: "error",
|
||||
code: "CHAR_NOT_FOUND",
|
||||
message: "未找到角色。"
|
||||
});
|
||||
}
|
||||
|
||||
const allowedFields = ['description', 'personality', 'scenario', 'first_mes', 'mes_example'];
|
||||
if (!allowedFields.includes(field)) {
|
||||
return `无效的字段。允许的字段: ${allowedFields.join(', ')}`;
|
||||
return JSON.stringify({
|
||||
status: "error",
|
||||
code: "INVALID_FIELD",
|
||||
message: `无效的字段。允许的字段: ${allowedFields.join(', ')}`
|
||||
});
|
||||
}
|
||||
|
||||
let content = char[field] || '';
|
||||
const changes = diff.split('------- SEARCH');
|
||||
|
||||
// Remove the first empty split if any
|
||||
|
||||
if (changes[0].trim() === '') changes.shift();
|
||||
|
||||
for (const change of changes) {
|
||||
@@ -136,14 +208,30 @@ export const tools = {
|
||||
const replaceBlock = parts[1].split('+++++++ REPLACE')[0].trim();
|
||||
|
||||
if (!content.includes(searchBlock)) {
|
||||
return `错误: 在字段 '${field}' 中未找到以下搜索块:\n${searchBlock}`;
|
||||
return JSON.stringify({
|
||||
status: "error",
|
||||
code: "SEARCH_NOT_FOUND",
|
||||
message: `在字段 '${field}' 中未找到搜索块。`,
|
||||
suggestion: "请确保 SEARCH 块与现有内容完全匹配(包括空格)。"
|
||||
});
|
||||
}
|
||||
|
||||
content = content.replace(searchBlock, replaceBlock);
|
||||
}
|
||||
|
||||
const success = charApi.updateCharacter(chid, { [field]: content });
|
||||
return success ? `字段 '${field}' 更新成功。` : `更新字段 '${field}' 失败。`;
|
||||
if (success) {
|
||||
return JSON.stringify({
|
||||
status: "success",
|
||||
message: `字段 '${field}' 更新成功。`
|
||||
});
|
||||
} else {
|
||||
return JSON.stringify({
|
||||
status: "error",
|
||||
code: "UPDATE_FAILED",
|
||||
message: `更新字段 '${field}' 失败。`
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
manage_first_message: async ({ action, chid, index, message }) => {
|
||||
@@ -159,16 +247,140 @@ export const tools = {
|
||||
success = charApi.removeFirstMessage(chid, index);
|
||||
break;
|
||||
default:
|
||||
return "无效的操作。";
|
||||
return JSON.stringify({
|
||||
status: "error",
|
||||
code: "INVALID_ACTION",
|
||||
message: "无效的操作。"
|
||||
});
|
||||
}
|
||||
|
||||
if (success) {
|
||||
return JSON.stringify({
|
||||
status: "success",
|
||||
message: `开场白 ${action} 成功。`
|
||||
});
|
||||
} else {
|
||||
return JSON.stringify({
|
||||
status: "error",
|
||||
code: "ACTION_FAILED",
|
||||
message: `开场白 ${action} 失败。`
|
||||
});
|
||||
}
|
||||
return success ? `开场白 ${action} 成功。` : `开场白 ${action} 失败。`;
|
||||
},
|
||||
|
||||
create_character: async ({ name }) => {
|
||||
const result = await charApi.createNewCharacter(name);
|
||||
if (result === -1) return "创建角色失败。";
|
||||
if (result === -2) return "角色创建请求已发送。请手动刷新角色列表以查看新角色。";
|
||||
return `角色创建成功,ID: ${result}`;
|
||||
if (result === -1) {
|
||||
return JSON.stringify({
|
||||
status: "error",
|
||||
code: "CREATE_FAILED",
|
||||
message: "创建角色失败。"
|
||||
});
|
||||
}
|
||||
if (result === -2) {
|
||||
return JSON.stringify({
|
||||
status: "warning",
|
||||
code: "CREATE_PENDING",
|
||||
message: "角色创建请求已发送。请手动刷新。"
|
||||
});
|
||||
}
|
||||
return JSON.stringify({
|
||||
status: "success",
|
||||
message: `角色创建成功。`,
|
||||
data: { id: result }
|
||||
});
|
||||
},
|
||||
|
||||
simulate_chat: async ({ chid, message }) => {
|
||||
const char = charApi.getCharacter(chid);
|
||||
if (!char) {
|
||||
return JSON.stringify({
|
||||
status: "error",
|
||||
code: "CHAR_NOT_FOUND",
|
||||
message: "未找到角色。"
|
||||
});
|
||||
}
|
||||
|
||||
const systemPrompt = `You are roleplaying as ${char.name}.
|
||||
Description: ${char.description}
|
||||
Personality: ${char.personality}
|
||||
Scenario: ${char.scenario}
|
||||
|
||||
Reply to the user's message in character. Stay in character.`;
|
||||
|
||||
const messages = [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
{ role: 'user', content: message }
|
||||
];
|
||||
|
||||
try {
|
||||
const response = await callAi('executor', messages, { temperature: 0.9 });
|
||||
return JSON.stringify({
|
||||
status: "success",
|
||||
data: {
|
||||
character: char.name,
|
||||
response: response
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
return JSON.stringify({
|
||||
status: "error",
|
||||
code: "SIMULATION_FAILED",
|
||||
message: `模拟对话失败: ${error.message}`
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
set_style_reference: async ({ style }) => {
|
||||
|
||||
return JSON.stringify({
|
||||
status: "success",
|
||||
message: `样式参考已设置为: ${style}`,
|
||||
_action: "update_task_state",
|
||||
_updates: { style_reference: style }
|
||||
});
|
||||
},
|
||||
|
||||
analyze_entities: async ({ text }) => {
|
||||
const systemPrompt = `You are an expert World Builder and Entity Extractor.
|
||||
Analyze the provided text and identify key entities that should have their own World Info (Lorebook) entries.
|
||||
Focus on:
|
||||
- Proper Nouns (People, Places, Organizations, Artifacts)
|
||||
- Unique Concepts (Magic systems, Historical events, Species)
|
||||
|
||||
Return a JSON object with a "entities" array. Each entity should have:
|
||||
- "name": The name of the entity.
|
||||
- "type": The type (Person, Location, Organization, etc.).
|
||||
- "description": A brief summary based on the text (1-2 sentences).
|
||||
- "confidence": A score (0-1) of how important this entity seems.
|
||||
|
||||
Output ONLY valid JSON.`;
|
||||
|
||||
const messages = [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
{ role: 'user', content: text }
|
||||
];
|
||||
|
||||
try {
|
||||
const response = await callAi('executor', messages, { temperature: 0.1 });
|
||||
const cleanResponse = response.replace(/```json/g, '').replace(/```/g, '').trim();
|
||||
return cleanResponse;
|
||||
} catch (error) {
|
||||
return JSON.stringify({
|
||||
status: "error",
|
||||
code: "ANALYSIS_FAILED",
|
||||
message: `实体分析失败: ${error.message}`
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
ask_user: async ({ question }) => {
|
||||
return JSON.stringify({
|
||||
status: "success",
|
||||
message: `已向用户提问: ${question}`,
|
||||
_action: "stop_and_wait",
|
||||
data: { question }
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -176,50 +388,50 @@ export function getToolDefinitions() {
|
||||
return [
|
||||
{
|
||||
name: "read_world_info",
|
||||
description: "Read the index (list of entries with keys and comments) of a world book. Does NOT return full content.",
|
||||
description: "读取世界书的索引(包含关键字和注释的条目列表)。不返回完整内容。",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
book_name: { type: "string", description: "The name of the world book." }
|
||||
book_name: { type: "string", description: "世界书名称。" }
|
||||
},
|
||||
required: ["book_name"]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "read_world_entry",
|
||||
description: "Read the full content of a specific world book entry.",
|
||||
description: "读取特定世界书条目的完整内容。",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
book_name: { type: "string", description: "The name of the world book." },
|
||||
uid: { type: "number", description: "The UID of the entry to read." }
|
||||
book_name: { type: "string", description: "世界书名称。" },
|
||||
uid: { type: "number", description: "要读取的条目 UID。" }
|
||||
},
|
||||
required: ["book_name", "uid"]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "write_world_info_entry",
|
||||
description: "Create or update entries in a world book.",
|
||||
description: "创建或更新世界书中的条目。",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
book_name: { type: "string", description: "The name of the world book." },
|
||||
book_name: { type: "string", description: "世界书名称。" },
|
||||
entries: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
properties: {
|
||||
uid: { type: "number", description: "Entry ID (optional, for update)." },
|
||||
comment: { type: "string", description: "Entry title/comment." },
|
||||
content: { type: "string", description: "Entry content." },
|
||||
key: { type: "array", items: { type: "string" }, description: "Keywords." },
|
||||
enabled: { type: "boolean", description: "Is enabled." },
|
||||
constant: { type: "boolean", description: "Constant (Blue light)." },
|
||||
position: { type: "string", enum: ["before_character_definition", "after_character_definition", "before_author_note", "after_author_note", "at_depth_as_system"], description: "Insertion position." },
|
||||
depth: { type: "number", description: "Insertion depth." },
|
||||
scanDepth: { type: "number", description: "Scan depth." },
|
||||
exclude_recursion: { type: "boolean", description: "Exclude from recursion." },
|
||||
prevent_recursion: { type: "boolean", description: "Prevent recursion." }
|
||||
uid: { type: "number", description: "条目 ID(可选,用于更新)。" },
|
||||
comment: { type: "string", description: "条目标题/注释。" },
|
||||
content: { type: "string", description: "条目内容。" },
|
||||
key: { type: "array", items: { type: "string" }, description: "关键字。" },
|
||||
enabled: { type: "boolean", description: "是否启用。" },
|
||||
constant: { type: "boolean", description: "常驻(蓝灯)。" },
|
||||
position: { type: "string", enum: ["before_character_definition", "after_character_definition", "before_author_note", "after_author_note", "at_depth_as_system"], description: "插入位置。" },
|
||||
depth: { type: "number", description: "插入深度。" },
|
||||
scanDepth: { type: "number", description: "扫描深度。" },
|
||||
exclude_recursion: { type: "boolean", description: "排除递归。" },
|
||||
prevent_recursion: { type: "boolean", description: "防止递归。" }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -229,33 +441,33 @@ export function getToolDefinitions() {
|
||||
},
|
||||
{
|
||||
name: "create_world_book",
|
||||
description: "Create a new empty world book.",
|
||||
description: "创建一个新的空世界书。",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
book_name: { type: "string", description: "The name of the new world book." }
|
||||
book_name: { type: "string", description: "新世界书的名称。" }
|
||||
},
|
||||
required: ["book_name"]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "read_character_card",
|
||||
description: "Read character card data.",
|
||||
description: "读取角色卡数据。",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
chid: { type: "number", description: "Character ID." }
|
||||
chid: { type: "number", description: "角色 ID。" }
|
||||
},
|
||||
required: ["chid"]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "update_character_card",
|
||||
description: "Update character card fields (overwrite).",
|
||||
description: "更新角色卡字段(覆盖)。",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
chid: { type: "number", description: "Character ID." },
|
||||
chid: { type: "number", description: "角色 ID。" },
|
||||
name: { type: "string" },
|
||||
description: { type: "string" },
|
||||
personality: { type: "string" },
|
||||
@@ -268,15 +480,15 @@ export function getToolDefinitions() {
|
||||
},
|
||||
{
|
||||
name: "edit_character_text",
|
||||
description: "Edit a specific text field of a character using SEARCH/REPLACE blocks.",
|
||||
description: "使用 搜索/替换 块编辑角色的特定文本字段。",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
chid: { type: "number", description: "Character ID." },
|
||||
field: { type: "string", enum: ["description", "personality", "scenario", "first_mes", "mes_example"], description: "The field to edit." },
|
||||
chid: { type: "number", description: "角色 ID。" },
|
||||
field: { type: "string", enum: ["description", "personality", "scenario", "first_mes", "mes_example"], description: "要编辑的字段。" },
|
||||
diff: {
|
||||
type: "string",
|
||||
description: "One or more SEARCH/REPLACE blocks following this exact format:\n------- SEARCH\n[exact content to find]\n=======\n[new content to replace with]\n+++++++ REPLACE"
|
||||
description: "一个或多个遵循此确切格式的 搜索/替换 块:\n------- SEARCH\n[exact content to find]\n=======\n[new content to replace with]\n+++++++ REPLACE"
|
||||
}
|
||||
},
|
||||
required: ["chid", "field", "diff"]
|
||||
@@ -284,28 +496,73 @@ export function getToolDefinitions() {
|
||||
},
|
||||
{
|
||||
name: "manage_first_message",
|
||||
description: "Add, update, or remove alternate greetings.",
|
||||
description: "添加、更新或删除候补开场白。",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
action: { type: "string", enum: ["add", "update", "remove"] },
|
||||
chid: { type: "number", description: "Character ID." },
|
||||
index: { type: "number", description: "Index of the greeting (required for update/remove)." },
|
||||
message: { type: "string", description: "Content of the greeting (required for add/update)." }
|
||||
chid: { type: "number", description: "角色 ID。" },
|
||||
index: { type: "number", description: "开场白索引(更新/删除时必需)。" },
|
||||
message: { type: "string", description: "开场白内容(添加/更新时必需)。" }
|
||||
},
|
||||
required: ["action", "chid"]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "create_character",
|
||||
description: "Create a new character card.",
|
||||
description: "创建一个新角色卡。",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
name: { type: "string", description: "Name of the new character." }
|
||||
name: { type: "string", description: "新角色的名字。" }
|
||||
},
|
||||
required: ["name"]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "simulate_chat",
|
||||
description: "与角色模拟对话以测试其性格和设定。",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
chid: { type: "number", description: "角色 ID。" },
|
||||
message: { type: "string", description: "发送给角色的消息。" }
|
||||
},
|
||||
required: ["chid", "message"]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "set_style_reference",
|
||||
description: "设置生成内容的风格参考或模板(例如:'黑暗奇幻风格','莎士比亚风格','JSON格式模板')。",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
style: { type: "string", description: "风格描述或模板内容。" }
|
||||
},
|
||||
required: ["style"]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "analyze_entities",
|
||||
description: "分析文本并提取潜在的世界书条目(实体)。",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
text: { type: "string", description: "要分析的文本。" }
|
||||
},
|
||||
required: ["text"]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "ask_user",
|
||||
description: "向用户提问以获取更多信息或确认。这将暂停自动执行并等待用户回复。",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
question: { type: "string", description: "要问的问题。" }
|
||||
},
|
||||
required: ["question"]
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { extensionName } from "../../utils/settings.js";
|
||||
import { AgentManager } from "./agent-manager.js";
|
||||
import { characters, this_chid, saveSettingsDebounced } from "/script.js";
|
||||
import { characters, this_chid, saveSettingsDebounced, getCharacters } from "/script.js";
|
||||
import { world_names } from "/scripts/world-info.js";
|
||||
import { getApiConfig, setApiConfig, testConnection, fetchModels } from "./api.js";
|
||||
import { tools } from "./tools.js";
|
||||
@@ -77,6 +77,22 @@ function populateDropdowns() {
|
||||
});
|
||||
}
|
||||
|
||||
async function handleContextUpdate(type, value) {
|
||||
console.log(`[Amily2 AutoCharCard] Context Update: ${type} -> ${value}`);
|
||||
|
||||
if (type === 'char') {
|
||||
await getCharacters(); // Force refresh character list
|
||||
}
|
||||
|
||||
populateDropdowns();
|
||||
|
||||
if (type === 'char') {
|
||||
$('#acc-target-char').val(value);
|
||||
} else if (type === 'world') {
|
||||
$('#acc-target-world').val(value);
|
||||
}
|
||||
}
|
||||
|
||||
function renderRulesList() {
|
||||
const list = $('#acc-rules-list');
|
||||
list.empty();
|
||||
@@ -145,7 +161,6 @@ function bindEvents() {
|
||||
const chid = id;
|
||||
const field = subId;
|
||||
|
||||
// Check if already open
|
||||
const fileId = `char-${chid}-${field}`;
|
||||
if (openedFiles.has(fileId)) {
|
||||
activeFileId = fileId;
|
||||
@@ -153,32 +168,33 @@ function bindEvents() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch content if needed (we might have it cached in previousCharData or need to fetch)
|
||||
// For simplicity, fetch again or use cache
|
||||
let content = '';
|
||||
if (previousCharData && previousCharData.name) { // Simple check if loaded
|
||||
if (field.startsWith('greeting_')) {
|
||||
const index = parseInt(field.split('_')[1]);
|
||||
content = previousCharData.alternate_greetings[index];
|
||||
} else {
|
||||
content = previousCharData[field];
|
||||
}
|
||||
} else {
|
||||
// Fetch
|
||||
try {
|
||||
const charData = await tools.read_character_card({ chid });
|
||||
const char = JSON.parse(charData);
|
||||
previousCharData = char;
|
||||
if (field.startsWith('greeting_')) {
|
||||
const index = parseInt(field.split('_')[1]);
|
||||
content = char.alternate_greetings[index];
|
||||
} else {
|
||||
content = char[field];
|
||||
}
|
||||
} catch (e) {
|
||||
toastr.error('无法读取角色卡内容');
|
||||
return;
|
||||
|
||||
|
||||
try {
|
||||
console.log(`[AutoCharCard] Reading char ${chid}, field ${field}`);
|
||||
const charData = await tools.read_character_card({ chid });
|
||||
const response = JSON.parse(charData);
|
||||
|
||||
if (response.status !== 'success' || !response.data) {
|
||||
throw new Error(response.message || 'Unknown error');
|
||||
}
|
||||
|
||||
const char = response.data;
|
||||
previousCharData = char;
|
||||
console.log(`[AutoCharCard] Char data:`, char);
|
||||
|
||||
if (field.startsWith('greeting_')) {
|
||||
const index = parseInt(field.split('_')[1]);
|
||||
content = char.alternate_greetings[index];
|
||||
} else {
|
||||
content = char[field];
|
||||
}
|
||||
console.log(`[AutoCharCard] Content for ${field}:`, content);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
toastr.error('无法读取角色卡内容');
|
||||
return;
|
||||
}
|
||||
|
||||
openedFiles.set(fileId, {
|
||||
@@ -203,7 +219,13 @@ function bindEvents() {
|
||||
|
||||
try {
|
||||
const entryData = await tools.read_world_entry({ book_name: bookName, uid: uid });
|
||||
const entry = JSON.parse(entryData);
|
||||
const response = JSON.parse(entryData);
|
||||
|
||||
if (response.status !== 'success' || !response.data) {
|
||||
throw new Error(response.message || 'Unknown error');
|
||||
}
|
||||
|
||||
const entry = response.data;
|
||||
|
||||
let keys = entry.key;
|
||||
if (Array.isArray(keys)) keys = keys.join(', ');
|
||||
@@ -217,6 +239,7 @@ function bindEvents() {
|
||||
activeFileId = fileId;
|
||||
renderEditor();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
toastr.error('无法读取世界书条目');
|
||||
}
|
||||
}
|
||||
@@ -269,9 +292,6 @@ function bindEvents() {
|
||||
}
|
||||
});
|
||||
|
||||
// Removed old approval buttons handlers
|
||||
|
||||
// Add Refresh Button to Preview Header if not exists
|
||||
const previewHeader = $('.acc-right-panel .acc-panel-header');
|
||||
if (previewHeader.find('#acc-refresh-preview').length === 0) {
|
||||
const refreshBtn = $('<button>')
|
||||
@@ -342,7 +362,7 @@ function bindEvents() {
|
||||
apiUrl: $('#acc-executor-url').val().trim(),
|
||||
apiKey: $('#acc-executor-key').val().trim(),
|
||||
model: $('#acc-executor-model').val() || '',
|
||||
maxTokens: isNaN(execMaxTokens) ? 0 : execMaxTokens // 0 means unlimited
|
||||
maxTokens: isNaN(execMaxTokens) ? 0 : execMaxTokens
|
||||
};
|
||||
|
||||
setApiConfig('executor', executorConfig);
|
||||
@@ -403,20 +423,17 @@ function bindEvents() {
|
||||
}
|
||||
});
|
||||
|
||||
// Mobile Navigation Logic
|
||||
$('.acc-nav-btn').on('click', function() {
|
||||
const targetClass = $(this).data('target');
|
||||
|
||||
// Update buttons
|
||||
$('.acc-nav-btn').removeClass('active');
|
||||
$(this).addClass('active');
|
||||
|
||||
// Update panels
|
||||
$('.acc-column').removeClass('mobile-active');
|
||||
$(`.${targetClass}`).addClass('mobile-active');
|
||||
});
|
||||
|
||||
// Initialize mobile view (default to center panel)
|
||||
|
||||
if (window.innerWidth <= 768) {
|
||||
$('.acc-center-panel').addClass('mobile-active');
|
||||
}
|
||||
@@ -430,32 +447,34 @@ async function handleSendMessage() {
|
||||
if (!agentManager) return;
|
||||
|
||||
isWaitingForApproval = false;
|
||||
// Reset UI
|
||||
|
||||
const btn = $('#acc-send-btn');
|
||||
btn.html('<i class="fas fa-paper-plane"></i>');
|
||||
btn.prop('title', '发送');
|
||||
btn.removeClass('acc-btn-success');
|
||||
$('#acc-reject-btn').remove();
|
||||
|
||||
input.attr('placeholder', '描述您的需求...');
|
||||
input.val('');
|
||||
|
||||
if (message) {
|
||||
// User typed something -> Reject with feedback
|
||||
addMessage('user', message); // Show feedback in chat
|
||||
addMessage('user', message);
|
||||
await agentManager.resumeWithApproval(
|
||||
false,
|
||||
message,
|
||||
(content, role) => addMessage(role, content),
|
||||
(toolName, args) => updatePreview(toolName, args),
|
||||
showApprovalRequest
|
||||
showApprovalRequest,
|
||||
handleContextUpdate
|
||||
);
|
||||
} else {
|
||||
// Empty input -> Approve
|
||||
await agentManager.resumeWithApproval(
|
||||
true,
|
||||
null,
|
||||
(content, role) => addMessage(role, content),
|
||||
(toolName, args) => updatePreview(toolName, args),
|
||||
showApprovalRequest
|
||||
showApprovalRequest,
|
||||
handleContextUpdate
|
||||
);
|
||||
}
|
||||
return;
|
||||
@@ -495,7 +514,8 @@ async function handleSendMessage() {
|
||||
(toolName, args) => {
|
||||
updatePreview(toolName, args);
|
||||
},
|
||||
showApprovalRequest
|
||||
showApprovalRequest,
|
||||
handleContextUpdate
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Agent Error:', error);
|
||||
@@ -510,14 +530,60 @@ async function handleSendMessage() {
|
||||
function showApprovalRequest(toolName, args) {
|
||||
isWaitingForApproval = true;
|
||||
|
||||
// Update UI to Approval Mode
|
||||
const btn = $('#acc-send-btn');
|
||||
btn.html('<i class="fas fa-check"></i>');
|
||||
btn.prop('title', '批准执行 (点击批准,或输入文字拒绝)');
|
||||
btn.prop('title', '批准执行');
|
||||
btn.addClass('acc-btn-success');
|
||||
$('#acc-user-input').attr('placeholder', '输入反馈以拒绝/修改,或直接点击右侧按钮批准...');
|
||||
$('#acc-user-input').attr('placeholder', '输入反馈以修改,或点击 √ 批准,点击 X 拒绝...');
|
||||
|
||||
if ($('#acc-reject-btn').length === 0) {
|
||||
const rejectBtn = $('<button>')
|
||||
.attr('id', 'acc-reject-btn')
|
||||
.addClass('acc-btn-danger')
|
||||
.html('<i class="fas fa-times"></i>')
|
||||
.attr('title', '拒绝执行')
|
||||
.css({
|
||||
'margin-right': '5px',
|
||||
'width': '40px',
|
||||
'height': '40px',
|
||||
'border-radius': '50%',
|
||||
'border': 'none',
|
||||
'cursor': 'pointer',
|
||||
'display': 'flex',
|
||||
'align-items': 'center',
|
||||
'justify-content': 'center'
|
||||
});
|
||||
|
||||
rejectBtn.insertBefore(btn);
|
||||
|
||||
rejectBtn.on('click', async () => {
|
||||
if (!isWaitingForApproval) return;
|
||||
isWaitingForApproval = false;
|
||||
|
||||
const input = $('#acc-user-input');
|
||||
const message = input.val().trim();
|
||||
|
||||
btn.html('<i class="fas fa-paper-plane"></i>');
|
||||
btn.prop('title', '发送');
|
||||
btn.removeClass('acc-btn-success');
|
||||
rejectBtn.remove();
|
||||
input.attr('placeholder', '描述您的需求...');
|
||||
input.val('');
|
||||
|
||||
const feedback = message || "用户拒绝了操作。";
|
||||
addMessage('user', `[拒绝] ${feedback}`);
|
||||
|
||||
await agentManager.resumeWithApproval(
|
||||
false,
|
||||
feedback,
|
||||
(content, role) => addMessage(role, content),
|
||||
(toolName, args) => updatePreview(toolName, args),
|
||||
showApprovalRequest,
|
||||
handleContextUpdate
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Show tool call in chat (Collapsible)
|
||||
const toolDisplay = `
|
||||
<div class="acc-tool-request">
|
||||
<details>
|
||||
@@ -537,10 +603,7 @@ function addMessage(role, content) {
|
||||
|
||||
if (role === 'stream-assistant') {
|
||||
let lastMsg = stream.children().last();
|
||||
// Check if the last message is a streaming assistant message
|
||||
if (!lastMsg.hasClass('assistant') || !lastMsg.hasClass('acc-streaming')) {
|
||||
// Create new message if last one wasn't a streaming assistant
|
||||
// We use 'assistant' role but add 'acc-streaming' class
|
||||
const msgDiv = $('<div>').addClass('acc-message assistant acc-streaming');
|
||||
const avatarDiv = $('<div>').addClass('acc-avatar').html('<i class="fas fa-robot" style="color: #4caf50;"></i>');
|
||||
const contentDiv = $('<div>').addClass('acc-message-content');
|
||||
@@ -551,7 +614,6 @@ function addMessage(role, content) {
|
||||
|
||||
const contentDiv = lastMsg.find('.acc-message-content');
|
||||
|
||||
// Simple escaping for stream chunks
|
||||
const escapedContent = content
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
@@ -568,11 +630,10 @@ function addMessage(role, content) {
|
||||
|
||||
let displayContent = content;
|
||||
if (role === 'executor' || role === 'assistant') {
|
||||
// Remove thinking blocks and tool_code wrappers for final display
|
||||
// Use case-insensitive and robust matching
|
||||
|
||||
displayContent = displayContent
|
||||
.replace(/<thinking(?:\s+[^>]*)?>[\s\S]*?<\/thinking>/gi, '')
|
||||
.replace(/<\/thinking>/gi, '') // Remove residual closing tags
|
||||
.replace(/<\/thinking>/gi, '')
|
||||
.replace(/<tool_code(?:\s+[^>]*)?>[\s\S]*?<\/tool_code>/gi, '')
|
||||
.trim();
|
||||
|
||||
@@ -581,7 +642,6 @@ function addMessage(role, content) {
|
||||
'read_character_card', 'update_character_card', 'edit_character_text',
|
||||
'manage_first_message', 'use_tool'
|
||||
];
|
||||
// Match tools with potential attributes and whitespace
|
||||
const regex = new RegExp(`<(${tools.join('|')})(?:\\s+[^>]*)?>[\\s\\S]*?<\\/\\1>`, 'gi');
|
||||
displayContent = displayContent.replace(regex, '').trim();
|
||||
|
||||
@@ -589,15 +649,13 @@ function addMessage(role, content) {
|
||||
displayContent = "<i>(正在执行操作...)</i>";
|
||||
}
|
||||
|
||||
// If this is a final assistant message, remove ANY streaming message
|
||||
if (role === 'assistant') {
|
||||
stream.find('.acc-streaming').remove();
|
||||
}
|
||||
}
|
||||
|
||||
let formattedContent;
|
||||
|
||||
// Check if content is pre-formatted HTML (e.g. tool requests)
|
||||
|
||||
if (displayContent.trim().startsWith('<div class="acc-tool-request"')) {
|
||||
formattedContent = displayContent;
|
||||
} else {
|
||||
@@ -641,43 +699,27 @@ function addMessage(role, content) {
|
||||
function parseMarkdown(text) {
|
||||
if (!text) return '';
|
||||
|
||||
// 1. Escape HTML (basic)
|
||||
let html = text
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">");
|
||||
|
||||
// 2. Code Blocks (```...```)
|
||||
html = html.replace(/```([\s\S]*?)```/g, '<pre><code>$1</code></pre>');
|
||||
|
||||
// 3. Inline Code (`...`)
|
||||
html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
|
||||
|
||||
// 4. Headers
|
||||
html = html.replace(/^#### (.*$)/gm, '<h4>$1</h4>');
|
||||
html = html.replace(/^### (.*$)/gm, '<h3>$1</h3>');
|
||||
html = html.replace(/^## (.*$)/gm, '<h2>$1</h2>');
|
||||
html = html.replace(/^# (.*$)/gm, '<h1>$1</h1>');
|
||||
|
||||
// 5. Bold & Italic
|
||||
html = html.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
|
||||
html = html.replace(/\*(.*?)\*/g, '<em>$1</em>');
|
||||
|
||||
// 6. Horizontal Rules
|
||||
html = html.replace(/^[\*\-]{3,}$/gm, '<hr>');
|
||||
|
||||
// 7. Lists
|
||||
// Replace * or - at start of line with a bullet point
|
||||
html = html.replace(/^\s*[\-\*]\s+(.*$)/gm, '<li>$1</li>');
|
||||
|
||||
// 8. Line breaks
|
||||
// We want to preserve line breaks, but block elements (h1-h6, pre, li) handle their own spacing.
|
||||
// We can replace newlines with <br> ONLY if they are not around block tags.
|
||||
// A simpler approach for chat: just replace all \n with <br>, and let CSS handle the extra spacing or remove it.
|
||||
// But <pre> content shouldn't have <br>.
|
||||
// Let's just replace \n with <br> globally, but we need to protect <pre> blocks.
|
||||
// Actually, the code block regex above already consumed the newlines inside it.
|
||||
// So we can just replace remaining \n.
|
||||
|
||||
html = html.replace(/\n/g, '<br>');
|
||||
|
||||
@@ -688,9 +730,6 @@ function renderEditor() {
|
||||
const container = $('#acc-preview-container');
|
||||
const tabsContainer = $('.acc-preview-tabs');
|
||||
|
||||
// Don't fully empty if we want to preserve state, but for simplicity we re-render tabs
|
||||
// To preserve scroll position or focus, we might need more complex logic.
|
||||
// For now, re-rendering is acceptable as long as data is in openedFiles.
|
||||
|
||||
container.empty();
|
||||
tabsContainer.empty();
|
||||
@@ -700,7 +739,6 @@ function renderEditor() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure activeFileId is valid
|
||||
if (!activeFileId || !openedFiles.has(activeFileId)) {
|
||||
activeFileId = openedFiles.keys().next().value;
|
||||
}
|
||||
@@ -713,13 +751,12 @@ function renderEditor() {
|
||||
.attr('title', file.title)
|
||||
.on('click', () => {
|
||||
activeFileId = id;
|
||||
renderEditor(); // Re-render to switch tabs
|
||||
renderEditor();
|
||||
});
|
||||
|
||||
const icon = $('<i class="fas fa-file-alt"></i>');
|
||||
const titleSpan = $('<span>').addClass('acc-tab-title').text(file.title);
|
||||
|
||||
// Close button for tab
|
||||
const closeBtn = $('<span>')
|
||||
.html('×')
|
||||
.addClass('acc-tab-close')
|
||||
@@ -774,7 +811,6 @@ function renderEditor() {
|
||||
})
|
||||
.on('input', function() {
|
||||
file.content = $(this).val();
|
||||
// file.isDirty = true; // Could add dirty indicator
|
||||
});
|
||||
|
||||
contentDiv.append(textarea);
|
||||
@@ -796,17 +832,23 @@ async function saveFile(id) {
|
||||
try {
|
||||
let result;
|
||||
if (meta.type === 'char') {
|
||||
result = await tools.update_character_card({
|
||||
chid: meta.chid,
|
||||
[meta.field]: file.content
|
||||
});
|
||||
if (meta.field.startsWith('greeting_')) {
|
||||
const index = parseInt(meta.field.split('_')[1]);
|
||||
|
||||
result = await tools.manage_first_message({
|
||||
action: 'update',
|
||||
chid: meta.chid,
|
||||
index: index + 1,
|
||||
message: file.content
|
||||
});
|
||||
} else {
|
||||
result = await tools.update_character_card({
|
||||
chid: meta.chid,
|
||||
[meta.field]: file.content
|
||||
});
|
||||
}
|
||||
} else if (meta.type === 'wi') {
|
||||
// For WI, we need to construct the entry object
|
||||
// We assume file.content is the 'content' field of the entry
|
||||
// We need other fields like keys, etc. stored in metadata or parsed?
|
||||
// If we only have content, we might lose keys if we just write content.
|
||||
// But wait, write_world_info_entry takes an array of entry objects.
|
||||
// If we only update content, we need the UID.
|
||||
|
||||
|
||||
if (meta.uid !== undefined) {
|
||||
result = await tools.write_world_info_entry({
|
||||
@@ -814,8 +856,7 @@ async function saveFile(id) {
|
||||
entries: [{ uid: meta.uid, content: file.content }]
|
||||
});
|
||||
} else {
|
||||
// New entry? We need keys.
|
||||
// If it's a raw JSON view, we can parse it.
|
||||
|
||||
try {
|
||||
const entry = JSON.parse(file.content);
|
||||
result = await tools.write_world_info_entry({
|
||||
@@ -823,8 +864,7 @@ async function saveFile(id) {
|
||||
entries: [entry]
|
||||
});
|
||||
} catch (e) {
|
||||
// If it's just text content, we can't save it as a new entry without keys.
|
||||
// Unless we assume it's an update to an existing entry we know about.
|
||||
|
||||
toastr.error('保存失败: 内容必须是有效的 JSON (针对新建条目) 或包含 UID');
|
||||
return;
|
||||
}
|
||||
@@ -852,9 +892,15 @@ async function loadContextToEditor() {
|
||||
if (chid && chid !== 'new') {
|
||||
try {
|
||||
const charData = await tools.read_character_card({ chid });
|
||||
const char = JSON.parse(charData);
|
||||
previousCharData = char; // Cache for selector
|
||||
|
||||
const response = JSON.parse(charData);
|
||||
|
||||
if (response.status !== 'success' || !response.data) {
|
||||
console.error("Failed to read character:", response);
|
||||
return;
|
||||
}
|
||||
|
||||
const char = response.data;
|
||||
previousCharData = char;
|
||||
const charGroup = $('<optgroup label="角色卡字段">');
|
||||
const fields = ['description', 'personality', 'first_mes', 'scenario', 'mes_example'];
|
||||
fields.forEach(field => {
|
||||
@@ -868,7 +914,6 @@ async function loadContextToEditor() {
|
||||
}
|
||||
selector.append(charGroup);
|
||||
|
||||
// Open Description by default if nothing open
|
||||
if (openedFiles.size === 0 && char.description) {
|
||||
const id = `char-${chid}-description`;
|
||||
openedFiles.set(id, {
|
||||
@@ -887,7 +932,6 @@ async function loadContextToEditor() {
|
||||
|
||||
if (bookName && bookName !== 'new') {
|
||||
try {
|
||||
// Use return_full: false to get index only
|
||||
const indexData = await tools.read_world_info({ book_name: bookName, return_full: false });
|
||||
const index = JSON.parse(indexData);
|
||||
|
||||
@@ -936,27 +980,22 @@ async function updatePreview(toolName, args, isPartial = false) {
|
||||
const id = `char-${chid}-${field}`;
|
||||
|
||||
if (isPartial) {
|
||||
// For partial diffs, we might want to show the diff itself in a temp tab?
|
||||
// Or if we have the original content, try to apply it?
|
||||
// Applying partial diff is hard.
|
||||
// Let's show the diff in a separate tab for now.
|
||||
|
||||
const diffId = `diff-${chid}-${field}`;
|
||||
openedFiles.set(diffId, {
|
||||
title: `Diff: ${field}`,
|
||||
content: diff,
|
||||
type: 'diff',
|
||||
metadata: null // Cannot save raw diff easily
|
||||
metadata: null
|
||||
});
|
||||
activeFileId = diffId;
|
||||
} else {
|
||||
// Full diff available. Try to apply to existing content.
|
||||
|
||||
let originalContent = '';
|
||||
if (openedFiles.has(id)) {
|
||||
originalContent = openedFiles.get(id).content;
|
||||
} else {
|
||||
// Try to fetch if not open?
|
||||
// For now, just assume it's open or we can't apply.
|
||||
// If we can't apply, show diff.
|
||||
|
||||
}
|
||||
|
||||
if (originalContent) {
|
||||
@@ -988,10 +1027,8 @@ async function updatePreview(toolName, args, isPartial = false) {
|
||||
metadata: { type: 'char', chid, field }
|
||||
});
|
||||
activeFileId = id;
|
||||
// Close diff tab if open
|
||||
openedFiles.delete(`diff-${chid}-${field}`);
|
||||
} else {
|
||||
// Failed to apply, show diff
|
||||
const diffId = `diff-${chid}-${field}`;
|
||||
openedFiles.set(diffId, {
|
||||
title: `Diff: ${field} (Failed to Apply)`,
|
||||
@@ -1002,7 +1039,6 @@ async function updatePreview(toolName, args, isPartial = false) {
|
||||
activeFileId = diffId;
|
||||
}
|
||||
} else {
|
||||
// Original not found, show diff
|
||||
const diffId = `diff-${chid}-${field}`;
|
||||
openedFiles.set(diffId, {
|
||||
title: `Diff: ${field}`,
|
||||
@@ -1018,7 +1054,6 @@ async function updatePreview(toolName, args, isPartial = false) {
|
||||
let entries = args.entries;
|
||||
|
||||
if (isPartial && typeof entries === 'string') {
|
||||
// Show raw JSON for partial
|
||||
const id = `wi-raw-partial`;
|
||||
openedFiles.set(id, {
|
||||
title: 'WI Entry (Generating...)',
|
||||
@@ -1046,7 +1081,6 @@ async function updatePreview(toolName, args, isPartial = false) {
|
||||
});
|
||||
activeFileId = id;
|
||||
});
|
||||
// Remove partial tab
|
||||
openedFiles.delete(`wi-raw-partial`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "Amily2号聊天优化助手",
|
||||
"display_name": "Amily2号助手",
|
||||
"version": "1.7.8",
|
||||
"version": "1.7.9",
|
||||
"author": "Wx-2025",
|
||||
"description": "一个拥有独立UI的智能引擎,正文优化、自动总结、记忆表格、rag向量、隐藏楼层、剧情推进等多功能整合。",
|
||||
"minSillyTavernVersion": "1.10.0",
|
||||
@@ -49,5 +49,6 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user