3 Commits

Author SHA1 Message Date
bf9a9119c7 Update manifest.json 2026-01-02 09:24:21 +08:00
bb1800d880 Add files via upload 2026-01-02 09:11:00 +08:00
c65a607dc4 Add files via upload 2026-01-02 09:10:13 +08:00
9 changed files with 1003 additions and 196 deletions

View File

@@ -106,7 +106,8 @@
<div class="acc-message system"> <div class="acc-message system">
<div class="acc-message-content"> <div class="acc-message-content">
欢迎使用 Amily2 自动构建器。<br> 欢迎使用 Amily2 自动构建器。<br>
请在左侧配置工作区,然后在下方输入您的需求。 请在左侧配置工作区,然后在下方输入您的需求。<br>
当使用时,最好不要进入所选的角色卡中,以便后台执行即时生效。
</div> </div>
</div> </div>
</div> </div>
@@ -128,9 +129,9 @@
<!-- 右栏:实时预览/Diff --> <!-- 右栏:实时预览/Diff -->
<div class="acc-column acc-right-panel"> <div class="acc-column acc-right-panel">
<div class="acc-panel-header" style="display: flex; justify-content: space-between; align-items: center;"> <div class="acc-panel-header" style="display: flex; justify-content: space-between; align-items: center;">
<div style="display: flex; align-items: center; gap: 5px;"> <div style="display: flex; align-items: center; gap: 5px; flex: 1; min-width: 0;">
<i class="fas fa-eye"></i> <i class="fas fa-eye" style="flex-shrink: 0;"></i>
<select id="acc-file-selector" class="acc-select" style="height: 24px; padding: 0 5px; font-size: 12px; max-width: 150px;"> <select id="acc-file-selector" class="acc-select" style="height: 24px; padding: 0 5px; font-size: 12px; width: auto; flex: 1; min-width: 100px;">
<option value="">-- 选择文件 --</option> <option value="">-- 选择文件 --</option>
</select> </select>
</div> </div>

View File

