7 Commits

Author SHA1 Message Date
859cfb2fca Update Memorisation-forms.html 2025-12-31 14:08:35 +08:00
ff61e176c1 Update index.js 2025-12-31 14:07:57 +08:00
06b36f2c93 Update style.css 2025-12-31 14:07:04 +08:00
7c2cc13579 Update index.html 2025-12-31 14:06:45 +08:00
bb25daeb2a Update amily2-modal.html 2025-12-31 14:06:19 +08:00
0992664ab3 Update settings.js 2025-12-31 14:05:16 +08:00
7a0eb06ad0 Add files via upload 2025-12-31 13:59:51 +08:00
13 changed files with 1845 additions and 526 deletions

View File

@@ -219,7 +219,7 @@
<fieldset class="settings-group">
<legend><i class="fas fa-puzzle-piece"></i> 附加功能</legend>
<div class="button-group" style="display: flex; justify-content: space-between; gap: 8px;">
<button id="amily2_open_plot_optimization" class="menu_button wide_button"><i class="fas fa-feather-alt"></i> 剧情优化</button>
<button id="amily2_open_plot_optimization" class="menu_button wide_button"><i class="fas fa-feather-alt"></i> 记忆管理</button>
<button id="amily2_open_text_optimization" class="menu_button wide_button"><i class="fas fa-cogs"></i> 正文优化</button>
<button id="amily2_open_world_editor" class="menu_button wide_button"><i class="fas fa-globe"></i> 世界编辑</button>
<button id="amily2_open_glossary" class="menu_button wide_button"><i class="fas fa-book"></i> 术语表单</button>

View File

@@ -45,15 +45,28 @@
<div class="acc-divider"></div>
<div class="acc-panel-header" style="cursor: pointer;" id="acc-rules-toggle">
<i class="fas fa-book"></i> 动态规则 <i class="fas fa-chevron-down" style="float: right;"></i>
</div>
<div id="acc-rules-content" style="display: none; padding-top: 10px;">
<div class="acc-form-group">
<label>添加新规则 (格式: 关键词|规则内容)</label>
<div style="display: flex; gap: 5px;">
<input type="text" id="acc-new-rule-input" class="acc-input" placeholder="例如: 魔法|描写魔法时必须包含咒语">
<button id="acc-add-rule-btn" class="acc-btn-secondary"><i class="fas fa-plus"></i></button>
</div>
</div>
<div id="acc-rules-list" class="acc-rules-list">
<!-- Rules will be added here -->
</div>
</div>
<div class="acc-divider"></div>
<div class="acc-panel-header" style="cursor: pointer;" id="acc-api-settings-toggle">
<i class="fas fa-network-wired"></i> API 配置 <i class="fas fa-chevron-down" style="float: right;"></i>
</div>
<div id="acc-api-settings-content" style="display: none; padding-top: 10px;">
<div class="acc-tabs">
<button class="acc-tab-btn active" data-target="executor">模型 A (执行)</button>
<button class="acc-tab-btn" data-target="reviewer">模型 B (规划)</button>
</div>
<div id="acc-api-executor" class="acc-api-group">
<div class="acc-form-group">
<label>API URL</label>
@@ -72,29 +85,12 @@
<button id="acc-executor-refresh-models" class="acc-btn-secondary" title="刷新模型列表"><i class="fas fa-sync-alt"></i></button>
</div>
</div>
<div class="acc-form-group">
<label>Max Tokens</label>
<input type="number" id="acc-executor-max-tokens" class="acc-input" placeholder="4000" value="4000">
</div>
<button id="acc-executor-test" class="acc-btn-secondary" style="width: 100%;">测试连接</button>
</div>
<div id="acc-api-reviewer" class="acc-api-group" style="display: none;">
<div class="acc-form-group">
<label>API URL</label>
<input type="text" id="acc-reviewer-url" class="acc-input" placeholder="http://localhost:3000/v1">
</div>
<div class="acc-form-group">
<label>API Key</label>
<input type="password" id="acc-reviewer-key" class="acc-input" placeholder="sk-...">
</div>
<div class="acc-form-group">
<label>Model</label>
<div style="display: flex; gap: 5px;">
<select id="acc-reviewer-model" class="acc-select" style="flex: 1;">
<option value="">请刷新获取模型</option>
</select>
<button id="acc-reviewer-refresh-models" class="acc-btn-secondary" title="刷新模型列表"><i class="fas fa-sync-alt"></i></button>
</div>
</div>
<button id="acc-reviewer-test" class="acc-btn-secondary" style="width: 100%;">测试连接</button>
</div>
<button id="acc-save-api" class="acc-btn-primary" style="width: 100%; margin-top: 10px;">保存配置</button>
</div>
@@ -120,18 +116,26 @@
<button id="acc-send-btn" class="acc-send-btn"><i class="fas fa-paper-plane"></i></button>
</div>
<div class="acc-input-controls">
<label class="acc-checkbox-label" title="开启后,每次工具调用前都需要您确认">
<input type="checkbox" id="acc-require-approval"> 需要确认
</label>
<button id="acc-stop-btn" class="acc-btn-danger" style="display: none;"><i class="fas fa-stop"></i> 停止生成</button>
</div>
</div>
</div>
<!-- 右栏:实时预览/Diff -->
<div class="acc-column acc-right-panel">
<div class="acc-panel-header">
<i class="fas fa-eye"></i> 内容预览
<div class="acc-preview-tabs">
<button class="acc-tab-btn active" data-tab="diff">变更对比</button>
<button class="acc-tab-btn" data-tab="preview">最终效果</button>
<div class="acc-panel-header" style="display: flex; justify-content: space-between; align-items: center;">
<div style="display: flex; align-items: center; gap: 5px;">
<i class="fas fa-eye"></i>
<select id="acc-file-selector" class="acc-select" style="height: 24px; padding: 0 5px; font-size: 12px; max-width: 150px;">
<option value="">-- 选择文件 --</option>
</select>
</div>
<div class="acc-preview-tabs" style="display: flex; gap: 2px; overflow-x: auto; max-width: 60%;">
<!-- Tabs will be injected here -->
</div>
</div>
<div class="acc-panel-content" id="acc-preview-container">

View File

