mirror of
https://github.com/Wx-2025/ST-Amily2-Chat-Optimisation.git
synced 2026-06-06 12:45:51 +00:00
Add files via upload
This commit is contained in:
@@ -36,18 +36,25 @@ export class AgentManager {
|
||||
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) {
|
||||
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);
|
||||
await this.runTaskLoop(onStreamUpdate, onPreviewUpdate, onApprovalRequest, onContextUpdate, onPromptGenerated);
|
||||
} else {
|
||||
this.status = 'running';
|
||||
this.pendingToolCall = null;
|
||||
@@ -55,7 +62,7 @@ export class AgentManager {
|
||||
role: 'user',
|
||||
content: `[工具执行被拒绝] 用户反馈: ${feedback || "未提供原因。"}`
|
||||
});
|
||||
await this.runTaskLoop(onStreamUpdate, onPreviewUpdate, onApprovalRequest, onContextUpdate);
|
||||
await this.runTaskLoop(onStreamUpdate, onPreviewUpdate, onApprovalRequest, onContextUpdate, onPromptGenerated);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,12 +127,18 @@ ${this.taskState.getPromptContext()}
|
||||
if (this.currentChid !== undefined && this.currentChid !== 'new') {
|
||||
try {
|
||||
const charData = await tools.read_character_card({ chid: this.currentChid });
|
||||
const char = JSON.parse(charData);
|
||||
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`;
|
||||
}
|
||||
@@ -192,6 +205,11 @@ Example:
|
||||
- **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.
|
||||
`;
|
||||
@@ -207,17 +225,17 @@ Example:
|
||||
return null;
|
||||
}
|
||||
|
||||
async handleUserMessage(message, onStreamUpdate, onPreviewUpdate, onApprovalRequest, onContextUpdate) {
|
||||
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);
|
||||
await this.runTaskLoop(onStreamUpdate, onPreviewUpdate, onApprovalRequest, onContextUpdate, onPromptGenerated);
|
||||
}
|
||||
|
||||
async runTaskLoop(onStreamUpdate, onPreviewUpdate, onApprovalRequest, onContextUpdate) {
|
||||
async runTaskLoop(onStreamUpdate, onPreviewUpdate, onApprovalRequest, onContextUpdate, onPromptGenerated) {
|
||||
let maxTurns = 20;
|
||||
let currentTurn = 0;
|
||||
|
||||
@@ -250,6 +268,10 @@ Example:
|
||||
config.maxTokens
|
||||
);
|
||||
|
||||
if (onPromptGenerated) {
|
||||
onPromptGenerated(messages);
|
||||
}
|
||||
|
||||
let responseContent;
|
||||
let fullStreamedContent = "";
|
||||
try {
|
||||
@@ -326,7 +348,7 @@ Example:
|
||||
toolCall.arguments.chid = parseInt(this.currentChid);
|
||||
}
|
||||
}
|
||||
if (toolCall.name === 'write_world_info_entry' || toolCall.name === 'read_world_info') {
|
||||
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;
|
||||
}
|
||||
@@ -419,7 +441,7 @@ Example:
|
||||
}
|
||||
|
||||
if (onPreviewUpdate && !isError) {
|
||||
onPreviewUpdate(toolCall.name, toolCall.arguments);
|
||||
onPreviewUpdate(toolCall.name, toolCall.arguments, false, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ export async function callAi(role, messages, options = {}, onChunk = null) {
|
||||
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
const lines = buffer.split('\n');
|
||||
buffer = lines.pop(); // Keep incomplete line in buffer
|
||||
buffer = lines.pop();
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmedLine = line.trim();
|
||||
@@ -89,7 +89,7 @@ export async function callAi(role, messages, options = {}, onChunk = null) {
|
||||
onChunk(delta);
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore parse errors for partial chunks
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -107,6 +107,14 @@ export async function callAi(role, messages, options = {}, onChunk = null) {
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -117,12 +125,17 @@ export async function callAi(role, messages, options = {}, onChunk = null) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function testConnection(role) {
|
||||
export async function testConnection(role, config = {}) {
|
||||
try {
|
||||
const response = await callAi(role, [
|
||||
{ role: 'user', content: 'Hi' }
|
||||
], { maxTokens: 10 });
|
||||
return { success: !!response };
|
||||
{ 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 };
|
||||
|
||||
@@ -1,24 +1,91 @@
|
||||
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) {
|
||||
const char = characters[chid];
|
||||
if (!char) return;
|
||||
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(),
|
||||
body: JSON.stringify(char)
|
||||
headers: getRequestHeaders({ omitContentType: true }),
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error(`[Amily2 CharAPI] Failed to save character ${chid}:`, response.statusText);
|
||||
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}`);
|
||||
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}` };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +97,7 @@ export function getCharacter(chid = this_chid) {
|
||||
return characters[chid];
|
||||
}
|
||||
|
||||
export function updateCharacter(chid, updates) {
|
||||
export async function updateCharacter(chid, updates) {
|
||||
const char = getCharacter(chid);
|
||||
if (!char) return false;
|
||||
|
||||
@@ -45,12 +112,15 @@ export function updateCharacter(chid, updates) {
|
||||
});
|
||||
|
||||
if (changed) {
|
||||
saveCharacterById(chid);
|
||||
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);
|
||||
@@ -63,7 +133,7 @@ export function getFirstMessages(chid) {
|
||||
return messages;
|
||||
}
|
||||
|
||||
export function addFirstMessage(chid, message) {
|
||||
export async function addFirstMessage(chid, message) {
|
||||
const char = getCharacter(chid);
|
||||
if (!char) return false;
|
||||
|
||||
@@ -73,12 +143,15 @@ export function addFirstMessage(chid, message) {
|
||||
}
|
||||
|
||||
char.data.alternate_greetings.push(message);
|
||||
saveCharacterById(chid);
|
||||
const success = await saveCharacterById(chid);
|
||||
if (success) {
|
||||
console.log(`[Amily2 CharAPI] Added alternate greeting to character ${chid}`);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function updateFirstMessage(chid, index, message) {
|
||||
export async function updateFirstMessage(chid, index, message) {
|
||||
const char = getCharacter(chid);
|
||||
if (!char) return false;
|
||||
|
||||
@@ -93,12 +166,15 @@ export function updateFirstMessage(chid, index, message) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
saveCharacterById(chid);
|
||||
const success = await saveCharacterById(chid);
|
||||
if (success) {
|
||||
console.log(`[Amily2 CharAPI] Updated greeting ${index} for character ${chid}`);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function removeFirstMessage(chid, index) {
|
||||
export async function removeFirstMessage(chid, index) {
|
||||
const char = getCharacter(chid);
|
||||
if (!char) return false;
|
||||
|
||||
@@ -114,10 +190,13 @@ export function removeFirstMessage(chid, index) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
saveCharacterById(chid);
|
||||
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 {
|
||||
|
||||
@@ -85,7 +85,7 @@ ${taskState.getPromptContext()}
|
||||
shouldSummarize(history, tokenCount, maxTokens) {
|
||||
const tokenUsageRatio = tokenCount / maxTokens;
|
||||
if (tokenUsageRatio > 0.7) return true;
|
||||
if (history.length > 15) return true;
|
||||
if (history.length > 35) return true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,7 +159,7 @@ export const tools = {
|
||||
const { chid, ...updates } = args;
|
||||
const finalUpdates = args.updates || updates;
|
||||
|
||||
const success = charApi.updateCharacter(chid, finalUpdates);
|
||||
const success = await charApi.updateCharacter(chid, finalUpdates);
|
||||
if (success) {
|
||||
const updatedFields = Object.keys(finalUpdates).join(', ');
|
||||
return JSON.stringify({
|
||||
@@ -171,7 +171,7 @@ export const tools = {
|
||||
return JSON.stringify({
|
||||
status: "error",
|
||||
code: "UPDATE_FAILED",
|
||||
message: "更新角色卡失败。"
|
||||
message: "更新角色卡失败。请确保您正在编辑当前选中的角色(暂不支持后台编辑其他角色)。"
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -196,18 +196,43 @@ export const tools = {
|
||||
}
|
||||
|
||||
let content = char[field] || '';
|
||||
const changes = diff.split('------- SEARCH');
|
||||
|
||||
|
||||
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;
|
||||
if (parts.length < 2) continue;
|
||||
|
||||
const searchBlock = parts[0].trim();
|
||||
const replaceBlock = parts[1].split('+++++++ REPLACE')[0].trim();
|
||||
|
||||
if (!content.includes(searchBlock)) {
|
||||
|
||||
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",
|
||||
@@ -216,10 +241,7 @@ export const tools = {
|
||||
});
|
||||
}
|
||||
|
||||
content = content.replace(searchBlock, replaceBlock);
|
||||
}
|
||||
|
||||
const success = charApi.updateCharacter(chid, { [field]: content });
|
||||
const success = await charApi.updateCharacter(chid, { [field]: content });
|
||||
if (success) {
|
||||
return JSON.stringify({
|
||||
status: "success",
|
||||
@@ -229,7 +251,81 @@ export const tools = {
|
||||
return JSON.stringify({
|
||||
status: "error",
|
||||
code: "UPDATE_FAILED",
|
||||
message: `更新字段 '${field}' 失败。`
|
||||
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}] 失败。`
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -238,13 +334,13 @@ export const tools = {
|
||||
let success = false;
|
||||
switch (action) {
|
||||
case 'add':
|
||||
success = charApi.addFirstMessage(chid, message);
|
||||
success = await charApi.addFirstMessage(chid, message);
|
||||
break;
|
||||
case 'update':
|
||||
success = charApi.updateFirstMessage(chid, index, message);
|
||||
success = await charApi.updateFirstMessage(chid, index, message);
|
||||
break;
|
||||
case 'remove':
|
||||
success = charApi.removeFirstMessage(chid, index);
|
||||
success = await charApi.removeFirstMessage(chid, index);
|
||||
break;
|
||||
default:
|
||||
return JSON.stringify({
|
||||
@@ -494,6 +590,22 @@ export function getToolDefinitions() {
|
||||
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: "添加、更新或删除候补开场白。",
|
||||
|
||||
@@ -12,8 +12,9 @@ let agentManager = null;
|
||||
let previousCharData = {};
|
||||
let previousWorldData = {};
|
||||
let isWaitingForApproval = false;
|
||||
let openedFiles = new Map(); // Key: string ID, Value: { title, content, type, metadata }
|
||||
let openedFiles = new Map();
|
||||
let activeFileId = null;
|
||||
let promptLogContent = "=== Prompt Log ===\n\n";
|
||||
|
||||
export async function openAutoCharCardWindow() {
|
||||
if ($('#acc-window').length > 0) {
|
||||
@@ -57,31 +58,51 @@ export async function openAutoCharCardWindow() {
|
||||
|
||||
function populateDropdowns() {
|
||||
const charSelect = $('#acc-target-char');
|
||||
const prevCharId = charSelect.val();
|
||||
|
||||
charSelect.empty().append('<option value="">-- 请选择 --</option>');
|
||||
charSelect.append('<option value="new">新建角色卡</option>');
|
||||
|
||||
let isPrevCharStillPresent = false;
|
||||
characters.forEach((char, index) => {
|
||||
if (char) {
|
||||
const option = $('<option>').val(index).text(char.name);
|
||||
if (index === this_chid) option.prop('selected', true);
|
||||
charSelect.append(option);
|
||||
charSelect.append($('<option>').val(index).text(char.name));
|
||||
if (String(index) === prevCharId) {
|
||||
isPrevCharStillPresent = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (isPrevCharStillPresent) {
|
||||
charSelect.val(prevCharId);
|
||||
} else if (this_chid !== undefined) {
|
||||
charSelect.val(this_chid);
|
||||
}
|
||||
|
||||
const worldSelect = $('#acc-target-world');
|
||||
const prevWorldName = worldSelect.val();
|
||||
|
||||
worldSelect.empty().append('<option value="">-- 请选择 --</option>');
|
||||
worldSelect.append('<option value="new">新建世界书</option>');
|
||||
|
||||
let isPrevWorldStillPresent = false;
|
||||
world_names.forEach(name => {
|
||||
worldSelect.append($('<option>').val(name).text(name));
|
||||
if (name === prevWorldName) {
|
||||
isPrevWorldStillPresent = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (isPrevWorldStillPresent) {
|
||||
worldSelect.val(prevWorldName);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleContextUpdate(type, value) {
|
||||
console.log(`[Amily2 AutoCharCard] Context Update: ${type} -> ${value}`);
|
||||
|
||||
if (type === 'char') {
|
||||
await getCharacters(); // Force refresh character list
|
||||
await getCharacters();
|
||||
}
|
||||
|
||||
populateDropdowns();
|
||||
@@ -93,6 +114,29 @@ async function handleContextUpdate(type, value) {
|
||||
}
|
||||
}
|
||||
|
||||
function handlePromptLog(messages) {
|
||||
const userType = localStorage.getItem("plugin_user_type");
|
||||
if (userType !== "3") return;
|
||||
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
let logEntry = `\n\n--- [${timestamp}] New Request ---\n`;
|
||||
|
||||
messages.forEach(msg => {
|
||||
logEntry += `\n[${msg.role.toUpperCase()}]\n${msg.content}\n`;
|
||||
});
|
||||
|
||||
promptLogContent += logEntry;
|
||||
|
||||
|
||||
if (openedFiles.has('debug-prompt-log')) {
|
||||
const file = openedFiles.get('debug-prompt-log');
|
||||
file.content = promptLogContent;
|
||||
if (activeFileId === 'debug-prompt-log') {
|
||||
renderEditor();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function renderRulesList() {
|
||||
const list = $('#acc-rules-list');
|
||||
list.empty();
|
||||
@@ -157,10 +201,32 @@ function bindEvents() {
|
||||
|
||||
const [type, id, subId] = val.split('|');
|
||||
|
||||
if (type === 'debug' && id === 'log') {
|
||||
const userType = localStorage.getItem("plugin_user_type");
|
||||
if (userType !== "3") {
|
||||
toastr.warning('权限不足:仅开发者可查看调试日志。');
|
||||
$(this).val('');
|
||||
return;
|
||||
}
|
||||
|
||||
const fileId = 'debug-prompt-log';
|
||||
openedFiles.set(fileId, {
|
||||
title: 'Prompt Log',
|
||||
content: promptLogContent,
|
||||
type: 'log',
|
||||
metadata: null
|
||||
});
|
||||
activeFileId = fileId;
|
||||
renderEditor();
|
||||
$(this).val('');
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'char') {
|
||||
const chid = id;
|
||||
const field = subId;
|
||||
|
||||
|
||||
const fileId = `char-${chid}-${field}`;
|
||||
if (openedFiles.has(fileId)) {
|
||||
activeFileId = fileId;
|
||||
@@ -168,9 +234,13 @@ function bindEvents() {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
|
||||
let content = '';
|
||||
|
||||
|
||||
|
||||
|
||||
try {
|
||||
console.log(`[AutoCharCard] Reading char ${chid}, field ${field}`);
|
||||
const charData = await tools.read_character_card({ chid });
|
||||
@@ -244,7 +314,7 @@ function bindEvents() {
|
||||
}
|
||||
}
|
||||
|
||||
// Reset selector
|
||||
|
||||
$(this).val('');
|
||||
});
|
||||
|
||||
@@ -292,6 +362,9 @@ function bindEvents() {
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
const previewHeader = $('.acc-right-panel .acc-panel-header');
|
||||
if (previewHeader.find('#acc-refresh-preview').length === 0) {
|
||||
const refreshBtn = $('<button>')
|
||||
@@ -423,12 +496,15 @@ function bindEvents() {
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
$('.acc-nav-btn').on('click', function() {
|
||||
const targetClass = $(this).data('target');
|
||||
|
||||
|
||||
$('.acc-nav-btn').removeClass('active');
|
||||
$(this).addClass('active');
|
||||
|
||||
|
||||
$('.acc-column').removeClass('mobile-active');
|
||||
$(`.${targetClass}`).addClass('mobile-active');
|
||||
});
|
||||
@@ -458,23 +534,27 @@ async function handleSendMessage() {
|
||||
input.val('');
|
||||
|
||||
if (message) {
|
||||
|
||||
addMessage('user', message);
|
||||
await agentManager.resumeWithApproval(
|
||||
false,
|
||||
message,
|
||||
(content, role) => addMessage(role, content),
|
||||
(toolName, args) => updatePreview(toolName, args),
|
||||
updatePreview,
|
||||
showApprovalRequest,
|
||||
handleContextUpdate
|
||||
handleContextUpdate,
|
||||
handlePromptLog
|
||||
);
|
||||
} else {
|
||||
|
||||
await agentManager.resumeWithApproval(
|
||||
true,
|
||||
null,
|
||||
(content, role) => addMessage(role, content),
|
||||
(toolName, args) => updatePreview(toolName, args),
|
||||
updatePreview,
|
||||
showApprovalRequest,
|
||||
handleContextUpdate
|
||||
handleContextUpdate,
|
||||
handlePromptLog
|
||||
);
|
||||
}
|
||||
return;
|
||||
@@ -511,11 +591,10 @@ async function handleSendMessage() {
|
||||
(content, role) => {
|
||||
addMessage(role, content);
|
||||
},
|
||||
(toolName, args) => {
|
||||
updatePreview(toolName, args);
|
||||
},
|
||||
updatePreview,
|
||||
showApprovalRequest,
|
||||
handleContextUpdate
|
||||
handleContextUpdate,
|
||||
handlePromptLog
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Agent Error:', error);
|
||||
@@ -530,12 +609,17 @@ async function handleSendMessage() {
|
||||
function showApprovalRequest(toolName, args) {
|
||||
isWaitingForApproval = true;
|
||||
|
||||
|
||||
updatePreview(toolName, args, false);
|
||||
|
||||
|
||||
const btn = $('#acc-send-btn');
|
||||
btn.html('<i class="fas fa-check"></i>');
|
||||
btn.prop('title', '批准执行');
|
||||
btn.addClass('acc-btn-success');
|
||||
$('#acc-user-input').attr('placeholder', '输入反馈以修改,或点击 √ 批准,点击 X 拒绝...');
|
||||
|
||||
|
||||
if ($('#acc-reject-btn').length === 0) {
|
||||
const rejectBtn = $('<button>')
|
||||
.attr('id', 'acc-reject-btn')
|
||||
@@ -563,6 +647,7 @@ function showApprovalRequest(toolName, args) {
|
||||
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');
|
||||
@@ -570,6 +655,9 @@ function showApprovalRequest(toolName, args) {
|
||||
input.attr('placeholder', '描述您的需求...');
|
||||
input.val('');
|
||||
|
||||
|
||||
updatePreview(toolName, args, false, true);
|
||||
|
||||
const feedback = message || "用户拒绝了操作。";
|
||||
addMessage('user', `[拒绝] ${feedback}`);
|
||||
|
||||
@@ -577,13 +665,15 @@ function showApprovalRequest(toolName, args) {
|
||||
false,
|
||||
feedback,
|
||||
(content, role) => addMessage(role, content),
|
||||
(toolName, args) => updatePreview(toolName, args),
|
||||
updatePreview,
|
||||
showApprovalRequest,
|
||||
handleContextUpdate
|
||||
handleContextUpdate,
|
||||
handlePromptLog
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
const toolDisplay = `
|
||||
<div class="acc-tool-request">
|
||||
<details>
|
||||
@@ -603,7 +693,10 @@ function addMessage(role, content) {
|
||||
|
||||
if (role === 'stream-assistant') {
|
||||
let lastMsg = stream.children().last();
|
||||
|
||||
if (!lastMsg.hasClass('assistant') || !lastMsg.hasClass('acc-streaming')) {
|
||||
|
||||
|
||||
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');
|
||||
@@ -614,6 +707,7 @@ function addMessage(role, content) {
|
||||
|
||||
const contentDiv = lastMsg.find('.acc-message-content');
|
||||
|
||||
|
||||
const escapedContent = content
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
@@ -631,6 +725,7 @@ function addMessage(role, content) {
|
||||
let displayContent = content;
|
||||
if (role === 'executor' || role === 'assistant') {
|
||||
|
||||
|
||||
displayContent = displayContent
|
||||
.replace(/<thinking(?:\s+[^>]*)?>[\s\S]*?<\/thinking>/gi, '')
|
||||
.replace(/<\/thinking>/gi, '')
|
||||
@@ -640,8 +735,10 @@ function addMessage(role, content) {
|
||||
const tools = [
|
||||
'read_world_info', 'read_world_entry', 'write_world_info_entry', 'create_world_book',
|
||||
'read_character_card', 'update_character_card', 'edit_character_text',
|
||||
'edit_world_info_entry',
|
||||
'manage_first_message', 'use_tool'
|
||||
];
|
||||
|
||||
const regex = new RegExp(`<(${tools.join('|')})(?:\\s+[^>]*)?>[\\s\\S]*?<\\/\\1>`, 'gi');
|
||||
displayContent = displayContent.replace(regex, '').trim();
|
||||
|
||||
@@ -649,6 +746,7 @@ function addMessage(role, content) {
|
||||
displayContent = "<i>(正在执行操作...)</i>";
|
||||
}
|
||||
|
||||
|
||||
if (role === 'assistant') {
|
||||
stream.find('.acc-streaming').remove();
|
||||
}
|
||||
@@ -656,6 +754,7 @@ function addMessage(role, content) {
|
||||
|
||||
let formattedContent;
|
||||
|
||||
|
||||
if (displayContent.trim().startsWith('<div class="acc-tool-request"')) {
|
||||
formattedContent = displayContent;
|
||||
} else {
|
||||
@@ -699,28 +798,46 @@ function addMessage(role, content) {
|
||||
function parseMarkdown(text) {
|
||||
if (!text) return '';
|
||||
|
||||
|
||||
let html = text
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">");
|
||||
|
||||
|
||||
html = html.replace(/```([\s\S]*?)```/g, '<pre><code>$1</code></pre>');
|
||||
|
||||
|
||||
html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
|
||||
|
||||
|
||||
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>');
|
||||
|
||||
|
||||
html = html.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
|
||||
html = html.replace(/\*(.*?)\*/g, '<em>$1</em>');
|
||||
|
||||
|
||||
html = html.replace(/~~(.*?)~~/g, '<del>$1</del>');
|
||||
|
||||
|
||||
html = html.replace(/^[\*\-]{3,}$/gm, '<hr>');
|
||||
|
||||
|
||||
|
||||
html = html.replace(/^\s*[\-\*]\s+(.*$)/gm, '<li>$1</li>');
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
html = html.replace(/\n/g, '<br>');
|
||||
|
||||
return html;
|
||||
@@ -731,6 +848,9 @@ function renderEditor() {
|
||||
const tabsContainer = $('.acc-preview-tabs');
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
container.empty();
|
||||
tabsContainer.empty();
|
||||
|
||||
@@ -739,6 +859,7 @@ function renderEditor() {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (!activeFileId || !openedFiles.has(activeFileId)) {
|
||||
activeFileId = openedFiles.keys().next().value;
|
||||
}
|
||||
@@ -757,6 +878,7 @@ function renderEditor() {
|
||||
const icon = $('<i class="fas fa-file-alt"></i>');
|
||||
const titleSpan = $('<span>').addClass('acc-tab-title').text(file.title);
|
||||
|
||||
|
||||
const closeBtn = $('<span>')
|
||||
.html('×')
|
||||
.addClass('acc-tab-close')
|
||||
@@ -777,7 +899,7 @@ function renderEditor() {
|
||||
.css('flex-direction', 'column')
|
||||
.css('height', '100%');
|
||||
|
||||
// Toolbar
|
||||
|
||||
const toolbar = $('<div>').addClass('acc-editor-toolbar').css({
|
||||
'padding': '5px',
|
||||
'border-bottom': '1px solid #444',
|
||||
@@ -792,9 +914,121 @@ function renderEditor() {
|
||||
.on('click', () => saveFile(id));
|
||||
|
||||
toolbar.append(saveBtn);
|
||||
|
||||
|
||||
contentDiv.append(toolbar);
|
||||
|
||||
// Textarea
|
||||
if (file.type === 'diff-view') {
|
||||
const editorDiv = $('<div>')
|
||||
.addClass('acc-editor-diff-view')
|
||||
.css({
|
||||
'flex': '1',
|
||||
'width': '100%',
|
||||
'background': '#1e1e1e',
|
||||
'color': '#d4d4d4',
|
||||
'padding': '10px',
|
||||
'font-family': 'monospace',
|
||||
'overflow-y': 'auto',
|
||||
'white-space': 'pre-wrap'
|
||||
});
|
||||
|
||||
if (file.segments) {
|
||||
file.segments.forEach((segment) => {
|
||||
if (segment.type === 'text') {
|
||||
editorDiv.append($('<span>').text(segment.content));
|
||||
} else if (segment.type === 'change') {
|
||||
const container = $('<div>').addClass('acc-diff-container').css({
|
||||
'display': 'block',
|
||||
'margin': '10px 0',
|
||||
'border': '1px solid #444',
|
||||
'padding': '5px',
|
||||
'border-radius': '4px'
|
||||
});
|
||||
|
||||
const renderChange = () => {
|
||||
container.empty();
|
||||
if (segment.active) {
|
||||
const removed = $('<div>')
|
||||
.text(segment.original)
|
||||
.css({
|
||||
'background-color': 'rgba(255, 0, 0, 0.2)',
|
||||
'cursor': 'pointer',
|
||||
'padding': '5px',
|
||||
'margin-bottom': '2px',
|
||||
'white-space': 'pre-wrap',
|
||||
'color': '#d4d4d4'
|
||||
})
|
||||
.attr('title', '点击恢复 (Click to restore)');
|
||||
|
||||
const added = $('<div>')
|
||||
.attr('contenteditable', 'true')
|
||||
.css({
|
||||
'background-color': 'rgba(0, 255, 0, 0.2)',
|
||||
'cursor': 'text',
|
||||
'padding': '5px',
|
||||
'white-space': 'pre-wrap',
|
||||
'color': '#d4d4d4',
|
||||
'outline': 'none'
|
||||
})
|
||||
.attr('title', '点击编辑');
|
||||
|
||||
const toggle = () => {
|
||||
segment.active = false;
|
||||
renderChange();
|
||||
if (agentManager) {
|
||||
const newDiff = reconstructDiff(file.segments);
|
||||
agentManager.updatePendingToolArgs({ diff: newDiff });
|
||||
}
|
||||
};
|
||||
|
||||
removed.on('click', toggle);
|
||||
|
||||
added.on('input', function() {
|
||||
segment.new = $(this).text();
|
||||
if (agentManager) {
|
||||
const newDiff = reconstructDiff(file.segments);
|
||||
agentManager.updatePendingToolArgs({ diff: newDiff });
|
||||
}
|
||||
});
|
||||
|
||||
container.append(removed).append(added);
|
||||
} else {
|
||||
|
||||
const restored = $('<div>')
|
||||
.text(segment.original)
|
||||
.css({
|
||||
'cursor': 'pointer',
|
||||
'border-left': '3px solid #666',
|
||||
'padding': '5px',
|
||||
'white-space': 'pre-wrap',
|
||||
'opacity': '0.7'
|
||||
})
|
||||
.attr('title', '点击重新应用修改 (Click to re-apply change)');
|
||||
|
||||
restored.on('click', () => {
|
||||
segment.active = true;
|
||||
renderChange();
|
||||
if (agentManager) {
|
||||
const newDiff = reconstructDiff(file.segments);
|
||||
agentManager.updatePendingToolArgs({ diff: newDiff });
|
||||
}
|
||||
});
|
||||
|
||||
container.append(restored);
|
||||
}
|
||||
};
|
||||
|
||||
renderChange();
|
||||
editorDiv.append(container);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
editorDiv.text('Error: No segments found for diff view.');
|
||||
}
|
||||
|
||||
contentDiv.append(editorDiv);
|
||||
} else {
|
||||
|
||||
const textarea = $('<textarea>')
|
||||
.addClass('acc-editor-textarea')
|
||||
.val(file.content)
|
||||
@@ -814,6 +1048,8 @@ function renderEditor() {
|
||||
});
|
||||
|
||||
contentDiv.append(textarea);
|
||||
}
|
||||
|
||||
container.append(contentDiv);
|
||||
}
|
||||
});
|
||||
@@ -829,42 +1065,61 @@ async function saveFile(id) {
|
||||
return;
|
||||
}
|
||||
|
||||
let contentToSave = file.content;
|
||||
if (file.type === 'diff-view' && file.segments) {
|
||||
contentToSave = file.segments.map(seg => {
|
||||
if (seg.type === 'text') return seg.content;
|
||||
if (seg.type === 'change') {
|
||||
return seg.active ? seg.new : seg.original;
|
||||
}
|
||||
return '';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
try {
|
||||
let result;
|
||||
if (meta.type === 'char') {
|
||||
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
|
||||
message: contentToSave
|
||||
});
|
||||
} else {
|
||||
result = await tools.update_character_card({
|
||||
chid: meta.chid,
|
||||
[meta.field]: file.content
|
||||
[meta.field]: contentToSave
|
||||
});
|
||||
}
|
||||
} else if (meta.type === 'wi') {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
if (meta.uid !== undefined) {
|
||||
result = await tools.write_world_info_entry({
|
||||
book_name: meta.bookName,
|
||||
entries: [{ uid: meta.uid, content: file.content }]
|
||||
entries: [{ uid: meta.uid, content: contentToSave }]
|
||||
});
|
||||
} else {
|
||||
|
||||
|
||||
try {
|
||||
const entry = JSON.parse(file.content);
|
||||
const entry = JSON.parse(contentToSave);
|
||||
result = await tools.write_world_info_entry({
|
||||
book_name: meta.bookName,
|
||||
entries: [entry]
|
||||
});
|
||||
} catch (e) {
|
||||
|
||||
|
||||
toastr.error('保存失败: 内容必须是有效的 JSON (针对新建条目) 或包含 UID');
|
||||
return;
|
||||
}
|
||||
@@ -873,6 +1128,15 @@ async function saveFile(id) {
|
||||
|
||||
if (result && !result.startsWith('Error') && !result.includes('失败')) {
|
||||
toastr.success('保存成功');
|
||||
|
||||
|
||||
if (file.type === 'diff-view') {
|
||||
file.type = 'normal';
|
||||
delete file.segments;
|
||||
|
||||
file.content = contentToSave;
|
||||
renderEditor();
|
||||
}
|
||||
} else {
|
||||
toastr.error(result || '保存失败');
|
||||
}
|
||||
@@ -901,6 +1165,7 @@ async function loadContextToEditor() {
|
||||
|
||||
const char = response.data;
|
||||
previousCharData = char;
|
||||
|
||||
const charGroup = $('<optgroup label="角色卡字段">');
|
||||
const fields = ['description', 'personality', 'first_mes', 'scenario', 'mes_example'];
|
||||
fields.forEach(field => {
|
||||
@@ -914,6 +1179,13 @@ async function loadContextToEditor() {
|
||||
}
|
||||
selector.append(charGroup);
|
||||
|
||||
|
||||
const userType = localStorage.getItem("plugin_user_type");
|
||||
if (userType === "3") {
|
||||
selector.append('<option value="debug|log">Debug: Prompt Log</option>');
|
||||
}
|
||||
|
||||
|
||||
if (openedFiles.size === 0 && char.description) {
|
||||
const id = `char-${chid}-description`;
|
||||
openedFiles.set(id, {
|
||||
@@ -932,6 +1204,7 @@ async function loadContextToEditor() {
|
||||
|
||||
if (bookName && bookName !== 'new') {
|
||||
try {
|
||||
|
||||
const indexData = await tools.read_world_info({ book_name: bookName, return_full: false });
|
||||
const index = JSON.parse(indexData);
|
||||
|
||||
@@ -952,9 +1225,18 @@ async function loadContextToEditor() {
|
||||
renderEditor();
|
||||
}
|
||||
|
||||
async function updatePreview(toolName, args, isPartial = false) {
|
||||
const chid = args.chid !== undefined ? args.chid : $('#acc-target-char').val();
|
||||
const bookName = args.book_name !== undefined ? args.book_name : $('#acc-target-world').val();
|
||||
async function updatePreview(toolName, args, isPartial = false, isExecuted = false) {
|
||||
let chid = args.chid;
|
||||
if (chid === undefined || chid === null || chid === '') {
|
||||
chid = $('#acc-target-char').val();
|
||||
}
|
||||
chid = String(chid);
|
||||
|
||||
let bookName = args.book_name;
|
||||
if (bookName === undefined || bookName === null || bookName === '') {
|
||||
bookName = $('#acc-target-world').val();
|
||||
}
|
||||
bookName = String(bookName);
|
||||
|
||||
if (toolName === 'update_character_card') {
|
||||
const fields = ['description', 'personality', 'first_mes', 'scenario', 'mes_example'];
|
||||
@@ -976,11 +1258,30 @@ async function updatePreview(toolName, args, isPartial = false) {
|
||||
|
||||
} else if (toolName === 'edit_character_text') {
|
||||
const field = args.field || 'Unknown Field';
|
||||
|
||||
|
||||
if (field !== 'Unknown Field') {
|
||||
const unknownDiffId = `diff-${chid}-Unknown Field`;
|
||||
if (openedFiles.has(unknownDiffId)) {
|
||||
openedFiles.delete(unknownDiffId);
|
||||
}
|
||||
|
||||
|
||||
const unknownId = `char-${chid}-Unknown Field`;
|
||||
if (openedFiles.has(unknownId)) {
|
||||
const file = openedFiles.get(unknownId);
|
||||
openedFiles.delete(unknownId);
|
||||
file.title = field;
|
||||
file.metadata.field = field;
|
||||
openedFiles.set(id, file);
|
||||
if (activeFileId === unknownId) activeFileId = id;
|
||||
}
|
||||
}
|
||||
|
||||
const diff = args.diff || '';
|
||||
const id = `char-${chid}-${field}`;
|
||||
|
||||
if (isPartial) {
|
||||
|
||||
const diffId = `diff-${chid}-${field}`;
|
||||
openedFiles.set(diffId, {
|
||||
title: `Diff: ${field}`,
|
||||
@@ -989,6 +1290,91 @@ async function updatePreview(toolName, args, isPartial = false) {
|
||||
metadata: null
|
||||
});
|
||||
activeFileId = diffId;
|
||||
} else if (isExecuted) {
|
||||
|
||||
let success = false;
|
||||
let content = '';
|
||||
|
||||
try {
|
||||
const charData = await tools.read_character_card({ chid });
|
||||
const response = JSON.parse(charData);
|
||||
if (response.status === 'success' && response.data) {
|
||||
const char = response.data;
|
||||
if (field.startsWith('greeting_')) {
|
||||
const index = parseInt(field.split('_')[1]);
|
||||
content = char.alternate_greetings[index];
|
||||
} else {
|
||||
content = char[field];
|
||||
}
|
||||
success = true;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to refresh content after edit", e);
|
||||
}
|
||||
|
||||
openedFiles.delete(`diff-${chid}-${field}`);
|
||||
|
||||
|
||||
if (!openedFiles.has(id)) {
|
||||
for (const [key, val] of openedFiles) {
|
||||
if (val.metadata && val.metadata.chid == chid && val.metadata.field == field) {
|
||||
id = key;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
let foundAndFixed = false;
|
||||
openedFiles.forEach((file, fileId) => {
|
||||
if (file.metadata && file.metadata.chid == chid && file.metadata.field == field) {
|
||||
if (file.type === 'diff-view') {
|
||||
|
||||
if (file.segments) {
|
||||
const newContent = file.segments.map(s => s.type === 'change' ? s.new : s.content).join('');
|
||||
file.content = newContent;
|
||||
}
|
||||
file.type = 'normal';
|
||||
delete file.segments;
|
||||
|
||||
|
||||
if (fileId !== id) {
|
||||
openedFiles.delete(fileId);
|
||||
openedFiles.set(id, file);
|
||||
if (activeFileId === fileId) activeFileId = id;
|
||||
}
|
||||
foundAndFixed = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (success) {
|
||||
|
||||
|
||||
|
||||
|
||||
openedFiles.set(id, {
|
||||
title: field,
|
||||
content: content,
|
||||
type: 'normal',
|
||||
metadata: { type: 'char', chid, field }
|
||||
});
|
||||
activeFileId = id;
|
||||
} else if (!foundAndFixed) {
|
||||
|
||||
|
||||
|
||||
if (openedFiles.has(id)) {
|
||||
|
||||
const file = openedFiles.get(id);
|
||||
file.type = 'normal';
|
||||
activeFileId = id;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
renderEditor();
|
||||
} else {
|
||||
|
||||
let originalContent = '';
|
||||
@@ -996,52 +1382,121 @@ async function updatePreview(toolName, args, isPartial = false) {
|
||||
originalContent = openedFiles.get(id).content;
|
||||
} else {
|
||||
|
||||
try {
|
||||
const charData = await tools.read_character_card({ chid });
|
||||
const response = JSON.parse(charData);
|
||||
if (response.status === 'success' && response.data) {
|
||||
const char = response.data;
|
||||
if (field.startsWith('greeting_')) {
|
||||
const index = parseInt(field.split('_')[1]);
|
||||
originalContent = char.alternate_greetings[index];
|
||||
} else {
|
||||
originalContent = char[field];
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to fetch original content for diff view", e);
|
||||
}
|
||||
}
|
||||
|
||||
if (originalContent) {
|
||||
// Apply diff logic
|
||||
const changes = diff.split('------- SEARCH');
|
||||
if (changes[0].trim() === '') changes.shift();
|
||||
|
||||
let newContent = originalContent;
|
||||
let applied = true;
|
||||
|
||||
for (const change of changes) {
|
||||
const parts = change.split('=======');
|
||||
if (parts.length === 2) {
|
||||
const searchBlock = parts[0].trim();
|
||||
const replaceBlock = parts[1].split('+++++++ REPLACE')[0].trim();
|
||||
if (newContent.includes(searchBlock)) {
|
||||
newContent = newContent.replace(searchBlock, replaceBlock);
|
||||
} else {
|
||||
applied = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (applied) {
|
||||
const segments = parseDiff(originalContent, diff);
|
||||
openedFiles.set(id, {
|
||||
title: field,
|
||||
content: newContent,
|
||||
type: 'normal',
|
||||
content: originalContent,
|
||||
segments: segments,
|
||||
type: 'diff-view',
|
||||
metadata: { type: 'char', chid, field }
|
||||
});
|
||||
activeFileId = id;
|
||||
openedFiles.delete(`diff-${chid}-${field}`);
|
||||
} else {
|
||||
|
||||
const diffId = `diff-${chid}-${field}`;
|
||||
openedFiles.set(diffId, {
|
||||
title: `Diff: ${field} (Failed to Apply)`,
|
||||
title: `Diff: ${field}`,
|
||||
content: diff,
|
||||
type: 'diff',
|
||||
metadata: null
|
||||
});
|
||||
activeFileId = diffId;
|
||||
}
|
||||
} else {
|
||||
const diffId = `diff-${chid}-${field}`;
|
||||
}
|
||||
|
||||
} else if (toolName === 'edit_world_info_entry') {
|
||||
const uid = args.uid;
|
||||
|
||||
|
||||
if (uid !== undefined) {
|
||||
const unknownDiffId = `diff-wi-${bookName}-undefined`;
|
||||
if (openedFiles.has(unknownDiffId)) {
|
||||
openedFiles.delete(unknownDiffId);
|
||||
}
|
||||
}
|
||||
|
||||
const diff = args.diff || '';
|
||||
const id = `wi-${bookName}-${uid}`;
|
||||
|
||||
if (isPartial) {
|
||||
const diffId = `diff-wi-${bookName}-${uid}`;
|
||||
openedFiles.set(diffId, {
|
||||
title: `Diff: ${field}`,
|
||||
title: `Diff: WI ${uid}`,
|
||||
content: diff,
|
||||
type: 'diff',
|
||||
metadata: null
|
||||
});
|
||||
activeFileId = diffId;
|
||||
} else if (isExecuted) {
|
||||
|
||||
try {
|
||||
const entryData = await tools.read_world_entry({ book_name: bookName, uid: uid });
|
||||
const response = JSON.parse(entryData);
|
||||
if (response.status === 'success' && response.data) {
|
||||
openedFiles.delete(`diff-wi-${bookName}-${uid}`);
|
||||
openedFiles.delete(id);
|
||||
|
||||
openedFiles.set(id, {
|
||||
title: `WI: ${uid}`,
|
||||
content: response.data.content,
|
||||
type: 'normal',
|
||||
metadata: { type: 'wi', bookName, uid }
|
||||
});
|
||||
activeFileId = id;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to refresh WI content after edit", e);
|
||||
}
|
||||
} else {
|
||||
let originalContent = '';
|
||||
if (openedFiles.has(id)) {
|
||||
originalContent = openedFiles.get(id).content;
|
||||
} else {
|
||||
try {
|
||||
const entryData = await tools.read_world_entry({ book_name: bookName, uid: uid });
|
||||
const response = JSON.parse(entryData);
|
||||
if (response.status === 'success' && response.data) {
|
||||
originalContent = response.data.content;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to fetch original content for WI diff view", e);
|
||||
}
|
||||
}
|
||||
|
||||
if (originalContent) {
|
||||
const segments = parseDiff(originalContent, diff);
|
||||
openedFiles.set(id, {
|
||||
title: `WI: ${uid}`,
|
||||
content: originalContent,
|
||||
segments: segments,
|
||||
type: 'diff-view',
|
||||
metadata: { type: 'wi', bookName, uid }
|
||||
});
|
||||
activeFileId = id;
|
||||
openedFiles.delete(`diff-wi-${bookName}-${uid}`);
|
||||
} else {
|
||||
const diffId = `diff-wi-${bookName}-${uid}`;
|
||||
openedFiles.set(diffId, {
|
||||
title: `Diff: WI ${uid}`,
|
||||
content: diff,
|
||||
type: 'diff',
|
||||
metadata: null
|
||||
@@ -1054,6 +1509,7 @@ async function updatePreview(toolName, args, isPartial = false) {
|
||||
let entries = args.entries;
|
||||
|
||||
if (isPartial && typeof entries === 'string') {
|
||||
|
||||
const id = `wi-raw-partial`;
|
||||
openedFiles.set(id, {
|
||||
title: 'WI Entry (Generating...)',
|
||||
@@ -1081,9 +1537,65 @@ async function updatePreview(toolName, args, isPartial = false) {
|
||||
});
|
||||
activeFileId = id;
|
||||
});
|
||||
|
||||
openedFiles.delete(`wi-raw-partial`);
|
||||
}
|
||||
}
|
||||
|
||||
renderEditor();
|
||||
}
|
||||
|
||||
function parseDiff(originalContent, diff) {
|
||||
const segments = [];
|
||||
let currentIndex = 0;
|
||||
|
||||
const parts = diff.split('------- SEARCH');
|
||||
|
||||
for (let i = 1; i < parts.length; i++) {
|
||||
const part = parts[i];
|
||||
const split1 = part.split('=======');
|
||||
if (split1.length < 2) continue;
|
||||
|
||||
const searchContent = split1[0].trim();
|
||||
const split2 = split1[1].split('+++++++ REPLACE');
|
||||
if (split2.length < 1) continue;
|
||||
|
||||
const replaceContent = split2[0].trim();
|
||||
|
||||
const foundIndex = originalContent.indexOf(searchContent, currentIndex);
|
||||
|
||||
if (foundIndex !== -1) {
|
||||
if (foundIndex > currentIndex) {
|
||||
segments.push({
|
||||
type: 'text',
|
||||
content: originalContent.substring(currentIndex, foundIndex)
|
||||
});
|
||||
}
|
||||
|
||||
segments.push({
|
||||
type: 'change',
|
||||
original: searchContent,
|
||||
new: replaceContent,
|
||||
active: true
|
||||
});
|
||||
|
||||
currentIndex = foundIndex + searchContent.length;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentIndex < originalContent.length) {
|
||||
segments.push({
|
||||
type: 'text',
|
||||
content: originalContent.substring(currentIndex)
|
||||
});
|
||||
}
|
||||
|
||||
return segments;
|
||||
}
|
||||
|
||||
function reconstructDiff(segments) {
|
||||
return segments
|
||||
.filter(s => s.type === 'change' && s.active)
|
||||
.map(s => `------- SEARCH\n${s.original}\n=======\n${s.new}\n+++++++ REPLACE`)
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user