@@ -119,7 +119,7 @@
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
overflow: hidden; /* Prevent overflow */ /* overflow: hidden; Removed to allow dropdowns to show fully if custom, though native selects are fine. */
} }
.acc-panel-header i.fa-chevron-down, .acc-panel-header i.fa-chevron-down,
@@ -235,49 +235,53 @@
overflow-y: auto; overflow-y: auto;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 20px; gap: 24px;
background-color: #1e1e1e; background-color: #1e1e1e;
} }
.acc-message { .acc-message {
max-width: 95%;
display: flex; display: flex;
gap: 12px; gap: 16px;
margin-bottom: 5px; max-width: 100%;
padding: 0 10px;
} }
.acc-avatar { .acc-avatar {
width: 24px; width: 30px;
height: 24px; height: 30px;
border-radius: 4px; border-radius: 6px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: 14px; font-size: 16px;
flex-shrink: 0; flex-shrink: 0;
margin-top: 4px; margin-top: 2px;
background-color: #2d2d2d;
} }
.acc-message-content { .acc-message-content {
flex: 1;
font-size: 14px; font-size: 14px;
line-height: 1.6; line-height: 1.6;
color: #cccccc; color: #d4d4d4;
word-wrap: break-word; overflow-wrap: break-word;
flex: 1;
} }
/* User Message */ /* User Message */
.acc-message.user { .acc-message.user {
background-color: #2b2d31; /* VSCode sidebar/input bg */ background-color: rgba(255, 255, 255, 0.04);
padding: 12px 16px;
border-radius: 6px;
align-self: stretch; /* Full width block like Cline */
flex-direction: row; /* Keep avatar on left or remove it? Cline puts user msg in a box */
border: 1px solid #333; border: 1px solid #333;
border-radius: 8px;
padding: 16px;
margin-left: 10px;
margin-right: 10px;
width: auto;
} }
.acc-message.user .acc-avatar { .acc-message.user .acc-avatar {
display: none; /* Hide avatar for user, just show text box */ display: flex; /* Show avatar for user */
background-color: #0e639c;
color: #fff;
} }
.acc-message.user .acc-message-content { .acc-message.user .acc-message-content {
@@ -287,34 +291,90 @@
/* Assistant Message */ /* Assistant Message */
.acc-message.assistant { .acc-message.assistant {
align-self: stretch; padding: 16px;
padding: 0 10px; background-color: #252526;
border-radius: 8px;
border: 1px solid #333;
margin-left: 10px;
margin-right: 10px;
width: auto;
} }
.acc-message.assistant .acc-avatar { .acc-message.assistant .acc-avatar {
background-color: transparent; background-color: transparent;
color: #4caf50; /* Green robot */ color: #4caf50; /* Green robot */
font-size: 20px;
}
/* Markdown Styles */
.acc-message-content code {
background-color: rgba(110, 118, 129, 0.4); /* VSCode style inline code bg */
color: #e0e0e0;
padding: 2px 5px;
border-radius: 4px;
font-family: 'JetBrains Mono', 'Consolas', monospace;
font-size: 0.9em;
}
.acc-message-content pre {
background-color: #1e1e1e;
padding: 12px;
border-radius: 6px;
overflow-x: auto;
border: 1px solid #333;
margin: 12px 0;
}
.acc-message-content pre code {
background-color: transparent;
padding: 0;
border: none;
color: #9cdcfe;
}
.acc-message-content p {
margin: 0 0 10px 0;
}
.acc-message-content p:last-child {
margin-bottom: 0;
}
.acc-message-content ul, .acc-message-content ol {
margin: 10px 0;
padding-left: 24px;
}
.acc-message-content li {
margin-bottom: 6px;
} }
/* Tool Request Styles */ /* Tool Request Styles */
.acc-tool-request { .acc-tool-request {
background-color: #252526; background-color: #252526;
border: 1px solid #333; border: 1px solid #333;
border-radius: 6px; border-radius: 8px;
margin: 10px 0; margin: 12px 0;
overflow: hidden; overflow: hidden;
box-shadow: 0 2px 6px rgba(0,0,0,0.2);
} }
.acc-tool-header { .acc-tool-header {
background-color: #2d2d2d; background-color: #2d2d2d;
padding: 8px 12px; padding: 10px 14px;
font-size: 12px; font-size: 12px;
font-weight: 600; font-weight: 600;
color: #cccccc; color: #e0e0e0;
border-bottom: 1px solid #333; border-bottom: 1px solid #333;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 10px;
cursor: pointer;
transition: background-color 0.2s;
}
.acc-tool-header:hover {
background-color: #333;
} }
.acc-tool-header i { .acc-tool-header i {
@@ -322,28 +382,35 @@
} }
.acc-tool-content { .acc-tool-content {
padding: 12px; padding: 14px;
margin: 0; margin: 0;
background-color: #1e1e1e; background-color: #1e1e1e;
color: #9cdcfe; color: #9cdcfe;
font-family: 'JetBrains Mono', monospace; font-family: 'JetBrains Mono', 'Consolas', monospace;
font-size: 12px; font-size: 12px;
overflow-x: auto; overflow-x: auto;
white-space: pre-wrap; white-space: pre-wrap;
line-height: 1.5;
} }
/* System/Thought Message */ /* System/Thought Message */
.acc-message.system, .acc-message.thought { .acc-message.system, .acc-message.thought {
align-self: stretch;
padding: 0 10px; padding: 0 10px;
opacity: 0.8; opacity: 0.9;
}
.acc-message.thought .acc-avatar {
color: #9c27b0;
background-color: transparent;
} }
.acc-message.thought .acc-message-content { .acc-message.thought .acc-message-content {
color: #8b949e; color: #8b949e;
font-style: italic; font-style: italic;
border-left: 2px solid #333; background-color: rgba(156, 39, 176, 0.05);
padding-left: 10px; padding: 10px 14px;
border-radius: 6px;
border-left: 3px solid #9c27b0;
} }
/* 输入区域 */ /* 输入区域 */

View File

@@ -36,18 +36,25 @@ export class AgentManager {
this.approvalRequired = required; this.approvalRequired = required;
} }
updatePendingToolArgs(newArgs) {
if (this.pendingToolCall) {
this.pendingToolCall.arguments = { ...this.pendingToolCall.arguments, ...newArgs };
console.log("[AgentManager] Pending tool args updated:", this.pendingToolCall.arguments);
}
}
stop() { stop() {
this.status = 'idle'; 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 (this.status !== 'paused' || !this.pendingToolCall) return;
if (approved) { if (approved) {
this.status = 'running'; this.status = 'running';
await this.executePendingTool(onStreamUpdate, onPreviewUpdate, onContextUpdate); await this.executePendingTool(onStreamUpdate, onPreviewUpdate, onContextUpdate);
this.pendingToolCall = null; this.pendingToolCall = null;
await this.runTaskLoop(onStreamUpdate, onPreviewUpdate, onApprovalRequest, onContextUpdate); await this.runTaskLoop(onStreamUpdate, onPreviewUpdate, onApprovalRequest, onContextUpdate, onPromptGenerated);
} else { } else {
this.status = 'running'; this.status = 'running';
this.pendingToolCall = null; this.pendingToolCall = null;
@@ -55,7 +62,7 @@ export class AgentManager {
role: 'user', role: 'user',
content: `[工具执行被拒绝] 用户反馈: ${feedback || "未提供原因。"}` 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') { if (this.currentChid !== undefined && this.currentChid !== 'new') {
try { try {
const charData = await tools.read_character_card({ chid: this.currentChid }); 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 += `# Current Character\n`;
envDetails += `Name: ${char.name}\n`; envDetails += `Name: ${char.name}\n`;
envDetails += `Description Length: ${char.description?.length || 0}\n`; envDetails += `Description Length: ${char.description?.length || 0}\n`;
envDetails += `First Message Length: ${char.first_mes?.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`; 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) { } catch (e) {
envDetails += `# Current Character\nError reading character: ${e.message}\n\n`; 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. - **Detailed Writing**: When writing content (Description, First Message, World Info), be creative and detailed.
- World Info entries: > 300 words. - World Info entries: > 300 words.
- First Message: > 1500 words, including environment, psychology, and action. - 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. - **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. - **Completion**: When the task is done, provide a final summary to the user.
`; `;
@@ -207,17 +225,17 @@ Example:
return null; return null;
} }
async handleUserMessage(message, onStreamUpdate, onPreviewUpdate, onApprovalRequest, onContextUpdate) { async handleUserMessage(message, onStreamUpdate, onPreviewUpdate, onApprovalRequest, onContextUpdate, onPromptGenerated) {
if (this.history.length === 0) { if (this.history.length === 0) {
this.taskState.init(message); this.taskState.init(message);
} }
this.history.push({ role: 'user', content: message }); this.history.push({ role: 'user', content: message });
this.status = 'running'; 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 maxTurns = 20;
let currentTurn = 0; let currentTurn = 0;
@@ -250,6 +268,10 @@ Example:
config.maxTokens config.maxTokens
); );
if (onPromptGenerated) {
onPromptGenerated(messages);
}
let responseContent; let responseContent;
let fullStreamedContent = ""; let fullStreamedContent = "";
try { try {
@@ -326,7 +348,7 @@ Example:
toolCall.arguments.chid = parseInt(this.currentChid); 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) { if (!toolCall.arguments.book_name && this.currentBookName) {
toolCall.arguments.book_name = this.currentBookName; toolCall.arguments.book_name = this.currentBookName;
} }
@@ -419,7 +441,7 @@ Example:
} }
if (onPreviewUpdate && !isError) { if (onPreviewUpdate && !isError) {
onPreviewUpdate(toolCall.name, toolCall.arguments); onPreviewUpdate(toolCall.name, toolCall.arguments, false, true);
} }
} }

View File

@@ -74,7 +74,7 @@ export async function callAi(role, messages, options = {}, onChunk = null) {
buffer += decoder.decode(value, { stream: true }); buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n'); const lines = buffer.split('\n');
buffer = lines.pop(); // Keep incomplete line in buffer buffer = lines.pop();
for (const line of lines) { for (const line of lines) {
const trimmedLine = line.trim(); const trimmedLine = line.trim();
@@ -89,7 +89,7 @@ export async function callAi(role, messages, options = {}, onChunk = null) {
onChunk(delta); onChunk(delta);
} }
} catch (e) { } 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; 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}`); console.log(`[自动构建器] AI (${roleName}) 响应接收成功。长度: ${content?.length}`);
return content; 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 { try {
const response = await callAi(role, [ const response = await callAi(role, [
{ role: 'user', content: 'Hi' } { role: 'user', content: 'Say hello' }
], { maxTokens: 10 }); ], { maxTokens: 50, ...config });
return { success: !!response };
if (!response) {
return { success: false, error: "API 返回了空内容 (可能是被安全过滤或模型无响应)" };
}
return { success: true };
} catch (error) { } catch (error) {
console.error(`[自动构建器] ${role} 连接测试失败:`, error); console.error(`[自动构建器] ${role} 连接测试失败:`, error);
return { success: false, error: error.message }; return { success: false, error: error.message };

View File

@@ -1,24 +1,91 @@
import { characters, saveCharacterDebounced, this_chid, getCharacters, getRequestHeaders } from "/script.js"; import { characters, saveCharacterDebounced, this_chid, getCharacters, getRequestHeaders } from "/script.js";
import { getContext } from "/scripts/extensions.js";
import { extensionName } from "../../utils/settings.js"; import { extensionName } from "../../utils/settings.js";
async function saveCharacterById(chid) { async function saveCharacterById(chid) {
const char = characters[chid]; let currentChid = undefined;
if (!char) return;
try { 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', { const response = await fetch('/api/characters/edit', {
method: 'POST', method: 'POST',
headers: getRequestHeaders(), headers: getRequestHeaders({ omitContentType: true }),
body: JSON.stringify(char) body: formData
}); });
if (!response.ok) { 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 { } else {
console.log(`[Amily2 CharAPI] Successfully saved character ${chid}`); console.log(`[Amily2 CharAPI] Successfully saved character ${chid} (Background)`);
return { success: true };
} }
} catch (e) { } catch (e) {
console.error(`[Amily2 CharAPI] Error saving character ${chid}:`, 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]; return characters[chid];
} }
export function updateCharacter(chid, updates) { export async function updateCharacter(chid, updates) {
const char = getCharacter(chid); const char = getCharacter(chid);
if (!char) return false; if (!char) return false;
@@ -45,11 +112,14 @@ export function updateCharacter(chid, updates) {
}); });
if (changed) { if (changed) {
saveCharacterById(chid); const success = await saveCharacterById(chid);
if (success) {
console.log(`[Amily2 CharAPI] Updated character ${chid}:`, Object.keys(updates)); console.log(`[Amily2 CharAPI] Updated character ${chid}:`, Object.keys(updates));
return true; return true;
} }
return false; return false;
}
return false;
} }
export function getFirstMessages(chid) { export function getFirstMessages(chid) {
@@ -63,7 +133,7 @@ export function getFirstMessages(chid) {
return messages; return messages;
} }
export function addFirstMessage(chid, message) { export async function addFirstMessage(chid, message) {
const char = getCharacter(chid); const char = getCharacter(chid);
if (!char) return false; if (!char) return false;
@@ -73,12 +143,15 @@ export function addFirstMessage(chid, message) {
} }
char.data.alternate_greetings.push(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}`); console.log(`[Amily2 CharAPI] Added alternate greeting to character ${chid}`);
return true; return true;
}
return false;
} }
export function updateFirstMessage(chid, index, message) { export async function updateFirstMessage(chid, index, message) {
const char = getCharacter(chid); const char = getCharacter(chid);
if (!char) return false; if (!char) return false;
@@ -93,12 +166,15 @@ export function updateFirstMessage(chid, index, message) {
return false; return false;
} }
} }
saveCharacterById(chid); const success = await saveCharacterById(chid);
if (success) {
console.log(`[Amily2 CharAPI] Updated greeting ${index} for character ${chid}`); console.log(`[Amily2 CharAPI] Updated greeting ${index} for character ${chid}`);
return true; return true;
}
return false;
} }
export function removeFirstMessage(chid, index) { export async function removeFirstMessage(chid, index) {
const char = getCharacter(chid); const char = getCharacter(chid);
if (!char) return false; if (!char) return false;
@@ -114,9 +190,12 @@ export function removeFirstMessage(chid, index) {
return false; return false;
} }
} }
saveCharacterById(chid); const success = await saveCharacterById(chid);
if (success) {
console.log(`[Amily2 CharAPI] Removed greeting ${index} for character ${chid}`); console.log(`[Amily2 CharAPI] Removed greeting ${index} for character ${chid}`);
return true; return true;
}
return false;
} }
export async function createNewCharacter(name) { export async function createNewCharacter(name) {

View File

@@ -85,7 +85,7 @@ ${taskState.getPromptContext()}
shouldSummarize(history, tokenCount, maxTokens) { shouldSummarize(history, tokenCount, maxTokens) {
const tokenUsageRatio = tokenCount / maxTokens; const tokenUsageRatio = tokenCount / maxTokens;
if (tokenUsageRatio > 0.7) return true; if (tokenUsageRatio > 0.7) return true;
if (history.length > 15) return true; if (history.length > 35) return true;
return false; return false;
} }
} }