@@ -115,6 +115,12 @@
display: flex;
justify-content: space-between;
align-items: center;
overflow: hidden; /* Prevent overflow */
}
.acc-panel-header i.fa-chevron-down,
.acc-panel-header i.fa-chevron-up {
margin-left: auto; /* Use flexbox alignment instead of float */
}
.acc-panel-content {
@@ -144,6 +150,53 @@
border-radius: 4px;
}
.acc-input {
width: 100%;
padding: 8px;
background-color: #3c3c3c;
border: 1px solid #555;
color: #fff;
border-radius: 4px;
box-sizing: border-box; /* Ensure padding doesn't affect width */
}
.acc-input:focus {
outline: none;
border-color: #0e639c;
}
.acc-btn-primary {
background-color: #0e639c;
color: #fff;
border: none;
padding: 8px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
transition: background-color 0.2s;
}
.acc-btn-primary:hover {
background-color: #1177bb;
}
.acc-btn-secondary {
background-color: #3c3c3c;
color: #ccc;
border: 1px solid #555;
padding: 8px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
transition: all 0.2s;
}
.acc-btn-secondary:hover {
background-color: #444;
color: #fff;
border-color: #666;
}
.acc-divider {
height: 1px;
background-color: #333;
@@ -171,77 +224,122 @@
background-color: rgba(255, 193, 7, 0.1);
}
/* 聊天区域 */
/* 聊天区域 (Cline Style) */
.acc-chat-stream {
flex: 1;
padding: 20px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 15px;
gap: 20px;
background-color: #1e1e1e;
}
.acc-message {
max-width: 90%;
max-width: 95%;
display: flex;
gap: 10px;
margin-bottom: 10px;
gap: 12px;
margin-bottom: 5px;
}
.acc-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
background-color: #444;
width: 24px;
height: 24px;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
font-size: 14px;
flex-shrink: 0;
margin-top: 4px;
}
.acc-message-content {
padding: 10px 15px;
border-radius: 8px;
font-size: 14px;
line-height: 1.5;
position: relative;
line-height: 1.6;
color: #cccccc;
word-wrap: break-word;
flex: 1;
}
/* User Message */
.acc-message.user {
align-self: flex-end;
flex-direction: row-reverse;
background-color: #2b2d31; /* VSCode sidebar/input bg */
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;
}
.acc-message.user .acc-avatar {
display: none; /* Hide avatar for user, just show text box */
}
.acc-message.user .acc-message-content {
background-color: #0e639c;
color: #fff;
border-bottom-right-radius: 0;
color: #ffffff;
font-weight: 500;
}
/* Assistant Message */
.acc-message.assistant {
align-self: flex-start;
align-self: stretch;
padding: 0 10px;
}
.acc-message.assistant .acc-message-content {
background-color: #333;
border: 1px solid #444;
border-bottom-left-radius: 0;
}
.acc-message.system {
align-self: center;
justify-content: center;
}
.acc-message.system .acc-message-content {
.acc-message.assistant .acc-avatar {
background-color: transparent;
color: #888;
font-style: italic;
color: #4caf50; /* Green robot */
}
/* Tool Request Styles */
.acc-tool-request {
background-color: #252526;
border: 1px solid #333;
border-radius: 6px;
margin: 10px 0;
overflow: hidden;
}
.acc-tool-header {
background-color: #2d2d2d;
padding: 8px 12px;
font-size: 12px;
padding: 5px;
border: none;
font-weight: 600;
color: #cccccc;
border-bottom: 1px solid #333;
display: flex;
align-items: center;
gap: 8px;
}
.acc-tool-header i {
color: #e5c07b; /* Gold icon */
}
.acc-tool-content {
padding: 12px;
margin: 0;
background-color: #1e1e1e;
color: #9cdcfe;
font-family: 'JetBrains Mono', monospace;
font-size: 12px;
overflow-x: auto;
white-space: pre-wrap;
}
/* System/Thought Message */
.acc-message.system, .acc-message.thought {
align-self: stretch;
padding: 0 10px;
opacity: 0.8;
}
.acc-message.thought .acc-message-content {
color: #8b949e;
font-style: italic;
border-left: 2px solid #333;
padding-left: 10px;
}
/* 输入区域 */
@@ -296,24 +394,80 @@
font-size: 12px;
}
/* 预览区域 */
/* 预览区域 (Editor Tabs) */
.acc-preview-tabs {
display: flex;
gap: 5px;
background-color: #252526;
border-bottom: 1px solid #2b2b2b;
overflow-x: auto;
}
.acc-tab-btn {
background: none;
background: #2d2d2d;
border: none;
color: #888;
border-right: 1px solid #252526;
color: #969696;
cursor: pointer;
padding: 2px 8px;
font-size: 12px;
padding: 8px 15px;
font-size: 13px;
display: flex;
align-items: center;
gap: 6px;
min-width: 100px;
max-width: 200px;
white-space: nowrap;
overflow: hidden;
}
.acc-tab-title {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
}
.acc-tab-btn:hover {
background-color: #2a2d2e;
color: #cccccc;
}
.acc-tab-btn.active {
background-color: #1e1e1e;
color: #ffffff;
border-top: 2px solid #0e639c; /* Active tab indicator */
}
.acc-tab-btn i {
font-size: 14px;
color: #e7c05e; /* JS/File icon color */
flex-shrink: 0;
}
.acc-tab-close {
flex-shrink: 0;
margin-left: 5px;
cursor: pointer;
opacity: 0.7;
}
.acc-tab-close:hover {
opacity: 1;
color: #fff;
border-bottom: 2px solid #0e639c;
}
.acc-editor-content {
flex: 1;
overflow-y: auto;
background-color: #1e1e1e;
padding: 0; /* Remove padding to let editor fill space */
}
/* Hide scrollbar for tabs but allow scrolling */
.acc-preview-tabs::-webkit-scrollbar {
height: 3px;
}
.acc-preview-tabs::-webkit-scrollbar-thumb {
background: #444;
}
.acc-empty-state {
@@ -365,15 +519,124 @@
border: 2px solid #1e1e1e;
}
/* Diff 高亮样式 */
/* Cursor-like Editor Styles */
.cursor-card {
background-color: #181818; /* Cursor dark background */
color: #cccccc;
font-family: 'JetBrains Mono', 'Consolas', 'Courier New', monospace;
font-size: 13px;
line-height: 1.6;
border: 1px solid #2b2b2b;
border-radius: 6px;
overflow: hidden;
margin-bottom: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
.cursor-header {
background-color: #202020;
padding: 8px 12px;
border-bottom: 1px solid #2b2b2b;
font-size: 12px;
color: #8b949e;
display: flex;
align-items: center;
justify-content: space-between;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
.cursor-header-left {
display: flex;
align-items: center;
gap: 8px;
}
.cursor-filename {
color: #c9d1d9;
font-weight: 500;
}
.cursor-actions {
display: flex;
gap: 6px;
}
.cursor-action-btn {
background: transparent;
border: 1px solid #30363d;
color: #c9d1d9;
padding: 2px 8px;
border-radius: 4px;
font-size: 11px;
cursor: pointer;
transition: all 0.2s;
}
.cursor-action-btn:hover {
background-color: #30363d;
border-color: #8b949e;
}
.cursor-content {
padding: 4px 0;
overflow-x: auto;
}
.cursor-line {
display: flex;
padding: 0;
min-height: 21px; /* Ensure empty lines have height */
}
.cursor-line-number {
color: #484f58;
min-width: 40px;
text-align: right;
padding-right: 16px;
user-select: none;
font-size: 12px;
line-height: 1.7; /* Align with content */
background-color: #181818;
border-right: 1px solid transparent; /* Optional separator */
}
.cursor-line-content {
white-space: pre-wrap;
flex: 1;
padding-left: 8px;
padding-right: 8px;
}
/* Syntax Highlighting (Cursor/Dark Modern) */
.syntax-key { color: #ff7b72; } /* Red/Pink */
.syntax-string { color: #a5d6ff; } /* Light Blue */
.syntax-number { color: #79c0ff; } /* Blue */
.syntax-comment { color: #8b949e; font-style: italic; } /* Grey */
.syntax-func { color: #d2a8ff; } /* Purple */
/* Diff Styles (Cursor Style) */
.diff-added {
background-color: rgba(76, 175, 80, 0.2);
border-left: 3px solid #4CAF50;
background-color: rgba(46, 160, 67, 0.15);
}
.diff-added .cursor-line-number {
background-color: rgba(46, 160, 67, 0.15); /* Match line bg */
color: rgba(255, 255, 255, 0.6);
border-left: 3px solid #2ea043; /* Green marker */
}
.diff-removed {
background-color: rgba(244, 67, 54, 0.2);
border-left: 3px solid #f44336;
text-decoration: line-through;
opacity: 0.7;
background-color: rgba(248, 81, 73, 0.15);
opacity: 0.8;
}
.diff-removed .cursor-line-number {
background-color: rgba(248, 81, 73, 0.15);
color: rgba(255, 255, 255, 0.6);
border-left: 3px solid #f85149; /* Red marker */
}
.diff-removed .cursor-line-content {
text-decoration: line-through;
text-decoration-color: rgba(255, 255, 255, 0.4);
}

View File

@@ -1,256 +1,348 @@
import { callAi, getApiConfig } from "./api.js";
import { tools, getToolDefinitions } from "./tools.js";
import { ContextManager } from "./context-manager.js";
import { TaskState } from "./task-state.js";
import { MemorySystem } from "./memory-system.js";
export class AgentManager {
constructor() {
this.history = [];
this.executorHistory = [];
this.reviewerHistory = [];
this.executorSystemPrompt = this.buildExecutorSystemPrompt();
this.reviewerSystemPrompt = this.buildReviewerSystemPrompt();
this.contextManager = new ContextManager();
this.taskState = new TaskState();
this.memorySystem = new MemorySystem();
this.currentChid = undefined;
this.currentBookName = undefined;
this.status = 'idle'; // idle, running, paused
this.approvalRequired = false;
this.pendingToolCall = null;
}
setContext(chid, bookName) {
async setContext(chid, bookName) {
this.currentChid = chid;
this.currentBookName = bookName;
this.executorSystemPrompt = this.buildExecutorSystemPrompt();
}
buildReviewerSystemPrompt() {
const toolDefs = getToolDefinitions();
let prompt = `你是一个经验丰富的角色卡设计师和辅导员Reviewer。你的搭档是一个执行力强且富有创造力的 AI 助手Executor
你的目标是根据用户的需求,设计出高质量的角色卡和世界书方案,并指导 Executor 一步步实现。
Executor 拥有以下工具(你不能直接使用,但需要知道它能做什么):
`;
toolDefs.forEach(tool => {
prompt += `- ${tool.name}: ${tool.description}\n`;
});
prompt += `
### 世界书高级设置指南 (World Info Settings)
- **constant (蓝灯)**: 如果为 true该条目将始终被激活并包含在上下文中忽略关键词触发。
- **position (插入位置)**: 决定条目内容在 Prompt 中的位置。
- \`before/after_character_definition\`: 角色定义前后。
- \`before/after_author_note\`: 作者注释前后。
- \`at_depth_as_system\`: 在指定深度作为系统消息插入(推荐)。
- **depth (插入深度)**: 仅当 position 为 \`at_depth_as_system\` 时有效。表示条目距离最新消息的距离(例如 0 为最新4 为倒数第 4 条消息后)。
- **scanDepth (扫描深度)**: 系统扫描关键词的消息范围。例如 2 表示只扫描最近 2 条消息。
- **exclude_recursion**: 如果为 true此条目的内容不会触发其他条目。
- **prevent_recursion**: 如果为 true其他条目的内容不会触发此条目。
你的工作流程:
1. 分析用户需求。
2. 制定详细的实施计划(大纲)。
3. 将计划拆解为 Executor 可以执行的**指导性指令**。
4. 审查 Executor 的执行结果,提出修改意见。
**关键原则:**
- **只给方案,不给成品**:你负责提供创意方向、关键设定点和风格指导,让 Executor 去进行具体的文本创作和扩写。不要直接把完整的角色描述或世界书内容写出来让 Executor 照抄。
- **示例**
- ❌ 错误:“请写入以下描述:她有一头金发,性格傲娇...”
- ✅ 正确:“请为角色撰写一段详细的外貌和性格描述。外貌上要突出她的金发和贵族气质,性格上要体现出‘傲娇’的特点,即外表冷漠但内心渴望被关怀。请发挥你的文采。”
交互规则:
- 当你需要 Executor 执行操作时,请在回复的最后一行使用标签:<instruction>你的指令</instruction>
- **重要**<instruction> 标签内必须是**自然语言指令**。**严禁**直接输出 JSON 代码块作为指令。
- **单步原则**:每次指令**只能包含一个**具体的任务(例如:只创建一个世界书条目,或只更新角色描述)。严禁一次性下达多个任务。
- **字数强制**:在指令中必须明确要求 Executor 进行深度扩写。
- 世界书条目:要求**不低于 300 字**。
- 角色开场白:要求**不低于 1500 字**。
- 当你认为任务已完成或需要用户反馈时,直接回复用户即可,不要包含 <instruction> 标签。
`;
return prompt;
}
buildExecutorSystemPrompt() {
const toolDefs = getToolDefinitions();
let contextInfo = "";
if (bookName && bookName !== 'new') {
try {
// Use return_full: true to get content for ContextManager (RAG)
const bookData = await tools.read_world_info({ book_name: bookName, return_full: true });
const entries = JSON.parse(bookData);
this.contextManager.setWorldInfo(entries);
} catch (e) {
console.error("Failed to load world info for context:", e);
}
}
}
setApprovalRequired(required) {
this.approvalRequired = required;
}
stop() {
this.status = 'idle';
}
async resumeWithApproval(approved, feedback, onStreamUpdate, onPreviewUpdate, onApprovalRequest) {
if (this.status !== 'paused' || !this.pendingToolCall) return;
if (approved) {
this.status = 'running';
await this.executePendingTool(onStreamUpdate, onPreviewUpdate);
this.pendingToolCall = null;
await this.runTaskLoop(onStreamUpdate, onPreviewUpdate, onApprovalRequest);
} else {
this.status = 'running';
this.pendingToolCall = null;
// Add feedback as user message to guide correction
this.history.push({
role: 'user',
content: `[Tool Execution Denied] User Feedback: ${feedback || "No reason provided."}`
});
await this.runTaskLoop(onStreamUpdate, onPreviewUpdate, onApprovalRequest);
}
}
async buildSystemPrompt() {
const toolDefs = getToolDefinitions();
// 1. Role & Objective
let prompt = `You are an expert Character Card Designer and World Builder.
You accomplish a given task iteratively, breaking it down into clear steps and working through them methodically.
IMPORTANT: You MUST speak in Chinese (Simplified) for all your responses and reasoning.
**Core Capabilities**:
- You can create and modify **Single Character Cards**.
- You can create and modify **Multi-Character World Cards** (Group Cards). Do not assume the user only wants a single character.
- You can manage **World Info (Lorebooks)**.
**Workflow**:
1. **Analyze & Explore First**: The World Book Index is provided in the context below. Review it to see what entries exist. If you need the *content* of a specific entry, use \`read_world_entry\` with its UID. Do not use \`read_world_info\` unless you need to refresh the index.
2. **Plan & Execute**: Based on the read data, formulate a plan and execute it step-by-step.
3. **Tool Usage**: Use one tool per turn. Always explain your reasoning in \`<thinking>\` tags before using a tool.
4. **Completion**: When finished, provide a concise summary.
# Memory & Task State
${this.taskState.getPromptContext()}
# Current Context
`;
if (this.currentChid === 'new') {
contextInfo += `**注意:用户希望创建一个新角色。**\n请首先使用 \`create_character\` 工具创建角色。创建成功后,你将获得新的角色 ID请使用该 ID 进行后续操作(如 \`update_character_card\`)。\n`;
prompt += `- **Status**: Creating a NEW character.\n`;
prompt += `- **Action Required**: Use \`create_character\` first to get a Character ID.\n`;
} else if (this.currentChid !== undefined) {
contextInfo += `当前操作的角色ID: ${this.currentChid}\n`;
prompt += `- **Character ID**: ${this.currentChid}\n`;
}
if (this.currentBookName) {
contextInfo += `当前操作的世界书: ${this.currentBookName}\n`;
prompt += `- **World Info Book**: ${this.currentBookName}\n`;
}
let prompt = `你是一个专业的角色卡构建助手Executor。你的目标是根据 Reviewer 的指导和用户的需求,在当前选定的“工作区”(角色卡和世界书)中进行**创作**和修改。
// Dynamic Context Injection (Rules & World Info)
const contextText = this.getLastUserMessage() || "";
const { rules, worldInfo } = this.contextManager.getRelevantContext(contextText);
if (rules.length > 0) {
prompt += `\n# Style Guides & Rules\n`;
rules.forEach(rule => {
prompt += `- ${rule.content}\n`;
});
}
${contextInfo}
if (worldInfo.length > 0) {
prompt += `\n# World Info Context\n`;
worldInfo.forEach(entry => {
prompt += `## Entry: ${entry.keys.join(', ')}\n${entry.content}\n\n`;
});
}
**你的职责:**
1. **理解指令**:仔细阅读 Reviewer 的指导性指令。
2. **深度扩写**这是你的核心任务。Reviewer 给出的只是大纲,你需要将其扩写成丰富、细腻的文学作品。
- **世界书条目**:必须丰富细节,字数**不低于 300 字**。
- **角色开场白**:必须包含环境描写、心理活动、动作细节,字数**不低于 1500 字**。
3. **执行操作**:使用工具将你创作的内容写入系统。
TOOL USE
你拥有以下工具可以使用。你可以使用这些工具来完成任务。每次回复只能使用一个工具。
# Tools
`;
// Environment Details Injection
let envDetails = `\n<environment_details>\n`;
envDetails += `# Current Time\n${new Date().toLocaleString()}\n\n`;
if (this.currentChid !== undefined && this.currentChid !== 'new') {
try {
const charData = await tools.read_character_card({ chid: this.currentChid });
const char = JSON.parse(charData);
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`;
} catch (e) {
envDetails += `# Current Character\nError reading character: ${e.message}\n\n`;
}
}
if (this.currentBookName && this.currentBookName !== 'new') {
try {
// Get the index for the system prompt
const bookData = await tools.read_world_info({ book_name: this.currentBookName, return_full: false });
const result = JSON.parse(bookData);
envDetails += `# Current World Book Index\n`;
envDetails += `Name: ${this.currentBookName}\n`;
envDetails += `Total Entries: ${result.total_entries}\n`;
envDetails += `Entries List (UID | Name | Keys):\n`;
if (result.entries && result.entries.length > 0) {
result.entries.forEach(entry => {
const keys = Array.isArray(entry.keys) ? entry.keys.join(', ') : entry.keys;
const name = entry.comment || keys || "Unnamed";
envDetails += `- [${entry.uid}] ${name} (Keys: ${keys})\n`;
});
} else {
envDetails += `(No entries found)\n`;
}
envDetails += `\n`;
} catch (e) {
envDetails += `# Current World Book\nError reading world book: ${e.message}\n\n`;
}
}
envDetails += `</environment_details>\n`;
prompt += envDetails;
// 2. Tools
prompt += `\n# Tools\n\n`;
toolDefs.forEach(tool => {
prompt += `## ${tool.name}\n`;
prompt += `Description: ${tool.description}\n`;
prompt += `Parameters:\n${JSON.stringify(tool.parameters, null, 2)}\n\n`;
});
// 3. Tool Use Formatting
prompt += `
### 世界书高级设置指南 (World Info Settings)
- **constant (蓝灯)**: 如果为 true该条目将始终被激活并包含在上下文中忽略关键词触发。
- **position (插入位置)**: 决定条目内容在 Prompt 中的位置。
- \`before/after_character_definition\`: 角色定义前后。
- \`before/after_author_note\`: 作者注释前后。
- \`at_depth_as_system\`: 在指定深度作为系统消息插入(推荐)。
- **depth (插入深度)**: 仅当 position 为 \`at_depth_as_system\` 时有效。表示条目距离最新消息的距离(例如 0 为最新4 为倒数第 4 条消息后)。
- **scanDepth (扫描深度)**: 系统扫描关键词的消息范围。例如 2 表示只扫描最近 2 条消息。
- **exclude_recursion**: 如果为 true此条目的内容不会触发其他条目。
- **prevent_recursion**: 如果为 true其他条目的内容不会触发此条目。
# Tool Use Formatting
工具调用必须使用以下 XML 格式。工具名称包含在开始和结束标签中,每个参数也包含在自己的标签中:
Tool use is formatted using XML-style tags. The tool name is enclosed in opening and closing tags, and each parameter is similarly enclosed within its own set of tags.
<工具名称>
<参数1>值1</参数1>
<参数2>值2</参数2>
<tool_name>
<parameter1_name>value1</parameter1_name>
<parameter2_name>value2</parameter2_name>
...
</工具名称>
</tool_name>
**注意**:对于复杂参数(如数组或对象),请直接在标签内写入 **JSON 字符串**。
**Important**: For complex parameters (arrays or objects), write the **JSON string** directly inside the tag.
例如:
Example:
<write_world_info_entry>
<book_name>MyWorld</book_name>
<entries>[{"key": "Entry1", "content": "..."}]</entries>
</write_world_info_entry>
# Tool Use Guidelines
1. **必须思考 (Mandatory Thinking)**: 在调用任何工具之前,你**必须**先输出一段思考过程,解释你为什么要这样做,以及你打算如何创作内容。请使用 \`<thinking>\` 标签包裹你的思考。**严禁**直接输出工具调用而不进行思考。
2. **单步执行**: 每次回复只能使用**一个**工具。必须等待工具执行结果(成功或失败)后,才能决定并执行下一步操作。
3. **等待确认**: 永远不要假设工具执行成功。必须根据实际返回的结果来判断。
4. **参数完整性**: 确保提供所有必需的参数。
# Capabilities
- 你可以读取和修改当前绑定的世界书World Info
- 你可以读取和修改当前角色的详细信息Name, Description, Personality, Scenario, First Message, etc.)。
- 你可以管理角色的开场白(添加、修改、删除)。
# Rules
1. **工作区**: 你始终在当前选定的角色卡和世界书上下文中操作。
2. **路径**: 如果涉及文件路径(虽然主要通过 API 操作),请认为是相对于工作区的虚拟路径。
3. **完成任务**: 当你认为任务已经完成时,请向用户汇报结果。不要在汇报结果后继续提问。
现在,请开始你的工作。
- **Think First**: Before using any tool, you MUST output a \`<thinking>\` block explaining your plan and reasoning.
- **One Tool Per Turn**: You can only use ONE tool per message. Wait for the result before proceeding.
- **Verify Results**: Always check the [Tool Result] to ensure success. If a tool fails, analyze the error and try again.
- **Detailed Writing**: When writing content (Description, First Message, World Info), be creative and detailed.
- World Info entries: > 300 words.
- First Message: > 1500 words, including environment, psychology, and action.
- **Do not ask for more information than necessary**: Use the tools provided to accomplish the user's request efficiently and effectively.
- **Completion**: When the task is done, provide a final summary to the user.
`;
return prompt;
}
async handleUserMessage(message, onStreamUpdate, onPreviewUpdate) {
this.history.push({ role: 'user', content: message });
this.reviewerHistory.push({ role: 'user', content: message });
await this.runDualAgentLoop(onStreamUpdate, onPreviewUpdate);
}
async runDualAgentLoop(onStreamUpdate, onPreviewUpdate) {
let maxLoops = 3;
let currentLoop = 0;
while (currentLoop < maxLoops) {
currentLoop++;
onStreamUpdate("Reviewer (模型B) 正在思考...", 'system');
const reviewerConfig = getApiConfig('reviewer');
const reviewerMessages = this.contextManager.buildMessages(
this.reviewerSystemPrompt,
this.reviewerHistory,
reviewerConfig.maxTokens
);
let reviewerResponse;
try {
reviewerResponse = await callAi('reviewer', reviewerMessages);
} catch (error) {
onStreamUpdate(`[Reviewer 错误] ${error.message}`, 'system');
return;
}
const instructionMatch = reviewerResponse.match(/<instruction>([\s\S]*?)<\/instruction>/);
const instruction = instructionMatch ? instructionMatch[1].trim() : null;
const displayContent = reviewerResponse.replace(/<instruction>[\s\S]*?<\/instruction>/, '').trim();
if (displayContent) {
onStreamUpdate(displayContent, 'assistant');
this.history.push({ role: 'assistant', content: displayContent });
this.reviewerHistory.push({ role: 'assistant', content: displayContent });
}
if (!instruction) {
break;
}
onStreamUpdate(`Reviewer 指令: ${instruction}`, 'system');
this.executorHistory.push({ role: 'user', content: instruction });
await this.runExecutorLoop(onStreamUpdate, onPreviewUpdate);
const lastExecutorResponse = this.executorHistory[this.executorHistory.length - 1];
if (lastExecutorResponse && lastExecutorResponse.role === 'assistant') {
this.reviewerHistory.push({
role: 'user',
content: `[Executor 执行结果]\n${lastExecutorResponse.content}`
});
getLastUserMessage() {
for (let i = this.history.length - 1; i >= 0; i--) {
if (this.history[i].role === 'user') {
return this.history[i].content;
}
}
return null;
}
async runExecutorLoop(onStreamUpdate, onPreviewUpdate) {
let maxTurns = 5;
async handleUserMessage(message, onStreamUpdate, onPreviewUpdate, onApprovalRequest) {
// Initialize task state if it's the first message or explicitly requested
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);
}
async runTaskLoop(onStreamUpdate, onPreviewUpdate, onApprovalRequest) {
let maxTurns = 20; // Safety limit
let currentTurn = 0;
while (currentTurn < maxTurns) {
while (this.status === 'running' && currentTurn < maxTurns) {
currentTurn++;
const executorConfig = getApiConfig('executor');
// 0. Check Memory/Context
const config = getApiConfig('executor');
const currentTokens = this.contextManager.estimateTokens(JSON.stringify(this.history));
if (this.memorySystem.shouldSummarize(this.history, currentTokens, config.maxTokens)) {
onStreamUpdate("Context limit approaching. Summarizing memory...", 'system');
const summary = await this.memorySystem.summarize(this.history, this.taskState);
if (summary) {
this.taskState.updateSummary(summary);
// Optional: Compress history here. For now, we just rely on the summary being injected.
// A simple compression strategy: Keep the last 5 messages, and replace the rest with a system note.
if (this.history.length > 5) {
const lastMessages = this.history.slice(-5);
this.history = [
{ role: 'system', content: `[History Compressed] Previous conversation has been summarized in the "Memory & Task State" section. Resuming from recent messages.` },
...lastMessages
];
}
}
}
// 1. Build System Prompt (Dynamic)
const systemPrompt = await this.buildSystemPrompt();
// 2. Build Messages
const messages = this.contextManager.buildMessages(
this.executorSystemPrompt,
this.executorHistory,
executorConfig.maxTokens
systemPrompt,
this.history,
config.maxTokens
);
// 3. Call AI
let responseContent;
let fullStreamedContent = "";
try {
responseContent = await callAi('executor', messages);
onStreamUpdate("Thinking...", 'system');
responseContent = await callAi('executor', messages, {}, (chunk) => {
onStreamUpdate(chunk, 'stream-assistant');
fullStreamedContent += chunk;
// Try to parse partial tool call for real-time preview
if (onPreviewUpdate) {
const partialTool = this.parsePartialToolCall(fullStreamedContent);
if (partialTool) {
onPreviewUpdate(partialTool.name, partialTool.arguments, true); // true = isPartial
}
}
});
} catch (error) {
onStreamUpdate(`[Executor 错误] ${error.message}`, 'system');
onStreamUpdate(`[Error] ${error.message}`, 'system');
this.status = 'idle'; // Stop on API error
return;
}
onStreamUpdate(responseContent, 'executor');
this.executorHistory.push({ role: 'assistant', content: responseContent });
if (this.status !== 'running') return; // Check if stopped during await
// 4. Process Response
// Check for truncation (Auto-Continue)
const lastChar = responseContent.trim().slice(-1);
const isTruncated = !['.', '!', '?', '"', "'", '}', ']', '>', '*'].includes(lastChar) && responseContent.length > 100;
if (isTruncated && currentTurn < maxTurns) {
console.log("检测到回复截断,正在自动继续...");
try {
// Append a continue message
const continueMsg = { role: 'user', content: "Continue" };
const continueMessages = [...messages, { role: 'assistant', content: responseContent }, continueMsg];
const continuation = await callAi('executor', continueMessages, {}, (chunk) => {
onStreamUpdate(chunk, 'stream-assistant');
});
responseContent += continuation;
console.log("自动合并接续内容完成");
} catch (e) {
console.warn("Auto-continue failed:", e);
}
}
this.history.push({ role: 'assistant', content: responseContent });
const thinkingMatch = responseContent.match(/<thinking>([\s\S]*?)<\/thinking>/);
if (thinkingMatch) {
onStreamUpdate(thinkingMatch[1].trim(), 'thought');
}
// Clean up content for UI display
let cleanContent = responseContent
.replace(/<thinking(?:\s+[^>]*)?>[\s\S]*?<\/thinking>/gi, '')
.replace(/<\/thinking>/gi, ''); // Remove residual tags
const toolNames = Object.keys(tools);
const toolRegex = new RegExp(`<(${toolNames.join('|')})(?:\\s+[^>]*)?>[\\s\\S]*?<\\/\\1>`, 'gi');
cleanContent = cleanContent.replace(toolRegex, '').trim();
// Update the UI with the final clean content (replacing the raw stream)
if (cleanContent) {
onStreamUpdate(cleanContent, 'assistant');
}
// 5. Parse Tool Call
const toolCall = this.parseToolCall(responseContent);
if (toolCall) {
// Check for duplicate tool calls to prevent loops
if (this.isDuplicateToolCall(toolCall)) {
const warningMsg = `[System Warning] You have just executed this exact tool call (${toolCall.name}). Do not repeat the same action immediately. If you need to check the result again, look at the conversation history. If the previous result was unsatisfactory, try a different approach.`;
this.history.push({ role: 'user', content: warningMsg });
continue;
}
// Inject Context if missing
if (toolCall.name === 'update_character_card' || toolCall.name === 'read_character_card' || toolCall.name === 'edit_character_text' || toolCall.name === 'manage_first_message') {
if (toolCall.arguments.chid === undefined && this.currentChid !== undefined) {
toolCall.arguments.chid = parseInt(this.currentChid);
@@ -262,41 +354,79 @@ TOOL USE
}
}
onStreamUpdate(`[Executor] 执行工具: ${toolCall.name}`, 'system');
let result;
try {
if (tools[toolCall.name]) {
result = await tools[toolCall.name](toolCall.arguments);
if (toolCall.name === 'create_character' && result.includes('ID:')) {
const match = result.match(/ID:\s*(\d+)/);
if (match) {
this.currentChid = parseInt(match[1]);
this.executorSystemPrompt = this.buildExecutorSystemPrompt();
}
}
} else {
result = `Error: Tool '${toolCall.name}' not found.`;
this.pendingToolCall = toolCall;
if (this.approvalRequired) {
this.status = 'paused';
if (onApprovalRequest) {
onApprovalRequest(toolCall.name, toolCall.arguments);
}
} catch (error) {
result = `Error executing tool '${toolCall.name}': ${error.message}`;
}
const toolResultMsg = `[Tool Result for ${toolCall.name}]\n${result}`;
this.executorHistory.push({ role: 'user', content: toolResultMsg });
onStreamUpdate(`[Executor] 工具结果: ${result.substring(0, 100)}...`, 'system');
if (onPreviewUpdate && !result.startsWith('Error')) {
onPreviewUpdate(toolCall.name, toolCall.arguments);
return; // Exit loop, wait for resumeWithApproval
} else {
await this.executePendingTool(onStreamUpdate, onPreviewUpdate);
this.pendingToolCall = null;
}
} else {
break;
this.status = 'idle';
}
}
}
async executePendingTool(onStreamUpdate, onPreviewUpdate) {
const toolCall = this.pendingToolCall;
if (!toolCall) return;
onStreamUpdate(`Executing: ${toolCall.name}`, 'system');
let result;
try {
if (tools[toolCall.name]) {
result = await tools[toolCall.name](toolCall.arguments);
if (toolCall.name === 'create_character' && result.includes('ID:')) {
const match = result.match(/ID:\s*(\d+)/);
if (match) {
this.currentChid = parseInt(match[1]);
}
}
} else {
result = `Error: Tool '${toolCall.name}' not found.`;
}
} catch (error) {
result = `Error executing tool '${toolCall.name}': ${error.message}`;
}
const toolResultMsg = `[Tool Result for ${toolCall.name}]\n${result}`;
this.history.push({ role: 'user', content: toolResultMsg });
if (onPreviewUpdate && !result.startsWith('Error')) {
onPreviewUpdate(toolCall.name, toolCall.arguments);
}
}
isDuplicateToolCall(toolCall) {
if (this.history.length < 3) return false;
// History structure:
// ...
// Assistant: <tool>A</tool> (index -3)
// User: [Tool Result A] (index -2)
// Assistant: <tool>A</tool> (index -1, current)
const prevAssistantMsg = this.history[this.history.length - 3];
const prevUserMsg = this.history[this.history.length - 2];
if (prevAssistantMsg.role === 'assistant' && prevUserMsg.role === 'user' && prevUserMsg.content.startsWith('[Tool Result')) {
const prevToolCall = this.parseToolCall(prevAssistantMsg.content);
if (prevToolCall &&
prevToolCall.name === toolCall.name &&
JSON.stringify(prevToolCall.arguments) === JSON.stringify(toolCall.arguments)) {
return true;
}
}
return false;
}
parseToolCall(content) {
const toolNames = Object.keys(tools);
for (const name of toolNames) {
@@ -327,10 +457,41 @@ TOOL USE
}
return null;
}
parsePartialToolCall(content) {
const toolNames = Object.keys(tools);
for (const name of toolNames) {
// Look for the opening tag
const openTagRegex = new RegExp(`<${name}>`);
const openMatch = content.match(openTagRegex);
if (openMatch) {
// We found a tool start. Now try to extract params, even if incomplete.
const startIndex = openMatch.index + openMatch[0].length;
const toolContent = content.slice(startIndex);
const args = {};
// Match complete tags or tags that are still open (at the end)
// <param>value...</param> OR <param>value...
const paramRegex = /<(\w+)>([\s\S]*?)(?:<\/\1>|$)/g;
let paramMatch;
while ((paramMatch = paramRegex.exec(toolContent)) !== null) {
const paramName = paramMatch[1];
let paramValue = paramMatch[2];
// Don't try to JSON parse partial content here, leave it as string
// The UI handler will deal with partial JSON
args[paramName] = paramValue;
}
return { name, arguments: args };
}
}
return null;
}
clearHistory() {
this.history = [];
this.executorHistory = [];
this.reviewerHistory = [];
}
}

View File

@@ -24,7 +24,7 @@ export function setApiConfig(role, config) {
extension_settings[extensionName][configKey] = { ...getApiConfig(role), ...config };
}
export async function callAi(role, messages, options = {}) {
export async function callAi(role, messages, options = {}, onChunk = null) {
const config = { ...getApiConfig(role), ...options };
const roleName = role === 'executor' ? '执行者(模型A)' : '规划者(模型B)';
@@ -32,7 +32,7 @@ export async function callAi(role, messages, options = {}) {
throw new Error(`[自动构建器] ${roleName} API 配置不完整,请检查 URL、Key 和模型设置。`);
}
console.log(`[自动构建器] 正在调用 AI (${roleName})...`, { model: config.model, messagesCount: messages.length });
console.log(`[自动构建器] 正在调用 AI (${roleName})...`, { model: config.model, messagesCount: messages.length, stream: !!onChunk });
const body = {
chat_completion_source: 'openai',
@@ -40,8 +40,8 @@ export async function callAi(role, messages, options = {}) {
model: config.model,
reverse_proxy: config.apiUrl,
proxy_password: config.apiKey,
stream: false,
max_tokens: config.maxTokens,
stream: !!onChunk,
max_tokens: config.maxTokens > 0 ? config.maxTokens : undefined,
temperature: config.temperature,
top_p: 1,
custom_prompt_post_processing: 'strict',
@@ -62,18 +62,54 @@ export async function callAi(role, messages, options = {}) {
throw new Error(`API 请求失败: ${response.status} - ${errorText}`);
}
const responseData = await response.json();
if (!responseData || !responseData.choices || responseData.choices.length === 0) {
if (responseData.error) {
throw new Error(`API 返回错误: ${responseData.error.message || JSON.stringify(responseData.error)}`);
}
throw new Error('API 返回了空响应。');
}
if (onChunk) {
const reader = response.body.getReader();
const decoder = new TextDecoder("utf-8");
let fullContent = "";
let buffer = "";
const content = responseData.choices[0].message?.content;
console.log(`[自动构建器] AI (${roleName}) 响应接收成功。长度: ${content?.length}`);
return content;
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop(); // Keep incomplete line in buffer
for (const line of lines) {
const trimmedLine = line.trim();
if (trimmedLine.startsWith('data: ')) {
const dataStr = trimmedLine.slice(6).trim();
if (dataStr === '[DONE]') continue;
try {
const data = JSON.parse(dataStr);
const delta = data.choices[0].delta?.content || "";
if (delta) {
fullContent += delta;
onChunk(delta);
}
} catch (e) {
// Ignore parse errors for partial chunks
}
}
}
}
console.log(`[自动构建器] AI (${roleName}) 流式响应结束。长度: ${fullContent.length}`);
return fullContent;
} else {
const responseData = await response.json();
if (!responseData || !responseData.choices || responseData.choices.length === 0) {
if (responseData.error) {
throw new Error(`API 返回错误: ${responseData.error.message || JSON.stringify(responseData.error)}`);
}
throw new Error('API 返回了空响应。');
}
const content = responseData.choices[0].message?.content;
console.log(`[自动构建器] AI (${roleName}) 响应接收成功。长度: ${content?.length}`);
return content;
}
} catch (error) {
console.error(`[自动构建器] AI (${roleName}) 调用失败:`, error);
@@ -86,10 +122,10 @@ export async function testConnection(role) {
const response = await callAi(role, [
{ role: 'user', content: 'Hi' }
], { maxTokens: 10 });
return !!response;
return { success: !!response };
} catch (error) {
console.error(`[自动构建器] ${role} 连接测试失败:`, error);
return false;
return { success: false, error: error.message };
}
}

View File

@@ -121,6 +121,11 @@ export async function createNewCharacter(name) {
formData.append('depth_prompt_depth', '4');
formData.append('depth_prompt_role', 'system');
const base64Png = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=";
const res = await fetch(`data:image/png;base64,${base64Png}`);
const blob = await res.blob();
formData.append('avatar', blob, 'default.png');
const response = await fetch('/api/characters/create', {
method: 'POST',
headers: getRequestHeaders({ omitContentType: true }),

View File

@@ -1,7 +1,55 @@
export class ContextManager {
constructor() {
this.keepToolOutputTurns = 3;
this.tokenLimit = 12000;
this.keepToolOutputTurns = 5;
this.tokenLimit = 100000;
this.rules = [];
this.worldInfo = [];
}
addRule(rule) {
this.rules.push({
id: rule.id || Date.now().toString(),
keyword: rule.keyword || null,
content: rule.content,
enabled: rule.enabled !== undefined ? rule.enabled : true
});
}
setWorldInfo(entries) {
this.worldInfo = entries.map(entry => {
let keys = [];
if (Array.isArray(entry.key)) {
keys = entry.key;
} else if (typeof entry.key === 'string') {
keys = entry.key.split(',').map(k => k.trim()).filter(k => k);
}
return {
id: entry.uid,
keys: keys,
content: entry.content,
enabled: entry.enabled !== false
};
});
}
getRelevantContext(contextText) {
const relevantRules = this.rules.filter(rule => {
if (!rule.enabled) return false;
if (!rule.keyword) return true;
return contextText.includes(rule.keyword);
});
const relevantWorldInfo = this.worldInfo.filter(entry => {
if (!entry.enabled) return false;
if (!entry.keys || entry.keys.length === 0) return false;
return entry.keys.some(key => contextText.includes(key));
});
return {
rules: relevantRules,
worldInfo: relevantWorldInfo
};
}
estimateTokens(text) {

View File

@@ -0,0 +1,54 @@
import { callAi, getApiConfig } from "./api.js";
export class MemorySystem {
constructor() {
this.summarizePrompt = `
The current conversation context is growing large. Your task is to create a comprehensive, structured summary of the character/world generation process so far.
This summary will be used as the "Memory" for the next steps, so it must be detailed enough to prevent information loss.
Please summarize the following:
1. **Core Identity**: Name, Age, Gender, Role, etc.
2. **Personality & Traits**: Key personality keywords, behavioral quirks, speech patterns.
3. **Appearance**: Physical description, clothing, accessories.
4. **Background & Lore**: Backstory, world setting, important relationships.
5. **Current Progress**: What has been completed, what is currently being worked on, and what is left to do.
6. **User Preferences**: Any specific constraints or requests made by the user (e.g., "Make her tsundere", "Don't use modern technology").
Format your response as a structured Markdown block.
`;
}
async summarize(history, taskState) {
const config = getApiConfig('executor');
const contextMsg = `
[System Note]: The following is the current Task State. Use this to inform your summary.
${taskState.getPromptContext()}
`;
const messages = [
{ role: 'system', content: this.summarizePrompt },
...history.slice(-10),
{ role: 'user', content: `Please summarize the session based on the history above. ${contextMsg}` }
];
try {
const response = await callAi('executor', messages, {
max_tokens: 2000,
temperature: 0.5
});
return response;
} catch (error) {
console.error("Failed to generate summary:", error);
return null;
}
}
shouldSummarize(history, tokenCount, maxTokens) {
const tokenUsageRatio = tokenCount / maxTokens;
if (tokenUsageRatio > 0.7) return true;
if (history.length > 15) return true;
return false;
}
}

View File

@@ -0,0 +1,86 @@
export class TaskState {
constructor() {
this.reset();
}
reset() {
this.originalRequest = "";
this.currentGoal = "";
this.completedSteps = []; // Array of strings
this.pendingSteps = []; // Array of strings
this.summary = ""; // The structured summary of the character/world so far
this.generatedData = {}; // Key-value pairs of generated attributes (e.g., name, personality)
this.lastSummaryTimestamp = 0;
}
init(request) {
this.reset();
this.originalRequest = request;
this.currentGoal = "Analyze request and plan steps";
this.lastSummaryTimestamp = Date.now();
}
updateSummary(newSummary) {
this.summary = newSummary;
this.lastSummaryTimestamp = Date.now();
}
addCompletedStep(step) {
this.completedSteps.push(step);
}
setPendingSteps(steps) {
this.pendingSteps = steps;
}
setCurrentGoal(goal) {
this.currentGoal = goal;
}
updateGeneratedData(key, value) {
this.generatedData[key] = value;
}
getPromptContext() {
let context = `\n# Task State\n`;
context += `- **Original Request**: ${this.originalRequest}\n`;
context += `- **Current Goal**: ${this.currentGoal}\n`;
if (this.completedSteps.length > 0) {
context += `- **Completed Steps**:\n${this.completedSteps.map(s => ` - ${s}`).join('\n')}\n`;
}
if (this.pendingSteps.length > 0) {
context += `- **Pending Steps**:\n${this.pendingSteps.map(s => ` - ${s}`).join('\n')}\n`;
}
if (this.summary) {
context += `\n# Memory & Context Summary\n${this.summary}\n`;
}
return context;
}
toJSON() {
return {
originalRequest: this.originalRequest,
currentGoal: this.currentGoal,
completedSteps: this.completedSteps,
pendingSteps: this.pendingSteps,
summary: this.summary,
generatedData: this.generatedData,
lastSummaryTimestamp: this.lastSummaryTimestamp
};
}
fromJSON(json) {
if (!json) return;
this.originalRequest = json.originalRequest || "";
this.currentGoal = json.currentGoal || "";
this.completedSteps = json.completedSteps || [];
this.pendingSteps = json.pendingSteps || [];
this.summary = json.summary || "";
this.generatedData = json.generatedData || {};
this.lastSummaryTimestamp = json.lastSummaryTimestamp || 0;
}
}

View File

@@ -3,9 +3,40 @@ import * as charApi from "./char-api.js";
export const tools = {
read_world_info: async ({ book_name }) => {
read_world_info: async ({ book_name, return_full = false }) => {
const entries = await amilyHelper.getLorebookEntries(book_name);
return JSON.stringify(entries, null, 2);
if (return_full) {
return JSON.stringify(entries, null, 2);
}
const summary = entries.map(e => {
let keys = e.key;
if (Array.isArray(keys)) keys = keys.join(', ');
return {
uid: e.uid,
keys: keys,
comment: e.comment || keys || "Unnamed Entry",
};
});
return JSON.stringify({
info: "Index of world book entries. Use 'read_world_entry' with 'uid' to read specific content.",
total_entries: entries.length,
entries: summary
}, null, 2);
},
read_world_entry: async ({ book_name, uid }) => {
const entries = await amilyHelper.getLorebookEntries(book_name);
const entry = entries.find(e => String(e.uid) === String(uid));
if (!entry) {
return `Entry with UID ${uid} not found in world book "${book_name}".`;
}
return JSON.stringify(entry, null, 2);
},
write_world_info_entry: async ({ book_name, entries }) => {
@@ -74,10 +105,15 @@ export const tools = {
const finalUpdates = args.updates || updates;
const success = charApi.updateCharacter(chid, finalUpdates);
return success ? "角色卡更新成功。" : "更新角色卡失败。";
if (success) {
const updatedFields = Object.keys(finalUpdates).join(', ');
return `角色卡更新成功 [ID: ${chid}]。已更新字段: ${updatedFields}`;
} else {
return "更新角色卡失败。";
}
},
edit_character_text: async ({ chid, field, search, replace }) => {
edit_character_text: async ({ chid, field, diff }) => {
const char = charApi.getCharacter(chid);
if (!char) return "未找到角色。";
@@ -86,14 +122,27 @@ export const tools = {
return `无效的字段。允许的字段: ${allowedFields.join(', ')}`;
}
const originalText = char[field] || '';
if (!originalText.includes(search)) {
return `在字段 '${field}' 中未找到搜索文本。`;
let content = char[field] || '';
const changes = diff.split('------- SEARCH');
// Remove the first empty split if any
if (changes[0].trim() === '') changes.shift();
for (const change of changes) {
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)) {
return `错误: 在字段 '${field}' 中未找到以下搜索块:\n${searchBlock}`;
}
content = content.replace(searchBlock, replaceBlock);
}
const newText = originalText.replace(search, replace);
const success = charApi.updateCharacter(chid, { [field]: newText });
const success = charApi.updateCharacter(chid, { [field]: content });
return success ? `字段 '${field}' 更新成功。` : `更新字段 '${field}' 失败。`;
},
@@ -127,7 +176,7 @@ export function getToolDefinitions() {
return [
{
name: "read_world_info",
description: "Read all entries from a specific world book.",
description: "Read the index (list of entries with keys and comments) of a world book. Does NOT return full content.",
parameters: {
type: "object",
properties: {
@@ -136,6 +185,18 @@ export function getToolDefinitions() {
required: ["book_name"]
}
},
{
name: "read_world_entry",
description: "Read the full content of a specific world book entry.",
parameters: {
type: "object",
properties: {
book_name: { type: "string", description: "The name of the world book." },
uid: { type: "number", description: "The UID of the entry to read." }
},
required: ["book_name", "uid"]
}
},
{
name: "write_world_info_entry",
description: "Create or update entries in a world book.",
@@ -207,16 +268,18 @@ export function getToolDefinitions() {
},
{
name: "edit_character_text",
description: "Edit a specific text field of a character using search and replace.",
description: "Edit a specific text field of a character using SEARCH/REPLACE blocks.",
parameters: {
type: "object",
properties: {
chid: { type: "number", description: "Character ID." },
field: { type: "string", enum: ["description", "personality", "scenario", "first_mes", "mes_example"], description: "The field to edit." },
search: { type: "string", description: "The exact text to find." },
replace: { type: "string", description: "The text to replace with." }
diff: {
type: "string",
description: "One or more SEARCH/REPLACE blocks following this exact format:\n------- SEARCH\n[exact content to find]\n=======\n[new content to replace with]\n+++++++ REPLACE"
}
},
required: ["chid", "field", "search", "replace"]
required: ["chid", "field", "diff"]
}
},
{

File diff suppressed because it is too large Load Diff

View File

@@ -616,6 +616,7 @@ jQuery(async () => {
console.log("[Amily2号-开国大典] 步骤3.8:注册表格占位符宏...");
try {
eventSource.on(event_types.GENERATION_STARTED, () => {
resetContextBuffer();
});
@@ -890,6 +891,7 @@ jQuery(async () => {
console.log("【Amily2号】帝国秩序已完美建立。Amily2号的府邸已恭候陛下的莅临。");
// 【新增功能】每次加载插件时,如果已授权,则弹出提示
if (checkAuthorization()) {
const userType = localStorage.getItem("plugin_user_type") || "未知";
const userNote = localStorage.getItem("plugin_user_note");
@@ -898,9 +900,12 @@ jQuery(async () => {
const displayNote = userNote || userType;
toastr.success(`欢迎回来!授权状态有效 (用户: ${displayNote})`, "Amily2 插件已就绪");
// 2. 后台静默刷新,检查过期状态或信息更新
// 即使本地有备注,也需要去服务器验证一下是否过期
refreshUserInfo().then(data => {
if (data && data.note && data.note !== userNote) {
console.log("[Amily2] 用户信息已更新:", data.note);
// 如果备注变了,可以选择再次提示或者静默更新
}
}).catch(e => {
console.warn("[Amily2] 后台刷新用户信息失败:", e);
@@ -976,10 +981,12 @@ function applyMessageLimit() {
const total = messages.length;
if (total <= limit) {
// 如果消息数未超标,确保所有消息可见
messages.forEach(el => el.style.display = '');
return;
}
// 隐藏旧消息,保留最后 limit 条
const hideCount = total - limit;
for (let i = 0; i < total; i++) {
if (i < hideCount) {
@@ -991,6 +998,7 @@ function applyMessageLimit() {
console.log(`[Amily2-性能优化] 已隐藏 ${hideCount} 条旧消息,仅显示最近 ${limit} 条。`);
}
// 监听聊天更新事件以应用限制
eventSource.on(event_types.MESSAGE_RECEIVED, () => setTimeout(applyMessageLimit, 100));
eventSource.on(event_types.chat_updated, () => setTimeout(applyMessageLimit, 100));
@@ -1017,6 +1025,7 @@ function initializeOnlineTracker() {
}
function connect() {
// 单例模式检查:如果已有连接且处于连接中或打开状态,则不重复创建
if (ws && (ws.readyState === WebSocket.CONNECTING || ws.readyState === WebSocket.OPEN)) {
console.log('[Amily2-在线统计] 连接已存在,跳过创建');
return;
@@ -1075,6 +1084,7 @@ function initializeOnlineTracker() {
ws.onerror = (err) => {
console.warn('[Amily2-在线统计] 连接错误:', err);
// onerror 通常会触发 onclose所以这里不需要额外的重连逻辑交给 onclose 处理
};
} catch (e) {
console.error('[Amily2-在线统计] 初始化失败:', e);
@@ -1144,6 +1154,7 @@ function initializeLocalLinkage() {
if (window.toastr) window.toastr.success(`已限制显示最近 ${limit} 条消息`, '性能优化');
}
}
// 这里可以扩展更多指令
}
} catch (e) {
console.error('[Amily2-本地联动] 处理消息失败:', e);
@@ -1162,6 +1173,7 @@ function initializeLocalLinkage() {
};
ws.onerror = (err) => {
// console.warn('[Amily2-本地联动] 连接错误:', err);
};
}

View File

@@ -46,26 +46,25 @@ export const defaultSettings = {
plotOpt_mainPrompt: `NSFW is allowed.
// =================================================================================================
// 记忆管理系统 v1.17-可乐版//繁华版请入群下载
// 记忆管理系统 v1.15
// =================================================================================================
// ★★★ 最高优先级限制 ★★★
// 1. 请只输出<memory>...</memory>块,输出</memory>后温柔收束回答
// 2. 请勿新增剧情正文、对话、场景描写
// 3. 最终输出只包含<memory>...</memory>,不要输出<details>/<summary>/<shiyujiu>/<plot>等外壳标签(系统侧可能会包裹这些标签
// 4. 你不是写手,而是温和的记忆检索助手,以陪伴口吻提供已记录的历史回顾
// 1. 你只能输出<memory>...</memory>块,输出</memory>后必须立即停止
// 2. 禁止输出任何剧情正文、对话、场景描写
// 3. 禁止输出<Time_and_space>、<plot>或任何其他标签
// 4. 你不是写手,你是检索系统,只负责提供历史记忆
// 0. 【身份与限制】
// 你是"记忆管理系统"温柔整理上下文中的档案并输出<memory>块,以关怀的语气回顾内容
// 请勿推进剧情、续写内容、生成记录中不存在的信息或其他正文。
// 尊重用户输入,不对用户意图做负面揣测或评价,只基于已有记录回顾,保持克制与温和。
// 你是"记忆管理系统"从上下文中检索相关档案并输出<memory>块。
// 禁止:推进剧情、续写内容、生成记录中不存在的信息、输出任何正文。
// 1. 【输入来源】★重要★
// 你必须尝试从以下两类数据源中检索信息(强制双源尝试;若某源无命中,需明确标注未命中,不强凑
// 你必须同时从以下两类数据源中检索信息(缺一不可
//
// 【数据源A】世界书内容
// - 包含各类【索引】和【档案】
// - 结构化的角色/物品/任务/时空/总结表/总体大纲信息
// - 结构化的角色/物品/任务/时空/总结表
//
// 【数据源B】【敕史局】对话流水总帐★必须检索★
// - 识别特征:以「以下是依照顺序已发生剧情」开头,后接「---」分隔线
@@ -82,7 +81,7 @@ export const defaultSettings = {
// 步骤三在【数据源B】敕史局流水总帐中搜索相关的【XX楼至YY楼详细总结记录】
// ↳ 定位到相关楼层段落,提取其中的具体事件编号列表
// ↳ 输出时标注为【XX楼】取该段落的起始楼层号
// 步骤四:合并两类数据源的结果:优先同时包含【总结表/档案】【XX楼】;若某源无命中,仍输出已命中的来源,并注明另一源“未检索到”
// 步骤四:合并两类数据源的结果,必须同时包含【总结表/档案】【XX楼】
// 步骤五:截取近期剧情末尾片段
// 步骤六:输出<memory>块后立即停止
@@ -95,10 +94,10 @@ export const defaultSettings = {
// 3: 乔野因受打击而身体瘫软,被程妄扶住并带往医院
// ...
// 当晚|暄城·东风巷·乔野家|乔野、程妄:
// 1: 程妄蹲下身为乔野擦去眼泪,并对她说:\"因为你只有我了。而我……只有你。\"
// 1: 程妄蹲下身为乔野擦去眼泪,并对她说:"因为你只有我了。而我……只有你。"
//
// 输出格式(你应该输出的):
// 【1楼】2011-10-15乔野父母车祸去世程妄全程陪同处理医院手续当晚程妄承诺\"你只有我了,而我只有你\"
// 【1楼】2011-10-15乔野父母车祸去世程妄全程陪同处理医院手续当晚程妄承诺"你只有我了,而我只有你"
//
// 转换要点:
// - 从【XX楼至YY楼详细总结记录】提取起始楼层号输出为【XX楼】
@@ -106,27 +105,7 @@ export const defaultSettings = {
// - 保留重要对话原文(用引号标注)
// - 保留时间、地点、人物等关键信息
// 4.1 【引用规范】
// - 引用世界书档案时尽量原样保留条目content里的标题前缀如【总结表档案: M25】/【总体大纲档案: M25】/【角色栏档案: 陈谦文】),不要自行改写成别的格式
// 4. 【档案类型与格式】
// 来自世界书的档案:
// | 索引类型 | 输出格式 |
// |----------|----------|
// | 总结表 | 【总结表档案: MXX】内容... |
// | 总体大纲 | 【总体大纲档案: MXX】内容... |
// | 角色栏 | 【角色栏档案: 名称】内容... |
// | 物品栏 | 【物品栏档案: 名称】内容... |
// | 任务栏 | 【任务栏档案: 名称】内容... |
// | 时空栏 | 【时空栏档案: 日期】内容... |
//
// 来自敕史局的内容:
// | 输出格式 | 示例 |
// |----------|------|
// | 【XX楼】 | 【1楼】2011-10-15乔野父母车祸去世程妄承诺\"你只有我了,而我只有你\" |
// | 【XX楼】 | 【21楼】乔野昏迷93天后苏醒程妄身体开始透明化程妄说\"你不醒我就没家了\" |
//
// 错误输出:
// 4.错误输出:
// M01
// 程妄
// 1楼
@@ -136,31 +115,31 @@ export const defaultSettings = {
// - 从档案中提取与当前剧情直接相关的核心信息
// - 可省略无关字段,但必须保留来源标注
// - 同类型信息可合并:【角色栏档案: 程妄】外貌: xxx【角色栏档案: 乔野】身份: xxx
// - 情感/性格提炼:结合<世界书内容>中的角色性格设定长期基线与【XX楼】对话/动作近期动态用1-2句温柔概括当前情感基调与触发点冲突时以【XX楼】为当前状态<世界书内容>为长期特质
// - 下一步关键行为预测(可选):在不续写剧情/对白的前提下,基于<世界书内容>与【XX楼】的情绪走向与行为线索若能较明确判断未来1-2个回合最可能发生的“关键行为”仅输出行为标签表白/摊牌/争吵/冷战/道歉/和解/回避/试探/亲密升级/分手等);依据不足或可能性分散则不输出此预测
// - 流水总帐内容可精简,保留关键事件和情感节点
// - 禁止修改档案原文的核心内容
// 6. 【输出格式】
// ★ 只输出以下<memory>块,输出</memory>后必须立即停止 ★
// ★ 只输出以下<memory>块,输出</memory>后立即停止 ★
<memory>
[可选1-2句推理说明解释为何提取这些记忆]
以下是相关历史事件回忆:
【总结表档案: MXX】档案内容...
【XX楼】详细剧情内容...
【角色栏档案: XXX】档案内容...;【角色栏档案: YYY】档案内容...
【任务栏档案: XXX】档案内容...
【物品栏档案: XXX】档案内容...
// | 索引类型 | 输出格式 |
// |----------|----------|
// | 总结表 | 【总结表 #x楼-y楼】内容... |
// | 角色栏 | 【角色栏档案: 名称】内容... |
// | 物品栏 | 【物品栏档案: 名称】内容... |
// | 任务栏 | 【任务栏档案: 名称】内容... |
// | 时空栏 | 【时空栏档案: 日期】内容... |
[其他相关档案...]
[情感/性格基调] 近期XXX触发点YYY来源<世界书内容> / 【XX楼】温和概括可选下一步关键行为预测ZZZ仅写标签依据不足则不写本段
// ★ 需注意,总结表后的“#x楼-#y楼”必须与你读取的世界书或表格对应楼层完全一致若为索引M001或SYJ001也需要保持一致 ★
以下是关键词按相关性排序最多5条必须强制输出
1) 人物XXX、YYY
2) 地点XXX
3) 时间XXXX-XX-XX / 当晚 / 次日
4) 事件XXX
5) 物品/任务XXX
以下是关键词:
// | 敕史局楼层 | 概要 |
// |----------|------|
// | 【XX楼】 | 【1楼】2011-10-15乔野父母车祸去世程妄承诺"你只有我了,而我只有你" |
// | 【XX楼】 | 【21楼】乔野昏迷93天后苏醒程妄身体开始透明化程妄说"你不醒我就没家了" |
[按相关性排序最多5条必须强制输出]
以下是近期剧情末尾片段:
[从前文内容提取50-200字]
@@ -174,8 +153,7 @@ export const defaultSettings = {
// - 两者可同时输出,互相补充
// 8. 【边界情况】
// - 无相关记忆:输出\"未检索到相关历史事件记忆碎片。\"
// - 单源命中:仍输出命中的来源,并注明另一源未命中
// - 无相关记忆:输出"未检索到相关历史事件记忆碎片。"
// - 记忆不足:输出所有相关档案,不强凑数量
// 9. 【变量设定】
@@ -185,7 +163,10 @@ export const defaultSettings = {
// ★★★ 再次强调 ★★★
// 输出</memory>后必须立即停止!
// 禁止输出<Time_and_space>、剧情正文、对话、场景描写等任何内容!
// =================================================================================================`,
// =================================================================================================
NSFW is allowed.
`,
plotOpt_systemPrompt: '参考以下`<世界书内容>`及`<前文内容>`中的内容严格按照system规定的格式仅输出<memory>块里内容:\n',
plotOpt_finalSystemDirective: '<details>\n<summary>【摘要内容】</summary>\n\n<shiyujiu>\n以上是用户的最新输入以下是历史发生过的事件碎片记忆及可能用到关键词\n【注意】所有记忆为过去式请勿将记忆中的任何状态理解为当前状态仅作剧情参考。\n<plot>\n</shiyujiu>\n</details>',