mirror of
https://github.com/Wx-2025/ST-Amily2-Chat-Optimisation.git
synced 2026-06-07 06:15:51 +00:00
Compare commits
7 Commits
ec09ed420c
...
859cfb2fca
| Author | SHA1 | Date | |
|---|---|---|---|
| 859cfb2fca | |||
| ff61e176c1 | |||
| 06b36f2c93 | |||
| 7c2cc13579 | |||
| bb25daeb2a | |||
| 0992664ab3 | |||
| 7a0eb06ad0 |
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 = [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 }),
|
||||
|
||||
@@ -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) {
|
||||
|
||||
54
core/auto-char-card/memory-system.js
Normal file
54
core/auto-char-card/memory-system.js
Normal 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;
|
||||
}
|
||||
}
|
||||
86
core/auto-char-card/task-state.js
Normal file
86
core/auto-char-card/task-state.js
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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
12
index.js
12
index.js
@@ -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);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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>',
|
||||
|
||||
|
||||
Reference in New Issue
Block a user