🎨 1.优化前端渲染功能(html和对话消息格式)

2.优化流式传输,新增流式渲染功能
3.优化正则处理逻辑
4.新增context budget管理系统
5.优化对话消息失败处理逻辑
6.新增前端卡功能(待完整测试)
This commit is contained in:
2026-03-13 15:58:33 +08:00
parent c267b6c76e
commit 4cecfd6589
22 changed files with 2492 additions and 2164 deletions

View File

@@ -1,67 +0,0 @@
# 云酒馆项目文档
## 目录
- [项目概述](./overview.md)
- [架构设计](./architecture.md)
- [API 文档](./api.md)
- [前端开发指南](./frontend-guide.md)
- [后端开发指南](./backend-guide.md)
- [部署指南](./deployment.md)
- [设计系统](./design-system/)
## 项目架构
```
云酒馆平台
├── 前端 (web-app)
│ ├── 公共页面(首页、角色广场、角色详情)
│ ├── 用户系统(登录、注册、用户中心)
│ ├── 管理功能(角色卡管理、预设管理)
│ └── 对话功能(聊天界面、历史记录)
└── 后端 (server)
├── 用户认证 (JWT)
├── 角色卡 API
├── 对话管理 API
├── 预设管理 API
└── AI 集成
```
## 核心功能
### 角色卡系统
- 支持 PNG 格式角色卡(嵌入 JSON 数据)
- 支持纯 JSON 格式角色卡
- 角色信息编辑
- 导入导出功能
### 预设系统
- 支持多种预设格式SillyTavern、TavernAI 等)
- 参数配置Temperature、Top P、Top K 等)
- 预设复制和导出
### 对话系统
- 实时消息发送
- 对话历史管理
- 多角色对话支持
- 对话导出功能
## 开发规范
### 代码风格
- 前端ESLint + Prettier
- 后端ESLint
- 提交信息Conventional Commits
### Git 工作流
- main: 生产环境
- develop: 开发环境
- feature/*: 功能分支
- bugfix/*: 修复分支
## 快速链接
- [前端 README](../web-app/README.md)
- [后端 README](../server/README.md)
- [设计系统](./design-system/)

View File

@@ -1,590 +0,0 @@
class StoryRenderer {
constructor(dataSourceId) {
this.dataSourceId = dataSourceId;
this.yamlData = null;
this.rootNode = null; // 根节点名称
this.initElements();
}
// 初始化DOM元素引用
initElements() {
this.elements = {
timeDisplay: document.getElementById('time-display'),
locationDisplay: document.getElementById('location-display'),
charactersContainer: document.getElementById('characters-container'),
actionOwner: document.getElementById('action-owner'),
optionsList: document.getElementById('options-list'),
};
}
// 初始化方法
init() {
try {
// 从script标签中加载YAML数据
this.loadYamlFromScriptTag();
// 如果没有有效数据(显示加载状态的情况),直接返回
if (!this.yamlData) {
this.setupEventListeners();
return;
}
// 找到根节点
this.findRootNode();
this.renderAll();
this.setupEventListeners();
} catch (error) {
this.handleError(error);
}
}
// 从script标签加载并解析YAML数据
loadYamlFromScriptTag() {
const scriptElement = document.getElementById(this.dataSourceId);
if (!scriptElement) {
throw new Error('未找到id为"yaml-data-source"的script标签');
}
let yamlContent = scriptElement.textContent.trim();
// 检查是否为真正的空内容
if (!yamlContent) {
// 当YAML内容为空时设置默认的加载状态但不抛出错误
this.showLoadingState();
return; // 直接返回,不抛出错误
}
// 如果是"加载中..."文本,也显示加载状态
if (yamlContent === '加载中...') {
this.showLoadingState();
return;
}
// 有内容尝试解析YAML
try {
this.yamlData = jsyaml.load(yamlContent);
} catch (e) {
// YAML格式错误应该弹出错误对话框
throw new Error(`YAML格式错误: ${e.message}`);
}
if (!this.yamlData || Object.keys(this.yamlData).length === 0) {
// 解析成功但数据为空,这是格式问题
throw new Error('YAML解析成功但数据为空请检查YAML格式是否正确');
}
}
// 显示加载状态的独立方法
showLoadingState() {
this.elements.timeDisplay.textContent = '⏰ 加载中...';
this.elements.locationDisplay.textContent = '📍 加载中...';
this.elements.actionOwner.textContent = '加载中...';
this.elements.charactersContainer.innerHTML = this.createEmptyState('数据加载中...');
this.elements.optionsList.innerHTML =
'<li class="text-gray-400"><div class="flex items-center"><div class="animate-spin rounded-full h-4 w-4 border-t-2 border-b-2 border-primary mr-2"></div>加载选项中...</div></li>';
}
// 查找根节点
findRootNode() {
const rootNodeNames = Object.keys(this.yamlData);
if (rootNodeNames.length === 0) {
throw new Error('YAML数据中未找到任何根节点');
}
this.rootNode = rootNodeNames[0];
}
// 格式化节点名称,使其更易读
formatNodeName(name) {
// 提取emoji后面的文本如果有emoji
const emojiMatch = name.match(/^(\p{Emoji}\s*)(.*)$/u);
if (emojiMatch && emojiMatch[2]) {
return emojiMatch[2];
}
return name;
}
// 渲染所有内容
renderAll() {
if (!this.rootNode || !this.yamlData[this.rootNode]) {
throw new Error('未找到有效的根节点数据');
}
const rootData = this.yamlData[this.rootNode];
this.renderHeaderInfo(rootData);
this.renderCharacters(rootData);
this.renderActionOptions(rootData);
}
// 渲染头部信息(日期和时间和地点)
renderHeaderInfo(rootData) {
// 查找日期时间字段
const dateTimeField = this.findFieldByKeywords(rootData, ['日期', '时间', 'datetime', 'time']);
// 查找地点字段
const locationField = this.findFieldByKeywords(rootData, ['地点', '位置', 'location', 'place']);
// 直接使用包含emoji的值
this.elements.timeDisplay.textContent = dateTimeField ? rootData[dateTimeField] : '⏰ 时间未知';
this.elements.locationDisplay.textContent = locationField ? rootData[locationField] : '📍 地点未知';
}
// 根据关键词查找字段名
findFieldByKeywords(data, keywords) {
if (!data || typeof data !== 'object') return null;
const fields = Object.keys(data);
for (const field of fields) {
for (const keyword of keywords) {
if (field.toLowerCase().includes(keyword.toLowerCase())) {
return field;
}
}
}
return null;
}
// 渲染角色列表
renderCharacters(rootData) {
// 查找用户列表字段
const userListField = this.findFieldByKeywords(rootData, ['用户', '角色', '列表', 'user', 'role', 'list']);
const userList = userListField && Array.isArray(rootData[userListField]) ? rootData[userListField] : [];
this.elements.charactersContainer.innerHTML = '';
if (userList.length === 0) {
this.elements.charactersContainer.innerHTML = this.createEmptyState('没有角色数据');
return;
}
// 处理每个用户项
userList.forEach(userItem => {
// 检查是否有外层包装
let userData = userItem;
if (typeof userItem === 'object' && userItem !== null) {
const userField = this.findFieldByKeywords(userItem, ['用户', 'user', '角色', 'role']);
if (userField) {
userData = userItem[userField];
}
}
const characterCard = this.createCharacterCard(userData);
if (characterCard) {
this.elements.charactersContainer.appendChild(characterCard);
}
});
}
// 创建单个角色卡片
createCharacterCard(userData) {
if (!userData || typeof userData !== 'object') return null;
const card = document.createElement('div');
card.className =
'bg-dark rounded-xl border border-gray-700/30 p-3.5 shadow-sm card-hover character-card theme-transition';
// 查找名字字段
const nameField = this.findFieldByKeywords(userData, ['名字', '姓名', '名称', 'name']);
const userName = nameField ? userData[nameField] : '👤 未知角色';
// 创建标题
const title = document.createElement('h3');
title.className = 'font-bold text-lg mb-2 pb-1 border-b border-gray-700/30 theme-transition';
title.textContent = `${this.formatNodeName(userName)}的状态`;
card.appendChild(title);
// 创建属性列表
const attributesList = document.createElement('ul');
attributesList.className = 'space-y-2 text-sm';
card.appendChild(attributesList);
// 处理所有属性
Object.keys(userData).forEach(key => {
// 跳过已经作为标题使用的名字节点
if (key === nameField) return;
// 创建属性项直接使用包含emoji的值
const attributeItem = this.createAttributeItem(key, userData[key]);
if (attributeItem) {
attributesList.appendChild(attributeItem);
}
});
return card;
}
// 创建属性项
createAttributeItem(key, value) {
const item = document.createElement('li');
// 处理数组类型
if (Array.isArray(value)) {
item.innerHTML = `<span class="font-medium text-primary">${this.formatNodeName(key)}:</span>`;
const subList = document.createElement('ul');
subList.className = 'list-disc list-inside ml-4 mt-1 space-y-1 text-gray-400 theme-transition';
value.forEach(itemData => {
if (typeof itemData === 'object' && itemData !== null) {
const subKey = Object.keys(itemData)[0];
const subValue = itemData[subKey];
const subItem = document.createElement('li');
subItem.textContent = subValue;
subList.appendChild(subItem);
} else {
const subItem = document.createElement('li');
subItem.textContent = itemData;
subList.appendChild(subItem);
}
});
item.appendChild(subList);
}
// 处理对象类型
else if (typeof value === 'object' && value !== null) {
item.innerHTML = `<span class="font-medium text-primary">${this.formatNodeName(key)}:</span>`;
const subList = document.createElement('ul');
subList.className = 'list-disc list-inside ml-4 mt-1 space-y-1 text-gray-400 theme-transition';
Object.keys(value).forEach(subKey => {
const subItem = document.createElement('li');
subItem.textContent = value[subKey];
subList.appendChild(subItem);
});
item.appendChild(subList);
}
// 处理普通文本值
else if (value !== null && value !== undefined && value.toString().trim() !== '') {
item.innerHTML = `<span class="font-medium text-primary">${this.formatNodeName(key)}:</span> ${value}`;
}
return item;
}
// 渲染行动选项
renderActionOptions(rootData) {
// 查找行动选项字段
const actionOptionsField = this.findFieldByKeywords(rootData, ['行动', '选项', 'action', 'option']);
const actionOptions =
actionOptionsField && typeof rootData[actionOptionsField] === 'object' ? rootData[actionOptionsField] : {};
// 设置行动所有者
const ownerField = this.findFieldByKeywords(actionOptions, ['名字', '姓名', '所有者', 'owner', 'name']);
this.elements.actionOwner.textContent = ownerField
? this.formatNodeName(actionOptions[ownerField])
: '未知角色';
// 渲染选项列表
const optionsField = this.findFieldByKeywords(actionOptions, ['选项', '选择', 'option', 'choice']);
const options = optionsField && Array.isArray(actionOptions[optionsField]) ? actionOptions[optionsField] : [];
this.elements.optionsList.innerHTML = '';
if (options.length === 0) {
this.elements.optionsList.innerHTML = this.createEmptyState('没有可用选项');
return;
}
options.forEach(optionText => {
const optionItem = document.createElement('li');
optionItem.className =
'pl-2 py-1 border-l-2 border-primary/30 ml-1 hover:border-primary/70 transition-colors text-gray-300 theme-transition';
optionItem.textContent = optionText;
this.elements.optionsList.appendChild(optionItem);
});
}
// 创建空状态提示
createEmptyState(message) {
return `<div class="text-center py-4 text-gray-500 theme-transition">
<i class="fa fa-info-circle mr-1"></i>${message}
</div>`;
}
// 设置事件监听器
setupEventListeners() {
const detailsElement = document.querySelector('details');
const contentElement = this.elements.charactersContainer;
// 初始化高度为0以实现动画效果
contentElement.style.maxHeight = '0';
// 监听详情展开/折叠事件
detailsElement.addEventListener('toggle', () => {
if (detailsElement.open) {
// 展开时设置实际高度
setTimeout(() => {
contentElement.style.maxHeight = contentElement.scrollHeight + 'px';
}, 10);
} else {
// 折叠时设置高度为0
contentElement.style.maxHeight = '0';
}
});
// 根据自动折叠设置决定默认状态
const autoCollapseToggle = document.getElementById('auto-collapse-toggle');
if (autoCollapseToggle) {
// 从本地存储读取设置默认为true折叠
const savedAutoCollapse = localStorage.getItem('autoCollapse');
const shouldCollapse = savedAutoCollapse === null ? true : savedAutoCollapse === 'true';
detailsElement.open = !shouldCollapse;
// 如果默认展开,需要设置正确的高度
if (!shouldCollapse) {
setTimeout(() => {
contentElement.style.maxHeight = contentElement.scrollHeight + 'px';
}, 100);
}
} else {
// 如果没有设置切换开关,默认折叠
detailsElement.open = false;
}
}
// 错误处理
handleError(error) {
console.error('渲染错误:', error);
// 使用美化的错误弹窗
showErrorModal(error.message);
// 在角色状态区域显示错误信息
this.elements.charactersContainer.innerHTML = `
<div class="bg-red-900/20 border border-red-800/30 text-red-400 px-4 py-3 rounded relative" role="alert">
<strong class="font-bold">状态栏渲染失败: </strong>
<span class="block sm:inline">${error.message}</span>
</div>
`;
// 在行动选项区域也显示错误信息
this.elements.optionsList.innerHTML = `
<li class="text-red-400 bg-red-900/20 border border-red-800/30 rounded p-3 theme-transition">
<div class="flex items-start">
<i class="fa fa-exclamation-triangle mr-2 mt-1"></i>
<div>
<div class="font-semibold mb-1">行动选项加载失败</div>
<div class="text-sm text-red-300">请检查YAML格式是否正确</div>
</div>
</div>
</li>
`;
}
}
$(document).ready(function () {
/**
* 获取本楼消息
* @returns {Object|null} 包含本楼消息信息的对象失败时返回null
*/
function getCurrentMessage() {
try {
if (typeof getCurrentMessageId !== 'function' || typeof getChatMessages !== 'function') {
return null;
}
const currentMessageId = getCurrentMessageId();
if (!currentMessageId && currentMessageId !== 0) return null;
const messageData = getChatMessages(currentMessageId);
if (!messageData) return null;
return Array.isArray(messageData) && messageData.length > 0 ? messageData[0] : messageData;
} catch (error) {
console.error('获取消息失败:', error);
return null;
}
}
function extractMaintext(message) {
if (!message || typeof message !== 'string') return '';
const match = message.match(/<maintext>([\s\S]*?)<\/maintext>/i);
return match ? match[1].trim() : '';
}
/**
* 从消息中提取Status_block内容
* @param {string} message 消息文本
* @returns {string} 提取的YAML状态内容
*/
function extractStatusBlock(message) {
if (!message || typeof message !== 'string') return '';
const match = message.match(/<Status_block>\s*([\s\S]*?)\s*<\/Status_block>/i);
return match ? cleanYamlContent(match[1].trim()) : '';
}
/**
* 清理YAML内容修复常见的格式问题
* @param {string} yamlContent 原始YAML内容
* @returns {string} 清理后的YAML内容
*/
function cleanYamlContent(yamlContent) {
if (!yamlContent) return '';
return yamlContent
.split('\n')
.map(line => {
if (line.trim() === '' || !line.trim().match(/^-\s*"/)) return line;
const match = line.match(/^(\s*-\s*)"(.*)"\s*$/);
if (!match) return line;
const [, indent, content] = match;
return content.includes('"') || content.includes("'")
? indent + "'" + content.replace(/'/g, "''") + "'"
: indent + '"' + content + '"';
})
.join('\n');
}
/**
* 更新YAML数据源
* @param {string} yamlContent YAML格式的状态内容
*/
function updateYamlDataSource(yamlContent) {
const yamlScript = document.getElementById('yaml-data-source');
if (!yamlScript) return;
// 如果内容为空或无效,设置为加载中状态
if (!yamlContent || typeof yamlContent !== 'string' || !yamlContent.trim()) {
yamlScript.textContent = ''; // 设置为空,让后续处理显示加载状态
return;
}
// 先设置内容让StoryRenderer能处理格式错误
yamlScript.textContent = yamlContent;
// 验证YAML格式如果有错误会被StoryRenderer捕获并处理
try {
jsyaml.load(yamlContent);
} catch (error) {
// 尝试修复常见的YAML错误
const fixedYaml = attemptYamlFix(yamlContent, error);
if (fixedYaml) {
try {
jsyaml.load(fixedYaml);
yamlScript.textContent = fixedYaml;
} catch (e) {
console.error('YAML修复失败:', e.message);
// 修复失败时保留原内容让StoryRenderer显示具体错误
}
}
// 如果无法修复保留原内容让StoryRenderer显示具体错误
}
}
/**
* 尝试修复常见的YAML错误
* @param {string} yamlContent 有问题的YAML内容
* @param {Error} error YAML解析错误
* @returns {string|null} 修复后的YAML或null
*/
function attemptYamlFix(yamlContent, error) {
if (!(error.message.includes('bad indentation') || error.message.includes('quote'))) {
return null;
}
return yamlContent
.split('\n')
.map(line => {
const match = line.match(/^(\s*-\s*)"(.*)"\s*$/);
if (!match) return line;
const [, indent, content] = match;
return content.includes('"')
? indent + "'" + content.replace(/'/g, "''") + "'"
: indent + '"' + content + '"';
})
.join('\n');
}
/**
* 更新maintext内容
* @param {string} maintextContent maintext内容
*/
function updateMaintext(maintextContent) {
try {
const maintextElement = document.getElementById('maintext');
if (!maintextElement) return;
// 如果内容为空或无效,设置为加载中状态
if (!maintextContent || typeof maintextContent !== 'string' || !maintextContent.trim()) {
maintextElement.textContent = '';
} else {
maintextElement.textContent = maintextContent;
}
formatMainText();
} catch (error) {
console.error('更新maintext失败:', error);
// 如果更新失败直接调用formatMainText它会处理错误
formatMainText();
}
}
/**
* 重新渲染状态栏
*/
function reRenderStatusBar() {
try {
const yamlScript = document.getElementById('yaml-data-source');
if (!yamlScript || !yamlScript.textContent) return;
const storyRenderer = new StoryRenderer('yaml-data-source');
storyRenderer.init();
} catch (error) {
console.error('重新渲染状态栏失败:', error);
// 状态栏渲染失败时错误处理由StoryRenderer.handleError处理
// 这里不需要额外处理因为StoryRenderer的init方法已经有handleError调用
}
}
/**
* 根据消息数据渲染整个页面
* @param {Object} messageData 消息数据对象格式参考test.json
*/
function renderPageFromMessage(messageData) {
let actualMessageData = Array.isArray(messageData) && messageData.length > 0 ? messageData[0] : messageData;
if (!actualMessageData || !actualMessageData.message || typeof actualMessageData.message !== 'string') {
return;
}
const messageContent = actualMessageData.message;
// 提取并更新maintext内容
const maintextContent = extractMaintext(messageContent);
if (maintextContent) {
updateMaintext(maintextContent);
}
// 提取并更新Status_block内容
const statusContent = extractStatusBlock(messageContent);
if (statusContent) {
updateYamlDataSource(statusContent);
setTimeout(() => reRenderStatusBar(), 100);
}
}
// 执行获取操作并处理结果
try {
const currentMessage = getCurrentMessage();
if (currentMessage && typeof currentMessage === 'object') {
renderPageFromMessage(currentMessage);
}
} catch (error) {
console.error('获取或渲染消息时出错:', error);
}
window.statusBlockInitialized = true;
});
</script>
pm
</body></html>

View File

@@ -1,356 +0,0 @@
## SillyTavern 完全兼容优化方案Go + Gin + Postgres + React
> 目标:基于现有 Go + React 系统,构建一个与 SillyTavern下称 ST高度兼容的角色卡 / 世界书 / 正则 / 预设 / 对话平台。
---
### 1. 总体目标与设计原则
- **技术栈统一**所有核心功能角色卡、世界书、正则、预设、聊天、AI 集成)全部收敛到:
- **后端**`server/` 下的 Go + Gin + PostgreSQL
- **前端**`projects/web-app` 下的 React + TypeScript + Tailwind
- **SillyTavern 完全兼容**
- 支持 ST 角色卡 V2/V3chara_card_v2 / v3导入导出
- 支持 ST 世界书字段及触发逻辑keys/secondary_keys/regex/depth/sticky/cooldown/probability/position 等);
- 支持 ST Regex Scripts 配置placement/runOnEdit/markdownOnly/promptOnly/substituteRegex/min/max depth
- 支持 ST 风格预设与 prompt 注入顺序prompt_order / injection depth/position
- **单一真相来源SSOT**
- **数据库即真相**Postgres 负责持久化所有 ST 相关实体;
- **前端只是 UI**React 只做编辑/展示,请求都经过 API不再有“前端内存版预设/世界书”。
- **可扩展性**
- 提前为插件系统预留 HookonUserInput/onWorldInfoScan/beforePromptBuild/onAssistantDone 等);
- 方便接入未来的 WebSocket/SSE 流式聊天、统计系统、市场/分享功能。
---
### 2. 当前 Go + React 系统现状(基于现有文档与代码)
#### 2.1 后端server/
根据 `projects/docs/development-progress.md`
-**用户系统** 已完成2024-02-26
- 模型:`AppUser`, `AppUserSession`
- API`/app/auth/register`, `/app/auth/login`, `/app/auth/refresh`, `/app/auth/logout`, `/app/auth/userinfo`
- JWT、会话、用户资料、密码修改均已实现
-**角色卡管理AICharacter** 已完成:
- 模型:`AICharacter`(完全兼容 ST V2 格式),使用 JSONB 存储 `tags/alternateGreetings/characterBook/extensions` 等复杂结构
- API
- `POST /app/character`
- `GET /app/character`(分页、搜索、标签筛选)
- `GET /app/character/:id`
- `PUT /app/character/:id`
- `DELETE /app/character/:id`
- `POST /app/character/upload`PNG/JSON 角色卡)
- `GET /app/character/:id/export`(导出 JSON
- 工具:`utils/character_card.go`PNG tEXt chunk / ST V2 工具)
- 🚧 **预设管理**
- 前端页面 `/presets` 已完成(导入 JSON、复制、导出、编辑参数
- 后端预设 API 尚未实现(`development-progress.md` 已给出规划端点)。
- 📋 **对话系统 & AI 集成**
- 前端基础 UI 已完成(`/chat` + 侧边栏 + 输入框)。
- 后端对话 API、AI 流式集成、世界书/正则逻辑尚在规划阶段。
#### 2.2 前端(`projects/web-app`
- **用户系统前端**`LoginPage/RegisterPage/ForgotPasswordPage/ProfilePage` 已完成,`api/client.ts + api/auth.ts` 已完成 token 注入与刷新。
- **角色卡管理前端**`CharacterManagePage` + `api/character.ts`
- 功能:
- 上传 PNG/JSON 角色卡(调用 `/app/character/upload`
- 编辑角色卡核心字段、内嵌 `characterBook`(世界书)字段
- 导出 JSON调用 `/app/character/:id/export`
- 搜索、分页、删除
- **预设管理前端**`PresetManagePage`
- 当前为 **前端内存里的假数据** + 文件导入/导出;尚未接入真实后端 `/app/preset`
- **聊天前端**`ChatPage + Sidebar + ChatArea + CharacterPanel + SettingsPanel`
- 已实现基础布局、会话切换、角色选择、背景图/主题色设置等 UI。
- 消息发送、历史加载、预设切换、世界书/正则开关还需后端配合。
- **AI 配置前端**`api/aiConfig.ts`(对应 `/app/ai-config` 系列)。
> 结论:**角色卡链路基本打通**,用户系统成熟;预设/对话/AI/世界书/正则目前主要停留在前端 UI 或规划层面,正好适合作为“集中下沉到 Go 后端”的突破口。
---
### 3. 目标架构SillyTavern 兼容实现)
#### 3.1 核心领域模型(后端)
建议在现有 `model/app` 中引入或规范以下模型(部分已存在,可扩展):
- `AICharacter`(已存在)
- ST 角色卡 V2/V3 的标准化版本 + 原始 JSON 存档。
- `Worldbook` & `WorldbookEntry`
- 支持角色内世界书与全局世界书。
- `RegexScript`
- 支持 scope`global | character | preset`
- `Preset` & `PresetPrompt` & `PresetRegexBinding`
- 表达 ST preset 的参数、prompt 列表和 regex 绑定。
- `Conversation` & `Message`
- 对话与消息记录,支持与 `AICharacter``Preset` 和 AI 配置关联。
#### 3.2 运行时 Pipeline
所有客户端(现阶段只有 React Web将来可增加其他统一通过 Go 后端完成以下流程:
1. **输入正则脚本处理**global/character/preset
2. **世界书扫描**keys/secondary_keys/regex/depth/sticky/cooldown 等)
3. **Prompt 构建**(角色 system + world info + preset prompts + 历史消息)
4. **AI 调用**OpenAI/Anthropic/custom via `/app/ai-config`
5. **输出正则脚本处理**
6. **持久化消息与统计**
React 前端只负责:
- 提供管理与编辑界面;
- 将用户选择的 preset/worldbook/regex 传给后端;
- 使用 SSE/WebSocket 将 AI 流输出展示给用户。
---
### 4. 后端详细优化方案Go + Gin + Postgres
#### 4.1 模型与数据库设计(概念级)
> 不强制你立刻改现有表名与字段;这部分作为“目标状态”,可通过迁移脚本或视图逐步对齐。
- **AICharacter**(已有)
- 新增/规范字段:
- `raw_card_json JSONB`:存原始 ST 角色卡,用于无损导出。
- `bound_worldbook_ids UUID[]`:角色绑定世界书 ID 列表(可选)。
- `bound_regex_ids UUID[]`:角色绑定正则脚本 ID 列表(可选)。
- **Worldbook / WorldbookEntry**
- Worldbook`id, name, owner_char_id (nullable), meta JSONB, created_at, updated_at`
- WorldbookEntry包含 ST 全字段:
- `uid, keys, secondary_keys, comment, content, constant, disabled, use_regex, case_sensitive, match_whole_words, selective, selective_logic, position, depth, order, probability, sticky, cooldown, delay, group, extra JSONB`
- **RegexScript**
- `id, name, find_regex, replace_with, trim_string`
- `placement INT[]`
- `disabled, markdown_only, run_on_edit, prompt_only`
- `substitute_regex, min_depth, max_depth`
- `scope, owner_char_id, owner_preset_id, raw_json`
- **Preset / PresetPrompt / PresetRegexBinding**
- 基本采样参数 + prompt 列表 + regex 绑定。
- **Conversation / Message**
- Conversation`id, user_id, character_id, preset_id, ai_config_id, title, settings JSONB, created_at, updated_at`
- Message`id, conversation_id, role, content, token_count, created_at`
#### 4.2 角色卡导入/导出(巩固现有实现)
> 从 `development-progress.md` 看,`/app/character` 模块已经“完全兼容 ST V2”这里更多是规范与扩展。
- **导入 `/app/character/upload` 已具备**
- PNG使用 `utils/character_card.go` 提取 `chara` tEXt chunk → JSON。
- JSON直接解析填充 `AICharacter`
- 优化点:
- 当角色卡中包含 `character_book` 时:
- 自动在 `worldbooks/worldbook_entries` 中创建对应记录;
- 将 worldbook.id 写入 `AICharacter.bound_worldbook_ids`(或冗余到 `characterBook` 字段中)。
- 当包含 `extensions.regex_scripts` 时:
- 自动创建 `RegexScript`scope=character, owner_char_id=该角色)。
- 将脚本 id 写入 `AICharacter.bound_regex_ids``extensions` 中。
- **导出 `/app/character/:id/export`**
-`raw_card_json` 存在,优先以它为基础;将 DB 中新增的信息 patch 回去(例如补上最新的 worldbook/regex 变更)。
- 若不存在,则按 ST V2 规范组装 JSON兼容 V1/V3 的 data 字段)。
#### 4.3 世界书World Info引擎
1. **世界书激活来源**
- 全局启用列表per-user 或全局 settings 中的 `active_worldbook_ids`)。
- 角色绑定:`AICharacter.bound_worldbook_ids`
- 会话特定选项:`conversation.settings.activeWorldbookIds`(从前端设置面板传入)。
2. **触发与过滤算法**
- 遍历所有激活 worldbook 的 entries
- 忽略 `disabled` 的 entry。
-`constant=true`:无视关键词匹配,直接进入候选(仍受 sticky/cooldown/delay/probability 控制)。
- keys 匹配:
- `use_regex=true`:将每个 key 作为正则(考虑 `caseSensitive``matchWholeWords` 标志)。
- 否则:普通字符串包含匹配(可选大小写敏感)。
-`selective=true`:根据 `selectiveLogic` 结合 secondary_keys
- `0=AND`secondary_keys 至少一个命中;
- `1=OR`:主 keys 或 secondary_keys 任一命中;
- `2=NOT`:主 keys 命中,但 secondary_keys 不命中。
- depth仅在最近 `entry.depth` 条消息(或 tokens中搜索关键字。
- sticky/cooldown/delay基于会话级状态比如 Redis/内存/DB存储 entry 上次触发时间或 sticky 状态。
- probability按百分比随机决定最终是否注入。
3. **注入到 prompt 中**
- 简化版:将所有命中 entry.content 拼接为 `[World Info]` 段,附加在 system prompt 后;
- 高级版:按 ST 行为,依据 position/depth/order放到不同位置角色描述前/后、历史中等)。
#### 4.4 正则脚本Regex Scripts引擎
1. **脚本来源**
- Global`scope='global'``disabled=false`
- Character`scope='character' AND owner_char_id=当前角色`
- Preset`preset_regex_bindings` 中绑定到当前 preset 的脚本。
2. **执行阶段placement**
- 输入阶段placement=1用户消息入库前
- 输出阶段placement=2模型输出生成后、入库/返回前;
- 世界书阶段placement=3扫描 / 注入 world info 内容前;
- 推理阶段placement=4如有单独 reasoning prompt可在构建 reasoning 时处理。
3. **实现细节Go**
- 解析 JS 风格正则 `/pattern/flags`,支持 `i/m/s`
- 替换宏:
- `substitute_regex=1` → 用当前用户名称替换 `{{user}}`
- `=2` → 用当前角色名替换 `{{char}}`
- `trim_string`:按行定义需要从文本中删掉的子串。
- minDepth/maxDepth结合消息历史深度决定是否执行脚本。
- runOnEdit在“消息编辑接口”中也调用该脚本集。
- markdownOnly/promptOnly通过标志决定作用在 UI 文本 or prompt 文本。
#### 4.5 Prompt Pipeline 与对话 API
将现有/规划中的对话 API 统一收敛到一个 pipeline例如 `/app/conversation/:id/message`(或 `/conversations/:id/messages`,按统一风格即可):
1. **接收请求**
```json
{
"content": "用户输入文本",
"options": {
"presetId": "uuid-xxx",
"overrideModel": "gpt-4",
"stream": true
}
}
```
2. **管线步骤**
1. 根据 conversationId 加载:
- Conversation、AICharacter、Preset、AI 配置、世界书/正则等上下文;
- 合并 settings会话内 + 用户全局)。
2. 输入文本经过 regex 引擎placement=1
3. 写入一条 user message 到 DB。
4. 从最近 N 条消息构造 world info 触发文本,调用世界书引擎得到 entries。
5. 构建 prompt
- system角色 system + scenario + authorsNote + preset.systemPrompt + world info
- history最近若干 user/assistant 消息;
- preset.prompts按 depth/position 注入额外 messages。
6. 调用 AI 服务(依据 `/app/ai-config` 中 baseUrl/apiKey/model
- 支持流式:通过 SSE/WebSocket 对前端推送 tokens。
7. 将完整 assistant 输出经过 regex 引擎placement=2
8. 写入一条 assistant message 到 DB。
9. 返回响应给前端(同步返回最终内容 + 可选流式过程)。
---
### 5. 前端详细优化方案React `projects/web-app`
#### 5.1 角色卡管理CharacterManagePage
目标:完全对齐 ST 角色卡 V2/V3 字段,并为世界书/正则后端拆分打好基础。
优化点:
- **字段映射明确化**
- 确保 `Character` 接口与后端模型一致,并在注释中标明 ST 对应字段:
- `firstMes``first_mes`
- `mesExample``mes_example`
- `systemPrompt``system_prompt`
- `postHistoryInstructions``post_history_instructions`
- `characterBook``character_book`
- `extensions``extensions`
- **世界书编辑 UI 扩展**
- 目前 `WorldBookEntry` 较简化keys/content/enabled/insertion_order/position
- 建议增加更多高级属性编辑项(可以先折叠到“高级设置”中):
- `secondary_keys` / `constant` / `use_regex` / `case_sensitive` / `match_whole_words`
- `selective` / `selective_logic`
- `depth` / `order` / `probability` / `sticky` / `cooldown` / `delay` / `group`
- 保存时将这些字段一并放入 `characterBook.entries`,后端负责拆分入 DB。
- **导入/导出保持 ST 兼容**
- 当前上传/导出流程基本正确;随着后端增强,无需大改,只要保证前端不破坏 JSON 结构即可。
#### 5.2 预设管理PresetManagePage
目标:把当前的“前端内存预设”完全改造成**后端驱动的 ST 预设系统**。
实施步骤:
1. 后端按 `development-progress.md` 规划实现 `/app/preset` API
```text
POST /app/preset
GET /app/preset
GET /app/preset/:id
PUT /app/preset/:id
DELETE /app/preset/:id
POST /app/preset/import
GET /app/preset/:id/export
```
2. 前端新建 `src/api/preset.ts` 对应这些端点。
3.`PresetManagePage` 中的 `useState` 初始数据删除,改为:
- `const { data } = await presetApi.getPresetList()`
- 导入、导出、编辑、删除全部通过后端。
4. 预设 JSON 的读写字段与 ST 对齐:
- `temperature/top_p/top_k/frequency_penalty/presence_penalty/system_prompt/stop_sequences/...`
#### 5.3 聊天与设置面板ChatPage / SettingsPanel / CharacterPanel
目标:把“选 preset / 选世界书 / 选正则”的入口统一放到设置面板,并通过 `conversation.settings` 与后端 pipeline 串联。
建议:
-`SettingsPanel` 中增加:
- 当前会话使用的 preset 选择器(下拉 list来自 `/app/preset`)。
- 可选的世界书列表(来自未来的 `/app/worldbook`,初期可只展示角色内 worldbook
- 可选的全局正则脚本列表(来自未来的 `/app/regex`)。
- 保存时调用 `/app/conversation/:id/settings`(或 `PUT /app/conversation/:id`
```ts
settings: {
backgroundImage: string
themeColor: string
presetId?: string
activeWorldbookIds?: string[]
activeRegexIds?: string[]
}
```
- 发送消息时,前端不再参与世界书/正则逻辑,只负责传 `content`,后端从 conversation/preset/character 中解析所有配置。
---
### 6. 分阶段落地路线图(仅 Go + React
#### 阶段 1打通所有核心 CRUD 与数据流(短期)
- 后端:
- 巩固 `/app/character` 模块(确保 ST V2/V3 完全兼容)。
- 实现 `/app/preset` 模块CRUD + 导入/导出)。
- 设计并实现 `worldbooks/worldbook_entries``regex_scripts` 的数据结构与基础 API。
- 前端:
- 改造 `PresetManagePage` 接入真实 API。
-`CharacterManagePage` 中补全世界书 entry 字段,保持 ST 兼容。
#### 阶段 2实现 Prompt Pipeline 与对话 API中期
- 后端:
- 聚合世界书与正则逻辑,形成独立的 `chatPipeline` service。
-`/app/conversation/:id/message`(或 `/conversations/:id/messages`)中调用 pipeline完成一次“完整的 ST 风格对话请求”。
- 引入流式输出SSE 或 WebSocket
- 前端:
-`ChatPage/SettingsPanel` 中增加 preset/worldbook/regex 的选择与设置保存。
- 调整 ChatArea 接收流式输出并实时渲染。
#### 阶段 3高级特性与插件系统长期
- 后端:
- 引入插件概念(`plugins` 表 + manifest + hooks
- 实现插件执行沙箱WASM 或 goja并在 pipeline 中注入插件 hooks。
- 增加统计、日志与审计功能。
- 前端:
- 增加插件管理页面与可视化配置。
- 对接统计与调试视图(例如:查看某次回复中哪些 world info/regex/插件生效)。
---
### 7. 总结
- 你当前的 Go + React 系统已经完成:
- **用户系统**(认证/资料);
- **角色卡管理**(完整 ST V2 兼容导入/导出);
- **预设管理与对话 UI 的前端骨架**。
- 接下来最重要的三件事是:
- **在后端固化 ST 兼容的领域模型与 Prompt Pipeline**
- **让 `/app/conversation` 成为唯一的“对话真相源”React 只是 UI 壳**。