View File

@@ -159,7 +159,7 @@ export const tools = {
const { chid, ...updates } = args; const { chid, ...updates } = args;
const finalUpdates = args.updates || updates; const finalUpdates = args.updates || updates;
const success = charApi.updateCharacter(chid, finalUpdates); const success = await charApi.updateCharacter(chid, finalUpdates);
if (success) { if (success) {
const updatedFields = Object.keys(finalUpdates).join(', '); const updatedFields = Object.keys(finalUpdates).join(', ');
return JSON.stringify({ return JSON.stringify({
@@ -171,7 +171,7 @@ export const tools = {
return JSON.stringify({ return JSON.stringify({
status: "error", status: "error",
code: "UPDATE_FAILED", code: "UPDATE_FAILED",
message: "更新角色卡失败。" message: "更新角色卡失败。请确保您正在编辑当前选中的角色(暂不支持后台编辑其他角色)。"
}); });
} }
}, },
@@ -196,18 +196,43 @@ export const tools = {
} }
let content = char[field] || ''; 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(); if (changes[0].trim() === '') changes.shift();
for (const change of changes) { for (const change of changes) {
const parts = change.split('======='); const parts = change.split('=======');
if (parts.length !== 2) continue; if (parts.length < 2) continue;
const searchBlock = parts[0].trim(); const searchBlock = parts[0].trim();
const replaceBlock = parts[1].split('+++++++ REPLACE')[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({ return JSON.stringify({
status: "error", status: "error",
code: "SEARCH_NOT_FOUND", code: "SEARCH_NOT_FOUND",
@@ -216,10 +241,7 @@ export const tools = {
}); });
} }
content = content.replace(searchBlock, replaceBlock); const success = await charApi.updateCharacter(chid, { [field]: content });
}
const success = charApi.updateCharacter(chid, { [field]: content });
if (success) { if (success) {
return JSON.stringify({ return JSON.stringify({
status: "success", status: "success",
@@ -229,7 +251,81 @@ export const tools = {
return JSON.stringify({ return JSON.stringify({
status: "error", status: "error",
code: "UPDATE_FAILED", 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; let success = false;
switch (action) { switch (action) {
case 'add': case 'add':
success = charApi.addFirstMessage(chid, message); success = await charApi.addFirstMessage(chid, message);
break; break;
case 'update': case 'update':
success = charApi.updateFirstMessage(chid, index, message); success = await charApi.updateFirstMessage(chid, index, message);
break; break;
case 'remove': case 'remove':
success = charApi.removeFirstMessage(chid, index); success = await charApi.removeFirstMessage(chid, index);
break; break;
default: default:
return JSON.stringify({ return JSON.stringify({
@@ -494,6 +590,22 @@ export function getToolDefinitions() {
required: ["chid", "field", "diff"] 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", name: "manage_first_message",
description: "添加、更新或删除候补开场白。", description: "添加、更新或删除候补开场白。",

View File

@@ -12,8 +12,9 @@ let agentManager = null;
let previousCharData = {}; let previousCharData = {};
let previousWorldData = {}; let previousWorldData = {};
let isWaitingForApproval = false; let isWaitingForApproval = false;
let openedFiles = new Map(); // Key: string ID, Value: { title, content, type, metadata } let openedFiles = new Map();
let activeFileId = null; let activeFileId = null;
let promptLogContent = "=== Prompt Log ===\n\n";
export async function openAutoCharCardWindow() { export async function openAutoCharCardWindow() {
if ($('#acc-window').length > 0) { if ($('#acc-window').length > 0) {
@@ -57,31 +58,51 @@ export async function openAutoCharCardWindow() {
function populateDropdowns() { function populateDropdowns() {
const charSelect = $('#acc-target-char'); const charSelect = $('#acc-target-char');
const prevCharId = charSelect.val();
charSelect.empty().append('<option value="">-- 请选择 --</option>'); charSelect.empty().append('<option value="">-- 请选择 --</option>');
charSelect.append('<option value="new">新建角色卡</option>'); charSelect.append('<option value="new">新建角色卡</option>');
let isPrevCharStillPresent = false;
characters.forEach((char, index) => { characters.forEach((char, index) => {
if (char) { if (char) {
const option = $('<option>').val(index).text(char.name); charSelect.append($('<option>').val(index).text(char.name));
if (index === this_chid) option.prop('selected', true); if (String(index) === prevCharId) {
charSelect.append(option); isPrevCharStillPresent = true;
}
} }
}); });
if (isPrevCharStillPresent) {
charSelect.val(prevCharId);
} else if (this_chid !== undefined) {
charSelect.val(this_chid);
}
const worldSelect = $('#acc-target-world'); const worldSelect = $('#acc-target-world');
const prevWorldName = worldSelect.val();
worldSelect.empty().append('<option value="">-- 请选择 --</option>'); worldSelect.empty().append('<option value="">-- 请选择 --</option>');
worldSelect.append('<option value="new">新建世界书</option>'); worldSelect.append('<option value="new">新建世界书</option>');
let isPrevWorldStillPresent = false;
world_names.forEach(name => { world_names.forEach(name => {
worldSelect.append($('<option>').val(name).text(name)); worldSelect.append($('<option>').val(name).text(name));
if (name === prevWorldName) {
isPrevWorldStillPresent = true;
}
}); });
if (isPrevWorldStillPresent) {
worldSelect.val(prevWorldName);
}
} }
async function handleContextUpdate(type, value) { async function handleContextUpdate(type, value) {
console.log(`[Amily2 AutoCharCard] Context Update: ${type} -> ${value}`); console.log(`[Amily2 AutoCharCard] Context Update: ${type} -> ${value}`);
if (type === 'char') { if (type === 'char') {
await getCharacters(); // Force refresh character list await getCharacters();
} }
populateDropdowns(); 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() { function renderRulesList() {
const list = $('#acc-rules-list'); const list = $('#acc-rules-list');
list.empty(); list.empty();
@@ -157,10 +201,32 @@ function bindEvents() {
const [type, id, subId] = val.split('|'); 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') { if (type === 'char') {
const chid = id; const chid = id;
const field = subId; const field = subId;
const fileId = `char-${chid}-${field}`; const fileId = `char-${chid}-${field}`;
if (openedFiles.has(fileId)) { if (openedFiles.has(fileId)) {
activeFileId = fileId; activeFileId = fileId;
@@ -168,9 +234,13 @@ function bindEvents() {
return; return;
} }
let content = ''; let content = '';
try { try {
console.log(`[AutoCharCard] Reading char ${chid}, field ${field}`); console.log(`[AutoCharCard] Reading char ${chid}, field ${field}`);
const charData = await tools.read_character_card({ chid }); const charData = await tools.read_character_card({ chid });
@@ -244,7 +314,7 @@ function bindEvents() {
} }
} }
// Reset selector
$(this).val(''); $(this).val('');
}); });
@@ -292,6 +362,9 @@ function bindEvents() {
} }
}); });
const previewHeader = $('.acc-right-panel .acc-panel-header'); const previewHeader = $('.acc-right-panel .acc-panel-header');
if (previewHeader.find('#acc-refresh-preview').length === 0) { if (previewHeader.find('#acc-refresh-preview').length === 0) {
const refreshBtn = $('<button>') const refreshBtn = $('<button>')
@@ -423,12 +496,15 @@ function bindEvents() {
} }
}); });
$('.acc-nav-btn').on('click', function() { $('.acc-nav-btn').on('click', function() {
const targetClass = $(this).data('target'); const targetClass = $(this).data('target');
$('.acc-nav-btn').removeClass('active'); $('.acc-nav-btn').removeClass('active');
$(this).addClass('active'); $(this).addClass('active');
$('.acc-column').removeClass('mobile-active'); $('.acc-column').removeClass('mobile-active');
$(`.${targetClass}`).addClass('mobile-active'); $(`.${targetClass}`).addClass('mobile-active');
}); });
@@ -458,23 +534,27 @@ async function handleSendMessage() {
input.val(''); input.val('');
if (message) { if (message) {
addMessage('user', message); addMessage('user', message);
await agentManager.resumeWithApproval( await agentManager.resumeWithApproval(
false, false,
message, message,
(content, role) => addMessage(role, content), (content, role) => addMessage(role, content),
(toolName, args) => updatePreview(toolName, args), updatePreview,
showApprovalRequest, showApprovalRequest,
handleContextUpdate handleContextUpdate,
handlePromptLog
); );
} else { } else {
await agentManager.resumeWithApproval( await agentManager.resumeWithApproval(
true, true,
null, null,
(content, role) => addMessage(role, content), (content, role) => addMessage(role, content),
(toolName, args) => updatePreview(toolName, args), updatePreview,
showApprovalRequest, showApprovalRequest,
handleContextUpdate handleContextUpdate,
handlePromptLog
); );
} }
return; return;
@@ -511,11 +591,10 @@ async function handleSendMessage() {
(content, role) => { (content, role) => {
addMessage(role, content); addMessage(role, content);
}, },
(toolName, args) => { updatePreview,
updatePreview(toolName, args);
},
showApprovalRequest, showApprovalRequest,
handleContextUpdate handleContextUpdate,
handlePromptLog
); );
} catch (error) { } catch (error) {
console.error('Agent Error:', error); console.error('Agent Error:', error);
@@ -530,12 +609,17 @@ async function handleSendMessage() {
function showApprovalRequest(toolName, args) { function showApprovalRequest(toolName, args) {
isWaitingForApproval = true; isWaitingForApproval = true;
updatePreview(toolName, args, false);
const btn = $('#acc-send-btn'); const btn = $('#acc-send-btn');
btn.html('<i class="fas fa-check"></i>'); btn.html('<i class="fas fa-check"></i>');
btn.prop('title', '批准执行'); btn.prop('title', '批准执行');
btn.addClass('acc-btn-success'); btn.addClass('acc-btn-success');
$('#acc-user-input').attr('placeholder', '输入反馈以修改,或点击 √ 批准,点击 X 拒绝...'); $('#acc-user-input').attr('placeholder', '输入反馈以修改,或点击 √ 批准,点击 X 拒绝...');
if ($('#acc-reject-btn').length === 0) { if ($('#acc-reject-btn').length === 0) {
const rejectBtn = $('<button>') const rejectBtn = $('<button>')
.attr('id', 'acc-reject-btn') .attr('id', 'acc-reject-btn')
@@ -563,6 +647,7 @@ function showApprovalRequest(toolName, args) {
const input = $('#acc-user-input'); const input = $('#acc-user-input');
const message = input.val().trim(); const message = input.val().trim();
btn.html('<i class="fas fa-paper-plane"></i>'); btn.html('<i class="fas fa-paper-plane"></i>');
btn.prop('title', '发送'); btn.prop('title', '发送');
btn.removeClass('acc-btn-success'); btn.removeClass('acc-btn-success');
@@ -570,6 +655,9 @@ function showApprovalRequest(toolName, args) {
input.attr('placeholder', '描述您的需求...'); input.attr('placeholder', '描述您的需求...');
input.val(''); input.val('');
updatePreview(toolName, args, false, true);
const feedback = message || "用户拒绝了操作。"; const feedback = message || "用户拒绝了操作。";
addMessage('user', `[拒绝] ${feedback}`); addMessage('user', `[拒绝] ${feedback}`);
@@ -577,13 +665,15 @@ function showApprovalRequest(toolName, args) {
false, false,
feedback, feedback,
(content, role) => addMessage(role, content), (content, role) => addMessage(role, content),
(toolName, args) => updatePreview(toolName, args), updatePreview,
showApprovalRequest, showApprovalRequest,
handleContextUpdate handleContextUpdate,
handlePromptLog
); );
}); });
} }
const toolDisplay = ` const toolDisplay = `
<div class="acc-tool-request"> <div class="acc-tool-request">
<details> <details>
@@ -603,7 +693,10 @@ function addMessage(role, content) {
if (role === 'stream-assistant') { if (role === 'stream-assistant') {
let lastMsg = stream.children().last(); let lastMsg = stream.children().last();
if (!lastMsg.hasClass('assistant') || !lastMsg.hasClass('acc-streaming')) { if (!lastMsg.hasClass('assistant') || !lastMsg.hasClass('acc-streaming')) {
const msgDiv = $('<div>').addClass('acc-message assistant 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 avatarDiv = $('<div>').addClass('acc-avatar').html('<i class="fas fa-robot" style="color: #4caf50;"></i>');
const contentDiv = $('<div>').addClass('acc-message-content'); const contentDiv = $('<div>').addClass('acc-message-content');
@@ -614,6 +707,7 @@ function addMessage(role, content) {
const contentDiv = lastMsg.find('.acc-message-content'); const contentDiv = lastMsg.find('.acc-message-content');
const escapedContent = content const escapedContent = content
.replace(/&/g, "&amp;") .replace(/&/g, "&amp;")
.replace(/</g, "&lt;") .replace(/</g, "&lt;")
@@ -631,6 +725,7 @@ function addMessage(role, content) {
let displayContent = content; let displayContent = content;
if (role === 'executor' || role === 'assistant') { if (role === 'executor' || role === 'assistant') {
displayContent = displayContent displayContent = displayContent
.replace(/<thinking(?:\s+[^>]*)?>[\s\S]*?<\/thinking>/gi, '') .replace(/<thinking(?:\s+[^>]*)?>[\s\S]*?<\/thinking>/gi, '')
.replace(/<\/thinking>/gi, '') .replace(/<\/thinking>/gi, '')
@@ -640,8 +735,10 @@ function addMessage(role, content) {
const tools = [ const tools = [
'read_world_info', 'read_world_entry', 'write_world_info_entry', 'create_world_book', 'read_world_info', 'read_world_entry', 'write_world_info_entry', 'create_world_book',
'read_character_card', 'update_character_card', 'edit_character_text', 'read_character_card', 'update_character_card', 'edit_character_text',
'edit_world_info_entry',
'manage_first_message', 'use_tool' 'manage_first_message', 'use_tool'
]; ];
const regex = new RegExp(`<(${tools.join('|')})(?:\\s+[^>]*)?>[\\s\\S]*?<\\/\\1>`, 'gi'); const regex = new RegExp(`<(${tools.join('|')})(?:\\s+[^>]*)?>[\\s\\S]*?<\\/\\1>`, 'gi');
displayContent = displayContent.replace(regex, '').trim(); displayContent = displayContent.replace(regex, '').trim();
@@ -649,6 +746,7 @@ function addMessage(role, content) {
displayContent = "<i>(正在执行操作...)</i>"; displayContent = "<i>(正在执行操作...)</i>";
} }
if (role === 'assistant') { if (role === 'assistant') {
stream.find('.acc-streaming').remove(); stream.find('.acc-streaming').remove();
} }
@@ -656,6 +754,7 @@ function addMessage(role, content) {
let formattedContent; let formattedContent;
if (displayContent.trim().startsWith('<div class="acc-tool-request"')) { if (displayContent.trim().startsWith('<div class="acc-tool-request"')) {
formattedContent = displayContent; formattedContent = displayContent;
} else { } else {
@@ -699,28 +798,46 @@ function addMessage(role, content) {
function parseMarkdown(text) { function parseMarkdown(text) {
if (!text) return ''; if (!text) return '';
let html = text let html = text
.replace(/&/g, "&") .replace(/&/g, "&")
.replace(/</g, "<") .replace(/</g, "<")
.replace(/>/g, ">"); .replace(/>/g, ">");
html = html.replace(/```([\s\S]*?)```/g, '<pre><code>$1</code></pre>'); html = html.replace(/```([\s\S]*?)```/g, '<pre><code>$1</code></pre>');
html = html.replace(/`([^`]+)`/g, '<code>$1</code>'); html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
html = html.replace(/^#### (.*$)/gm, '<h4>$1</h4>'); html = html.replace(/^#### (.*$)/gm, '<h4>$1</h4>');
html = html.replace(/^### (.*$)/gm, '<h3>$1</h3>'); html = html.replace(/^### (.*$)/gm, '<h3>$1</h3>');
html = html.replace(/^## (.*$)/gm, '<h2>$1</h2>'); html = html.replace(/^## (.*$)/gm, '<h2>$1</h2>');
html = html.replace(/^# (.*$)/gm, '<h1>$1</h1>'); html = html.replace(/^# (.*$)/gm, '<h1>$1</h1>');
html = html.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>'); html = html.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
html = html.replace(/\*(.*?)\*/g, '<em>$1</em>'); html = html.replace(/\*(.*?)\*/g, '<em>$1</em>');
html = html.replace(/~~(.*?)~~/g, '<del>$1</del>');
html = html.replace(/^[\*\-]{3,}$/gm, '<hr>'); html = html.replace(/^[\*\-]{3,}$/gm, '<hr>');
html = html.replace(/^\s*[\-\*]\s+(.*$)/gm, '<li>$1</li>'); html = html.replace(/^\s*[\-\*]\s+(.*$)/gm, '<li>$1</li>');
html = html.replace(/\n/g, '<br>'); html = html.replace(/\n/g, '<br>');
return html; return html;
@@ -731,6 +848,9 @@ function renderEditor() {
const tabsContainer = $('.acc-preview-tabs'); const tabsContainer = $('.acc-preview-tabs');
container.empty(); container.empty();
tabsContainer.empty(); tabsContainer.empty();
@@ -739,6 +859,7 @@ function renderEditor() {
return; return;
} }
if (!activeFileId || !openedFiles.has(activeFileId)) { if (!activeFileId || !openedFiles.has(activeFileId)) {
activeFileId = openedFiles.keys().next().value; activeFileId = openedFiles.keys().next().value;
} }
@@ -757,6 +878,7 @@ function renderEditor() {
const icon = $('<i class="fas fa-file-alt"></i>'); const icon = $('<i class="fas fa-file-alt"></i>');
const titleSpan = $('<span>').addClass('acc-tab-title').text(file.title); const titleSpan = $('<span>').addClass('acc-tab-title').text(file.title);
const closeBtn = $('<span>') const closeBtn = $('<span>')
.html('&times;') .html('&times;')
.addClass('acc-tab-close') .addClass('acc-tab-close')
@@ -777,7 +899,7 @@ function renderEditor() {
.css('flex-direction', 'column') .css('flex-direction', 'column')
.css('height', '100%'); .css('height', '100%');
// Toolbar
const toolbar = $('<div>').addClass('acc-editor-toolbar').css({ const toolbar = $('<div>').addClass('acc-editor-toolbar').css({
'padding': '5px', 'padding': '5px',
'border-bottom': '1px solid #444', 'border-bottom': '1px solid #444',
@@ -792,9 +914,121 @@ function renderEditor() {
.on('click', () => saveFile(id)); .on('click', () => saveFile(id));
toolbar.append(saveBtn); toolbar.append(saveBtn);
contentDiv.append(toolbar); 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>') const textarea = $('<textarea>')
.addClass('acc-editor-textarea') .addClass('acc-editor-textarea')
.val(file.content) .val(file.content)
@@ -814,6 +1048,8 @@ function renderEditor() {
}); });
contentDiv.append(textarea); contentDiv.append(textarea);
}
container.append(contentDiv); container.append(contentDiv);
} }
}); });
@@ -829,42 +1065,61 @@ async function saveFile(id) {
return; 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 { try {
let result; let result;
if (meta.type === 'char') { if (meta.type === 'char') {
if (meta.field.startsWith('greeting_')) { if (meta.field.startsWith('greeting_')) {
const index = parseInt(meta.field.split('_')[1]); const index = parseInt(meta.field.split('_')[1]);
result = await tools.manage_first_message({ result = await tools.manage_first_message({
action: 'update', action: 'update',
chid: meta.chid, chid: meta.chid,
index: index + 1, index: index + 1,
message: file.content message: contentToSave
}); });
} else { } else {
result = await tools.update_character_card({ result = await tools.update_character_card({
chid: meta.chid, chid: meta.chid,
[meta.field]: file.content [meta.field]: contentToSave
}); });
} }
} else if (meta.type === 'wi') { } else if (meta.type === 'wi') {
if (meta.uid !== undefined) { if (meta.uid !== undefined) {
result = await tools.write_world_info_entry({ result = await tools.write_world_info_entry({
book_name: meta.bookName, book_name: meta.bookName,
entries: [{ uid: meta.uid, content: file.content }] entries: [{ uid: meta.uid, content: contentToSave }]
}); });
} else { } else {
try { try {
const entry = JSON.parse(file.content); const entry = JSON.parse(contentToSave);
result = await tools.write_world_info_entry({ result = await tools.write_world_info_entry({
book_name: meta.bookName, book_name: meta.bookName,
entries: [entry] entries: [entry]
}); });
} catch (e) { } catch (e) {
toastr.error('保存失败: 内容必须是有效的 JSON (针对新建条目) 或包含 UID'); toastr.error('保存失败: 内容必须是有效的 JSON (针对新建条目) 或包含 UID');
return; return;
} }
@@ -873,6 +1128,15 @@ async function saveFile(id) {
if (result && !result.startsWith('Error') && !result.includes('失败')) { if (result && !result.startsWith('Error') && !result.includes('失败')) {
toastr.success('保存成功'); toastr.success('保存成功');
if (file.type === 'diff-view') {
file.type = 'normal';
delete file.segments;
file.content = contentToSave;
renderEditor();
}
} else { } else {
toastr.error(result || '保存失败'); toastr.error(result || '保存失败');
} }
@@ -901,6 +1165,7 @@ async function loadContextToEditor() {
const char = response.data; const char = response.data;
previousCharData = char; previousCharData = char;
const charGroup = $('<optgroup label="角色卡字段">'); const charGroup = $('<optgroup label="角色卡字段">');
const fields = ['description', 'personality', 'first_mes', 'scenario', 'mes_example']; const fields = ['description', 'personality', 'first_mes', 'scenario', 'mes_example'];
fields.forEach(field => { fields.forEach(field => {
@@ -914,6 +1179,13 @@ async function loadContextToEditor() {
} }
selector.append(charGroup); 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) { if (openedFiles.size === 0 && char.description) {
const id = `char-${chid}-description`; const id = `char-${chid}-description`;
openedFiles.set(id, { openedFiles.set(id, {
@@ -932,6 +1204,7 @@ async function loadContextToEditor() {
if (bookName && bookName !== 'new') { if (bookName && bookName !== 'new') {
try { try {
const indexData = await tools.read_world_info({ book_name: bookName, return_full: false }); const indexData = await tools.read_world_info({ book_name: bookName, return_full: false });
const index = JSON.parse(indexData); const index = JSON.parse(indexData);
@@ -952,9 +1225,18 @@ async function loadContextToEditor() {
renderEditor(); renderEditor();
} }
async function updatePreview(toolName, args, isPartial = false) { async function updatePreview(toolName, args, isPartial = false, isExecuted = false) {
const chid = args.chid !== undefined ? args.chid : $('#acc-target-char').val(); let chid = args.chid;
const bookName = args.book_name !== undefined ? args.book_name : $('#acc-target-world').val(); 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') { if (toolName === 'update_character_card') {
const fields = ['description', 'personality', 'first_mes', 'scenario', 'mes_example']; 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') { } else if (toolName === 'edit_character_text') {
const field = args.field || 'Unknown Field'; 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 diff = args.diff || '';
const id = `char-${chid}-${field}`; const id = `char-${chid}-${field}`;
if (isPartial) { if (isPartial) {
const diffId = `diff-${chid}-${field}`; const diffId = `diff-${chid}-${field}`;
openedFiles.set(diffId, { openedFiles.set(diffId, {
title: `Diff: ${field}`, title: `Diff: ${field}`,
@@ -989,6 +1290,91 @@ async function updatePreview(toolName, args, isPartial = false) {
metadata: null metadata: null
}); });
activeFileId = diffId; 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 { } else {
let originalContent = ''; let originalContent = '';
@@ -996,52 +1382,121 @@ async function updatePreview(toolName, args, isPartial = false) {
originalContent = openedFiles.get(id).content; originalContent = openedFiles.get(id).content;
} else { } 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) { if (originalContent) {
// Apply diff logic const segments = parseDiff(originalContent, diff);
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) {
openedFiles.set(id, { openedFiles.set(id, {
title: field, title: field,
content: newContent, content: originalContent,
type: 'normal', segments: segments,
type: 'diff-view',
metadata: { type: 'char', chid, field } metadata: { type: 'char', chid, field }
}); });
activeFileId = id; activeFileId = id;
openedFiles.delete(`diff-${chid}-${field}`); openedFiles.delete(`diff-${chid}-${field}`);
} else { } else {
const diffId = `diff-${chid}-${field}`; const diffId = `diff-${chid}-${field}`;
openedFiles.set(diffId, { openedFiles.set(diffId, {
title: `Diff: ${field} (Failed to Apply)`, title: `Diff: ${field}`,
content: diff, content: diff,
type: 'diff', type: 'diff',
metadata: null metadata: null
}); });
activeFileId = diffId; 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, { 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, content: diff,
type: 'diff', type: 'diff',
metadata: null metadata: null
@@ -1054,6 +1509,7 @@ async function updatePreview(toolName, args, isPartial = false) {
let entries = args.entries; let entries = args.entries;
if (isPartial && typeof entries === 'string') { if (isPartial && typeof entries === 'string') {
const id = `wi-raw-partial`; const id = `wi-raw-partial`;
openedFiles.set(id, { openedFiles.set(id, {
title: 'WI Entry (Generating...)', title: 'WI Entry (Generating...)',
@@ -1081,9 +1537,65 @@ async function updatePreview(toolName, args, isPartial = false) {
}); });
activeFileId = id; activeFileId = id;
}); });
openedFiles.delete(`wi-raw-partial`); openedFiles.delete(`wi-raw-partial`);
} }
} }
renderEditor(); 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');
}

View File

@@ -1,7 +1,7 @@
{ {
"name": "Amily2号聊天优化助手", "name": "Amily2号聊天优化助手",
"display_name": "Amily2号助手", "display_name": "Amily2号助手",
"version": "1.7.9", "version": "1.8.0",
"author": "Wx-2025", "author": "Wx-2025",
"description": "一个拥有独立UI的智能引擎正文优化、自动总结、记忆表格、rag向量、隐藏楼层、剧情推进等多功能整合。", "description": "一个拥有独立UI的智能引擎正文优化、自动总结、记忆表格、rag向量、隐藏楼层、剧情推进等多功能整合。",
"minSillyTavernVersion": "1.10.0", "minSillyTavernVersion": "1.10.0",
@@ -50,5 +50,6 @@