Compare commits

...

20 Commits

Author SHA1 Message Date
5520dafb56 🎨 添加文档和测试用例 2026-03-13 21:51:42 +08:00
cec46cabbe 🎨 优化登陆验证逻辑 2026-03-13 21:50:26 +08:00
16116a841d 添加独立管理后台 2026-03-13 21:12:58 +08:00
5e3380f5ef 🎉 更新系统版本 2026-03-13 21:12:17 +08:00
5ca65e3004 🎨 完善st兼容 && 完善支持前端卡 2026-03-13 20:27:11 +08:00
4cecfd6589 🎨 1.优化前端渲染功能(html和对话消息格式)
2.优化流式传输,新增流式渲染功能
3.优化正则处理逻辑
4.新增context budget管理系统
5.优化对话消息失败处理逻辑
6.新增前端卡功能(待完整测试)
2026-03-13 15:58:33 +08:00
c267b6c76e 🎨 优化前端渲染功能(html状态栏通信存在问题)
Signed-off-by: Echo <1711788888@qq.com>
2026-03-03 04:28:33 +08:00
cbb4034a91 🎨 优化正则模块和前端渲染功能
Signed-off-by: Echo <1711788888@qq.com>
2026-03-03 03:40:03 +08:00
3f8220340e 🎨 优化变量匹配
Signed-off-by: Echo <1711788888@qq.com>
2026-03-02 03:09:09 +08:00
0a7ebec780 📝 更新进度文档
Signed-off-by: Echo <1711788888@qq.com>
2026-03-02 02:56:13 +08:00
4100d908da 🐛 修复前端渲染的bug,优化html渲染和文本内容的显示格式
Signed-off-by: Echo <1711788888@qq.com>
2026-03-02 02:55:55 +08:00
d0582f6aad 📝 更新进度文档,填写未测试的功能模块
Signed-off-by: Echo <1711788888@qq.com>
2026-03-02 01:17:21 +08:00
a857917b53 🎨 引入mvu变量和全局正则到ai对话
Signed-off-by: Echo <1711788888@qq.com>
2026-03-02 01:15:22 +08:00
20d99cf3bf 🎨 优化对话消息前端渲染(未完成存在bug)
Signed-off-by: Echo <1711788888@qq.com>
2026-03-02 01:14:42 +08:00
8888d9ea85 🎨 新增正则管理 和 全局正则功能
Signed-off-by: Echo <1711788888@qq.com>
2026-03-02 01:14:16 +08:00
2b8be78fdc 🎨 引入mvu功能
Signed-off-by: Echo <1711788888@qq.com>
2026-03-02 01:13:58 +08:00
de6015c77e 🎨 完善正则脚本功能
Signed-off-by: Echo <1711788888@qq.com>
2026-03-02 00:51:23 +08:00
23396caeeb 🎨 优化前端渲染
Signed-off-by: Echo <1711788888@qq.com>
2026-03-02 00:51:11 +08:00
fd660c8804 🎨 优化角色卡编辑功能,新增正则编辑和测试
Signed-off-by: Echo <1711788888@qq.com>
2026-03-02 00:50:56 +08:00
aa461ec6c3 📝 更新进度文档
Signed-off-by: Echo <1711788888@qq.com>
2026-03-02 00:50:31 +08:00
323 changed files with 50621 additions and 1309 deletions

5
.gitignore vendored
View File

@@ -24,6 +24,7 @@ dist-ssr
*.sw?
uploads
#docs
.claude
#.claude
plugs
sillytavern
sillytavern
st

105
CLAUDE.md Normal file
View File

@@ -0,0 +1,105 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
云酒馆 (SillyTavern Cloud) — a full-stack AI role-playing chat platform. Frontend is React + TypeScript + Tailwind + Vite (`web-app/`). Backend is Go + Gin + GORM (`server/`). The two are fully separated and communicate via REST API + SSE streaming.
## Build & Run Commands
### Frontend (`web-app/`)
```bash
npm install
npm run dev # Dev server at http://localhost:5174
npm run build # tsc + vite build (type-check then bundle)
npm run preview # Preview production build
```
### Backend (`server/`)
```bash
go mod tidy
go build -o server .
./server -c config.yaml # Runs at http://localhost:8888
```
### Type-check only (no emit)
```bash
# Frontend
cd web-app && npx tsc --noEmit
# Backend
cd server && go build ./...
```
## Build Verification
After any code edit, always run the build/compile step before reporting success. For Go files: `go build ./...`. For TypeScript files: `npx tsc --noEmit`. Never assume edits are correct without verification.
## Architecture
### Backend (`server/`)
Layered architecture: **routes → API handlers → services → models/DB**
```
api/v1/app/ # App-facing HTTP handlers (auth, character, conversation, ai_config, etc.)
api/v1/system/ # Admin/system handlers
service/app/ # Business logic layer
model/app/ # GORM data models (AppUser, AiCharacter, Conversation, Preset, Worldbook, RegexScript)
router/app/ # Gin route registration
middleware/ # JWT auth, CORS, logging, recovery
initialize/ # DB, Redis, router, plugin setup (called from main.go)
core/ # Zap logging, Viper config loading, server startup
```
Key models: `AppUser`, `AiCharacter` (SillyTavern V2 card format), `Conversation`, `Preset` (sampling params), `Worldbook` (keyword-triggered context), `RegexScript` (message transforms).
AI providers: OpenAI-compatible + Anthropic. Unified through the AI config service. Conversations use SSE streaming for real-time token delivery.
### Frontend (`web-app/src/`)
MVU (Model-View-Update) pattern via **Zustand** with `persist` middleware (localStorage).
```
store/index.ts # Single global Zustand store — source of truth for user, currentCharacter,
# currentConversation, messages, variables, UI state
api/ # One file per domain: auth, character, conversation, preset, worldbook, regex
# All share the axios instance in api/client.ts (injects JWT, handles 401)
pages/ # Route-level components (ChatPage, CharacterManagePage, AdminPage, etc.)
components/ # Reusable UI — ChatArea, CharacterPanel, SettingsPanel, Sidebar, Navbar,
# MessageContent (renders markdown + HTML safely via rehype-sanitize)
```
Routes are defined in `App.tsx`. Auth guard redirects to `/login` when `isAuthenticated` is false.
The `variables: Record<string, string>` field in the store implements the MVU variable system used for template substitution in AI prompts (e.g., `{{user}}`, `{{char}}`).
### API Convention
- Base URL: `http://localhost:8888` (configured via `VITE_API_BASE_URL` in `.env.development`)
- Auth: `Authorization: Bearer <jwt>` header, injected automatically by `api/client.ts`
- App routes: `/app/auth/...`, `/app/user/...`, `/app/character/...`, `/app/conversation/...`
- Admin routes: `/system/...` (Casbin RBAC enforced)
## Go Development
When editing Go files, always check all call sites of modified functions. If a function signature changes (parameters or return values), grep for all usages and update them in the same edit batch.
Run `go build ./...` from `server/` to verify after every edit.
## Frontend Development
When editing Vue/TypeScript frontend files, verify all imports are present and no duplicate code remains after edits. Read the file back after editing to confirm structural integrity.
Run `npx tsc --noEmit` from `web-app/` to verify after every edit.
## Third-Party Integrations
When integrating third-party SDKs or APIs (payment providers, AI model APIs), read the actual SDK source or docs first before writing code. Do not guess method names, parameter types, or key sizes.
## Configuration
Backend config file: `server/config.yaml` (not committed; copy from `config.example.yaml` if present). Supports MySQL, PostgreSQL, SQLite, SQL Server, Oracle via GORM. Redis for sessions/caching.
Frontend env files: `web-app/.env.development` and `web-app/.env.production` — set `VITE_API_BASE_URL`.

BIN
docs/Clannad_v3.1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

View File

@@ -0,0 +1,357 @@
# Clannad_v3.1.png 案例分析与落地要点
## 目标角色卡概览
- **图像**@docs/Clannad_v3.1.png图片来源、版权信息见下方字段
- **风格**Clannad 系列日系校园少女题材,具有较强的情感表达与背景设定需求
- **已知需求**:在云酒馆系统中以“前端卡”方式呈现,并能注入背景信息、变量、互动 UI
---
## 10.1 数据字段与数据模型需求
| 字段名 | 说明 | 备注 |
|--------|------|------|
| portrait_url | 图片主地址 | 建议使用 CDN 托管,支持 webp/AVIF 高效格式 |
| portrait_alt | 无障碍文本描述 | 供屏幕阅读器使用,描述角色外观特征 |
| thumbnail_url | 缩略图地址 | 列表页展示使用,建议 200x200 以内 |
| portrait_caption | 图片简短说明 | 显示在图片下方的简短描述 |
| image_source | 版权信息/出处 | 记录图片来源、授权信息 |
| worldbook_id | 关联的 Worldbook | 用于注入背景设定 |
| worldbook_entries | 世界书条目集合 | 用于背景信息注入 |
| name | 角色名称 | 现有字段 |
| description | 角色描述 | 现有字段 |
| personality | 性格设定 | 现有字段 |
| scenario | 场景设定 | 现有字段 |
| first_mes | 开场白 | 现有字段 |
| mes_example | 对话示例 | 现有字段 |
| system_prompt | 系统提示词 | 现有字段 |
| extensions | 扩展数据 | MVU 相关字段 |
| variables | 变量定义 | MVU 相关字段 |
---
## 10.2 后端改动要点
### 10.2.1 数据模型扩展
`model/app/ai_character.go` 中新增以下字段:
```go
type AICharacter struct {
// ... 现有字段 ...
// 图片相关字段
PortraitURL string `json:"portraitUrl" gorm:"type:varchar(500)"`
PortraitAlt string `json:"portraitAlt" gorm:"type:varchar(200)"`
ThumbnailURL string `json:"thumbnailUrl" gorm:"type:varchar(500)"`
PortraitCaption string `json:"portraitCaption" gorm:"type:varchar(200)"`
ImageSource string `json:"imageSource" gorm:"type:varchar(500)"` // 版权信息
// Worldbook 关联
WorldbookID *uint `json:"worldbookId"`
}
```
### 10.2.2 API 接口
- **上传图片**`POST /api/v1/app/character/:id/portrait`
- 鉴权:需要用户登录
- 校验:文件大小(建议最大 5MB、格式支持 jpg/png/webp/gif
- 返回portrait_url、thumbnail_url
- **获取角色图片信息**`GET /api/v1/app/character/:id`
- 返回字段包含 portrait_url、portrait_alt、thumbnail_url、portrait_caption、image_source
### 10.2.3 Worldbook 关联增强
`conversation.go` 的 system prompt 构建流程中,增加对角色关联 Worldbook 的背景注入:
- 读取 `character.WorldbookID`
- 通过 WorldbookEngine 查询对应的启用条目
- 将背景信息注入到 system_prompt 中(需控制 token budget
### 10.2.4 数据迁移
为现有角色卡补充默认字段值:
```sql
ALTER TABLE ai_characters
ADD COLUMN IF NOT EXISTS portrait_url VARCHAR(500),
ADD COLUMN IF NOT EXISTS portrait_alt VARCHAR(200),
ADD COLUMN IF NOT EXISTS thumbnail_url VARCHAR(500),
ADD COLUMN IF NOT EXISTS portrait_caption VARCHAR(200),
ADD COLUMN IF NOT EXISTS image_source VARCHAR(500),
ADD COLUMN IF NOT EXISTS worldbook_id UINT REFERENCES worldbooks(id);
```
---
## 10.3 前端实现要点
### 10.3.1 角色卡片组件
`web-app/src/components/` 中新增或扩展角色卡片组件:
```tsx
interface CharacterCardProps {
portraitUrl?: string;
portraitAlt?: string;
thumbnailUrl?: string;
portraitCaption?: string;
// ... 其他字段
}
const CharacterPortrait: React.FC<CharacterCardProps> = ({
portraitUrl,
portraitAlt,
thumbnailUrl,
portraitCaption
}) => {
if (!portraitUrl) return null;
return (
<div className="character-portrait">
<img
src={portraitUrl}
alt={portraitAlt || '角色图像'}
loading="lazy"
className="portrait-image"
/>
{portraitCaption && (
<p className="portrait-caption">{portraitCaption}</p>
)}
</div>
);
};
```
### 10.3.2 样式隔离
使用 CSS Modules 或在组件级别定义样式:
```css
/* CharacterPortrait.module.css */
.character-portrait {
display: flex;
flex-direction: column;
align-items: center;
}
.portrait-image {
max-width: 300px;
border-radius: 8px;
}
.portrait-caption {
font-size: 14px;
color: #666;
margin-top: 8px;
}
```
### 10.3.3 沙箱渲染
对于需要执行脚本的前端卡,使用 iframe 沙箱:
```tsx
const FrontendCardSandbox: React.FC<{ htmlContent: string }> = ({ htmlContent }) => {
return (
<iframe
srcDoc={htmlContent}
sandbox="allow-scripts"
title="前端卡内容"
className="frontend-card-iframe"
/>
);
};
```
### 10.3.4 postMessage 通信
定义消息类型:
```ts
// types/frontend-card.ts
interface FrontendCardMessage {
type: 'GET_VARIABLE' | 'SET_VARIABLE' | 'SEND_MESSAGE' | 'TRIGGER_ACTION';
payload: Record<string, any>;
}
interface FrontendCardResponse {
type: 'VARIABLE_VALUE' | 'ACTION_RESULT' | 'ERROR';
payload: Record<string, any>;
}
```
---
## 10.4 Worldbook 与背景注入
### 10.4.1 背景信息转 Worldbook 条目
将 Clannad 角色的背景故事、人物关系、剧情要点等转换为 Worldbook 条目:
```json
{
"keys": ["冈崎汐", "汐", "女儿"],
"content": "冈崎汐主人公冈崎朋也的女儿。5岁时因事故去世后来以幽灵的形式再次出现在父亲面前...",
"enabled": true,
"constant": true,
"position": 1,
"order": 1
}
```
### 10.4.2 Token Budget 控制
`buildContextManagedSystemPrompt` 中,对 Worldbook 注入的内容进行 token 预算控制:
- **优先级**Worldbook 触发条目 > CharacterBook 内嵌条目 > MesExample
- **预算分配**system_prompt 总预算的 20-30% 用于 Worldbook 背景注入
- **截断策略**:超出预算时,从最低优先级条目开始丢弃
---
## 10.5 安全性与合规
### 10.5.1 图片安全
- **来源限制**:只允许从受信任的 CDN 或本地上传
- **内容审查**:上传图片需经过内容审核(可接入第三方审核服务)
- **元数据清理**:清理图片 EXIF 信息,防止泄露拍摄设备、地点等隐私
### 10.5.2 脚本安全
- **沙箱隔离**:前端卡内的 JavaScript 必须在 iframe 沙箱中运行
- **API 白名单**:只暴露受限的 API 接口,映射到后端受控服务
- **CSP 策略**
```
Content-Security-Policy:
default-src 'self';
script-src 'self' 'unsafe-inline' 'unsafe-eval' blob:;
style-src 'self' 'unsafe-inline';
img-src 'self' https://cdn.example.com data:;
connect-src 'self' https://api.example.com;
```
### 10.5.3 审核流程
- **新卡审核**:用户上传新角色卡后,需经过管理员审核
- **定期检查**:对已上线卡片进行定期安全扫描
- **举报机制**:提供用户举报入口
---
## 10.6 迁移策略
### 10.6.1 MVP 阶段(第一阶段)
- 只引入 portrait_url、portrait_alt、thumbnail_url 字段
- 前端仅展示图片,不改变现有对话逻辑
- 不支持前端卡脚本执行
### 10.6.2 进阶阶段(第二阶段)
- 接入 Worldbook 背景注入
- 支持 portrait_caption 显示
- 引入基础的 iframe 沙箱渲染
### 10.6.3 完整阶段(第三阶段)
- 支持完整的前端卡脚本执行
- 实现 postMessage 通信
- 支持 RPG、VN 等复杂交互系统
### 10.6.4 回滚策略
- 每个阶段保留回滚开关
- 遇到兼容性问题时,可一键回退到上一阶段
- 数据库字段支持版本标记
---
## 10.7 验证与测试
### 10.7.1 功能测试
| 测试用例 | 预期结果 |
|---------|---------|
| 上传角色图片 | 图片正确保存,返回正确的 portrait_url |
| 图片加载失败 | 显示默认占位图alt 文本正确 |
| 无图片角色卡 | 正常显示,不崩溃 |
| Worldbook 注入 | 背景信息正确注入到对话上下文 |
| 懒加载图片 | 首屏不加载,滚动后加载 |
### 10.7.2 安全性测试
| 测试用例 | 预期结果 |
|---------|---------|
| XSS 注入(图片描述) | 被转义,不执行 |
| 恶意脚本(前卡内) | 在沙箱中被阻止,无法访问父窗口 |
| 跨域请求 | 被 CSP 拦截 |
### 10.7.3 性能测试
| 指标 | 目标 |
|-----|------|
| 图片首次加载 | < 1s5G 网络 |
| 懒加载触发 | 视口进入后 200ms |
| 内存占用 | 前端卡渲染时 < 50MB 额外 |
---
## 10.8 版权信息与来源
### 10.8.1 必填字段
- **image_source**图片来源官方美术集」「同人创作」)
- **license_type**许可证类型MIT」「CC BY-SA 4.0」「原创」)
- **original_artist**原作者名称若为同人创作
---
## 10.9 附录/接口示例
### 10.9.1 角色图片上传 API
```bash
# 请求
POST /api/v1/app/character/1/portrait
Authorization: Bearer <token>
Content-Type: multipart/form-data
# 响应
{
"portraitUrl": "https://cdn.example.com/characters/1/portrait.webp",
"thumbnailUrl": "https://cdn.example.com/characters/1/thumb.webp",
"portraitAlt": "粉色头发的少女,穿着校服"
}
```
### 10.9.2 角色详情 API 扩展
```bash
# GET /api/v1/app/character/1
{
"id": 1,
"name": "冈崎汐",
"portraitUrl": "https://cdn.example.com/characters/1/portrait.webp",
"portraitAlt": "粉色头发的少女,穿着校服",
"thumbnailUrl": "https://cdn.example.com/characters/1/thumb.webp",
"portraitCaption": "Clannad 主角冈崎朋也的女儿",
"imageSource": "官方美术集",
"worldbookId": 5,
// ... 其他字段
}
```
---
## 结论
通过独立分析文档分离便于版本管理与跨团队协作请在实现过程中严格遵循安全沙箱与版权规范确保云酒馆的扩展能力可控且可维护
本落地要点覆盖了从数据模型后端 API前端渲染Worldbook 集成安全合规到迁移测试的完整流程建议按 MVP 进阶 完整三阶段逐步落地

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,6 +1,6 @@
# 云酒馆项目开发进度
> 最后更新2026-02-27
> 最后更新2026-03-02
## 项目概述
@@ -20,9 +20,9 @@
| 角色卡管理 | ✅ 已完成 | 100% | - | 完全兼容 ST V2前后端已打通 |
| 预设管理 | ✅ 已完成 | 100% | - | 前后端已打通,支持导入导出 |
| 对话系统 | ✅ 已完成 | 95% | - | 预设切换、消息重新生成已完成 |
| AI 集成 | 🚧 进行中 | 80% | - | AI 配置管理、API 调用已完成 |
| 世界书系统 | 📋 待开发 | 0% | - | 规划中 |
| 正则脚本 | 📋 待开发 | 0% | - | 规划中 |
| AI 集成 | ✅ 已完成 | 100% | - | AI 配置管理、API 调用已完成 |
| 世界书系统 | ✅ 已完成 | 100% | - | 前后端已打通,触发引擎已集成到对话系统 |
| 正则脚本 | ✅ 已完成 | 95% | - | 完全兼容 ST已集成到对话流程 |
**图例**
- ✅ 已完成
@@ -456,8 +456,8 @@ web-app/src/
- ✅ 世界书搜索和筛选
- ✅ 用户权限控制(只能编辑自己的世界书)
- ✅ 条目计数自动更新
- 📋 关键词触发算法(待与对话系统集成
- 📋 与对话系统集成(待实现
- 关键词触发引擎WorldbookEngine
- 与对话系统完全集成(非流式 + 流式
**文件清单**
```
@@ -467,6 +467,7 @@ server/
├── model/app/request/worldbook.go # 请求结构
├── model/app/response/worldbook.go # 响应结构
├── service/app/worldbook.go # 业务逻辑
├── service/app/worldbook_engine.go # 关键词触发引擎
└── router/app/worldbook.go # 路由配置
```
@@ -511,53 +512,211 @@ web-app/src/
- ✅ 错误处理和用户提示
- ✅ 登录状态检查
### 6.4 待完善功能
### 6.4 对话系统集成(已完成 - 2026-02-27
- 📋 与对话系统集成(在对话中激活世界书)
- 📋 关键词触发引擎实现
- 📋 选择性触发逻辑AND/NOT
- 📋 深度控制和位置注入
- 📋 概率触发机制
**触发引擎**`service/app/worldbook_engine.go`
- `WorldbookEngine.ScanAndTrigger` - 扫描消息历史并触发匹配条目
- `WorldbookEngine.shouldTrigger` - 逐条检查触发条件constant/keys/scanDepth
- `WorldbookEngine.matchKeys` - 关键词匹配useRegex/caseSensitive/matchWholeWords
- `WorldbookEngine.BuildPromptWithWorldbook` - 将触发条目注入到系统提示词
**集成点**
-`conversation.go:callAIService` - 非流式消息世界书注入
-`conversation.go:SendMessageStream` - 流式消息世界书注入
**执行流程**
```
用户消息 → 扫描最近 N 条消息 → 匹配关键词 → 触发条目排序 → 注入系统提示词 → AI 处理
```
**设置面板**`components/SettingsPanel.tsx`
- ✅ 世界书开关(启用/禁用)
- ✅ 世界书选择器(下拉列表,显示条目数量)
- ✅ 保存到 `conversation.worldbookId``conversation.worldbookEnabled`
### 6.5 待完善功能
- 📋 概率触发机制probability 字段已存储,引擎待实现)
- 📋 递归扫描
- 📋 角色绑定世界书
- 📋 角色绑定世界书Character Book
- 📋 全局世界书
---
## 七、正则脚本系统 📋
## 七、正则脚本系统
### 7.1 待实现功能(规划中
### 7.1 后端 API已完成 - 2026-03-02
**数据模型**
- 📋 `RegexScript` - 正则脚本模型
- 📋 支持全局、角色、预设三种作用域
- 📋 完全兼容 SillyTavern 正则脚本格式
- `RegexScript` - 正则脚本模型(完全兼容 SillyTavern 格式)
- 支持全局、角色、预设三种作用域
- ✅ 使用 JSONB 存储扩展字段和修剪字符串数组
**实现字段**
- 📋 find_regex/replace_with - 查找和替换表达式
- 📋 trim_string - 修剪字符串
- 📋 placement - 执行阶段(输入/输出/世界书/推理)
- 📋 disabled/markdown_only/run_on_edit/prompt_only - 执行选项
- 📋 substitute_regex - 宏替换({{user}}/{{char}}
- 📋 min_depth/max_depth - 深度控制
- 📋 scope/owner_char_id/owner_preset_id - 作用域管理
**实现字段**
- ✅ name - 脚本名称
- ✅ find_regex - 查找正则表达式
- ✅ replace_with - 替换字符串
- ✅ trim_strings - 修剪字符串数组
- ✅ placement - 执行阶段0=输入/1=输出/2=世界书/3=显示
- ✅ disabled - 禁用标志
- ✅ markdown_only - 仅 Markdown 模式
- ✅ run_on_edit - 编辑时运行
- ✅ prompt_only - 仅提示词模式
- ✅ substitute_regex - 宏替换({{user}}/{{char}}
- ✅ min_depth/max_depth - 深度控制
- ✅ scope - 作用域0=全局/1=角色/2=预设)
- ✅ owner_char_id/owner_preset_id - 所有者 ID
- ✅ order - 执行顺序
**计划 API 端点**
**API 端点**
-`POST /app/regex` - 创建正则脚本
-`GET /app/regex` - 获取脚本列表(分页、搜索、作用域筛选)
-`GET /app/regex/:id` - 获取脚本详情
-`PUT /app/regex/:id` - 更新脚本
-`DELETE /app/regex/:id` - 删除脚本
-`POST /app/regex/test` - 测试脚本执行
**核心功能**
- ✅ 完全兼容 SillyTavern 正则脚本格式
- ✅ 多阶段脚本执行(输入/输出/世界书/显示)
- ✅ 正则表达式解析和执行
- ✅ 宏替换系统({{user}}/{{char}}/{{original}}
- ✅ 深度控制和条件执行
- ✅ 与对话系统集成
- ✅ 角色卡导入时自动导入正则脚本
- ✅ 角色卡导出时自动导出正则脚本
**文件清单**
```
POST /app/regex # 创建正则脚本
GET /app/regex # 获取脚本列表
GET /app/regex/:id # 获取脚本详情
PUT /app/regex/:id # 更新脚本
DELETE /app/regex/:id # 删除脚本
POST /app/regex/:id/test # 测试脚本
server/
├── api/v1/app/regex_script.go # API 控制器
├── model/app/regex_script.go # 正则脚本模型
├── model/app/request/regex_script.go # 请求结构
├── model/app/response/regex_script.go # 响应结构
├── service/app/regex_script.go # 业务逻辑和执行引擎
├── router/app/regex_script.go # 路由配置
└── service/app/character.go # 角色卡导入导出集成
```
**核心功能规划**
- 📋 多阶段脚本执行(输入/输出/世界书/推理)
- 📋 正则表达式解析和执行
- 📋 宏替换系统
- 📋 深度控制和条件执行
- 📋 与对话系统集成
### 7.2 前端页面(已完成 - 2026-03-02
**功能特性**
- ✅ 角色卡管理页面集成正则脚本编辑
- ✅ 导入 JSON 正则脚本(兼容 SillyTavern 格式)
- ✅ 正则脚本创建和编辑
- ✅ 正则脚本列表展示
- ✅ 正则脚本删除
- ✅ 执行阶段选择(输入/输出/世界书/显示)
- ✅ 执行顺序配置
- ✅ 启用/禁用开关
- ✅ 作用域管理(全局/角色/预设)
**文件清单**
```
web-app/src/
├── api/regex.ts # 正则脚本 API 封装
├── pages/CharacterManagePage.tsx # 角色卡管理页面(集成正则脚本)
└── components/MessageContent.tsx # 消息渲染组件HTML/脚本渲染)
```
### 7.3 对话系统集成(已完成 - 2026-03-02
**集成点**
- ✅ 输入阶段Placement 0- 用户消息发送前处理
- ✅ 输出阶段Placement 1- AI 回复生成后处理
- ✅ 显示阶段Placement 3- 前端渲染前处理
- ✅ 开场白消息处理 - 角色首条消息应用正则脚本
**实现位置**
-`conversation.go:SendMessage` - 非流式消息发送
-`conversation.go:SendMessageStream` - 流式消息发送
-`conversation.go:CreateConversation` - 创建对话时处理开场白
**执行流程**
```
用户输入 → 输入阶段脚本 → AI 处理 → 输出阶段脚本 → 保存数据库 → 显示阶段脚本 → 前端渲染
```
### 7.4 前端渲染优化(已完成 - 2026-03-02
**HTML 渲染支持**
- ✅ 自动提取 markdown 代码块中的 HTML```html...```
- ✅ 移除 markdown 代码块标记(``` 符号)
- ✅ 自动启用脚本渲染(无需手动点击按钮)
- ✅ iframe 沙箱渲染(支持 JavaScript
- ✅ 直接 HTML 渲染(无脚本内容)
- ✅ 统一容器样式(避免宽高变化)
- ✅ 响应式宽度适配(强制覆盖固定宽度)
**选项按钮支持**
- ✅ 自动解析选项格式A:/A./A、/A
- ✅ 渲染为可点击按钮
- ✅ 点击后自动发送选项内容
- ✅ 支持多种选项格式
**文件位置**
-`MessageContent.tsx:173-243` - HTML 提取和处理逻辑
-`MessageContent.tsx:246-290` - iframe 渲染逻辑
-`MessageContent.tsx:348-372` - 渲染模式切换
### 7.5 全局正则脚本管理(已完成 - 2026-03-02
**功能特性**
- ✅ 独立的全局正则脚本管理页面 `/regex-scripts`
- ✅ 创建、编辑、删除全局正则脚本
- ✅ 导入导出 JSON 格式脚本(兼容 SillyTavern
- ✅ 只管理 scope=0 的全局脚本
- ✅ 支持批量导出所有脚本
**文件位置**
-`web-app/src/pages/RegexScriptManagePage.tsx` - 全局正则脚本管理页面
-`web-app/src/App.tsx` - 添加路由配置
### 7.6 变量系统扩展(已完成 - 2026-03-02
**后端新增变量** (`server/service/app/regex_script.go`):
-`{{time_12h}}` - 12小时制时间
-`{{date_short}}` - 短日期格式 (MM/DD/YY)
-`{{weekday}}` - 星期几
-`{{month}}` - 月份名称
-`{{year}}` - 年份
-`{{pick:option1|option2|option3}}` - 随机选择一个选项
-`{{tab}}` - 制表符
-`{{space}}` - 空格
-`{{empty}}` - 空字符串
**前端变量系统** (`web-app/src/store/index.ts`):
- ✅ 同步支持所有后端变量
-`substituteVariables()` 函数可在前端替换变量
-`extractVariables()` 函数可提取文本中的变量
### 7.7 MVU 状态管理架构(已完成 - 2026-03-02
**核心实现** (`web-app/src/store/index.ts`):
- ✅ 基于 Zustand 实现完整的 MVU 架构
- ✅ Model: 用户、角色、对话、消息、UI、变量状态
- ✅ Update: 完整的状态更新操作
- ✅ View: 状态选择器Selectors
- ✅ 持久化存储localStorage
- ✅ DevTools 支持
**页面集成**:
-`ChatPage.tsx` - 集成 MVU store 管理对话状态
-`CharacterManagePage.tsx` - 导入 MVU store
- ✅ 角色切换时自动更新变量系统
**工具函数**:
-`substituteVariables()` - 替换文本中的变量
-`extractVariables()` - 提取文本中的变量
### 7.8 待完善功能
- 📋 世界书阶段Placement 2脚本执行
- 📋 正则脚本测试界面
- 📋 正则脚本模板库
- 🧪 全局正则脚本功能测试
- 🧪 扩展变量系统测试
- 🧪 MVU store 集成测试
---
@@ -658,15 +817,123 @@ web-app/src/pages/
- 实现对话历史存储
-**完成对话系统前后端对接**
-**完成 AI 配置管理模块**
-**完成世界书系统**
- 实现世界书 CRUD 操作
- 实现世界书条目管理
- 实现导入导出功能
- 完全兼容 SillyTavern 格式
-**编写 SillyTavern 完全兼容优化方案**
-**更新开发进度文档**
### 下一阶段规划
### 2026-02-27
-**完成世界书与对话系统集成**
- 实现 WorldbookEngine 关键词触发引擎
- 集成到非流式callAIService和流式SendMessageStream消息路径
- 实现选择性触发逻辑constant/keys/useRegex/caseSensitive/matchWholeWords
- 实现 SettingsPanel 世界书选择器(启用开关 + 世界书下拉)
-**修复前端消息渲染 Bug**
- 修复通用代码块正则误判导致消息内容消失的问题(恢复仅识别 \`\`\`html 标识符)
- 修复 iframe 宽度坍缩为 300px 问题(助手消息列从 max-w-[70%] 改为 w-[70%]
- 保留 HTML 代码块渲染中的 ``` → `<pre><code>` 转换逻辑
### 2026-03-02
-**完成正则脚本系统后端 API**
- 实现正则脚本 CRUD 操作
- 实现多阶段脚本执行引擎
- 实现宏替换系统({{user}}/{{char}}/{{original}}
- 完全兼容 SillyTavern 正则脚本格式
-**完成正则脚本前端管理界面**
- 集成到角色卡管理页面
- 支持导入导出正则脚本
- 支持脚本创建、编辑、删除
-**完成正则脚本与对话系统集成**
- 输入阶段脚本处理(用户消息)
- 输出阶段脚本处理AI 回复)
- 显示阶段脚本处理(前端渲染)
- 开场白消息脚本处理
-**完成前端 HTML 渲染优化**
- 自动提取和渲染 markdown 代码块中的 HTML
- 移除 markdown 代码块标记
- 自动启用脚本渲染
- iframe 沙箱渲染支持
- 响应式宽度适配
-**完成选项按钮功能**
- 自动解析选项格式
- 渲染为可点击按钮
- 点击后自动发送
-**修复关键 Bug**
- 修复 GetScriptsForPlacement SQL 查询 bug
- 修复流式消息未应用正则脚本问题
- 修复 HTML 渲染宽度问题
- 修复 markdown 符号残留问题
### 2026-03-02今天完成
-**完成正则脚本前端 HTML 渲染优化**
- 修复 HTML 代码块渲染问题
- 移除 markdown ``` 符号残留
- 自动启用脚本渲染
- 统一 iframe 和直接 HTML 渲染的容器样式
- 修复宽度适配问题
-**完成全局正则脚本管理功能**
- 创建独立的全局正则脚本管理页面 `/regex-scripts`
- 支持创建、编辑、删除、导入、导出全局脚本
- 完全兼容 SillyTavern 格式
-**扩展变量系统**
- 后端新增 10+ 个变量类型
- 支持时间格式化、随机选择、特殊字符等
- 前端同步支持所有变量
-**实现 MVU 状态管理架构**
- 基于 Zustand 实现完整的 MVU 模式
- 集成到 ChatPage 和 CharacterManagePage
- 支持持久化存储和 DevTools
- 内置变量替换工具函数
-**完成混合内容渲染系统**
- 实现 HTML 和文本混合渲染(保持原始顺序)
- 支持代码块自动识别和转换
- 文本内容正确换行显示
- 对白内容自动高亮(紫色加粗)
- 支持多种引号格式:`""``"``「」``『』``''`
-**修复关键 Bug**
- 修复开场白消息未应用正则脚本问题
- 修复 API 导出名称不匹配导致页面空白问题
- 修复 HTML 内容渲染失效问题
- 修复文本换行和对白高亮不生效问题
- 修复内容渲染顺序错误问题HTML 被提取到顶部)
### 待测试功能(下次工作)
- 🧪 **全局正则脚本功能测试**
- 测试全局脚本在所有对话中的应用
- 测试导入导出功能
- 测试脚本执行顺序
- 🧪 **扩展变量系统测试**
- 测试所有新增变量类型
- 测试 `{{pick:}}` 随机选择功能
- 测试时间格式化变量
- 🧪 **MVU Store 集成测试**
- 测试状态持久化
- 测试变量系统自动更新
- 测试跨页面状态同步
- 🧪 **混合内容渲染测试**
- 测试 HTML 和文本混合渲染
- 测试对白高亮效果
- 测试各种引号格式
- 测试内容顺序保持
- 测试所有新增变量类型
- 测试 `{{pick:}}` 随机选择功能
- 测试时间格式化变量
- 🧪 **MVU Store 集成测试**
- 测试状态持久化
- 测试变量系统自动更新
- 测试跨页面状态同步
- 🧪 **HTML 渲染优化测试**
- 测试 markdown 代码块中的 HTML 渲染
- 测试 iframe 宽度自适应
- 测试脚本自动启用功能
- ✅ 实现实际 AI API 调用OpenAI/Anthropic
- 📋 实现世界书系统(完全兼容 SillyTavern
- 📋 实现正则脚本系统(完全兼容 SillyTavern
- 📋 实现 Prompt Pipeline世界书触发、正则处理、提示词构建
- 📋 完善对话系统高级功能(消息重新生成、编辑)
- 实现世界书系统(完全兼容 SillyTavern,包含触发引擎和对话集成
- 实现正则脚本系统(完全兼容 SillyTavern
- 实现 Prompt Pipeline世界书触发注入、正则处理、提示词构建)
- 📋 完善对话系统高级功能(消息编辑)
- 📋 性能优化与测试
- 📋 部署上线
@@ -682,21 +949,22 @@ web-app/src/pages/
5. ✅ 完善对话系统高级功能(消息重新生成)
### 中期目标(本月)
1. 📋 实现世界书系统(完全兼容 SillyTavern
- 数据模型设计
- 关键词触发算法
- 选择性触发逻辑
- 与对话系统集成
2. 📋 实现正则脚本系统(完全兼容 SillyTavern
- 数据模型设计
- 多阶段脚本执行
- 正则表达式引擎
- 宏替换系统
3. 📋 实现 Prompt Pipeline
- 世界书扫描与注入
- 正则脚本处理
- 提示词构建优化
1. 实现世界书系统(完全兼容 SillyTavern
- 数据模型设计
- 关键词触发算法WorldbookEngine
- 选择性触发逻辑
- 与对话系统集成
2. 实现正则脚本系统(完全兼容 SillyTavern
- 数据模型设计
- 多阶段脚本执行
- 正则表达式引擎
- 宏替换系统
3. 实现 Prompt Pipeline
- 世界书扫描与注入
- 正则脚本处理
- 提示词构建优化
4. 📋 基础功能测试与优化
5. 📋 消息编辑功能
### 长期目标(下月)
1. 📋 插件系统设计与实现

456
docs/flutter参考.md Normal file
View File

@@ -0,0 +1,456 @@
### NativeTavern 功能设计文档(供 Go + React 集成参考)
---
## 总览
本项目中,这几个模块的作用和依赖关系如下:
- **聊天功能Chat**统一负责从「用户消息」到「LLM 调用」再到「写回消息」的完整流程。
- **提示词管理Prompt Management**:把系统提示、人设、世界信息、作者注释、历史等拆成可配置的「段」,决定顺序与注入深度。
- **宏系统Macros**:在构建 prompt 时,根据上下文展开 `{{user}}``{{char}}``{{time}}``{{random}}` 等占位符。
- **变量系统Variables**:提供全局 / 每会话变量 + `{{setvar}}` / `{{getvar}}` 等变量宏,支持状态机、计数等逻辑。
- **思维链支持Chain-of-Thought / Reasoning**:对 o1/o3、Claude、Gemini 等的推理内容单独解析、存储,并在前端单独渲染。
Go + React 中建议同样分层:**数据模型DB + 领域服务Go + 状态/UIReact**。
---
## 一、变量系统Variables System
### 1.1 目标
- 支持两类变量:
- **全局变量**:跨所有聊天共用,用户级。
- **本地变量**:每个 chat 独立,存到 chat metadata。
- 通过宏语法在文本中读写变量,如:
- `{{setvar::score::10}}`
- `{{incvar::turn}}`
- `{{getvar::user_name}}`
### 1.2 数据模型(示例)
后端可参考:
```go
// 全局变量:按 userId 存
type GlobalVariables map[string]any // name -> value
// 本地变量:按 chatId 存
type LocalVariables map[string]map[string]any // chatId -> (name -> value)
type ChatMetadata struct {
Variables map[string]any `json:"variables,omitempty"`
// ... 其他 metadata 字段 ...
}
```
- 全局变量:存 Redis / Postgres JSONB / KV按 userId 分片。
- 本地变量:挂在 `chats.metadata.variables` 字段。
### 1.3 变量服务接口Go
```go
type VariablesService interface {
// 全局变量
GetGlobal(userId, name string) any
SetGlobal(userId, name string, v any) error
AddGlobal(userId, name string, delta any) (any, error)
IncGlobal(userId, name string) (any, error)
DecGlobal(userId, name string) (any, error)
// 本地变量(带 chatId
GetLocal(userId, chatId, name string) any
SetLocal(userId, chatId, name string, v any) error
AddLocal(userId, chatId, name string, delta any) (any, error)
IncLocal(userId, chatId, name string) (any, error)
DecLocal(userId, chatId, name string) (any, error)
LoadLocalFromMetadata(chatId string, metaVars map[string]any)
ExportLocalToMetadata(chatId string) map[string]any
}
```
### 1.4 变量宏语法
支持以下语法(与项目一致):
- 本地变量:
- `{{setvar::name::value}}`
- `{{addvar::name::value}}`
- `{{incvar::name}}`
- `{{decvar::name}}`
- `{{getvar::name}}`
- 全局变量:
- `{{setglobalvar::name::value}}`
- `{{addglobalvar::name::value}}`
- `{{incglobalvar::name}}`
- `{{decglobalvar::name}}`
- `{{getglobalvar::name}}`
解析函数示例:
```go
func ProcessVariableMacros(input, userId string, chatId *string, vars VariablesService) (string, error)
```
实现建议:
1. 为每种宏写一个正则,比如:
- `\{\{setvar::([^:]+)::([^}]*)\}\}`
- `\{\{getvar::([^}]+)\}\}` 等。
2. 按「写变量 → 读变量」顺序依次 `ReplaceAllStringFunc`
- set / add / inc / dec调用 `VariablesService`,返回空串或新值字符串。
- get用变量值替换整段 `{{...}}`
**集成位置**:在构建 prompt、处理用户输入时先跑一遍 `ProcessVariableMacros`
---
## 二、宏系统Macro System
### 2.1 目标
- 使用 `{{...}}` 宏在 prompt 中引入上下文信息,包括:
- 用户/人设:`{{user}}``{{persona}}``{{user_description}}`
- 角色:`{{char}}``{{char_description}}``{{system_prompt}}``{{jailbreak}}`
- 时间:`{{time}}``{{date}}``{{weekday}}`
- 聊天上下文:`{{lastMessage}}``{{lastUserMessage}}``{{messageCount}}`
- 随机:`{{random}}``{{random::min::max}}``{{roll::2d6}}``{{pick::a::b}}`
- 元信息:`{{model}}``{{provider}}``{{idle_duration}}` 等。
### 2.2 宏上下文Go
```go
type MacroContext struct {
UserName string
UserDescription string
CharacterName string
CharacterDescription string
CharacterSystemPrompt string
PostHistoryInstructions string
LastMessage string
LastUserMessage string
LastCharacterMessage string
MessageCount int
ChatID string
OriginalPrompt string
CurrentInput string
ModelName string
ProviderName string
IdleDurationMinutes int
Now time.Time
}
```
### 2.3 宏解析函数
```go
func ProcessMacros(text string, ctx MacroContext) string
```
推荐拆为多个子步骤(顺序类似原项目):
1. `_processRandomMacros`
- `{{random}}`
- `{{random::min::max}}`
- `{{roll::NdM}}`
- `{{pick::opt1::opt2}}`
- `{{uuid}}`
2. `_processConditionalMacros`
- `{{if::cond::then}}`
- `{{if::cond::then::else}}`(可以做简单布尔表达式解析)
3. `_processTimeDateMacros`
- `{{time}}` / `{{date}}` / `{{weekday}}` / 自定义时间格式。
4. `_processCharacterMacros`
- `{{char}}``{{char_description}}``{{system_prompt}}``{{post_history_instructions}}` 等。
5. `_processUserMacros`
- `{{user}}` / `{{persona}}` / `{{user_description}}`
6. `_processChatMacros`
- `{{lastMessage}}` / `{{lastUserMessage}}` / `{{lastCharMessage}}` / `{{messageCount}}` / `{{chatId}}`
7. `_processSpecialMacros`
- `{{newline}}` / `{{nl}}``{{trim}}``{{noop}}`
- `{{original}}`(原始 prompt用于局部覆盖
- `{{input}}`(当前用户输入)
- `{{model}}` / `{{provider}}`
- `{{idle_duration}}`
**集成顺序**(在构建 prompt 时):
1. 先跑变量宏 `ProcessVariableMacros`(会改变变量状态)。
2. 再跑文本宏 `ProcessMacros`
---
## 三、思维链支持Chain-of-Thought / Reasoning
### 3.1 目标
- 对支持推理的模型o1/o3、Claude Thinking、Gemini 思考流):
- 把推理内容单独抓出来(从 `reasoning_content` / `thinking` / `thought` 字段等)。
- 和正常回复一起存在消息上。
- 前端给一个可折叠的「思维链/推理」区域。
### 3.2 数据模型
```go
type ChatMessage struct {
ID string
ChatID string
Role string // "user" / "assistant" / "system"
Content string
Timestamp time.Time
Swipes []string
CurrentSwipeIdx int
Reasoning *string // 默认推理文本
ReasoningSwipes []string // 每个 swipe 的推理
// ... 其他字段attachments、characterId 等)
}
```
前端便于使用的派生属性(在 React 里实现):
- `currentReasoning`
- 如果 `ReasoningSwipes` 长度 > `CurrentSwipeIdx` → 用对应项;
- 否则用 `Reasoning`
- `hasReasoning`
- `Reasoning` 非空;或 `ReasoningSwipes` 中有任意非空字符串。
### 3.3 LLM 接口设计Go
**非流式:**
```go
type LLMReasoningResponse struct {
Content string
Reasoning *string
}
func GenerateWithReasoning(promptCtx PromptContext, cfg LLMConfig) (LLMReasoningResponse, error)
```
**流式:**
```go
type ReasoningChunk struct {
Content *string
Reasoning *string
IsReasoningChunk bool
}
func GenerateStreamWithReasoning(promptCtx PromptContext, cfg LLMConfig) (<-chan ReasoningChunk, error)
```
解析逻辑示例(伪代码):
```go
for chunk := range ch {
if chunk.IsReasoningChunk && chunk.Reasoning != nil {
reasoningBuffer.WriteString(*chunk.Reasoning)
}
if chunk.Content != nil {
contentBuffer.WriteString(*chunk.Content)
}
// 把当前 buffer 写回当前 assistant 消息,前端即可实时显示
}
```
- OpenAI o1/o3`choice.message.reasoning_content` 或自定义扩展字段中解析。
- Claude`thinking` 字段解析。
- Gemini`thought` 或增强字段解析。
### 3.4 前端渲染React
示例:
```tsx
function MessageView({ message }: { message: ChatMessage }) {
return (
<div className="message">
<div className="content">{renderMarkdownOrHtml(message.content)}</div>
{message.hasReasoning && (
<ReasoningPanel
label="Thinking"
content={message.currentReasoning ?? ""}
/>
)}
</div>
);
}
```
`ReasoningPanel`
- 折叠 / 展开。
- 文本可复制。
- UI 上与主内容区明显区分(浅色背景、小字体等)。
---
## 四、聊天功能Chat Flow
### 4.1 目标
- 封装完整一轮对话的流程:
1. 记录用户消息。
2. 做摘要/裁剪历史(可选)。
3. 根据 PromptManager + 世界信息 + 宏/变量 构造 prompt。
4. 调用 LLM流式/非流式 + reasoning
5. 写入 assistant 消息,并更新前端状态。
### 4.2 核心流程(伪代码)
```go
func (svc *ChatService) SendMessage(userId, chatId, input string, cfg LLMConfig, atts []ChatAttachment) error {
chat := repo.GetChat(chatId)
character := repo.GetCharacter(chat.CharacterID)
history := repo.ListMessages(chatId)
// 1. 写 user 消息
userMsg := ChatMessage{
ID: newID(),
ChatID: chatId,
Role: "user",
Content: input,
Timestamp: time.Now(),
Swipes: []string{input},
// Attachments: atts, ...
}
repo.AddMessage(userMsg)
// 2. 可选:基于 token 限制做摘要或丢弃最老历史
svc.checkAndSummarize(chat, history, cfg)
// 3. 构建 prompt 上下文
promptCtx := svc.BuildPromptContext(userId, chat, character, history, input)
// 4. LLM 调用
if cfg.Stream {
return svc.streamAssistantResponse(chatId, promptCtx, cfg)
}
return svc.oneShotAssistantResponse(chatId, promptCtx, cfg)
}
```
### 4.3 BuildPromptContext 的职责
- 从 PromptManager 获取启用的 PromptSection排序后
- 从世界信息系统取匹配条目(按关键词、优先级、位置)。
- 按「深度注入」规则遍历历史消息,决定在每条消息前插入哪些 PromptSection / 世界信息 / 作者注释:
- 定义 `depthFromEnd = total-1 - i`
-`section.InjectionDepth == depthFromEnd` → 在该消息之前插入对应提示。
- 对所有文本块跑:
1. `ProcessVariableMacros(...)`
2. `ProcessMacros(...)`
- 输出统一形式的 prompt例如 `[]OpenAIMessage{ {role, content}, ... }`,交给 LLMProvider 层。
---
## 五、提示词管理Prompt Management
### 5.1 目标
- 把 prompt 拆成若干「段」,每段可:
- 单独启用/禁用。
- 调整顺序。
- 指定注入深度(在倒数第几条消息前插入)。
- 与 SillyTavern 的 `prompts` / `prompt_order` JSON 兼容。
### 5.2 数据模型
```go
type PromptSectionType string
const (
SectionSystemPrompt PromptSectionType = "systemPrompt"
SectionPersona PromptSectionType = "persona"
SectionCharacterDescription PromptSectionType = "characterDescription"
SectionCharacterPersonality PromptSectionType = "characterPersonality"
SectionCharacterScenario PromptSectionType = "characterScenario"
SectionExampleMessages PromptSectionType = "exampleMessages"
SectionWorldInfo PromptSectionType = "worldInfo"
SectionWorldInfoAfter PromptSectionType = "worldInfoAfter"
SectionAuthorNote PromptSectionType = "authorNote"
SectionPostHistoryInstr PromptSectionType = "postHistoryInstructions"
SectionNsfw PromptSectionType = "nsfw"
SectionChatHistory PromptSectionType = "chatHistory"
SectionEnhanceDefinitions PromptSectionType = "enhanceDefinitions"
SectionCustom PromptSectionType = "custom"
)
type PromptSection struct {
Type PromptSectionType
Name string
Enabled bool
Order int
Content *string
Identifier *string // ST 中的 identifier
Role *string // "system" / "user" / "assistant"
InjectionPosition *int
InjectionDepth *int
}
```
### 5.3 PromptManager 服务
```go
type PromptManager struct {
Sections []PromptSection
}
func (pm *PromptManager) EnabledSections() []PromptSection {
// 过滤 Enabled并按 Order 排好
}
```
从 SillyTavern 导入:
1. 建一个 `identifier -> PromptSectionType` 映射。
2. 解析 `prompts`(内容)和 `prompt_order`(顺序 & enabled生成 `PromptSection`
3. 对不在映射表里的 identifier统一归类为 `SectionCustom`
### 5.4 深度注入逻辑
在构建 prompt 时,遍历历史消息 `messages`
```go
N := len(messages)
for i, msg := range messages {
depthFromEnd := N - 1 - i
// 1. 深度注入世界信息
for _, entry := range depthWorldInfo {
if entry.Depth == depthFromEnd {
systemMsg := buildWorldInfoMessage(entry)
promptMessages = append(promptMessages, systemMsg)
}
}
// 2. 深度注入 PromptSection
for _, sec := range depthBasedSections {
if sec.InjectionDepth != nil && *sec.InjectionDepth == depthFromEnd {
secMsgs := buildSectionMessages(sec, ctx)
promptMessages = append(promptMessages, secMsgs...)
}
}
// 3. Authors Note 深度注入
// ...
// 4. 加入当前 chat 消息本身user / assistant
promptMessages = append(promptMessages, convertChatMessage(msg))
}
```
---

1167
docs/html/index.html Normal file

File diff suppressed because it is too large Load Diff

1222
docs/html/my.html Normal file

File diff suppressed because it is too large Load Diff

1335
docs/html/st.html Normal file

File diff suppressed because it is too large Load Diff

913
docs/init/pgsql_init.sql Normal file
View File

@@ -0,0 +1,913 @@
-- SillyTavern Cloud / 云酒馆
-- PostgreSQL 手动初始化脚本,对应后端 PgsqlInitHandler.InitData 全量初始化数据
-- 使用方法(建议在全新空库上执行):
-- psql -h <host> -U <user> -d <db> -f pgsql_init.sql
--
-- 注意:
-- 1. 本脚本不会自动清理旧数据,如需在已有数据上重置,请自行 TRUNCATE 相关表。
-- 2. 用户密码字段留空(''),初始化后请通过接口或 SQL 自行重置管理员密码。
BEGIN;
/* =========================
* 1. 角色表 sys_authorities
* ========================= */
INSERT INTO sys_authorities (authority_id, authority_name, parent_id, default_router)
VALUES
(888, '普通用户', 0, 'dashboard'),
(9528, '测试角色', 0, 'dashboard'),
(8881, '普通用户子角色', 888, 'dashboard');
/* =========================
* 2. 用户表 sys_users
* 对应 initUser.InitializeData
* ========================= */
INSERT INTO sys_users (uuid, username, password, nick_name, header_img, authority_id, phone, email, enable)
VALUES
('00000000-0000-0000-0000-000000000001', 'admin', '', 'Lee', 'https://qmplusimg.henrongyi.top/gva_header.jpg', 888, '17611111111', '333333333@qq.com', 1),
('00000000-0000-0000-0000-000000000002', 'a303176530', '', '用户1', 'https://qmplusimg.henrongyi.top/1572075907logo.png', 9528, '17611111111', '333333333@qq.com', 1);
/* =====================================================
* 3. 字典表 & 字典详情表
* sys_dictionaries, sys_dictionary_details
* ===================================================== */
-- 3.1 字典表 sys_dictionaries
INSERT INTO sys_dictionaries (name, type, status, "desc")
VALUES
('性别', 'gender', TRUE, '性别字典'),
('数据库int类型', 'int', TRUE, 'int类型对应的数据库类型'),
('数据库时间日期类型', 'time.Time', TRUE, '数据库时间日期类型'),
('数据库浮点型', 'float64', TRUE, '数据库浮点型'),
('数据库字符串', 'string', TRUE, '数据库字符串'),
('数据库bool类型', 'bool', TRUE, '数据库bool类型');
-- 3.2 字典详情表 sys_dictionary_details
-- 使用 INSERT ... SELECT通过 type 反查 sys_dictionaries.id
-- 性别 gender
INSERT INTO sys_dictionary_details (label, value, extend, status, sort, sys_dictionary_id, parent_id, level, path)
SELECT
'' AS label, '1' AS value, '' AS extend, TRUE AS status, 1 AS sort,
d.id AS sys_dictionary_id, NULL::integer AS parent_id, 0 AS level, '' AS path
FROM sys_dictionaries d WHERE d.type = 'gender';
INSERT INTO sys_dictionary_details (label, value, extend, status, sort, sys_dictionary_id, parent_id, level, path)
SELECT
'', '2', '', TRUE, 2,
d.id, NULL::integer, 0, ''
FROM sys_dictionaries d WHERE d.type = 'gender';
-- int
INSERT INTO sys_dictionary_details (label, value, extend, status, sort, sys_dictionary_id, parent_id, level, path)
SELECT 'smallint', '1', 'mysql', TRUE, 1, d.id, NULL::integer, 0, '' FROM sys_dictionaries d WHERE d.type = 'int';
INSERT INTO sys_dictionary_details (label, value, extend, status, sort, sys_dictionary_id, parent_id, level, path)
SELECT 'mediumint', '2', 'mysql', TRUE, 2, d.id, NULL::integer, 0, '' FROM sys_dictionaries d WHERE d.type = 'int';
INSERT INTO sys_dictionary_details (label, value, extend, status, sort, sys_dictionary_id, parent_id, level, path)
SELECT 'int', '3', 'mysql', TRUE, 3, d.id, NULL::integer, 0, '' FROM sys_dictionaries d WHERE d.type = 'int';
INSERT INTO sys_dictionary_details (label, value, extend, status, sort, sys_dictionary_id, parent_id, level, path)
SELECT 'bigint', '4', 'mysql', TRUE, 4, d.id, NULL::integer, 0, '' FROM sys_dictionaries d WHERE d.type = 'int';
INSERT INTO sys_dictionary_details (label, value, extend, status, sort, sys_dictionary_id, parent_id, level, path)
SELECT 'int2', '5', 'pgsql', TRUE, 5, d.id, NULL::integer, 0, '' FROM sys_dictionaries d WHERE d.type = 'int';
INSERT INTO sys_dictionary_details (label, value, extend, status, sort, sys_dictionary_id, parent_id, level, path)
SELECT 'int4', '6', 'pgsql', TRUE, 6, d.id, NULL::integer, 0, '' FROM sys_dictionaries d WHERE d.type = 'int';
INSERT INTO sys_dictionary_details (label, value, extend, status, sort, sys_dictionary_id, parent_id, level, path)
SELECT 'int6', '7', 'pgsql', TRUE, 7, d.id, NULL::integer, 0, '' FROM sys_dictionaries d WHERE d.type = 'int';
INSERT INTO sys_dictionary_details (label, value, extend, status, sort, sys_dictionary_id, parent_id, level, path)
SELECT 'int8', '8', 'pgsql', TRUE, 8, d.id, NULL::integer, 0, '' FROM sys_dictionaries d WHERE d.type = 'int';
-- time.Time
INSERT INTO sys_dictionary_details (label, value, extend, status, sort, sys_dictionary_id, parent_id, level, path)
SELECT 'date', '0', 'mysql', TRUE, 0, d.id, NULL::integer, 0, '' FROM sys_dictionaries d WHERE d.type = 'time.Time';
INSERT INTO sys_dictionary_details (label, value, extend, status, sort, sys_dictionary_id, parent_id, level, path)
SELECT 'time', '1', 'mysql', TRUE, 1, d.id, NULL::integer, 0, '' FROM sys_dictionaries d WHERE d.type = 'time.Time';
INSERT INTO sys_dictionary_details (label, value, extend, status, sort, sys_dictionary_id, parent_id, level, path)
SELECT 'year', '2', 'mysql', TRUE, 2, d.id, NULL::integer, 0, '' FROM sys_dictionaries d WHERE d.type = 'time.Time';
INSERT INTO sys_dictionary_details (label, value, extend, status, sort, sys_dictionary_id, parent_id, level, path)
SELECT 'datetime', '3', 'mysql', TRUE, 3, d.id, NULL::integer, 0, '' FROM sys_dictionaries d WHERE d.type = 'time.Time';
INSERT INTO sys_dictionary_details (label, value, extend, status, sort, sys_dictionary_id, parent_id, level, path)
SELECT 'timestamp', '5', 'mysql', TRUE, 5, d.id, NULL::integer, 0, '' FROM sys_dictionaries d WHERE d.type = 'time.Time';
INSERT INTO sys_dictionary_details (label, value, extend, status, sort, sys_dictionary_id, parent_id, level, path)
SELECT 'timestamptz', '6', 'pgsql', TRUE, 5, d.id, NULL::integer, 0, '' FROM sys_dictionaries d WHERE d.type = 'time.Time';
-- float64
INSERT INTO sys_dictionary_details (label, value, extend, status, sort, sys_dictionary_id, parent_id, level, path)
SELECT 'float', '0', 'mysql', TRUE, 0, d.id, NULL::integer, 0, '' FROM sys_dictionaries d WHERE d.type = 'float64';
INSERT INTO sys_dictionary_details (label, value, extend, status, sort, sys_dictionary_id, parent_id, level, path)
SELECT 'double', '1', 'mysql', TRUE, 1, d.id, NULL::integer, 0, '' FROM sys_dictionaries d WHERE d.type = 'float64';
INSERT INTO sys_dictionary_details (label, value, extend, status, sort, sys_dictionary_id, parent_id, level, path)
SELECT 'decimal', '2', 'mysql', TRUE, 2, d.id, NULL::integer, 0, '' FROM sys_dictionaries d WHERE d.type = 'float64';
INSERT INTO sys_dictionary_details (label, value, extend, status, sort, sys_dictionary_id, parent_id, level, path)
SELECT 'numeric', '3', 'pgsql', TRUE, 3, d.id, NULL::integer, 0, '' FROM sys_dictionaries d WHERE d.type = 'float64';
INSERT INTO sys_dictionary_details (label, value, extend, status, sort, sys_dictionary_id, parent_id, level, path)
SELECT 'smallserial', '4', 'pgsql', TRUE, 4, d.id, NULL::integer, 0, '' FROM sys_dictionaries d WHERE d.type = 'float64';
-- string
INSERT INTO sys_dictionary_details (label, value, extend, status, sort, sys_dictionary_id, parent_id, level, path)
SELECT 'char', '0', 'mysql', TRUE, 0, d.id, NULL::integer, 0, '' FROM sys_dictionaries d WHERE d.type = 'string';
INSERT INTO sys_dictionary_details (label, value, extend, status, sort, sys_dictionary_id, parent_id, level, path)
SELECT 'varchar', '1', 'mysql', TRUE, 1, d.id, NULL::integer, 0, '' FROM sys_dictionaries d WHERE d.type = 'string';
INSERT INTO sys_dictionary_details (label, value, extend, status, sort, sys_dictionary_id, parent_id, level, path)
SELECT 'tinyblob', '2', 'mysql', TRUE, 2, d.id, NULL::integer, 0, '' FROM sys_dictionaries d WHERE d.type = 'string';
INSERT INTO sys_dictionary_details (label, value, extend, status, sort, sys_dictionary_id, parent_id, level, path)
SELECT 'tinytext', '3', 'mysql', TRUE, 3, d.id, NULL::integer, 0, '' FROM sys_dictionaries d WHERE d.type = 'string';
INSERT INTO sys_dictionary_details (label, value, extend, status, sort, sys_dictionary_id, parent_id, level, path)
SELECT 'text', '4', 'mysql', TRUE, 4, d.id, NULL::integer, 0, '' FROM sys_dictionaries d WHERE d.type = 'string';
INSERT INTO sys_dictionary_details (label, value, extend, status, sort, sys_dictionary_id, parent_id, level, path)
SELECT 'blob', '5', 'mysql', TRUE, 5, d.id, NULL::integer, 0, '' FROM sys_dictionaries d WHERE d.type = 'string';
INSERT INTO sys_dictionary_details (label, value, extend, status, sort, sys_dictionary_id, parent_id, level, path)
SELECT 'mediumblob', '6', 'mysql', TRUE, 6, d.id, NULL::integer, 0, '' FROM sys_dictionaries d WHERE d.type = 'string';
INSERT INTO sys_dictionary_details (label, value, extend, status, sort, sys_dictionary_id, parent_id, level, path)
SELECT 'mediumtext', '7', 'mysql', TRUE, 7, d.id, NULL::integer, 0, '' FROM sys_dictionaries d WHERE d.type = 'string';
INSERT INTO sys_dictionary_details (label, value, extend, status, sort, sys_dictionary_id, parent_id, level, path)
SELECT 'longblob', '8', 'mysql', TRUE, 8, d.id, NULL::integer, 0, '' FROM sys_dictionaries d WHERE d.type = 'string';
INSERT INTO sys_dictionary_details (label, value, extend, status, sort, sys_dictionary_id, parent_id, level, path)
SELECT 'longtext', '9', 'mysql', TRUE, 9, d.id, NULL::integer, 0, '' FROM sys_dictionaries d WHERE d.type = 'string';
-- bool
INSERT INTO sys_dictionary_details (label, value, extend, status, sort, sys_dictionary_id, parent_id, level, path)
SELECT 'tinyint', '1', 'mysql', TRUE, 0, d.id, NULL::integer, 0, '' FROM sys_dictionaries d WHERE d.type = 'bool';
INSERT INTO sys_dictionary_details (label, value, extend, status, sort, sys_dictionary_id, parent_id, level, path)
SELECT 'bool', '2', 'pgsql', TRUE, 0, d.id, NULL::integer, 0, '' FROM sys_dictionaries d WHERE d.type = 'bool';
/* =========================
* 4. API 表 sys_apis
* ========================= */
INSERT INTO sys_apis (api_group, method, path, description)
VALUES
('jwt', 'POST', '/jwt/jsonInBlacklist', 'jwt加入黑名单(退出,必选)'),
('登录日志', 'DELETE', '/sysLoginLog/deleteLoginLog', '删除登录日志'),
('登录日志', 'DELETE', '/sysLoginLog/deleteLoginLogByIds', '批量删除登录日志'),
('登录日志', 'GET', '/sysLoginLog/findLoginLog', '根据ID获取登录日志'),
('登录日志', 'GET', '/sysLoginLog/getLoginLogList', '获取登录日志列表'),
('API Token', 'POST', '/sysApiToken/createApiToken', '签发API Token'),
('API Token', 'POST', '/sysApiToken/getApiTokenList', '获取API Token列表'),
('API Token', 'POST', '/sysApiToken/deleteApiToken', '作废API Token'),
('系统用户', 'DELETE', '/user/deleteUser', '删除用户'),
('系统用户', 'POST', '/user/admin_register', '用户注册'),
('系统用户', 'POST', '/user/getUserList', '获取用户列表'),
('系统用户', 'PUT', '/user/setUserInfo', '设置用户信息'),
('系统用户', 'PUT', '/user/setSelfInfo', '设置自身信息(必选)'),
('系统用户', 'GET', '/user/getUserInfo', '获取自身信息(必选)'),
('系统用户', 'POST', '/user/setUserAuthorities', '设置权限组'),
('系统用户', 'POST', '/user/changePassword', '修改密码(建议选择)'),
('系统用户', 'POST', '/user/setUserAuthority', '修改用户角色(必选)'),
('系统用户', 'POST', '/user/resetPassword', '重置用户密码'),
('系统用户', 'PUT', '/user/setSelfSetting', '用户界面配置'),
('api', 'POST', '/api/createApi', '创建api'),
('api', 'POST', '/api/deleteApi', '删除Api'),
('api', 'POST', '/api/updateApi', '更新Api'),
('api', 'POST', '/api/getApiList', '获取api列表'),
('api', 'POST', '/api/getAllApis', '获取所有api'),
('api', 'POST', '/api/getApiById', '获取api详细信息'),
('api', 'DELETE', '/api/deleteApisByIds', '批量删除api'),
('api', 'GET', '/api/syncApi', '获取待同步API'),
('api', 'GET', '/api/getApiGroups', '获取路由组'),
('api', 'POST', '/api/enterSyncApi', '确认同步API'),
('api', 'POST', '/api/ignoreApi', '忽略API'),
('角色', 'POST', '/authority/copyAuthority', '拷贝角色'),
('角色', 'POST', '/authority/createAuthority', '创建角色'),
('角色', 'POST', '/authority/deleteAuthority', '删除角色'),
('角色', 'PUT', '/authority/updateAuthority', '更新角色信息'),
('角色', 'POST', '/authority/getAuthorityList', '获取角色列表'),
('角色', 'POST', '/authority/setDataAuthority', '设置角色资源权限'),
('casbin', 'POST', '/casbin/updateCasbin', '更改角色api权限'),
('casbin', 'POST', '/casbin/getPolicyPathByAuthorityId', '获取权限列表'),
('菜单', 'POST', '/menu/addBaseMenu', '新增菜单'),
('菜单', 'POST', '/menu/getMenu', '获取菜单树(必选)'),
('菜单', 'POST', '/menu/deleteBaseMenu', '删除菜单'),
('菜单', 'POST', '/menu/updateBaseMenu', '更新菜单'),
('菜单', 'POST', '/menu/getBaseMenuById', '根据id获取菜单'),
('菜单', 'POST', '/menu/getMenuList', '分页获取基础menu列表'),
('菜单', 'POST', '/menu/getBaseMenuTree', '获取用户动态路由'),
('菜单', 'POST', '/menu/getMenuAuthority', '获取指定角色menu'),
('菜单', 'POST', '/menu/addMenuAuthority', '增加menu和角色关联关系'),
('分片上传', 'GET', '/fileUploadAndDownload/findFile', '寻找目标文件(秒传)'),
('分片上传', 'POST', '/fileUploadAndDownload/breakpointContinue', '断点续传'),
('分片上传', 'POST', '/fileUploadAndDownload/breakpointContinueFinish', '断点续传完成'),
('分片上传', 'POST', '/fileUploadAndDownload/removeChunk', '上传完成移除文件'),
('文件上传与下载', 'POST', '/fileUploadAndDownload/upload', '文件上传(建议选择)'),
('文件上传与下载', 'POST', '/fileUploadAndDownload/deleteFile', '删除文件'),
('文件上传与下载', 'POST', '/fileUploadAndDownload/editFileName', '文件名或者备注编辑'),
('文件上传与下载', 'POST', '/fileUploadAndDownload/getFileList', '获取上传文件列表'),
('文件上传与下载', 'POST', '/fileUploadAndDownload/importURL', '导入URL'),
('系统服务', 'POST', '/system/getServerInfo', '获取服务器信息'),
('系统服务', 'POST', '/system/getSystemConfig', '获取配置文件内容'),
('系统服务', 'POST', '/system/setSystemConfig', '设置配置文件内容'),
('skills', 'GET', '/skills/getTools', '获取技能工具列表'),
('skills', 'POST', '/skills/getSkillList', '获取技能列表'),
('skills', 'POST', '/skills/getSkillDetail', '获取技能详情'),
('skills', 'POST', '/skills/saveSkill', '保存技能定义'),
('skills', 'POST', '/skills/createScript', '创建技能脚本'),
('skills', 'POST', '/skills/getScript', '读取技能脚本'),
('skills', 'POST', '/skills/saveScript', '保存技能脚本'),
('skills', 'POST', '/skills/createResource', '创建技能资源'),
('skills', 'POST', '/skills/getResource', '读取技能资源'),
('skills', 'POST', '/skills/saveResource', '保存技能资源'),
('skills', 'POST', '/skills/createReference', '创建技能参考'),
('skills', 'POST', '/skills/getReference', '读取技能参考'),
('skills', 'POST', '/skills/saveReference', '保存技能参考'),
('skills', 'POST', '/skills/createTemplate', '创建技能模板'),
('skills', 'POST', '/skills/getTemplate', '读取技能模板'),
('skills', 'POST', '/skills/saveTemplate', '保存技能模板'),
('skills', 'POST', '/skills/getGlobalConstraint', '读取全局约束'),
('skills', 'POST', '/skills/saveGlobalConstraint', '保存全局约束'),
('客户', 'PUT', '/customer/customer', '更新客户'),
('客户', 'POST', '/customer/customer', '创建客户'),
('客户', 'DELETE','/customer/customer', '删除客户'),
('客户', 'GET', '/customer/customer', '获取单一客户'),
('客户', 'GET', '/customer/customerList', '获取客户列表'),
('代码生成器', 'GET', '/autoCode/getDB', '获取所有数据库'),
('代码生成器', 'GET', '/autoCode/getTables', '获取数据库表'),
('代码生成器', 'POST', '/autoCode/createTemp', '自动化代码'),
('代码生成器', 'POST', '/autoCode/preview', '预览自动化代码'),
('代码生成器', 'GET', '/autoCode/getColumn', '获取所选table的所有字段'),
('代码生成器', 'POST', '/autoCode/installPlugin', '安装插件'),
('代码生成器', 'POST', '/autoCode/pubPlug', '打包插件'),
('代码生成器', 'POST', '/autoCode/removePlugin', '卸载插件'),
('代码生成器', 'GET', '/autoCode/getPluginList', '获取已安装插件'),
('代码生成器', 'POST', '/autoCode/mcp', '自动生成 MCP Tool 模板'),
('代码生成器', 'POST', '/autoCode/mcpTest', 'MCP Tool 测试'),
('代码生成器', 'POST', '/autoCode/mcpList', '获取 MCP ToolList'),
('模板配置', 'POST', '/autoCode/createPackage', '配置模板'),
('模板配置', 'GET', '/autoCode/getTemplates', '获取模板文件'),
('模板配置', 'POST', '/autoCode/getPackage', '获取所有模板'),
('模板配置', 'POST', '/autoCode/delPackage', '删除模板'),
('代码生成器历史', 'POST', '/autoCode/getMeta', '获取meta信息'),
('代码生成器历史', 'POST', '/autoCode/rollback', '回滚自动生成代码'),
('代码生成器历史', 'POST', '/autoCode/getSysHistory', '查询回滚记录'),
('代码生成器历史', 'POST', '/autoCode/delSysHistory', '删除回滚记录'),
('代码生成器历史', 'POST', '/autoCode/addFunc', '增加模板方法'),
('系统字典详情', 'PUT', '/sysDictionaryDetail/updateSysDictionaryDetail', '更新字典内容'),
('系统字典详情', 'POST', '/sysDictionaryDetail/createSysDictionaryDetail', '新增字典内容'),
('系统字典详情', 'DELETE','/sysDictionaryDetail/deleteSysDictionaryDetail', '删除字典内容'),
('系统字典详情', 'GET', '/sysDictionaryDetail/findSysDictionaryDetail', '根据ID获取字典内容'),
('系统字典详情', 'GET', '/sysDictionaryDetail/getSysDictionaryDetailList', '获取字典内容列表'),
('系统字典详情', 'GET', '/sysDictionaryDetail/getDictionaryTreeList', '获取字典数列表'),
('系统字典详情', 'GET', '/sysDictionaryDetail/getDictionaryTreeListByType', '根据分类获取字典数列表'),
('系统字典详情', 'GET', '/sysDictionaryDetail/getDictionaryDetailsByParent', '根据父级ID获取字典详情'),
('系统字典详情', 'GET', '/sysDictionaryDetail/getDictionaryPath', '获取字典详情的完整路径'),
('系统字典', 'POST', '/sysDictionary/createSysDictionary', '新增字典'),
('系统字典', 'DELETE','/sysDictionary/deleteSysDictionary', '删除字典'),
('系统字典', 'PUT', '/sysDictionary/updateSysDictionary', '更新字典'),
('系统字典', 'GET', '/sysDictionary/findSysDictionary', '根据ID获取字典建议选择'),
('系统字典', 'GET', '/sysDictionary/getSysDictionaryList', '获取字典列表'),
('系统字典', 'POST', '/sysDictionary/importSysDictionary', '导入字典JSON'),
('系统字典', 'GET', '/sysDictionary/exportSysDictionary', '导出字典JSON'),
('操作记录', 'POST', '/sysOperationRecord/createSysOperationRecord', '新增操作记录'),
('操作记录', 'GET', '/sysOperationRecord/findSysOperationRecord', '根据ID获取操作记录'),
('操作记录', 'GET', '/sysOperationRecord/getSysOperationRecordList', '获取操作记录列表'),
('操作记录', 'DELETE','/sysOperationRecord/deleteSysOperationRecord', '删除操作记录'),
('操作记录', 'DELETE','/sysOperationRecord/deleteSysOperationRecordByIds', '批量删除操作历史'),
('断点续传(插件版)', 'POST', '/simpleUploader/upload', '插件版分片上传'),
('断点续传(插件版)', 'GET', '/simpleUploader/checkFileMd5', '文件完整度验证'),
('断点续传(插件版)', 'GET', '/simpleUploader/mergeFileMd5', '上传完成合并文件'),
('email', 'POST', '/email/emailTest', '发送测试邮件'),
('email', 'POST', '/email/sendEmail', '发送邮件'),
('按钮权限', 'POST', '/authorityBtn/setAuthorityBtn', '设置按钮权限'),
('按钮权限', 'POST', '/authorityBtn/getAuthorityBtn', '获取已有按钮权限'),
('按钮权限', 'POST', '/authorityBtn/canRemoveAuthorityBtn', '删除按钮'),
('导出模板', 'POST', '/sysExportTemplate/createSysExportTemplate', '新增导出模板'),
('导出模板', 'DELETE','/sysExportTemplate/deleteSysExportTemplate', '删除导出模板'),
('导出模板', 'DELETE','/sysExportTemplate/deleteSysExportTemplateByIds', '批量删除导出模板'),
('导出模板', 'PUT', '/sysExportTemplate/updateSysExportTemplate', '更新导出模板'),
('导出模板', 'GET', '/sysExportTemplate/findSysExportTemplate', '根据ID获取导出模板'),
('导出模板', 'GET', '/sysExportTemplate/getSysExportTemplateList', '获取导出模板列表'),
('导出模板', 'GET', '/sysExportTemplate/exportExcel', '导出Excel'),
('导出模板', 'GET', '/sysExportTemplate/exportTemplate', '下载模板'),
('导出模板', 'GET', '/sysExportTemplate/previewSQL', '预览SQL'),
('导出模板', 'POST', '/sysExportTemplate/importExcel', '导入Excel'),
('错误日志', 'POST', '/sysError/createSysError', '新建错误日志'),
('错误日志', 'DELETE','/sysError/deleteSysError', '删除错误日志'),
('错误日志', 'DELETE','/sysError/deleteSysErrorByIds', '批量删除错误日志'),
('错误日志', 'PUT', '/sysError/updateSysError', '更新错误日志'),
('错误日志', 'GET', '/sysError/findSysError', '根据ID获取错误日志'),
('错误日志', 'GET', '/sysError/getSysErrorList', '获取错误日志列表'),
('错误日志', 'GET', '/sysError/getSysErrorSolution', '触发错误处理(异步)'),
('公告', 'POST', '/info/createInfo', '新建公告'),
('公告', 'DELETE','/info/deleteInfo', '删除公告'),
('公告', 'DELETE','/info/deleteInfoByIds', '批量删除公告'),
('公告', 'PUT', '/info/updateInfo', '更新公告'),
('公告', 'GET', '/info/findInfo', '根据ID获取公告'),
('公告', 'GET', '/info/getInfoList', '获取公告列表'),
('参数管理', 'POST', '/sysParams/createSysParams', '新建参数'),
('参数管理', 'DELETE','/sysParams/deleteSysParams', '删除参数'),
('参数管理', 'DELETE','/sysParams/deleteSysParamsByIds', '批量删除参数'),
('参数管理', 'PUT', '/sysParams/updateSysParams', '更新参数'),
('参数管理', 'GET', '/sysParams/findSysParams', '根据ID获取参数'),
('参数管理', 'GET', '/sysParams/getSysParamsList', '获取参数列表'),
('参数管理', 'GET', '/sysParams/getSysParam', '获取参数列表'),
('媒体库分类', 'GET', '/attachmentCategory/getCategoryList', '分类列表'),
('媒体库分类', 'POST', '/attachmentCategory/addCategory', '添加/编辑分类'),
('媒体库分类', 'POST', '/attachmentCategory/deleteCategory', '删除分类'),
('版本控制', 'GET', '/sysVersion/findSysVersion', '获取单一版本'),
('版本控制', 'GET', '/sysVersion/getSysVersionList', '获取版本列表'),
('版本控制', 'GET', '/sysVersion/downloadVersionJson', '下载版本json'),
('版本控制', 'POST', '/sysVersion/exportVersion', '创建版本'),
('版本控制', 'POST', '/sysVersion/importVersion', '同步版本'),
('版本控制', 'DELETE','/sysVersion/deleteSysVersion', '删除版本'),
('版本控制', 'DELETE','/sysVersion/deleteSysVersionByIds', '批量删除版本');
/* =========================
* 5. 忽略 API 表 sys_ignore_apis
* ========================= */
INSERT INTO sys_ignore_apis (method, path)
VALUES
('GET', '/swagger/*any'),
('GET', '/api/freshCasbin'),
('GET', '/uploads/file/*filepath'),
('GET', '/health'),
('HEAD', '/uploads/file/*filepath'),
('POST', '/autoCode/llmAuto'),
('POST', '/system/reloadSystem'),
('POST', '/base/login'),
('POST', '/base/captcha'),
('POST', '/init/initdb'),
('POST', '/init/checkdb'),
('GET', '/info/getInfoDataSource'),
('GET', '/info/getInfoPublic');
/* =========================
* 6. Casbin 规则表 casbin_rules
* ========================= */
INSERT INTO casbin_rules (ptype, v0, v1, v2)
VALUES
-- 888
('p', '888', '/user/admin_register', 'POST'),
('p', '888', '/sysLoginLog/deleteLoginLog', 'DELETE'),
('p', '888', '/sysLoginLog/deleteLoginLogByIds', 'DELETE'),
('p', '888', '/sysLoginLog/findLoginLog', 'GET'),
('p', '888', '/sysLoginLog/getLoginLogList', 'GET'),
('p', '888', '/sysApiToken/createApiToken', 'POST'),
('p', '888', '/sysApiToken/getApiTokenList', 'POST'),
('p', '888', '/sysApiToken/deleteApiToken', 'POST'),
('p', '888', '/api/createApi', 'POST'),
('p', '888', '/api/getApiList', 'POST'),
('p', '888', '/api/getApiById', 'POST'),
('p', '888', '/api/deleteApi', 'POST'),
('p', '888', '/api/updateApi', 'POST'),
('p', '888', '/api/getAllApis', 'POST'),
('p', '888', '/api/deleteApisByIds', 'DELETE'),
('p', '888', '/api/syncApi', 'GET'),
('p', '888', '/api/getApiGroups', 'GET'),
('p', '888', '/api/enterSyncApi', 'POST'),
('p', '888', '/api/ignoreApi', 'POST'),
('p', '888', '/authority/copyAuthority', 'POST'),
('p', '888', '/authority/updateAuthority', 'PUT'),
('p', '888', '/authority/createAuthority', 'POST'),
('p', '888', '/authority/deleteAuthority', 'POST'),
('p', '888', '/authority/getAuthorityList', 'POST'),
('p', '888', '/authority/setDataAuthority', 'POST'),
('p', '888', '/menu/getMenu', 'POST'),
('p', '888', '/menu/getMenuList', 'POST'),
('p', '888', '/menu/addBaseMenu', 'POST'),
('p', '888', '/menu/getBaseMenuTree', 'POST'),
('p', '888', '/menu/addMenuAuthority', 'POST'),
('p', '888', '/menu/getMenuAuthority', 'POST'),
('p', '888', '/menu/deleteBaseMenu', 'POST'),
('p', '888', '/menu/updateBaseMenu', 'POST'),
('p', '888', '/menu/getBaseMenuById', 'POST'),
('p', '888', '/user/getUserInfo', 'GET'),
('p', '888', '/user/setUserInfo', 'PUT'),
('p', '888', '/user/setSelfInfo', 'PUT'),
('p', '888', '/user/getUserList', 'POST'),
('p', '888', '/user/deleteUser', 'DELETE'),
('p', '888', '/user/changePassword', 'POST'),
('p', '888', '/user/setUserAuthority', 'POST'),
('p', '888', '/user/setUserAuthorities', 'POST'),
('p', '888', '/user/resetPassword', 'POST'),
('p', '888', '/user/setSelfSetting', 'PUT'),
('p', '888', '/fileUploadAndDownload/findFile', 'GET'),
('p', '888', '/fileUploadAndDownload/breakpointContinueFinish', 'POST'),
('p', '888', '/fileUploadAndDownload/breakpointContinue', 'POST'),
('p', '888', '/fileUploadAndDownload/removeChunk', 'POST'),
('p', '888', '/fileUploadAndDownload/upload', 'POST'),
('p', '888', '/fileUploadAndDownload/deleteFile', 'POST'),
('p', '888', '/fileUploadAndDownload/editFileName', 'POST'),
('p', '888', '/fileUploadAndDownload/getFileList', 'POST'),
('p', '888', '/fileUploadAndDownload/importURL', 'POST'),
('p', '888', '/casbin/updateCasbin', 'POST'),
('p', '888', '/casbin/getPolicyPathByAuthorityId', 'POST'),
('p', '888', '/jwt/jsonInBlacklist', 'POST'),
('p', '888', '/system/getSystemConfig', 'POST'),
('p', '888', '/system/setSystemConfig', 'POST'),
('p', '888', '/system/getServerInfo', 'POST'),
('p', '888', '/skills/getTools', 'GET'),
('p', '888', '/skills/getSkillList', 'POST'),
('p', '888', '/skills/getSkillDetail', 'POST'),
('p', '888', '/skills/saveSkill', 'POST'),
('p', '888', '/skills/createScript', 'POST'),
('p', '888', '/skills/getScript', 'POST'),
('p', '888', '/skills/saveScript', 'POST'),
('p', '888', '/skills/createResource', 'POST'),
('p', '888', '/skills/getResource', 'POST'),
('p', '888', '/skills/saveResource', 'POST'),
('p', '888', '/skills/createReference', 'POST'),
('p', '888', '/skills/getReference', 'POST'),
('p', '888', '/skills/saveReference', 'POST'),
('p', '888', '/skills/createTemplate', 'POST'),
('p', '888', '/skills/getTemplate', 'POST'),
('p', '888', '/skills/saveTemplate', 'POST'),
('p', '888', '/skills/getGlobalConstraint', 'POST'),
('p', '888', '/skills/saveGlobalConstraint', 'POST'),
('p', '888', '/customer/customer', 'GET'),
('p', '888', '/customer/customer', 'PUT'),
('p', '888', '/customer/customer', 'POST'),
('p', '888', '/customer/customer', 'DELETE'),
('p', '888', '/customer/customerList', 'GET'),
('p', '888', '/autoCode/getDB', 'GET'),
('p', '888', '/autoCode/getMeta', 'POST'),
('p', '888', '/autoCode/preview', 'POST'),
('p', '888', '/autoCode/getTables', 'GET'),
('p', '888', '/autoCode/getColumn', 'GET'),
('p', '888', '/autoCode/rollback', 'POST'),
('p', '888', '/autoCode/createTemp', 'POST'),
('p', '888', '/autoCode/delSysHistory', 'POST'),
('p', '888', '/autoCode/getSysHistory', 'POST'),
('p', '888', '/autoCode/createPackage', 'POST'),
('p', '888', '/autoCode/getTemplates', 'GET'),
('p', '888', '/autoCode/getPackage', 'POST'),
('p', '888', '/autoCode/delPackage', 'POST'),
('p', '888', '/autoCode/createPlug', 'POST'),
('p', '888', '/autoCode/installPlugin', 'POST'),
('p', '888', '/autoCode/pubPlug', 'POST'),
('p', '888', '/autoCode/removePlugin', 'POST'),
('p', '888', '/autoCode/getPluginList', 'GET'),
('p', '888', '/autoCode/addFunc', 'POST'),
('p', '888', '/autoCode/mcp', 'POST'),
('p', '888', '/autoCode/mcpTest', 'POST'),
('p', '888', '/autoCode/mcpList', 'POST'),
('p', '888', '/sysDictionaryDetail/findSysDictionaryDetail', 'GET'),
('p', '888', '/sysDictionaryDetail/updateSysDictionaryDetail', 'PUT'),
('p', '888', '/sysDictionaryDetail/createSysDictionaryDetail', 'POST'),
('p', '888', '/sysDictionaryDetail/getSysDictionaryDetailList', 'GET'),
('p', '888', '/sysDictionaryDetail/deleteSysDictionaryDetail', 'DELETE'),
('p', '888', '/sysDictionaryDetail/getDictionaryTreeList', 'GET'),
('p', '888', '/sysDictionaryDetail/getDictionaryTreeListByType', 'GET'),
('p', '888', '/sysDictionaryDetail/getDictionaryDetailsByParent', 'GET'),
('p', '888', '/sysDictionaryDetail/getDictionaryPath', 'GET'),
('p', '888', '/sysDictionary/findSysDictionary', 'GET'),
('p', '888', '/sysDictionary/updateSysDictionary', 'PUT'),
('p', '888', '/sysDictionary/getSysDictionaryList', 'GET'),
('p', '888', '/sysDictionary/createSysDictionary', 'POST'),
('p', '888', '/sysDictionary/deleteSysDictionary', 'DELETE'),
('p', '888', '/sysDictionary/importSysDictionary', 'POST'),
('p', '888', '/sysDictionary/exportSysDictionary', 'GET'),
('p', '888', '/sysOperationRecord/findSysOperationRecord', 'GET'),
('p', '888', '/sysOperationRecord/updateSysOperationRecord', 'PUT'),
('p', '888', '/sysOperationRecord/createSysOperationRecord', 'POST'),
('p', '888', '/sysOperationRecord/getSysOperationRecordList', 'GET'),
('p', '888', '/sysOperationRecord/deleteSysOperationRecord', 'DELETE'),
('p', '888', '/sysOperationRecord/deleteSysOperationRecordByIds', 'DELETE'),
('p', '888', '/email/emailTest', 'POST'),
('p', '888', '/email/sendEmail', 'POST'),
('p', '888', '/simpleUploader/upload', 'POST'),
('p', '888', '/simpleUploader/checkFileMd5', 'GET'),
('p', '888', '/simpleUploader/mergeFileMd5', 'GET'),
('p', '888', '/authorityBtn/setAuthorityBtn', 'POST'),
('p', '888', '/authorityBtn/getAuthorityBtn', 'POST'),
('p', '888', '/authorityBtn/canRemoveAuthorityBtn', 'POST'),
('p', '888', '/sysExportTemplate/createSysExportTemplate', 'POST'),
('p', '888', '/sysExportTemplate/deleteSysExportTemplate', 'DELETE'),
('p', '888', '/sysExportTemplate/deleteSysExportTemplateByIds', 'DELETE'),
('p', '888', '/sysExportTemplate/updateSysExportTemplate', 'PUT'),
('p', '888', '/sysExportTemplate/findSysExportTemplate', 'GET'),
('p', '888', '/sysExportTemplate/getSysExportTemplateList', 'GET'),
('p', '888', '/sysExportTemplate/exportExcel', 'GET'),
('p', '888', '/sysExportTemplate/exportTemplate', 'GET'),
('p', '888', '/sysExportTemplate/previewSQL', 'GET'),
('p', '888', '/sysExportTemplate/importExcel', 'POST'),
('p', '888', '/sysError/createSysError', 'POST'),
('p', '888', '/sysError/deleteSysError', 'DELETE'),
('p', '888', '/sysError/deleteSysErrorByIds', 'DELETE'),
('p', '888', '/sysError/updateSysError', 'PUT'),
('p', '888', '/sysError/findSysError', 'GET'),
('p', '888', '/sysError/getSysErrorList', 'GET'),
('p', '888', '/sysError/getSysErrorSolution', 'GET'),
('p', '888', '/info/createInfo', 'POST'),
('p', '888', '/info/deleteInfo', 'DELETE'),
('p', '888', '/info/deleteInfoByIds', 'DELETE'),
('p', '888', '/info/updateInfo', 'PUT'),
('p', '888', '/info/findInfo', 'GET'),
('p', '888', '/info/getInfoList', 'GET'),
('p', '888', '/sysParams/createSysParams', 'POST'),
('p', '888', '/sysParams/deleteSysParams', 'DELETE'),
('p', '888', '/sysParams/deleteSysParamsByIds', 'DELETE'),
('p', '888', '/sysParams/updateSysParams', 'PUT'),
('p', '888', '/sysParams/findSysParams', 'GET'),
('p', '888', '/sysParams/getSysParamsList', 'GET'),
('p', '888', '/sysParams/getSysParam', 'GET'),
('p', '888', '/attachmentCategory/getCategoryList', 'GET'),
('p', '888', '/attachmentCategory/addCategory', 'POST'),
('p', '888', '/attachmentCategory/deleteCategory', 'POST'),
('p', '888', '/sysVersion/findSysVersion', 'GET'),
('p', '888', '/sysVersion/getSysVersionList', 'GET'),
('p', '888', '/sysVersion/downloadVersionJson', 'GET'),
('p', '888', '/sysVersion/exportVersion', 'POST'),
('p', '888', '/sysVersion/importVersion', 'POST'),
('p', '888', '/sysVersion/deleteSysVersion', 'DELETE'),
('p', '888', '/sysVersion/deleteSysVersionByIds', 'DELETE'),
-- 8881
('p', '8881', '/user/admin_register', 'POST'),
('p', '8881', '/api/createApi', 'POST'),
('p', '8881', '/api/getApiList', 'POST'),
('p', '8881', '/api/getApiById', 'POST'),
('p', '8881', '/api/deleteApi', 'POST'),
('p', '8881', '/api/updateApi', 'POST'),
('p', '8881', '/api/getAllApis', 'POST'),
('p', '8881', '/authority/createAuthority', 'POST'),
('p', '8881', '/authority/deleteAuthority', 'POST'),
('p', '8881', '/authority/getAuthorityList', 'POST'),
('p', '8881', '/authority/setDataAuthority', 'POST'),
('p', '8881', '/menu/getMenu', 'POST'),
('p', '8881', '/menu/getMenuList', 'POST'),
('p', '8881', '/menu/addBaseMenu', 'POST'),
('p', '8881', '/menu/getBaseMenuTree', 'POST'),
('p', '8881', '/menu/addMenuAuthority', 'POST'),
('p', '8881', '/menu/getMenuAuthority', 'POST'),
('p', '8881', '/menu/deleteBaseMenu', 'POST'),
('p', '8881', '/menu/updateBaseMenu', 'POST'),
('p', '8881', '/menu/getBaseMenuById', 'POST'),
('p', '8881', '/user/changePassword', 'POST'),
('p', '8881', '/user/getUserList', 'POST'),
('p', '8881', '/user/setUserAuthority', 'POST'),
('p', '8881', '/fileUploadAndDownload/upload', 'POST'),
('p', '8881', '/fileUploadAndDownload/getFileList', 'POST'),
('p', '8881', '/fileUploadAndDownload/deleteFile', 'POST'),
('p', '8881', '/fileUploadAndDownload/editFileName', 'POST'),
('p', '8881', '/fileUploadAndDownload/importURL', 'POST'),
('p', '8881', '/casbin/updateCasbin', 'POST'),
('p', '8881', '/casbin/getPolicyPathByAuthorityId', 'POST'),
('p', '8881', '/jwt/jsonInBlacklist', 'POST'),
('p', '8881', '/system/getSystemConfig', 'POST'),
('p', '8881', '/system/setSystemConfig', 'POST'),
('p', '8881', '/customer/customer', 'POST'),
('p', '8881', '/customer/customer', 'PUT'),
('p', '8881', '/customer/customer', 'DELETE'),
('p', '8881', '/customer/customer', 'GET'),
('p', '8881', '/customer/customerList', 'GET'),
('p', '8881', '/user/getUserInfo', 'GET'),
-- 9528
('p', '9528', '/user/admin_register', 'POST'),
('p', '9528', '/api/createApi', 'POST'),
('p', '9528', '/api/getApiList', 'POST'),
('p', '9528', '/api/getApiById', 'POST'),
('p', '9528', '/api/deleteApi', 'POST'),
('p', '9528', '/api/updateApi', 'POST'),
('p', '9528', '/api/getAllApis', 'POST'),
('p', '9528', '/authority/createAuthority', 'POST'),
('p', '9528', '/authority/deleteAuthority', 'POST'),
('p', '9528', '/authority/getAuthorityList', 'POST'),
('p', '9528', '/authority/setDataAuthority', 'POST'),
('p', '9528', '/menu/getMenu', 'POST'),
('p', '9528', '/menu/getMenuList', 'POST'),
('p', '9528', '/menu/addBaseMenu', 'POST'),
('p', '9528', '/menu/getBaseMenuTree', 'POST'),
('p', '9528', '/menu/addMenuAuthority', 'POST'),
('p', '9528', '/menu/getMenuAuthority', 'POST'),
('p', '9528', '/menu/deleteBaseMenu', 'POST'),
('p', '9528', '/menu/updateBaseMenu', 'POST'),
('p', '9528', '/menu/getBaseMenuById', 'POST'),
('p', '9528', '/user/changePassword', 'POST'),
('p', '9528', '/user/getUserList', 'POST'),
('p', '9528', '/user/setUserAuthority', 'POST'),
('p', '9528', '/fileUploadAndDownload/upload', 'POST'),
('p', '9528', '/fileUploadAndDownload/getFileList', 'POST'),
('p', '9528', '/fileUploadAndDownload/deleteFile', 'POST'),
('p', '9528', '/fileUploadAndDownload/editFileName', 'POST'),
('p', '9528', '/fileUploadAndDownload/importURL', 'POST'),
('p', '9528', '/casbin/updateCasbin', 'POST'),
('p', '9528', '/casbin/getPolicyPathByAuthorityId', 'POST'),
('p', '9528', '/jwt/jsonInBlacklist', 'POST'),
('p', '9528', '/system/getSystemConfig', 'POST'),
('p', '9528', '/system/setSystemConfig', 'POST'),
('p', '9528', '/customer/customer', 'PUT'),
('p', '9528', '/customer/customer', 'GET'),
('p', '9528', '/customer/customer', 'POST'),
('p', '9528', '/customer/customer', 'DELETE'),
('p', '9528', '/customer/customerList', 'GET'),
('p', '9528', '/autoCode/createTemp', 'POST'),
('p', '9528', '/user/getUserInfo', 'GET');
/* =========================
* 7. 菜单表 sys_base_menus
* ========================= */
-- 7.1 顶级菜单
INSERT INTO sys_base_menus (menu_level, hidden, parent_id, path, name, component, sort, meta_title, meta_icon)
VALUES
(0, FALSE, 0, 'dashboard', 'dashboard', 'view/dashboard/index.vue', 1, '仪表盘', 'odometer'),
(0, FALSE, 0, 'about', 'about', 'view/about/index.vue', 9, '关于我们', 'info-filled'),
(0, FALSE, 0, 'admin', 'superAdmin', 'view/superAdmin/index.vue', 3, '超级管理员', 'user'),
(0, TRUE, 0, 'person', 'person', 'view/person/person.vue', 4, '个人信息', 'message'),
(0, FALSE, 0, 'example', 'example', 'view/example/index.vue', 7, '示例文件', 'management'),
(0, FALSE, 0, 'systemTools', 'systemTools', 'view/systemTools/index.vue', 5, '系统工具', 'tools'),
(0, FALSE, 0, 'https://www.gin-vue-admin.com', 'https://www.gin-vue-admin.com', '/', 0, '官方网站', 'customer-gva'),
(0, FALSE, 0, 'state', 'state', 'view/system/state.vue', 8, '服务器状态', 'cloudy'),
(0, FALSE, 0, 'plugin', 'plugin', 'view/routerHolder.vue', 6, '插件系统', 'cherry');
-- 7.2 子菜单(通过名称反查父级 ID
-- superAdmin 子菜单
INSERT INTO sys_base_menus (menu_level, hidden, parent_id, path, name, component, sort, meta_title, meta_icon, meta_keep_alive)
SELECT
1, FALSE, m.id, 'authority', 'authority', 'view/superAdmin/authority/authority.vue', 1, '角色管理', 'avatar', FALSE
FROM sys_base_menus m WHERE m.name = 'superAdmin';
INSERT INTO sys_base_menus (menu_level, hidden, parent_id, path, name, component, sort, meta_title, meta_icon, meta_keep_alive)
SELECT
1, FALSE, m.id, 'menu', 'menu', 'view/superAdmin/menu/menu.vue', 2, '菜单管理', 'tickets', TRUE
FROM sys_base_menus m WHERE m.name = 'superAdmin';
INSERT INTO sys_base_menus (menu_level, hidden, parent_id, path, name, component, sort, meta_title, meta_icon, meta_keep_alive)
SELECT
1, FALSE, m.id, 'api', 'api', 'view/superAdmin/api/api.vue', 3, 'api管理', 'platform', TRUE
FROM sys_base_menus m WHERE m.name = 'superAdmin';
INSERT INTO sys_base_menus (menu_level, hidden, parent_id, path, name, component, sort, meta_title, meta_icon)
SELECT
1, FALSE, m.id, 'user', 'user', 'view/superAdmin/user/user.vue', 4, '用户管理', 'coordinate'
FROM sys_base_menus m WHERE m.name = 'superAdmin';
INSERT INTO sys_base_menus (menu_level, hidden, parent_id, path, name, component, sort, meta_title, meta_icon)
SELECT
1, FALSE, m.id, 'dictionary', 'dictionary', 'view/superAdmin/dictionary/sysDictionary.vue', 5, '字典管理', 'notebook'
FROM sys_base_menus m WHERE m.name = 'superAdmin';
INSERT INTO sys_base_menus (menu_level, hidden, parent_id, path, name, component, sort, meta_title, meta_icon)
SELECT
1, FALSE, m.id, 'operation', 'operation', 'view/superAdmin/operation/sysOperationRecord.vue', 6, '操作历史', 'pie-chart'
FROM sys_base_menus m WHERE m.name = 'superAdmin';
INSERT INTO sys_base_menus (menu_level, hidden, parent_id, path, name, component, sort, meta_title, meta_icon)
SELECT
1, FALSE, m.id, 'sysParams', 'sysParams', 'view/superAdmin/params/sysParams.vue', 7, '参数管理', 'compass'
FROM sys_base_menus m WHERE m.name = 'superAdmin';
-- example 子菜单
INSERT INTO sys_base_menus (menu_level, hidden, parent_id, path, name, component, sort, meta_title, meta_icon)
SELECT
1, FALSE, m.id, 'upload', 'upload', 'view/example/upload/upload.vue', 5, '媒体库(上传下载)', 'upload'
FROM sys_base_menus m WHERE m.name = 'example';
INSERT INTO sys_base_menus (menu_level, hidden, parent_id, path, name, component, sort, meta_title, meta_icon)
SELECT
1, FALSE, m.id, 'breakpoint', 'breakpoint', 'view/example/breakpoint/breakpoint.vue', 6, '断点续传', 'upload-filled'
FROM sys_base_menus m WHERE m.name = 'example';
INSERT INTO sys_base_menus (menu_level, hidden, parent_id, path, name, component, sort, meta_title, meta_icon)
SELECT
1, FALSE, m.id, 'customer', 'customer', 'view/example/customer/customer.vue', 7, '客户列表(资源示例)', 'avatar'
FROM sys_base_menus m WHERE m.name = 'example';
-- systemTools 子菜单
INSERT INTO sys_base_menus (menu_level, hidden, parent_id, path, name, component, sort, meta_title, meta_icon, meta_keep_alive)
SELECT
1, FALSE, m.id, 'autoCode', 'autoCode', 'view/systemTools/autoCode/index.vue', 1, '代码生成器', 'cpu', TRUE
FROM sys_base_menus m WHERE m.name = 'systemTools';
INSERT INTO sys_base_menus (menu_level, hidden, parent_id, path, name, component, sort, meta_title, meta_icon, meta_keep_alive)
SELECT
1, FALSE, m.id, 'formCreate', 'formCreate', 'view/systemTools/formCreate/index.vue', 3, '表单生成器', 'magic-stick', TRUE
FROM sys_base_menus m WHERE m.name = 'systemTools';
INSERT INTO sys_base_menus (menu_level, hidden, parent_id, path, name, component, sort, meta_title, meta_icon)
SELECT
1, FALSE, m.id, 'system', 'system', 'view/systemTools/system/system.vue', 4, '系统配置', 'operation'
FROM sys_base_menus m WHERE m.name = 'systemTools';
INSERT INTO sys_base_menus (menu_level, hidden, parent_id, path, name, component, sort, meta_title, meta_icon)
SELECT
1, FALSE, m.id, 'autoCodeAdmin', 'autoCodeAdmin', 'view/systemTools/autoCodeAdmin/index.vue', 2, '自动化代码管理', 'magic-stick'
FROM sys_base_menus m WHERE m.name = 'systemTools';
INSERT INTO sys_base_menus (menu_level, hidden, parent_id, path, name, component, sort, meta_title, meta_icon)
SELECT
1, FALSE, m.id, 'loginLog', 'loginLog', 'view/systemTools/loginLog/index.vue', 5, '登录日志', 'monitor'
FROM sys_base_menus m WHERE m.name = 'systemTools';
INSERT INTO sys_base_menus (menu_level, hidden, parent_id, path, name, component, sort, meta_title, meta_icon)
SELECT
1, FALSE, m.id, 'apiToken', 'apiToken', 'view/systemTools/apiToken/index.vue', 6, 'API Token', 'key'
FROM sys_base_menus m WHERE m.name = 'systemTools';
INSERT INTO sys_base_menus (menu_level, hidden, parent_id, path, name, component, sort, hidden, meta_title, meta_icon)
SELECT
1, TRUE, m.id, 'autoCodeEdit/:id', 'autoCodeEdit', 'view/systemTools/autoCode/index.vue', 0, TRUE, '自动化代码-${id}', 'magic-stick'
FROM sys_base_menus m WHERE m.name = 'systemTools';
INSERT INTO sys_base_menus (menu_level, hidden, parent_id, path, name, component, sort, meta_title, meta_icon)
SELECT
1, FALSE, m.id, 'autoPkg', 'autoPkg', 'view/systemTools/autoPkg/autoPkg.vue', 0, '模板配置', 'folder'
FROM sys_base_menus m WHERE m.name = 'systemTools';
INSERT INTO sys_base_menus (menu_level, hidden, parent_id, path, name, component, sort, meta_title, meta_icon)
SELECT
1, FALSE, m.id, 'exportTemplate', 'exportTemplate', 'view/systemTools/exportTemplate/exportTemplate.vue', 5, '导出模板', 'reading'
FROM sys_base_menus m WHERE m.name = 'systemTools';
INSERT INTO sys_base_menus (menu_level, hidden, parent_id, path, name, component, sort, meta_title, meta_icon)
SELECT
1, FALSE, m.id, 'skills', 'skills', 'view/systemTools/skills/index.vue', 6, 'Skills管理', 'document'
FROM sys_base_menus m WHERE m.name = 'systemTools';
INSERT INTO sys_base_menus (menu_level, hidden, parent_id, path, name, component, sort, meta_title, meta_icon)
SELECT
1, FALSE, m.id, 'picture', 'picture', 'view/systemTools/autoCode/picture.vue', 6, 'AI页面绘制', 'picture-filled'
FROM sys_base_menus m WHERE m.name = 'systemTools';
INSERT INTO sys_base_menus (menu_level, hidden, parent_id, path, name, component, sort, meta_title, meta_icon)
SELECT
1, FALSE, m.id, 'mcpTool', 'mcpTool', 'view/systemTools/autoCode/mcp.vue', 7, 'Mcp Tools模板', 'magnet'
FROM sys_base_menus m WHERE m.name = 'systemTools';
INSERT INTO sys_base_menus (menu_level, hidden, parent_id, path, name, component, sort, meta_title, meta_icon)
SELECT
1, FALSE, m.id, 'mcpTest', 'mcpTest', 'view/systemTools/autoCode/mcpTest.vue', 7, 'Mcp Tools测试', 'partly-cloudy'
FROM sys_base_menus m WHERE m.name = 'systemTools';
INSERT INTO sys_base_menus (menu_level, hidden, parent_id, path, name, component, sort, meta_title, meta_icon)
SELECT
1, FALSE, m.id, 'sysVersion', 'sysVersion', 'view/systemTools/version/version.vue', 8, '版本管理', 'server'
FROM sys_base_menus m WHERE m.name = 'systemTools';
INSERT INTO sys_base_menus (menu_level, hidden, parent_id, path, name, component, sort, meta_title, meta_icon)
SELECT
1, FALSE, m.id, 'sysError', 'sysError', 'view/systemTools/sysError/sysError.vue', 9, '错误日志', 'warn'
FROM sys_base_menus m WHERE m.name = 'systemTools';
-- plugin 子菜单
INSERT INTO sys_base_menus (menu_level, hidden, parent_id, path, name, component, sort, meta_title, meta_icon)
SELECT
1, FALSE, m.id, 'https://plugin.gin-vue-admin.com/', 'https://plugin.gin-vue-admin.com/', 'https://plugin.gin-vue-admin.com/', 0, '插件市场', 'shop'
FROM sys_base_menus m WHERE m.name = 'plugin';
INSERT INTO sys_base_menus (menu_level, hidden, parent_id, path, name, component, sort, meta_title, meta_icon)
SELECT
1, FALSE, m.id, 'installPlugin', 'installPlugin', 'view/systemTools/installPlugin/index.vue', 1, '插件安装', 'box'
FROM sys_base_menus m WHERE m.name = 'plugin';
INSERT INTO sys_base_menus (menu_level, hidden, parent_id, path, name, component, sort, meta_title, meta_icon)
SELECT
1, FALSE, m.id, 'pubPlug', 'pubPlug', 'view/systemTools/pubPlug/pubPlug.vue', 3, '打包插件', 'files'
FROM sys_base_menus m WHERE m.name = 'plugin';
INSERT INTO sys_base_menus (menu_level, hidden, parent_id, path, name, component, sort, meta_title, meta_icon)
SELECT
1, FALSE, m.id, 'plugin-email', 'plugin-email', 'plugin/email/view/index.vue', 4, '邮件插件', 'message'
FROM sys_base_menus m WHERE m.name = 'plugin';
INSERT INTO sys_base_menus (menu_level, hidden, parent_id, path, name, component, sort, meta_title, meta_icon)
SELECT
1, FALSE, m.id, 'anInfo', 'anInfo', 'plugin/announcement/view/info.vue', 5, '公告管理[示例]', 'scaleToOriginal'
FROM sys_base_menus m WHERE m.name = 'plugin';
/* =========================
* 8. 示例文件表 exa_file_upload_and_downloads
* ========================= */
INSERT INTO exa_file_upload_and_downloads (name, class_id, url, tag, key)
VALUES
('10.png', '0', 'https://qmplusimg.henrongyi.top/gvalogo.png', 'png', '158787308910.png'),
('logo.png', '0', 'https://qmplusimg.henrongyi.top/1576554439myAvatar.png', 'png', '1587973709logo.png');
/* =========================
* 9. 导出模板表 sys_export_templates
* ========================= */
INSERT INTO sys_export_templates (db_name, name, table_name, template_id, template_info)
VALUES
('', 'api', 'sys_apis', 'api',
'{
"path":"路径",
"method":"方法(大写)",
"description":"方法介绍",
"api_group":"方法分组"
}');
/* =========================
* 10. 多对多关联表
* sys_data_authority_id, sys_user_authority, sys_authority_menus
* ========================= */
-- 10.1 数据权限表 sys_data_authority_id
INSERT INTO sys_data_authority_id (sys_authority_authority_id, data_authority_id)
VALUES
(888, 888),
(888, 9528),
(888, 8881),
(9528, 9528),
(9528, 8881);
-- 10.2 用户-角色表 sys_user_authority
INSERT INTO sys_user_authority (sys_user_id, sys_authority_authority_id)
SELECT u.id, a.authority_id
FROM sys_users u
JOIN sys_authorities a ON a.authority_id IN (888, 9528, 8881)
WHERE u.username = 'admin';
INSERT INTO sys_user_authority (sys_user_id, sys_authority_authority_id)
SELECT u.id, a.authority_id
FROM sys_users u
JOIN sys_authorities a ON a.authority_id = 888
WHERE u.username = 'a303176530';
-- 10.3 角色-菜单表 sys_authority_menus
-- 888: 拥有所有菜单
INSERT INTO sys_authority_menus (sys_authority_authority_id, sys_base_menu_id)
SELECT 888, id FROM sys_base_menus;
-- 8881: 仅拥有 dashboard / about / person / state 顶级菜单
INSERT INTO sys_authority_menus (sys_authority_authority_id, sys_base_menu_id)
SELECT 8881, id
FROM sys_base_menus
WHERE parent_id = 0 AND name IN ('dashboard', 'about', 'person', 'state');
-- 9528: 所有顶级菜单 + systemTools/example 下的子菜单
INSERT INTO sys_authority_menus (sys_authority_authority_id, sys_base_menu_id)
SELECT 9528, id
FROM sys_base_menus
WHERE parent_id = 0;
INSERT INTO sys_authority_menus (sys_authority_authority_id, sys_base_menu_id)
SELECT 9528, id
FROM sys_base_menus
WHERE parent_id IN (
SELECT id FROM sys_base_menus WHERE name IN ('systemTools', 'example')
);
COMMIT;

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 壳**。

179
docs/前端卡.md Normal file
View File

@@ -0,0 +1,179 @@
在 SillyTavernST的圈子里**“前端卡”**Frontend Card / HTML Character Card是一个进阶概念它打破了传统角色卡“纯文字描述”的限制。
作为开发者,在重构“云酒馆”时,理解前端卡至关重要,因为它是 ST 社区目前最活跃、最“花哨”的部分。
### 1. 什么是前端卡?
传统角色卡只包含姓名、描述、问候语等文本数据。而**前端卡**本质上是一个**以角色卡为载体的“微型网页应用”**。
它利用 ST 的扩展系统Extensions和 HTML 渲染能力,在聊天界面注入自定义的 HTML、CSS 和 JavaScript。常见的表现形式形式包括
* **状态栏**:显示角色的好感度、心情、体力值。
* **互动 UI**:点击按钮触发特定的指令或对话。
* **视觉小说VN模式**:将背景、立绘和对话框完全重新设计。
* **RPG 系统**:自带背包、商店、任务系统。
---
### 2. 实现原理ST 源码逻辑)
前端卡的实现主要依赖以下几个技术点:
#### A. HTML 注入
ST 允许在 `World Info`(世界书)或者角色卡描述中使用特定的标签。最常用的是配合 **Quick Reply快速回复** 扩展或特定的 **Extension 钩子**
* **逻辑**:后端读取到包含 HTML 字符串的数据,前端通过 `$.append()``innerHTML` 将其塞进 DOM。
#### B. JavaScript 脚本执行
这是前端卡的“灵魂”。制作者通常会将加密或压缩后的 JS 代码嵌入在角色卡的“深度提示词”Depth Prompt或世界书条目中。
* **执行机制**ST 并没有为这些脚本提供沙箱环境。脚本运行在全局作用域下,可以直接通过 `window.SillyTavern` 对象访问 ST 的内部函数(如 `printMessage()`, `setVar()`, `token` 等)。
#### C. 通信与状态同步
前端卡通过以下方式与 ST 核心交互:
* **Macros**:利用 `{{getvar::hp}}` 渲染变量。
* **Slash Commands**:通过 JS 调用 `/setvar key=value` 来更新后端数据。
* **Event Listeners**:监听 `character_selected``message_received` 事件来刷新 UI。
---
### 3. 重构时的“重难点”与注意事项
如果你希望“云酒馆”完全兼容这些前端卡,你在使用 **React + Go** 重构时会面临巨大的架构挑战:
#### **1. 安全性(最致命的问题)**
* **ST 原版**因为是单机运行JS 随便跑,顶多是用户自己害自己。
* **你的公共平台**:这是 **跨站脚本攻击XSS** 的重灾区。如果用户 A 上传了一张“前端卡”,用户 B 导入后,这张卡里的 JS 脚本可以直接偷走用户 B 的浏览器 `LocalStorage`(包含你的平台 Token或者劫持 API Key。
* **解决方案**
* **必须隔离**:绝对不能直接在 React 主页面插入角色卡的 HTML/JS。
* **Iframe 沙箱**:将前端卡的渲染逻辑放在一个独立的、跨域的 `<iframe>` 中,限制 `sandbox` 属性(禁止访问父窗口)。
* **消息传递**React 宿主环境与 Iframe 之间通过 `postMessage` 进行受限的通信。
#### **2. API 兼容层**
前端卡通常会调用 `SillyTavern.XXX` 这种全局对象。
* **重构方案**:你需要在 Iframe 沙箱中预先注入一个“模拟对象”,模拟出 ST 的全局 API 环境,将这些调用映射到你自己的后端 API 或 Zustand Store。
#### **3. 数据持久化**
前端卡依赖大量的自定义变量Variables
* **重构方案**:确保你的 PostgreSQL 数据库中 `user_variables` 表能够支持高频读写,因为一些前端卡会通过 JS 脚本每秒多次更新变量(例如计时器或动态动画)。
#### **4. 样式的冲突**
前端卡自带的 CSS 经常会全局污染样式。
* **重构方案**:使用 **Shadow DOM****CSS Modules**。当然,如果用了 Iframe天然就解决了这个问题。
---
### 4. 架构要点(结合你们现有系统)
#### 安全与隔离
- 使用 iframe 沙箱模式来渲染前端卡内容,避免直接在宿主页面全局作用域执行脚本。
- 通过 postMessage 实现前端卡与宿主系统的受控通信,限定可访问的接口集合。
- 设定严格的内容策略Content Security Policy对内联脚本、外部资源、以及跨域请求进行管理。
#### 数据与 API 设计
- 后端存储前端卡的 HTML/CSS/JS 片段、扩展数据、以及变量定义。后端仅做存储与校验,不执行脚本。
- 前端卡通过受控接口访问后端数据(如变量、角色信息、事件触发等)。
- 提供可版本化的 API 接口,用于加载、更新、禁用前端卡,以及回滚到稳定版本。
#### 渲染与交互
- 前端卡在 iframe 内部渲染,用 Shadow DOM/CSS Modules/或独立的样式域来避免全局样式冲突。
- 提供一个明确的 UI 生命周期:初始化、加载资源、渲染、更新、销毁。
- 交互事件通过 postMessage 传递,宿主统一进行权限校验与路由。
#### 兼容性与迁移
- 对现有角色卡数据结构进行向后兼容处理Gradual Migration 路线图:先支持最小化前端卡,再逐步引入扩展能力。
- 做好版本回滚与降级策略,确保单卡或单次迁移失败时系统可继续工作。
#### 性能与监控
- 对前端卡资源进行容量与时间上的监控(加载时间、渲染时间、内存占用)。
- 对复杂卡片设定资源上限,避免页面整体降级。
---
### 5. 实施工具与注意事项
#### 安全策略
- 明确规定 iframe sandbox 属性例如sandbox="allow-scripts"(可选限制更多能力),禁用同源策略外的访问,严格限制跨域能力。
- 使用 CSP、Content Security Policy 报告与阻断机制,避免内联脚本被滥用。
#### 数据边界
- 设置前端卡大小的上限HTML/JS 总长度、资源大小),避免 UI 注入导致页面阻塞或崩溃。
#### 日志与审计
- 记录哪些前端卡被加载、哪些代码被执行、以及潜在的安全事件。
#### 流程与回滚
- 卡片的版本化、灰度发布、快速回滚机制,确保单卡异常不会影响全局。
#### 测试要点
- 安全性测试XSS/恶意脚本注入)、性能压测、跨域通信正确性测试、回滚演练。
---
### 6. 开发路线与任务清单(建议)
- 确定沙箱实现方案iframe + postMessage与接口白名单。
- 设计并实现一个前端卡沙箱适配层:在宿主页面暴露受控 API映射到后端或前端状态商店。
- 定义前端卡数据模型HTML/CSS/JS、Extensions、World Info 引用、变量定义等的数据库字段与版本控制。
- 引入内容审核/过滤机制,对上传的前端卡进行静态/动态分析,防止危害。
- 提供一个最小可用版本MVP只支持简单的 UI 注入与变量调用逐步扩展互动组件、VN 模式、背包/商店等系统。
- 编写迁移指南与回滚策略,保障升级过程的可控性。
- 与前端团队协同,建立 UI/UX 一致性规范渐进式增强、UI 风格、交互动画等)。
---
### 7. 风险与注意事项
- 安全性是第一位,绝不能让前端卡访问宿主的敏感 API、Token、Key 等。
- 资源占用管理,避免长时间执行脚本导致内存/CPU 过高。
- 兼容性测试要覆盖历史卡、扩展卡、以及新卡的回滚路径。
- 版本与数据一致性,确保升级/回滚时的卡状态不会导致数据错乱。
---
### 8. 参考与链接
- ST 官方文档(前端卡相关章节)
- 安全最佳实践Iframe 沙箱、postMessage、Content Security Policy
- 你们的现有实现片段:后端 API、WorldbookEngine、Extensions、Macros、Variables
---
### 9. 结论
**前端卡 = 角色卡数据 + 动态 UI 脚本。**
在重构时,我建议你将其视为**“第三方不可信插件”**来处理。
1. **后端Go**:负责存储和校验 HTML/JS 字符串,但不执行。
2. **前端React**:负责开辟一个“隔离区”(沙箱),把脚本关进去运行。
3. **兼容性**:你需要实现一个适配器,让那些习惯于 ST 语境的脚本以为自己还在 ST 里,但实际上是在受你监控的“云酒馆”容器中。
如果你能完美解决“安全隔离”的同时保留“交互能力”,你的云酒馆将直接降维打击目前市面上大部分只能聊天的网页端酒馆。
---
### 10. 案例分析Clannad_v3.1.png 落地要点
请参阅独立文档 [docs/Clannad_v3.1_analysis.md](./Clannad_v3.1_analysis.md) 以获取完整的落地要点、字段设计、实现步骤与验收标准。

458
docs/重构文档.md Normal file
View File

@@ -0,0 +1,458 @@
这是一个需要长期演进、但方向已经比较清晰的重构计划。为了让“云酒馆”既能承载多用户公共场景,又能高度兼容 SillyTavern以下简称 **ST**)生态,本方案结合当前代码与 `st/` 源码,对整体架构和落地路径进行整理与校准。
---
## 1. 总体架构与设计原则
### 1.1 技术栈与目录结构
- **前端**React 18 + TypeScript + Tailwind CSS + Zustand
- 代码目录:`web-app/`
- **后端**Go + Gin + GORM + pgvector
- 代码目录:`server/`
- **数据库**PostgreSQL 15+(大量使用 JSONB部分向量字段使用 pgvector
- **文件存储**S3 兼容MinIO / OSS 等),用于 PNG 角色卡 / 头像等
- **ST 源码**`st/` 目录,用作兼容参考与数据样例
### 1.2 云版 vs 本地版的根本差异
- **ST 当前形态**
- 本地应用,配置与数据以文件形式存在 `default/content/**``data/**`
- 单机 / 小规模用户,无严格多租户和权限隔离
- **云酒馆目标形态**
- **数据库优先**:所有业务数据(用户、角色、预设、世界书、对话、正则脚本等)统一落到 PostgreSQL
- **多租户隔离**:所有核心表带 `user_id`,中间件强制 `WHERE user_id = ?`
- **前后端分层清晰**
- 后端 → 唯一“真相来源”SSOT负责 Prompt pipeline、世界书触发、正则处理、变量替换、AI 调用
- 前端 → 纯 UI 壳:负责编辑与展示,不再私自保存“内存版世界书/预设”
---
## 2. 当前系统现状(结合现有代码)
### 2.1 后端(`server/`
`server/model/app/README.md` 以及各模型文件来看,当前状态大致为:
- **用户与会话**(已完成)
- 模型:`AppUser``AppUserSession`
- JWT 登录 / 刷新 / 注销 / 用户资料修改等已具备
- **角色卡管理AICharacter**(已完成)
- 模型:`AICharacter`(使用 JSONB 存 ST V2 数据,如 `CardData` / `Tags` 等)
- 支持 PNG / JSON 角色卡导入、角色列表、详情、编辑、删除、导出
- **预设管理AIPreset**模型已就绪API 待补齐)
- 模型:`AIPreset`,已拆分采样参数字段 + `Extensions JSONB`
- **世界书AIWorldInfo与正则脚本RegexScript**(模型已存在,逻辑未完全落地)
- 模型:`AIWorldInfo`(世界书实体)
- 模型:`RegexScript`(完全对齐 ST 正则脚本字段)
- **对话与消息**(基础模型已存在)
- 模型:`AIChat``AIMessage``AIMessageSwipe`,用于会话与多候选
- **AI 配置与向量记忆**(已具备)
- 模型:`AIProvider``AIModel``AIMemoryVector`
> 小结后端数据模型已经为「ST 兼容 + 云平台」打好了基础,但 Prompt pipeline、世界书触发引擎、正则处理引擎等运行时逻辑仍需集中落地。
### 2.2 前端(`web-app/`
- **状态管理Zustand**`src/store/index.ts`
- 单一 Store明确区分 `Model` / `Actions` / `Selectors`
- 内置变量系统 `variables: { user, char, ... }``substituteVariables()` 工具
- **用户系统**:登录 / 注册 / 用户资料页已接入后端
- **角色卡管理**:角色列表 / 上传 PNG/JSON / 编辑 / 导出已接入 `/app/character`
- **预设管理**:有 UI 骨架,但部分仍依赖前端假数据,需要接入真实 `/app/preset`
- **聊天界面**`ChatPage` + `ChatArea` + `MessageContent`
- 布局与基本会话切换已完成
- 近期已接入:
- `regexEngine.ts`:前端正则脚本执行引擎
- `textRenderer.ts`:文本渲染管线(解析 `<maintext>` / `<Status_block>` / choices 等)
- `ChatArea.tsx` 中从后端加载 `RegexScript` 列表,将用户 / 角色变量与脚本一并传入 `MessageContent`
> 小结:前端已具备 MVU 风格的状态管理与 ST 风格文本渲染能力,下一步是把预设 / 世界书 / 正则选择与后端 pipeline 串联起来。
---
## 3. 目标架构SillyTavern 兼容实现)
### 3.1 核心领域模型(后端视角)
基于当前 `server/model/app`,目标是让以下模型整体表达 ST 的角色卡 / 世界书 / 正则 / 预设 / 对话体系:
- **AICharacter**
- 存 ST 角色卡 V2/V3 标准化版本(结构化字段)+ 原始 JSON 存档(用于无损导出)
- 预留:
- `raw_card_json JSONB`(若尚未添加,可在后续迁移中补上)
- `bound_worldinfo_ids`:角色绑定世界书 ID 列表(可选)
- `bound_regex_ids`:角色绑定正则脚本 ID 列表(可选)
- **AIWorldInfoWorldbook & WorldbookEntry**
- 支持:
- 角色内世界书(挂在某个角色下)
- 用户级 / 全局世界书(多个角色共享)
- 字段需覆盖 ST 世界书的完整配置:
- `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 / 额外 JSON`
- **RegexScript**
- 已与 ST 字段对齐:
- `findRegex / replaceWith / trimStrings(JSONB)`
- `placement`(输入 / 输出 / 世界书 / 显示等阶段)
- `disabled / markdownOnly / runOnEdit / promptOnly`
- `substituteRegex / minDepth / maxDepth`
- `scope (global | character | preset)` + `ownerCharId / ownerPresetId`
- `order`(执行顺序)
- **AIPreset + 绑定关系**
- `AIPreset` 存采样参数 + systemPrompt + 其他配置
- 预留 `PresetPrompt` / `PresetRegexBinding` 等概念,用于:
- 描述 prompt 列表(不同位置、深度)
- 绑定正则脚本到特定预设
- **AIChat / AIMessage**
- 对话Conversation与消息Message
-`AICharacter``AIPreset`、AI 配置等建立关联
- `AIChat.Settings JSONB` 存储会话级设置:
- 当前 presetId
- 活动世界书 ID 列表
- 活动正则脚本 ID 列表
### 3.2 运行时 Prompt Pipeline目标形态
所有客户端(目前只有 React Web未来可能有更多统一通过 Go 后端完成以下完整流程:
1. **加载上下文**
- 根据 conversationId 加载:`AIChat``AICharacter``AIPreset`、AIConfig、绑定的 `AIWorldInfo``RegexScript`
2. **输入正则处理placement = input**
- 对用户输入文本先跑一遍 RegexScriptglobal + character + preset
3. **写入 user message** 到数据库
4. **世界书扫描World Info Engine**
- 在最近 N 条消息 + 角色描述 / 场景 等组成的扫描文本上,套用 ST 的 world-info 算法:
- 主 / 副关键词匹配,支持 `use_regex``caseSensitive``matchWholeWords`
- `selectiveLogic` 控制主 / 副键组合逻辑
- `depth``sticky``cooldown``delay``probability` 等控制触发频次与时间窗
5. **Prompt 构建**
- system 部分:角色 system + scenario + 作者注 + preset.systemPrompt + world info
- history 部分:最近若干 user/assistant 消息
- 预设附加 prompt按 depth / position / order 注入其他内容
6. **模型调用**
- 根据 `AIProvider` / `AIModel` / `/app/ai-config` 配置,调用相应厂商 APIOpenAI 兼容 / Anthropic 等)
- 支持 SSE 流式输出,将 token 流推送给前端
7. **输出正则处理placement = output**
- 对完整 AI 输出再次执行 RegexScriptglobal + character + preset
8. **写入 assistant message** 到数据库
9. **返回前端**
- 返回最终完整消息内容;若使用 SSE还需流式推送增量 token
---
## 4. 关键子系统的详细设计
### 4.1 用户与多租户隔离
- 所有“用户私有”数据表必须带 `user_id`
- 角色:`ai_characters.user_id`
- 预设:`ai_presets.user_id`
- 世界书:`ai_world_info.user_id`
- 正则脚本:`regex_scripts.user_id`
- 对话 / 消息:`ai_chats.user_id``ai_messages.user_id`
- **共有资源**
- 官方 Demo 角色 / 预设,可以 `user_id = 0``NULL`
- **中间件**
- 在 Gin 中实现统一鉴权中间件,从 JWT 中解析 `user_id`
- 在 Service 层封装“自动附带 `user_id` 条件”的查询方法,避免重复书写 `WHERE user_id = ?`
### 4.2 预设系统AIPreset与 ST 兼容
- **ST 现状**
- 预设分散在 `st/default/content/presets/**` 的 JSON 文件中,按 API 类型区分(`context` / `instruct` / `openai` / `textgen` 等)
- 前端 `public/scripts/preset-manager.js` 在内存管理这些预设
- **云酒馆设计**
- 使用 `AIPreset` 表存储统一预设:
- 常见采样参数拆为字段(`Temperature/TopP/TopK/...`
- 复杂或厂商特定配置放入 `Extensions jsonb`
- 导入工具:
- 后台脚本扫描 ST `presets` 目录 → 解析 JSON → 映射为 `AIPreset`
- 前端:
- 新建 `src/api/preset.ts` 封装 `/app/preset` 系列接口
- `PresetManagePage` 完全改为“后端驱动”(删除假数据 `useState`
### 4.3 变量与宏系统Variables & Macros
> 这一块需要区分“现状”与“目标”。
- **ST 当前做法(前端)**
- `public/scripts/variables.js`:维护局部 / 全局变量(基于 `chat_metadata``extension_settings`
- `public/scripts/macros.js` + `scripts/macros/**`:处理 `{{user}} / {{char}} / {{random}} / {{pick:...}}` 等宏
- 宏替换发生在前端构造 prompt 的阶段
- **云酒馆现状(前端)**
- `web-app/src/store/index.ts`
- `variables: Record<string, string>``user` / `char`
- `substituteVariables(text, customVars?)` 支持时间宏、随机数、`pick`
- `web-app/src/lib/textRenderer.ts` 中也实现了一套独立的变量替换逻辑
- **目标设计(后端统一化)**
- 中短期可以继续在前端做变量替换(已实现),减少一次性改动量
- 中长期建议:
- 后端增加 `user_variables` 表,存 `(user_id, key, value)`
- 在 Prompt pipeline 最后一步由后端统一执行递归变量 / 宏替换(直到不再出现 `{{...}}`
- 文档中要清楚标注:**“后端统一替换变量”是重构目标,而非当前 ST 现状,也不是当前实现状态。**
### 4.4 世界书引擎World Info Engine
- **ST 实现(真实代码)**`st/public/scripts/world-info.js`
- 非常复杂,包含:
- 多种扫描策略(按 persona / description / scenario / creatorNotes 等)
- 关键字与正则混用(`use_regex`
- 主键 / 副键组合逻辑(`selectiveLogic`
- `depth` / `sticky` / `cooldown` / `delay` / `probability` / `position` / `group` 等参数
- **云酒馆建议做法**
1. 从以下来源收集“激活世界书列表”:
- 用户全局启用:例如用户偏好中的 `activeWorldInfoIds`
- 角色绑定:`AICharacter` 上的世界书绑定字段
- 会话级设置:`AIChat.Settings.activeWorldInfoIds`
2. 遍历所有激活世界书的 entries
- 跳过 `disabled`
-`constant = true`,无视关键词匹配直接候选(仍受 sticky / cooldown / delay / probability 影响)
- 根据 `use_regex` / `caseSensitive` / `matchWholeWords``keys` / `secondary_keys` 做匹配
-`selectiveLogic` 结合主副键匹配结果
- 应用 `depth` / `sticky` / `cooldown` / `delay` / `probability` 等控制触发频次
3. 将激活的 entries 按 `order` / `position` 规则,注入到 Prompt 中对应位置
- **现实建议**
- 为保证兼容性,尽量参考 / 复刻 ST `world-info.js` 的排序和筛选逻辑,而不是“重写一套简化版”。
### 4.5 正则脚本引擎Regex Scripts
- **后端模型**`RegexScript`(已实现,字段与 ST 对齐)
- **前端执行(显示层)**
- `web-app/src/lib/regexEngine.ts`
- `applyRegexScripts(text, placement, scripts, depth?)`
- `processAIOutput` / `processUserInput` 等便捷函数
- 支持 `/pattern/flags`、捕获组 `$1/$2/...``{{match}}``trimStrings`
- `ChatArea.tsx`:加载 `/api/regex` 列表,过滤掉 `disabled`,传给 `MessageContent`
- **后端执行Prompt 层)**
- 输入阶段:用户文本入库 / 调用模型前,对应 ST 的“input placement”
- 输出阶段:模型输出保存 / 返回前,对应 ST 的“output placement”
- 世界书阶段:若需要在构造 world info 前后也跑一次 Regex可以利用 `placement` 细分
---
## 5. 前端重构与集成方案(`web-app/`
### 5.1 MVU 状态管理Zustand
- **现状**
- `src/store/index.ts` 已以 MVU 思路实现:
- `AppState` = Model
- `AppActions` = Update
- React 组件 = View
- 使用 `persist``devtools` 扩展,变量系统内置在 `variables` 字段中
- **建议**
- 将以下信息也纳入 Store而不是零散地用本地 `useState`
- 当前会话使用的 `presetId`
- 当前会话启用的世界书 ID 列表
- 当前会话启用的正则脚本 ID 列表
- 通过一个统一的 `ConversationSettings` 结构,与后端 `AIChat.Settings` 对应
### 5.2 角色卡管理页面CharacterManagePage
目标:前端字段与 ST V2/V3 完全对齐,为后端世界书 / 正则拆分做好数据基础。
- **字段映射建议**
- `firstMes``first_mes`
- `mesExample``mes_example`
- `systemPrompt``system_prompt`
- `postHistoryInstructions``post_history_instructions`
- `characterBook``character_book`
- `extensions``extensions`
- **世界书编辑 UI**
- 在现有 `keys / content / enabled / position` 基础上,逐步引入高级字段(折叠到“高级设置”):
- `secondary_keys``constant``use_regex``case_sensitive``match_whole_words`
- `selective``selective_logic`
- `depth``order``probability``sticky``cooldown``delay``group`
- 保存时将上述字段完整写入 `characterBook.entries` 中,后端再拆分到 `AIWorldInfo` 相关表
- **导入/导出**
- 前端不对 ST JSON 结构做“智能改写”,只做表单 ↔ JSON 的字段映射
- ST 兼容性由后端 `utils/character_card.go` + `AICharacter` 来保证
### 5.3 预设管理页面PresetManagePage
目标:彻底从“前端内存预设”升级为“后端驱动的 ST 预设系统”。
- 实施步骤:
1. 后端按规划实现 `/app/preset` 的 CRUD + 导入/导出
2. 前端新建 `src/api/preset.ts`,统一管理预设相关请求
3.`PresetManagePage` 中的本地假数据与 `useState` 初始值删除,全部改为从 API 加载
4. 预设 JSON 字段对齐 ST
- 采样参数:`temperature/top_p/top_k/frequency_penalty/presence_penalty/...`
- Prompt 配置:`system_prompt/stop_sequences/...`
### 5.4 聊天与设置面板ChatPage / SettingsPanel / CharacterPanel
- **SettingsPanel 增强**
- 增加:
- 当前会话使用的 preset下拉列表来自 `/app/preset`
- 启用的世界书列表(来自 `/app/worldinfo`,初期可只展示角色内世界书)
- 启用的正则脚本列表(来自 `/app/regex`
- 保存时调用 `/app/chat/:id/settings`(或 `PUT /app/conversation/:id`)更新 `AIChat.Settings`
- **ChatArea & MessageContent 集成**
- `ChatArea.tsx`
- 已从 `useAppStore` 取出 `user``variables`
- 已加载 `RegexScript` 列表,并传给 `MessageContent`
- `MessageContent.tsx`
- 利用 `regexEngine` + `textRenderer` 完成:
- 正则处理AI 输出修饰)
- `<maintext>` / `<Status_block>` / choices 解析
- 变量替换 / HTML 转义 / 代码块提取 / XSS 防护
---
## 6. 分阶段落地路线图
### 阶段 1打通 CRUD 与数据流(短期)
- **后端**
- 巩固 `/app/character`(保证 ST V2/V3 导入/导出稳定)
- 实现 `/app/preset` 全套 CRUD + 导入/导出
- 补齐世界书 `/app/worldinfo` 与正则 `/app/regex` 接口(基于现有模型)
- **前端**
- `PresetManagePage` 完全接入 `/app/preset`
- 角色编辑页面补全世界书 entry 字段,保持与 ST 的字段对齐
### 阶段 2实现 Prompt Pipeline 与对话 API中期
- **后端**
- 在 service 层实现统一的 `chatPipeline`串接世界书、正则、变量、预设、AI 调用逻辑
- 统一对话入口,例如:`POST /app/chat/:id/messages``POST /app/conversation/:id/message`
- 支持 SSE 流式输出,将 token 流推送给前端
- **前端**
- Chat 页面改造为消费 SSE 流(追加消息 / 局部更新)
- SettingsPanel / CharacterPanel 配置的 preset / 世界书 / 正则 与后端 `AIChat.Settings` 打通
### 阶段 3插件系统与高级特性长期
- **后端**
- 引入插件表与 Hook 定义(`onUserInput` / `onWorldInfoScan` / `beforePromptBuild` / `onAssistantDone`
- 使用 goja / WASM 等方式实现安全的插件执行沙箱
- 增强审计、统计与限流机制
- **前端**
- 插件管理页:安装 / 启用 / 配置 / 查看日志
- 调试视图:展示某次回复中有哪些 world info / 正则 / 插件生效
---
## 7. 重要澄清与修正(相对早期文档)
- **关于 Zod**
- 当前 `st/` 源码并未使用 Zod而是通过 `TavernCardValidator` 等自写校验器验证角色卡结构
-`web-app` 中引入 Zod 进行前端 schema 校验是一个“改进方向”,不是 ST 现状
- **关于 MVU**
- ST 当前前端仍是 jQuery + 全局脚本,不是典型 MVU 架构
- MVU 已在云酒馆前端的 Zustand Store 中落地,应把 MVU 归于“云酒馆前端架构”,而非 ST 现状
- **关于变量 / 宏所在层级**
- ST 将变量与宏主要放在前端处理
- 云酒馆的目标是:短期兼容这种做法,中长期逐步迁移到后端统一替换
- **关于 PNG 块类型**
- ST 将角色卡 JSON 植入 PNG 文本 chunk`tEXt` / 自定义块),不必限定为 `iTXt`
- Go 侧应兼容多种文本块,保证最大兼容性
## 8. 核心模块开发方案
### 8.1 用户管理与数据隔离
**ST 现状**: 物理文件夹隔离(`public/users/admin/...`)。
**重构方案**:
* **数据库设计**: 所有表(`characters`, `chats`, `presets`, `lorebooks`)必须包含 `user_id` 字段。
* **权限控制**: 后端中间件统一拦截请求,从 JWT 中提取 `user_id`,并在所有 SQL 查询中强制加入 `WHERE user_id = ?` 约束。
* **隔离策略**:
* **共有资源**: 官方提供的默认预设、系统角色卡(`user_id` 为 0 或 NULL
* **私有资源**: 用户上传或创建的内容。
### 8.2 预设系统 (Presets) 兼容方案
**ST 现状**: 大量的 JSON 文件,分为 API、Instruct、Context 等类别。
**重构方案**:
* **存储**: 在 PostgreSQL 中创建 `presets` 表,核心配置字段设为 `jsonb` 类型。
* **导入逻辑**: 编写一个 Go 工具类,读取 ST 的 `presets` 文件夹,将 JSON 解析后存入数据库。
* **注意事项**: ST 的预设经常更新字段Go 的结构体定义要使用 `map[string]interface{}` 或冗余字段来保证在 ST 更新协议时,你的后端不会因为缺失字段而崩溃。
### 8.3 变量管理与宏引擎 (Variables & Macros)
这是兼容性的灵魂。
**实现原理**:
1. **基础宏**: `{{user}}`, `{{char}}`, `{{description}}` 等。
2. **动态变量**: 用户通过 `/setvar` 定义的变量。
**重构方案**:
* **后端处理**: 不要在前端替换变量,而是在后端构建 Prompt 的最后一刻进行正则替换。
* **变量表**: `user_variables` 表,存储 `(user_id, key, value)`
* **递归解析**: ST 支持嵌套宏,你的 Go 后端需要实现一个递归替换函数,直到字符串中不再包含 `{{...}}`
### 8.4 前端渲染与通信兼容 (Iframe/Sandbox)
**ST 现状**: jQuery 操作 DOM扩展直接注入 HTML。
**重构方案**:
* **渲染分离**: 使用 React 开发 UI。消息列表应支持 Markdown 解析(推荐使用 `react-markdown` 并配合插件)。
* **通信协议**:
* 前端 React 通过 **WebSocket****SSE** 与 Go 后端保持长连接,模拟 ST 的流式输出。
* 定义一套标准的 JSON API 响应格式,模仿 ST 的 `/api/...` 接口,以便未来的脚本迁移。
### 8.5 扩展系统 (Extensions) 的安全性重构
**ST 现状**: 动态加载 JS 脚本,安全性极低。
**重构方案 (公共平台推荐)**:
* **后端扩展**: 模仿 ST 的插件钩子,但在 Go 后端实现。例如,在发送 Prompt 前,调用一个“拦截器”逻辑。
* **前端扩展**: 如果要兼容 ST 的 JS 插件,**必须将其运行在沙箱Sandbox中**。
* 使用 `<iframe>` 隔离扩展 UI。
* 通过 `postMessage` 机制进行通信。
* **注意事项**: 绝不允许第三方扩展直接访问浏览器的 `localStorage` 或获取用户的 `user_token`
---
## 9. Zod 与 MVU 的重构实现
### 9.1 Zod 变量的作用与迁移
在 ST 源码中Zod 用于 **Runtime Schema Validation**
* **作用**: 当用户上传一个外部角色卡JSON 或 PNG 嵌入数据系统无法确定数据是否完整。Zod 会在前端校验数据格式,如果缺失关键字段,会提供默认值或报错。
* **重构建议**:
* **前端**: 继续在 React 中使用 `zod`。在处理角色卡导入逻辑时,定义与 ST 完全一致的 `CharacterSchema`
* **后端**: Go 端使用 `go-playground/validator` 或直接解析 `jsonb`。确保你的 Go 结构体Struct打上 `json:",omitempty"` 标签,以处理 ST 角色卡中不稳定的可选字段。
### 9.2 MVU (Model-View-Update) 的应用
* **作用**: ST 引入 MVU 是为了解决 jQuery 状态难以管理的问题。
* **重构建议**:
* 在 React 中MVU 就是 **Zustand/Redux + React Components**
* **Model**: Zustand 中的 `store`
* **Update**: Store 中的 `actions`
* **View**: React 组件。
* 你不需要在代码里写 `mvu` 变量名,只需遵循这种单向数据流即可实现比 ST 稳定得多的状态管理。
---
## 10. 开发注意事项 (踩坑预警)
1. **角色卡解析**: ST 的 PNG 角色卡数据存放在 PNG 的 `iTXt` 块中。Go 语言需要使用 `image/png` 包并手动读取这些 Metadata 块。不要只支持 JSON因为大部分玩家的资源都是 PNG。
2. **Lorebook (世界书) 逻辑**:
* 这是最难兼容的部分。它涉及:关键字扫描、递归深度、插入位置、插入顺序。
* **建议**: 直接复刻 ST 的 `world-info.js` 中的排序算法。如果这部分算法不一致AI 的表现会与原版 ST 大相径庭。
3. **计算 Token**: ST 使用 `Tiktoken` 或特定模型的 Tokenizer。为了节省服务器资源建议在前端进行初次计算后端在调用 API 前再次校准。
4. **跨域与 API 代理**: 公共平台需要处理大量大模型 API 的转发。Go 后端要实现一个高性能的 **Proxy Layer**处理超时、重试以及不同厂商OpenAI, Anthropic, Google的协议转换。

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,267 @@
## 预设模块Preset导入与启用逻辑优化设计
### 一、背景与问题
当前系统支持从 SillyTavern / TavernAI 风格的 JSON 文件导入预设并将其中的采样参数和提示词配置prompts / prompt_order存入后端 `AIPreset.Extensions` 字段中,前端在 `PresetManagePage` / `EditPresetModal` 中提供基本的编辑能力。
在实际使用中暴露出一个明显问题:
- **问题导入预设后提示词块prompts的启用 / 禁用状态与原始 JSON 文件不一致。**
具体表现:
- SillyTavern 预设中的「默认启用 / 禁用」信息主要体现在:
- `prompts[*].enabled`(整体默认开关,可能存在)
- `prompt_order[*].order[*].enabled`(更精细的 per-character 启用信息,是真正生效的一层)
- 我们当前前端 UI 在编辑时:
- 仅从 `extensions.prompts` 中取出 prompts
-`marker` 字段反向推导「启用」状态(`checked={!prompt.marker}`
- 完全没有解析和尊重 `prompt_order` 里的 `enabled` 状态
因此,对于像 `Sudachi.Next1.21.json` 这种复杂预设:
- 文件中已经通过 `prompt_order` 精确控制了某些块默认 `enabled: false`
- 导入后数据虽然保存在 `extensions.prompt_order` 中,但前端不读取,导致 UI 展示的勾选状态与原始预设不一致
### 二、设计目标
1. **忠实还原 ST 预设的启用 / 禁用行为**
- 导入后,在不改动 ST 原始结构的前提下,让 UI 所见的启用状态尽可能与 ST 一致。
2. **保持数据的完整与可扩展性**
- 保留 `prompts``prompt_order` 的原始结构,未来可以继续扩展更高级行为(多角色、多上下文)。
3. **简化前端编辑模型**
- 前端在编辑时只需要关注:
- 哪些 prompt 当前“启用”
- 顺序如何
- 不强制实现 ST 全量复杂逻辑,但要保证基础行为正确。
4. **向后兼容现有数据**
- 对于已存在的 `AIPreset`,在没有 `prompt_order` / `enabled` 信息时,仍然能正常工作。
### 三、数据结构与语义梳理
#### 3.1 ST 预设结构(简化视图)
- 顶层采样 / 行为字段(已正确映射到 `AIPreset`
- `temperature`, `top_p`, `top_k`, `min_p`, `top_a`
- `frequency_penalty`, `presence_penalty`, `repetition_penalty`
- `openai_max_tokens`, `openai_max_context`, `stream_openai`, 等
- `prompts: Prompt[]`
- `name: string`
- `identifier: string`(如 `"main"`, `"nsfw"`, `"jailbreak"` 或 UUID
- `role: "system" | "user" | "assistant"`
- `content: string`
- `system_prompt: boolean`
- `marker: boolean`(占位块;本身不一定等于“禁用”)
- `enabled?: boolean`(整体缺省启用的开关)
- `injection_position: number`
- `injection_depth: number`
- `injection_order: number`
- `injection_trigger: any[]`
- `forbid_overrides: boolean`
- `prompt_order: Array<{ character_id: number; order: Array<{ identifier: string; enabled: boolean }> }>`
- 按「角色 / 场景」维度,指定各 identifier 的启用状态与顺序
**关键点:**
真正决定「是否启用」的是 `prompt_order[*].order[*].enabled``prompts[*].enabled` 更多是全局默认值。
#### 3.2 当前系统存储结构
后端 `PresetService.ImportPresetFromJSON` 当前的行为:
-`prompts` / `prompt_order` 原样塞入:
```go
extensions := map[string]interface{}{
"prompts": stPreset.Prompts,
"prompt_order": stPreset.PromptOrder,
}
```
前端 `EditPresetModal` 当前的行为:
- 通过 `preset.extensions.prompts` 取得 `prompts` 列表
- 「启用」复选框用的是:
- `checked={!prompt.marker}`
- 也就是:只要不是 marker就视为启用
- 没有考虑:
- `prompt.enabled`
- `prompt_order[*].order[*].enabled`
### 四、优化方案总览
整体策略:
- **后端保持“原样存储”原则**,继续把 ST 的 `prompts``prompt_order` 保存在 `extensions` 里,方便以后做高级兼容。
- **新增一层“系统视角配置”**(推荐写在 `extensions.stMapping` 内),用于:
- 提取出我们当前真正要用到的“默认启用 / 禁用信息”
- 为前端提供简单直观的数据结构
- **前端 UI 只依赖这层映射**来决定默认勾选状态、顺序等,避免直接耦合 ST 的复杂行为。
### 五、后端导入逻辑优化设计
#### 5.1 导入时的启用状态归一化
`PresetService.ImportPresetFromJSON` 中,对 `stPreset.Prompts``stPreset.PromptOrder` 做一次归一化处理,得到:
- `map[string]bool``identifier``enabled`(系统视角下的默认启用状态)
处理规则建议如下:
1.`prompt_order` 中选出一个「主视角 character_id」
- 优先策略:取 `prompt_order`**第一个条目**`character_id`(例如 Sudachi 文件中的 `100000`)。
- 这样可理解为“该预设的主角 / 默认角色对应的一组配置”。
2. 针对该 `character_id``order` 数组:
- 遍历每一项 `{ identifier, enabled }`
- 记入 `enabledMap[identifier] = enabled`
3. 对于 `prompts` 中存在但在 `order` 中未出现的 identifier
-`prompt.enabled` 明确存在,则用该字段
- 否则默认 `true`(保持向后兼容,除非 ST 未来规范有特别说明)
归一化后的结果示意:
```json
{
"stMapping": {
"mainCharacterId": 100000,
"enabledByIdentifier": {
"main": true,
"worldInfoBefore": true,
"enhanceDefinitions": false,
"nsfw": true,
...
}
}
}
```
注意:
- 原始的 `prompts` / `prompt_order` 仍然原封不动地存放在 `extensions.prompts` / `extensions.prompt_order` 中。
- `stMapping` 只是我们系统内部使用的“视图”,不影响原始数据。
#### 5.2 可选:为 prompts 注入统一的 `enabled` 字段
为了前端更好使用,也可以在导入时(或者在后端读取时)对 `prompts` 做一次增强:
- 对每个 `prompt`
- 查询 `enabled := stMapping.enabledByIdentifier[prompt.identifier]`
- 如果找到了,就在该 `prompt` 上写入 / 覆盖 `prompt["enabled"] = enabled`
这样,前端只要拿到 `extensions.prompts`,就能直接看到每条的 `enabled` 字段,不必额外关联 `prompt_order`
> 兼容建议:
> - 不删除原有的 `enabled`,只是统一成一个规范值;
> - 如果未来 ST 升级行为,我们仍保留原始 JSON可以再次调整映射逻辑。
### 六、前端编辑行为优化设计
#### 6.1 EditPresetModal 中的启用逻辑修改
当前逻辑:
- 通过 `getPrompts()``extensions.prompts` 取出数组
- 复选框逻辑:
```tsx
<input
type="checkbox"
checked={!prompt.marker}
onChange={(e) => handlePromptChange(index, 'marker', !e.target.checked)}
/>
```
问题:
- 把 “是否 marker” 当成了 “是否启用”,与 ST 语义不符。
优化方案:
1. 新增对 `enabled` 字段的支持
- 解析时,优先使用:
- `prompt.enabled`(如果存在)
- 否则回退为 `!prompt.marker`(为了兼容老数据)
2. UI 含义调整:
- 勾选框表示「此 prompt 是否启用」,而非「是否 marker」
- `marker` 仍然保留,用于区分“占位块”(如 `chatHistory``dialogueExamples`)和“实际有内容的块”,但不再被直接当作启用条件。
示例(伪代码):
```tsx
const isEnabled = prompt.enabled !== undefined
? prompt.enabled
: !prompt.marker
<input
type="checkbox"
checked={isEnabled}
onChange={(e) => handlePromptChange(index, 'enabled', e.target.checked)}
/>
```
3. 保存时,将 `enabled` 写回 `extensions.prompts` 中对应项。
> 提醒:
> - 短期内可以不在前端去写 `prompt_order`,只要我们的对话生成逻辑是“按 prompts + enabled 过滤”就足够;
> - 如果未来要做到 ST 完整语义(如多角色不同顺序),再在前端提供更高级的顺序管理 UI。
#### 6.2 默认显示顺序
当前 UI 在 EditPresetModal 里对 prompts 没有太多排序逻辑,直接用数组顺序:
- 初期优化阶段,可以继续沿用 ST 原始 `prompts` 顺序
- 如果希望更贴近 ST 行为,可以:
- 根据 `injection_position` / `injection_depth` / `injection_order` 做一个简易排序
- 或者读取 `stMapping.mainCharacterId` 对应 `prompt_order` 的顺序,按其中 identifier 顺序排列
建议分阶段:
1. **第一阶段(快速修复启用逻辑)**
- 先只修复 `enabled` 使用,仍按原数组顺序展示
2. **第二阶段(顺序优化)**
- 再根据 `prompt_order` / injection 字段优化展示顺序
### 七、对话生成逻辑的使用建议(可选)
目前后端对话生成Convesation Service是如何使用预设参数需要在代码中再具体确认。但从设计角度建议
1. 生成最终 prompt 时:
-`extensions.prompts` 中筛选:`enabled != false` 的 prompt
- 按既定顺序(可先用原始数组顺序,或更细致地用 `injection_*` / `prompt_order`)拼接成系统提示。
2. 如需高度还原 ST 行为:
- 在后端增加一个专门的「ST Prompt 编译器」:
- 输入:`prompts` + `prompt_order` + 当前角色 / 会话上下文
- 输出:一串系统消息数组(包含角色、内容、插入位置信息)
- 这一步可以在后续迭代中实现,本次优化先以「启用状态正确」为目标。
### 八、兼容性与迁移策略
1. **已有预设数据**
- 已有的 `AIPreset.Extensions` 中可能只有 `prompts`,没有 `prompt_order` / `stMapping` / `enabled`
- `EditPresetModal` 在解析时:
- 若找不到 `prompt.enabled`,则默认 `enabled = !marker`
- 新导入的预设,将会有完整的 `enabled` / `stMapping` 信息。
2. **导出为 ST JSON**
- 目前导出的逻辑是从 `AIPreset` 再组装为 ST 样式的 JSON
- 建议导出时仍以 `extensions.prompts` / `extensions.prompt_order` 为主
- `enabled` 字段如果已更新,也可以同步写回到导出 JSON 中对应位置,保持一致性
3. **前后端升级顺序**
- 先在后端实现 `stMapping` 和 prompts 的 `enabled` 归一化逻辑
- 再在前端切换到从 `prompt.enabled` 读取启用状态
- 测试导入 Sudachi 预设后,界面上默认启用 / 禁用是否与原文件一致
### 九、后续扩展方向(可选)
1. **多角色 / 多场景支持**
- 完整解析 `prompt_order` 中所有 `character_id` 条目,根据当前对话绑定的角色 ID 切换不同启用方案。
2. **可视化 prompt 顺序编辑**
- 在 Preset 管理页面增加「提示词顺序拖拽调整」、分组展示(角色定义 / 文风 / COT / 变量初始化等)。
3. **预设版本管理与对比**
- 由于 ST 预设经常迭代,可考虑为 `AIPreset` 增加版本号 / 变更日志,以便回滚与对比。
---
以上设计以「最小代价修复启用/禁用逻辑不一致」为优先目标,同时为后续更深入兼容 ST 行为(如多角色、多 prompt_order 视图)预留空间。后续具体实现时,可以按本文件拆分为:
- 后端改动:`PresetService.ImportPresetFromJSON` + 可选的 Prompt 编译辅助函数
- 前端改动:`PresetManagePage` / `EditPresetModal` 中的 prompts 解析与复选框逻辑

View File

@@ -255,7 +255,7 @@ func (a *ConversationApi) regenerateMessageStream(c *gin.Context, userID, conver
go func() {
if err := service.ServiceGroupApp.AppServiceGroup.ConversationService.RegenerateMessageStream(
userID, conversationID, streamChan, doneChan,
c.Request.Context(), userID, conversationID, streamChan, doneChan,
); err != nil {
errorChan <- err
}
@@ -347,7 +347,7 @@ func (a *ConversationApi) SendMessageStream(c *gin.Context, userID, conversation
// 启动流式传输
go func() {
err := service.ServiceGroupApp.AppServiceGroup.ConversationService.SendMessageStream(
userID, conversationID, req, streamChan, doneChan,
c.Request.Context(), userID, conversationID, req, streamChan, doneChan,
)
if err != nil {
errorChan <- err

View File

@@ -51,6 +51,13 @@ func (a *RegexScriptApi) GetRegexScriptList(c *gin.Context) {
scopeInt, _ := strconv.Atoi(scope)
req.Scope = &scopeInt
}
if ownerCharID := c.Query("ownerCharId"); ownerCharID != "" {
ownerCharIDUint, err := strconv.ParseUint(ownerCharID, 10, 32)
if err == nil {
v := uint(ownerCharIDUint)
req.OwnerCharID = &v
}
}
if req.Page < 1 {
req.Page = 1

View File

@@ -321,3 +321,61 @@ func (s *SystemApiApi) FreshCasbin(c *gin.Context) {
}
response.OkWithMessage("刷新成功", c)
}
// GetApiRoles
// @Tags SysApi
// @Summary 获取拥有指定API权限的角色ID列表
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param path query string true "API路径"
// @Param method query string true "请求方法"
// @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "获取成功"
// @Router /api/getApiRoles [get]
func (s *SystemApiApi) GetApiRoles(c *gin.Context) {
path := c.Query("path")
method := c.Query("method")
if path == "" || method == "" {
response.FailWithMessage("API路径和请求方法不能为空", c)
return
}
authorityIds, err := casbinService.GetAuthoritiesByApi(path, method)
if err != nil {
global.GVA_LOG.Error("获取失败!", zap.Error(err))
response.FailWithMessage("获取失败"+err.Error(), c)
return
}
if authorityIds == nil {
authorityIds = []uint{}
}
response.OkWithDetailed(authorityIds, "获取成功", c)
}
// SetApiRoles
// @Tags SysApi
// @Summary 全量覆盖某API关联的角色列表
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body systemReq.SetApiAuthorities true "API路径、请求方法和角色ID列表"
// @Success 200 {object} response.Response{msg=string} "设置成功"
// @Router /api/setApiRoles [post]
func (s *SystemApiApi) SetApiRoles(c *gin.Context) {
var req systemReq.SetApiAuthorities
if err := c.ShouldBindJSON(&req); err != nil {
response.FailWithMessage(err.Error(), c)
return
}
if req.Path == "" || req.Method == "" {
response.FailWithMessage("API路径和请求方法不能为空", c)
return
}
if err := casbinService.SetApiAuthorities(req.Path, req.Method, req.AuthorityIds); err != nil {
global.GVA_LOG.Error("设置失败!", zap.Error(err))
response.FailWithMessage("设置失败"+err.Error(), c)
return
}
// 刷新casbin缓存使策略立即生效
_ = casbinService.FreshCasbin()
response.OkWithMessage("设置成功", c)
}

View File

@@ -4,6 +4,7 @@ import (
"git.echol.cn/loser/st/server/global"
"git.echol.cn/loser/st/server/model/common/response"
"git.echol.cn/loser/st/server/model/system"
systemReq "git.echol.cn/loser/st/server/model/system/request"
systemRes "git.echol.cn/loser/st/server/model/system/response"
"git.echol.cn/loser/st/server/utils"
@@ -200,3 +201,57 @@ func (a *AuthorityApi) SetDataAuthority(c *gin.Context) {
}
response.OkWithMessage("设置成功", c)
}
// GetUsersByAuthority
// @Tags Authority
// @Summary 获取拥有指定角色的用户ID列表
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param authorityId query uint true "角色ID"
// @Success 200 {object} response.Response{data=[]uint,msg=string} "获取成功"
// @Router /authority/getUsersByAuthority [get]
func (a *AuthorityApi) GetUsersByAuthority(c *gin.Context) {
var req systemReq.SetRoleUsers
if err := c.ShouldBindQuery(&req); err != nil {
response.FailWithMessage(err.Error(), c)
return
}
userIds, err := authorityService.GetUserIdsByAuthorityId(req.AuthorityId)
if err != nil {
global.GVA_LOG.Error("获取失败!", zap.Error(err))
response.FailWithMessage("获取失败"+err.Error(), c)
return
}
if userIds == nil {
userIds = []uint{}
}
response.OkWithDetailed(userIds, "获取成功", c)
}
// SetRoleUsers
// @Tags Authority
// @Summary 全量覆盖某角色关联的用户列表
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body systemReq.SetRoleUsers true "角色ID和用户ID列表"
// @Success 200 {object} response.Response{msg=string} "设置成功"
// @Router /authority/setRoleUsers [post]
func (a *AuthorityApi) SetRoleUsers(c *gin.Context) {
var req systemReq.SetRoleUsers
if err := c.ShouldBindJSON(&req); err != nil {
response.FailWithMessage(err.Error(), c)
return
}
if req.AuthorityId == 0 {
response.FailWithMessage("角色ID不能为空", c)
return
}
if err := authorityService.SetRoleUsers(req.AuthorityId, req.UserIds); err != nil {
global.GVA_LOG.Error("设置失败!", zap.Error(err))
response.FailWithMessage("设置失败"+err.Error(), c)
return
}
response.OkWithMessage("设置成功", c)
}

View File

@@ -244,6 +244,76 @@ func (a *AuthorityMenuApi) GetBaseMenuById(c *gin.Context) {
response.OkWithDetailed(systemRes.SysBaseMenuResponse{Menu: menu}, "获取成功", c)
}
// GetMenuRoles
// @Tags AuthorityMenu
// @Summary 获取拥有指定菜单的角色ID列表
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param menuId query uint true "菜单ID"
// @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "获取成功"
// @Router /menu/getMenuRoles [get]
func (a *AuthorityMenuApi) GetMenuRoles(c *gin.Context) {
var req systemReq.SetMenuAuthorities
if err := c.ShouldBindQuery(&req); err != nil {
response.FailWithMessage(err.Error(), c)
return
}
if req.MenuId == 0 {
response.FailWithMessage("菜单ID不能为空", c)
return
}
authorityIds, err := menuService.GetAuthoritiesByMenuId(req.MenuId)
if err != nil {
global.GVA_LOG.Error("获取失败!", zap.Error(err))
response.FailWithMessage("获取失败"+err.Error(), c)
return
}
if authorityIds == nil {
authorityIds = []uint{}
}
defaultRouterAuthorityIds, err := menuService.GetDefaultRouterAuthorityIds(req.MenuId)
if err != nil {
global.GVA_LOG.Error("获取首页角色失败!", zap.Error(err))
response.FailWithMessage("获取失败"+err.Error(), c)
return
}
if defaultRouterAuthorityIds == nil {
defaultRouterAuthorityIds = []uint{}
}
response.OkWithDetailed(gin.H{
"authorityIds": authorityIds,
"defaultRouterAuthorityIds": defaultRouterAuthorityIds,
}, "获取成功", c)
}
// SetMenuRoles
// @Tags AuthorityMenu
// @Summary 全量覆盖某菜单关联的角色列表
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body systemReq.SetMenuAuthorities true "菜单ID和角色ID列表"
// @Success 200 {object} response.Response{msg=string} "设置成功"
// @Router /menu/setMenuRoles [post]
func (a *AuthorityMenuApi) SetMenuRoles(c *gin.Context) {
var req systemReq.SetMenuAuthorities
if err := c.ShouldBindJSON(&req); err != nil {
response.FailWithMessage(err.Error(), c)
return
}
if req.MenuId == 0 {
response.FailWithMessage("菜单ID不能为空", c)
return
}
if err := menuService.SetMenuAuthorities(req.MenuId, req.AuthorityIds); err != nil {
global.GVA_LOG.Error("设置失败!", zap.Error(err))
response.FailWithMessage("设置失败"+err.Error(), c)
return
}
response.OkWithMessage("设置成功", c)
}
// GetMenuList
// @Tags Menu
// @Summary 分页获取基础menu列表

View File

@@ -1,6 +1,8 @@
package system
import (
"net/http"
"git.echol.cn/loser/st/server/global"
"git.echol.cn/loser/st/server/model/common/response"
"git.echol.cn/loser/st/server/model/system/request"
@@ -55,6 +57,17 @@ func (s *SkillsApi) SaveSkill(c *gin.Context) {
response.OkWithMessage("保存成功", c)
}
func (s *SkillsApi) DeleteSkill(c *gin.Context) {
var req request.SkillDeleteRequest
_ = c.ShouldBindJSON(&req)
if err := skillsService.Delete(c.Request.Context(), req); err != nil {
global.GVA_LOG.Error("删除技能失败", zap.Error(err))
response.FailWithMessage("删除技能失败: "+err.Error(), c)
return
}
response.OkWithMessage("删除成功", c)
}
func (s *SkillsApi) CreateScript(c *gin.Context) {
var req request.SkillScriptCreateRequest
_ = c.ShouldBindJSON(&req)
@@ -217,3 +230,34 @@ func (s *SkillsApi) SaveGlobalConstraint(c *gin.Context) {
}
response.OkWithMessage("保存成功", c)
}
func (s *SkillsApi) PackageSkill(c *gin.Context) {
var req request.SkillPackageRequest
_ = c.ShouldBindJSON(&req)
fileName, data, err := skillsService.Package(c.Request.Context(), req)
if err != nil {
global.GVA_LOG.Error("打包技能失败", zap.Error(err))
response.FailWithMessage("打包技能失败: "+err.Error(), c)
return
}
c.Header("Content-Type", "application/zip")
c.Header("Content-Disposition", "attachment; filename=\""+fileName+"\"")
c.Data(http.StatusOK, "application/zip", data)
}
func (s *SkillsApi) DownloadOnlineSkill(c *gin.Context) {
var req request.DownloadOnlineSkillReq
if err := c.ShouldBindJSON(&req); err != nil {
response.FailWithMessage("参数错误", c)
return
}
if err := skillsService.DownloadOnlineSkill(c.Request.Context(), req); err != nil {
global.GVA_LOG.Error("下载在线技能失败", zap.Error(err))
response.FailWithMessage("下载在线技能失败: "+err.Error(), c)
return
}
response.OkWithMessage("下载成功", c)
}

View File

@@ -12,5 +12,4 @@ type System struct {
UseMongo bool `mapstructure:"use-mongo" json:"use-mongo" yaml:"use-mongo"` // 使用mongo
UseStrictAuth bool `mapstructure:"use-strict-auth" json:"use-strict-auth" yaml:"use-strict-auth"` // 使用树形角色分配模式
DisableAutoMigrate bool `mapstructure:"disable-auto-migrate" json:"disable-auto-migrate" yaml:"disable-auto-migrate"` // 自动迁移数据库表结构生产环境建议设为false手动迁移
DataDir string `mapstructure:"data-dir" json:"data-dir" yaml:"data-dir"` // 数据目录
}

View File

@@ -35,10 +35,13 @@ func RunServer() {
address := fmt.Sprintf(":%d", global.GVA_CONFIG.System.Addr)
fmt.Printf(`
欢迎使用 gin-vue-admin
当前版本:%s
插件市场:https://plugin.gin-vue-admin.com
默认自动化文档地址:http://127.0.0.1%s/swagger/index.html
默认MCP SSE地址:http://127.0.0.1%s%s
默认MCP Message地址:http://127.0.0.1%s%s
默认前端文件运行地址:http://127.0.0.1:8080
`, address, address, global.GVA_CONFIG.MCP.SSEPath, address, global.GVA_CONFIG.MCP.MessagePath)
initServer(address, Router, 10*time.Minute, 10*time.Minute)
initServer(address, Router, 10*time.Minute, 0)
}

View File

@@ -4,7 +4,7 @@ package global
// 目前只有Version正式使用 其余为预留
const (
// Version 当前版本号
Version = "v2.8.9"
Version = "v2.9.0"
// AppName 应用名称
AppName = "Gin-Vue-Admin"
// Description 应用描述

View File

@@ -6,7 +6,7 @@ toolchain go1.24.2
require (
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible
github.com/aws/aws-sdk-go v1.55.6
github.com/aws/aws-sdk-go v1.55.8
github.com/casbin/casbin/v2 v2.103.0
github.com/casbin/gorm-adapter/v3 v3.32.0
github.com/dzwvip/gorm-oracle v0.1.2
@@ -20,13 +20,11 @@ require (
github.com/gookit/color v1.5.4
github.com/huaweicloud/huaweicloud-sdk-go-obs v3.24.9+incompatible
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible
github.com/lib/pq v1.10.9
github.com/mark3labs/mcp-go v0.41.1
github.com/mholt/archives v0.1.1
github.com/minio/minio-go/v7 v7.0.84
github.com/mojocn/base64Captcha v1.3.8
github.com/otiai10/copy v1.14.1
github.com/pgvector/pgvector-go v0.3.0
github.com/pkg/errors v0.9.1
github.com/qiniu/go-sdk/v7 v7.25.2
github.com/qiniu/qmgo v1.1.9
@@ -53,6 +51,7 @@ require (
gorm.io/driver/mysql v1.5.7
gorm.io/driver/postgres v1.5.11
gorm.io/driver/sqlserver v1.5.4
gorm.io/gen v0.3.26
gorm.io/gorm v1.25.12
)
@@ -121,7 +120,6 @@ require (
github.com/magiconair/properties v1.8.9 // indirect
github.com/mailru/easyjson v0.9.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v1.14.16 // indirect
github.com/microsoft/go-mssqldb v1.8.0 // indirect
github.com/minio/md5-simd v1.1.2 // indirect
github.com/minio/minlz v1.0.0 // indirect
@@ -174,13 +172,14 @@ require (
golang.org/x/arch v0.13.0 // indirect
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect
golang.org/x/image v0.23.0 // indirect
golang.org/x/net v0.37.0 // indirect
golang.org/x/mod v0.22.0 // indirect
golang.org/x/net v0.35.0 // indirect
golang.org/x/sys v0.32.0 // indirect
golang.org/x/time v0.9.0 // indirect
golang.org/x/tools v0.31.0 // indirect
golang.org/x/tools v0.29.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gorm.io/driver/sqlite v1.5.0 // indirect
gorm.io/hints v1.1.2 // indirect
gorm.io/plugin/dbresolver v1.5.3 // indirect
modernc.org/fileutil v1.3.0 // indirect
modernc.org/libc v1.61.9 // indirect

View File

@@ -15,8 +15,6 @@ cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
entgo.io/ent v0.14.3 h1:wokAV/kIlH9TeklJWGGS7AYJdVckr0DloWjIcO9iIIQ=
entgo.io/ent v0.14.3/go.mod h1:aDPE/OziPEu8+OWbzy4UlvWmD2/kbRuWfK2A40hcxJM=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q=
@@ -56,8 +54,8 @@ github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible h1:8psS8a+wKfiLt1iVDX79F
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8=
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/aws/aws-sdk-go v1.55.6 h1:cSg4pvZ3m8dgYcgqB97MrcdjUmZ1BeMYKUxMMB89IPk=
github.com/aws/aws-sdk-go v1.55.6/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
github.com/aws/aws-sdk-go v1.55.8 h1:JRmEUbU52aJQZ2AjX4q4Wu7t4uZjOu71uyNmaWlUkJQ=
github.com/aws/aws-sdk-go v1.55.8/go.mod h1:ZkViS9AqA6otK+JBBNH2++sx1sgxrPKcSzPPvQkUtXk=
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
@@ -152,10 +150,6 @@ github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9Z
github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk=
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/go-pg/pg/v10 v10.11.0 h1:CMKJqLgTrfpE/aOVeLdybezR2om071Vh38OLZjsyMI0=
github.com/go-pg/pg/v10 v10.11.0/go.mod h1:4BpHRoxE61y4Onpof3x1a2SQvi9c+q1dJnrNdMjsroA=
github.com/go-pg/zerochecker v0.2.0 h1:pp7f72c3DobMWOb2ErtZsnrPaSvHd2W4o9//8HtF4mU=
github.com/go-pg/zerochecker v0.2.0/go.mod h1:NJZ4wKL0NmTtz0GKCoJ8kym6Xn/EQzXRl2OnAe7MmDo=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
@@ -214,9 +208,8 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
@@ -284,8 +277,6 @@ github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9Y
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible h1:jdpOPRN1zP63Td1hDQbZW73xKmzDvZHzVdNYxhnTMDA=
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible/go.mod h1:1c7szIrayyPPB/987hsnvNzLushdWf4o/79s3P08L8A=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
@@ -322,8 +313,8 @@ github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgx
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 h1:7UMa6KCCMjZEMDtTVdcGu0B1GmmC7QJKiCCjyTAWQy0=
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k=
github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM=
@@ -377,8 +368,6 @@ github.com/otiai10/mint v1.6.3 h1:87qsV/aw1F5as1eH1zS/yqHY85ANKVMgkDrf9rcxbQs=
github.com/otiai10/mint v1.6.3/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM=
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
github.com/pgvector/pgvector-go v0.3.0 h1:Ij+Yt78R//uYqs3Zk35evZFvr+G0blW0OUN+Q2D1RWc=
github.com/pgvector/pgvector-go v0.3.0/go.mod h1:duFy+PXWfW7QQd5ibqutBO4GxLsUZ9RVXhFZGIBsWSA=
github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
@@ -481,8 +470,6 @@ github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZ
github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY=
github.com/tklauser/numcpus v0.9.0 h1:lmyCHtANi8aRUgkckBgoDk1nHCux3n2cgkJLXdQGPDo=
github.com/tklauser/numcpus v0.9.0/go.mod h1:SN6Nq1O3VychhC1npsWostA+oW+VOQTxZrS604NSRyI=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
@@ -492,24 +479,8 @@ github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc=
github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/unrolled/secure v1.17.0 h1:Io7ifFgo99Bnh0J7+Q+qcMzWM6kaDPCA5FroFZEdbWU=
github.com/unrolled/secure v1.17.0/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40=
github.com/uptrace/bun v1.1.12 h1:sOjDVHxNTuM6dNGaba0wUuz7KvDE1BmNu9Gqs2gJSXQ=
github.com/uptrace/bun v1.1.12/go.mod h1:NPG6JGULBeQ9IU6yHp7YGELRa5Agmd7ATZdz4tGZ6z0=
github.com/uptrace/bun/dialect/pgdialect v1.1.12 h1:m/CM1UfOkoBTglGO5CUTKnIKKOApOYxkcP2qn0F9tJk=
github.com/uptrace/bun/dialect/pgdialect v1.1.12/go.mod h1:Ij6WIxQILxLlL2frUBxUBOZJtLElD2QQNDcu/PWDHTc=
github.com/uptrace/bun/driver/pgdriver v1.1.12 h1:3rRWB1GK0psTJrHwxzNfEij2MLibggiLdTqjTtfHc1w=
github.com/uptrace/bun/driver/pgdriver v1.1.12/go.mod h1:ssYUP+qwSEgeDDS1xm2XBip9el1y9Mi5mTAvLoiADLM=
github.com/vmihailenco/bufpool v0.1.11 h1:gOq2WmBrq0i2yW5QJ16ykccQ4wH9UyEsgLm6czKAd94=
github.com/vmihailenco/bufpool v0.1.11/go.mod h1:AFf/MOy3l2CFTKbxwt0mp2MwnqjNEs5H/UxrkA5jxTQ=
github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU=
github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc=
github.com/vmihailenco/tagparser v0.1.2 h1:gnjoVuB/kljJ5wICEEOpx98oXMWPLj22G67Vbd1qPqc=
github.com/vmihailenco/tagparser v0.1.2/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
@@ -606,8 +577,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -636,8 +607,8 @@ golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -761,8 +732,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE=
golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -827,12 +798,17 @@ gorm.io/driver/sqlite v1.5.0 h1:zKYbzRCpBrT1bNijRnxLDJWPjVfImGEn0lSnUY5gZ+c=
gorm.io/driver/sqlite v1.5.0/go.mod h1:kDMDfntV9u/vuMmz8APHtHF0b4nyBB7sfCieC6G8k8I=
gorm.io/driver/sqlserver v1.5.4 h1:xA+Y1KDNspv79q43bPyjDMUgHoYHLhXYmdFcYPobg8g=
gorm.io/driver/sqlserver v1.5.4/go.mod h1:+frZ/qYmuna11zHPlh5oc2O6ZA/lS88Keb0XSH1Zh/g=
gorm.io/gen v0.3.26 h1:sFf1j7vNStimPRRAtH4zz5NiHM+1dr6eA9aaRdplyhY=
gorm.io/gen v0.3.26/go.mod h1:a5lq5y3w4g5LMxBcw0wnO6tYUCdNutWODq5LrIt75LE=
gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
gorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
gorm.io/gorm v1.25.0/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
gorm.io/hints v1.1.2 h1:b5j0kwk5p4+3BtDtYqqfY+ATSxjj+6ptPgVveuynn9o=
gorm.io/hints v1.1.2/go.mod h1:/ARdpUHAtyEMCh5NNi3tI7FsGh+Cj/MIUlvNxCNCFWg=
gorm.io/plugin/dbresolver v1.5.3 h1:wFwINGZZmttuu9h7XpvbDHd8Lf9bb8GNzp/NpAMV2wU=
gorm.io/plugin/dbresolver v1.5.3/go.mod h1:TSrVhaUg2DZAWP3PrHlDlITEJmNOkL0tFTjvTEsQ4XE=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
@@ -840,8 +816,6 @@ honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWh
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
mellium.im/sasl v0.3.1 h1:wE0LW6g7U83vhvxjC1IY8DnXM+EU095yeo8XClvCdfo=
mellium.im/sasl v0.3.1/go.mod h1:xm59PUYpZHhgQ9ZqoJ5QaCqzWMi8IeS49dhp6plPCzw=
modernc.org/cc/v4 v4.24.4 h1:TFkx1s6dCkQpd6dKurBNmpo+G8Zl4Sq/ztJ+2+DEsh0=
modernc.org/cc/v4 v4.24.4/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.23.13 h1:PFiaemQwE/jdwi8XEHyEV+qYWoIuikLP3T4rvDeJb00=

View File

@@ -5,6 +5,7 @@ import (
"git.echol.cn/loser/st/server/model/example"
sysModel "git.echol.cn/loser/st/server/model/system"
"git.echol.cn/loser/st/server/plugin/announcement/model"
"git.echol.cn/loser/st/server/service/system"
adapter "github.com/casbin/gorm-adapter/v3"
"gorm.io/gorm"
@@ -64,6 +65,8 @@ func (e *ensureTables) MigrateTable(ctx context.Context) (context.Context, error
example.ExaFileChunk{},
example.ExaFileUploadAndDownload{},
example.ExaAttachmentCategory{},
model.Info{},
}
for _, t := range tables {
_ = db.AutoMigrate(&t)
@@ -103,6 +106,8 @@ func (e *ensureTables) TableCreated(ctx context.Context) bool {
example.ExaFileChunk{},
example.ExaFileUploadAndDownload{},
example.ExaAttachmentCategory{},
model.Info{},
}
yes := true
for _, t := range tables {

View File

@@ -35,21 +35,12 @@ func (fs justFilesFilesystem) Open(name string) (http.File, error) {
func Routers() *gin.Engine {
Router := gin.New()
// 设置文件上传大小限制10MB
Router.MaxMultipartMemory = 10 << 20 // 10 MB
// 使用自定义的 Recovery 中间件,记录 panic 并入库
Router.Use(middleware.GinRecovery(true))
if gin.Mode() == gin.DebugMode {
Router.Use(gin.Logger())
}
// 跨域配置(前台应用需要)
// 必须在静态文件路由之前注册,否则静态文件跨域会失败
Router.Use(middleware.Cors())
global.GVA_LOG.Info("use middleware cors")
if !global.GVA_CONFIG.MCP.Separate {
sseServer := McpRun()
@@ -67,25 +58,6 @@ func Routers() *gin.Engine {
systemRouter := router.RouterGroupApp.System
exampleRouter := router.RouterGroupApp.Example
appRouter := router.RouterGroupApp.App // 前台应用路由
// SillyTavern 核心脚本静态文件服务
// 所有核心文件存储在 data/st-core-scripts/ 下,完全独立于 web-app/ 目录
stCorePath := "data/st-core-scripts"
if _, err := os.Stat(stCorePath); err == nil {
Router.Static("/scripts", stCorePath+"/scripts")
Router.Static("/css", stCorePath+"/css")
Router.Static("/img", stCorePath+"/img")
Router.Static("/webfonts", stCorePath+"/webfonts")
Router.Static("/lib", stCorePath+"/lib") // SillyTavern 依赖的第三方库
Router.Static("/locales", stCorePath+"/locales") // 国际化文件
Router.StaticFile("/script.js", stCorePath+"/script.js") // SillyTavern 主入口
Router.StaticFile("/lib.js", stCorePath+"/lib.js") // Webpack 编译后的 lib.js
global.GVA_LOG.Info("SillyTavern 核心脚本服务已启动: " + stCorePath)
} else {
global.GVA_LOG.Warn("SillyTavern 核心脚本目录不存在: " + stCorePath)
}
// 管理后台前端静态文件web
// 如果想要不使用nginx代理前端网页可以修改 web/.env.production 下的
// VUE_APP_BASE_API = /
// VUE_APP_BASE_PATH = http://localhost
@@ -95,7 +67,10 @@ func Routers() *gin.Engine {
// Router.StaticFile("/", "./dist/index.html") // 前端网页入口页面
Router.StaticFS(global.GVA_CONFIG.Local.StorePath, justFilesFilesystem{http.Dir(global.GVA_CONFIG.Local.StorePath)}) // Router.Use(middleware.LoadTls()) // 如果需要使用https 请打开此中间件 然后前往 core/server.go 将启动模式 更变为 Router.RunTLS("端口","你的cre/pem文件","你的key文件")
// 跨域,如需跨域可以打开下面的注释
Router.Use(middleware.Cors()) // 直接放行全部跨域请求
//Router.Use(middleware.CorsByRules()) // 按照配置的规则放行跨域请求
global.GVA_LOG.Info("use middleware cors")
docs.SwaggerInfo.BasePath = global.GVA_CONFIG.System.RouterPrefix
Router.GET(global.GVA_CONFIG.System.RouterPrefix+"/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
global.GVA_LOG.Info("register swagger handler")
@@ -137,13 +112,12 @@ func Routers() *gin.Engine {
systemRouter.InitSysErrorRouter(PrivateGroup, PublicGroup) // 错误日志
systemRouter.InitLoginLogRouter(PrivateGroup) // 登录日志
systemRouter.InitApiTokenRouter(PrivateGroup) // apiToken签发
systemRouter.InitSkillsRouter(PrivateGroup) // Skills 定义器
systemRouter.InitSkillsRouter(PrivateGroup, PublicGroup) // Skills 定义器
exampleRouter.InitCustomerRouter(PrivateGroup) // 客户路由
exampleRouter.InitFileUploadAndDownloadRouter(PrivateGroup) // 文件上传下载功能路由
exampleRouter.InitAttachmentCategoryRouterRouter(PrivateGroup) // 文件上传下载分类
}
// 前台应用路由(新增)
{
appGroup := PublicGroup.Group("app") // 统一使用 /app 前缀

View File

@@ -19,12 +19,13 @@ type Conversation struct {
Title string `gorm:"type:varchar(200)" json:"title"` // 对话标题
// 对话配置
PresetID *uint `gorm:"index" json:"presetId"` // 使用的预设ID
WorldbookID *uint `gorm:"index" json:"worldbookId"` // 使用的世界书ID
AIProvider string `gorm:"type:varchar(50)" json:"aiProvider"` // AI提供商 (openai/anthropic)
Model string `gorm:"type:varchar(100)" json:"model"` // 使用的模型
Settings datatypes.JSON `gorm:"type:jsonb" json:"settings"` // 对话设置 (temperature等)
WorldbookEnabled bool `gorm:"default:false" json:"worldbookEnabled"` // 是否启用世界书
PresetID *uint `gorm:"index" json:"presetId"` // 使用的预设ID
WorldbookID *uint `gorm:"index" json:"worldbookId"` // 使用的世界书ID
AIProvider string `gorm:"type:varchar(50)" json:"aiProvider"` // AI提供商 (openai/anthropic)
Model string `gorm:"type:varchar(100)" json:"model"` // 使用的模型
Settings datatypes.JSON `gorm:"type:jsonb" json:"settings"` // 对话设置 (temperature等)
WorldbookEnabled bool `gorm:"default:false" json:"worldbookEnabled"` // 是否启用世界书
Variables datatypes.JSON `gorm:"type:jsonb;default:'{}'" json:"variables"` // 变量存储 ({{setvar::}}/{{getvar::}})
// 统计信息
MessageCount int `gorm:"default:0" json:"messageCount"` // 消息数量

View File

@@ -42,10 +42,11 @@ type UpdateRegexScriptRequest struct {
// GetRegexScriptListRequest 获取正则脚本列表请求
type GetRegexScriptListRequest struct {
Page int `json:"page"`
PageSize int `json:"pageSize"`
Keyword string `json:"keyword"`
Scope *int `json:"scope"`
Page int `json:"page"`
PageSize int `json:"pageSize"`
Keyword string `json:"keyword"`
Scope *int `json:"scope"`
OwnerCharID *uint `json:"ownerCharId"` // 过滤指定角色的脚本scope=1时有效
}
// TestRegexScriptRequest 测试正则脚本请求

View File

@@ -6,9 +6,9 @@ import (
// PageInfo Paging common input parameter structure
type PageInfo struct {
Page int `json:"page" form:"page,default=1"` // 页码
PageSize int `json:"pageSize" form:"pageSize,default=20"` // 每页大小
Keyword string `json:"keyword" form:"keyword"` // 关键字
Page int `json:"page" form:"page"` // 页码
PageSize int `json:"pageSize" form:"pageSize"` // 每页大小
Keyword string `json:"keyword" form:"keyword"` // 关键字
}
func (r *PageInfo) Paginate() func(db *gorm.DB) *gorm.DB {

View File

@@ -12,3 +12,10 @@ type SearchApiParams struct {
OrderKey string `json:"orderKey"` // 排序
Desc bool `json:"desc"` // 排序方式:升序false(默认)|降序true
}
// SetApiAuthorities 通过API路径和方法全量覆盖关联角色列表
type SetApiAuthorities struct {
Path string `json:"path" form:"path"` // API路径
Method string `json:"method" form:"method"` // 请求方法
AuthorityIds []uint `json:"authorityIds" form:"authorityIds"` // 角色ID列表
}

View File

@@ -11,6 +11,12 @@ type AddMenuAuthorityInfo struct {
AuthorityId uint `json:"authorityId"` // 角色ID
}
// SetMenuAuthorities 通过菜单ID全量覆盖关联角色列表
type SetMenuAuthorities struct {
MenuId uint `json:"menuId" form:"menuId"` // 菜单ID
AuthorityIds []uint `json:"authorityIds" form:"authorityIds"` // 角色ID列表
}
func DefaultMenu() []system.SysBaseMenu {
return []system.SysBaseMenu{{
GVA_MODEL: global.GVA_MODEL{ID: 1},

View File

@@ -11,6 +11,16 @@ type SkillDetailRequest struct {
Skill string `json:"skill"`
}
type SkillDeleteRequest struct {
Tool string `json:"tool"`
Skill string `json:"skill"`
}
type SkillPackageRequest struct {
Tool string `json:"tool"`
Skill string `json:"skill"`
}
type SkillSaveRequest struct {
Tool string `json:"tool"`
Skill string `json:"skill"`
@@ -62,3 +72,9 @@ type SkillGlobalConstraintSaveRequest struct {
Content string `json:"content"`
SyncTools []string `json:"syncTools"`
}
type DownloadOnlineSkillReq struct {
Tool string `json:"tool" binding:"required"`
ID uint `json:"id" binding:"required"`
Version string `json:"version" binding:"required"`
}

View File

@@ -66,4 +66,12 @@ type GetUserList struct {
NickName string `json:"nickName" form:"nickName"`
Phone string `json:"phone" form:"phone"`
Email string `json:"email" form:"email"`
OrderKey string `json:"orderKey" form:"orderKey"` // 排序
Desc bool `json:"desc" form:"desc"` // 排序方式:升序false(默认)|降序true
}
// SetRoleUsers 通过角色ID全量覆盖关联用户列表
type SetRoleUsers struct {
AuthorityId uint `json:"authorityId" form:"authorityId"` // 角色ID
UserIds []uint `json:"userIds" form:"userIds"` // 用户ID列表
}

View File

@@ -0,0 +1,10 @@
package api
import "git.echol.cn/loser/st/server/plugin/announcement/service"
var (
Api = new(api)
serviceInfo = service.Service.Info
)
type api struct{ Info info }

View File

@@ -0,0 +1,183 @@
package api
import (
"git.echol.cn/loser/st/server/global"
"git.echol.cn/loser/st/server/model/common/response"
"git.echol.cn/loser/st/server/plugin/announcement/model"
"git.echol.cn/loser/st/server/plugin/announcement/model/request"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
var Info = new(info)
type info struct{}
// CreateInfo 创建公告
// @Tags Info
// @Summary 创建公告
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body model.Info true "创建公告"
// @Success 200 {object} response.Response{msg=string} "创建成功"
// @Router /info/createInfo [post]
func (a *info) CreateInfo(c *gin.Context) {
var info model.Info
err := c.ShouldBindJSON(&info)
if err != nil {
response.FailWithMessage(err.Error(), c)
return
}
err = serviceInfo.CreateInfo(&info)
if err != nil {
global.GVA_LOG.Error("创建失败!", zap.Error(err))
response.FailWithMessage("创建失败", c)
return
}
response.OkWithMessage("创建成功", c)
}
// DeleteInfo 删除公告
// @Tags Info
// @Summary 删除公告
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body model.Info true "删除公告"
// @Success 200 {object} response.Response{msg=string} "删除成功"
// @Router /info/deleteInfo [delete]
func (a *info) DeleteInfo(c *gin.Context) {
ID := c.Query("ID")
err := serviceInfo.DeleteInfo(ID)
if err != nil {
global.GVA_LOG.Error("删除失败!", zap.Error(err))
response.FailWithMessage("删除失败", c)
return
}
response.OkWithMessage("删除成功", c)
}
// DeleteInfoByIds 批量删除公告
// @Tags Info
// @Summary 批量删除公告
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Success 200 {object} response.Response{msg=string} "批量删除成功"
// @Router /info/deleteInfoByIds [delete]
func (a *info) DeleteInfoByIds(c *gin.Context) {
IDs := c.QueryArray("IDs[]")
if err := serviceInfo.DeleteInfoByIds(IDs); err != nil {
global.GVA_LOG.Error("批量删除失败!", zap.Error(err))
response.FailWithMessage("批量删除失败", c)
return
}
response.OkWithMessage("批量删除成功", c)
}
// UpdateInfo 更新公告
// @Tags Info
// @Summary 更新公告
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body model.Info true "更新公告"
// @Success 200 {object} response.Response{msg=string} "更新成功"
// @Router /info/updateInfo [put]
func (a *info) UpdateInfo(c *gin.Context) {
var info model.Info
err := c.ShouldBindJSON(&info)
if err != nil {
response.FailWithMessage(err.Error(), c)
return
}
err = serviceInfo.UpdateInfo(info)
if err != nil {
global.GVA_LOG.Error("更新失败!", zap.Error(err))
response.FailWithMessage("更新失败", c)
return
}
response.OkWithMessage("更新成功", c)
}
// FindInfo 用id查询公告
// @Tags Info
// @Summary 用id查询公告
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data query model.Info true "用id查询公告"
// @Success 200 {object} response.Response{data=model.Info,msg=string} "查询成功"
// @Router /info/findInfo [get]
func (a *info) FindInfo(c *gin.Context) {
ID := c.Query("ID")
reinfo, err := serviceInfo.GetInfo(ID)
if err != nil {
global.GVA_LOG.Error("查询失败!", zap.Error(err))
response.FailWithMessage("查询失败", c)
return
}
response.OkWithData(reinfo, c)
}
// GetInfoList 分页获取公告列表
// @Tags Info
// @Summary 分页获取公告列表
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data query request.InfoSearch true "分页获取公告列表"
// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "获取成功"
// @Router /info/getInfoList [get]
func (a *info) GetInfoList(c *gin.Context) {
var pageInfo request.InfoSearch
err := c.ShouldBindQuery(&pageInfo)
if err != nil {
response.FailWithMessage(err.Error(), c)
return
}
list, total, err := serviceInfo.GetInfoInfoList(pageInfo)
if err != nil {
global.GVA_LOG.Error("获取失败!", zap.Error(err))
response.FailWithMessage("获取失败", c)
return
}
response.OkWithDetailed(response.PageResult{
List: list,
Total: total,
Page: pageInfo.Page,
PageSize: pageInfo.PageSize,
}, "获取成功", c)
}
// GetInfoDataSource 获取Info的数据源
// @Tags Info
// @Summary 获取Info的数据源
// @accept application/json
// @Produce application/json
// @Success 200 {object} response.Response{data=object,msg=string} "查询成功"
// @Router /info/getInfoDataSource [get]
func (a *info) GetInfoDataSource(c *gin.Context) {
// 此接口为获取数据源定义的数据
dataSource, err := serviceInfo.GetInfoDataSource()
if err != nil {
global.GVA_LOG.Error("查询失败!", zap.Error(err))
response.FailWithMessage("查询失败", c)
return
}
response.OkWithData(dataSource, c)
}
// GetInfoPublic 不需要鉴权的公告接口
// @Tags Info
// @Summary 不需要鉴权的公告接口
// @accept application/json
// @Produce application/json
// @Param data query request.InfoSearch true "分页获取公告列表"
// @Success 200 {object} response.Response{data=object,msg=string} "获取成功"
// @Router /info/getInfoPublic [get]
func (a *info) GetInfoPublic(c *gin.Context) {
// 此接口不需要鉴权 示例为返回了一个固定的消息接口一般本接口用于C端服务需要自己实现业务逻辑
response.OkWithDetailed(gin.H{"info": "不需要鉴权的公告接口信息"}, "获取成功", c)
}

View File

@@ -0,0 +1,4 @@
package config
type Config struct {
}

View File

@@ -0,0 +1,18 @@
package main
import (
"path/filepath" //go:generate go mod tidy
"gorm.io/gen"
//go:generate go mod download
//go:generate go run gen.go
"git.echol.cn/loser/st/server/plugin/announcement/model"
)
func main() {
g := gen.NewGenerator(gen.Config{OutPath: filepath.Join("..", "..", "..", "announcement", "blender", "model", "dao"), Mode: gen.WithoutContext | gen.WithDefaultQuery | gen.WithQueryInterface})
g.ApplyBasic(
new(model.Info),
)
g.Execute()
}

View File

@@ -0,0 +1,50 @@
package initialize
import (
"context"
model "git.echol.cn/loser/st/server/model/system"
"git.echol.cn/loser/st/server/plugin/plugin-tool/utils"
)
func Api(ctx context.Context) {
entities := []model.SysApi{
{
Path: "/info/createInfo",
Description: "新建公告",
ApiGroup: "公告",
Method: "POST",
},
{
Path: "/info/deleteInfo",
Description: "删除公告",
ApiGroup: "公告",
Method: "DELETE",
},
{
Path: "/info/deleteInfoByIds",
Description: "批量删除公告",
ApiGroup: "公告",
Method: "DELETE",
},
{
Path: "/info/updateInfo",
Description: "更新公告",
ApiGroup: "公告",
Method: "PUT",
},
{
Path: "/info/findInfo",
Description: "根据ID获取公告",
ApiGroup: "公告",
Method: "GET",
},
{
Path: "/info/getInfoList",
Description: "获取公告列表",
ApiGroup: "公告",
Method: "GET",
},
}
utils.RegisterApis(entities...)
}

View File

@@ -0,0 +1,13 @@
package initialize
import (
"context"
model "git.echol.cn/loser/st/server/model/system"
"git.echol.cn/loser/st/server/plugin/plugin-tool/utils"
)
func Dictionary(ctx context.Context) {
entities := []model.SysDictionary{}
utils.RegisterDictionaries(entities...)
}

View File

@@ -0,0 +1,21 @@
package initialize
import (
"context"
"fmt"
"git.echol.cn/loser/st/server/global"
"git.echol.cn/loser/st/server/plugin/announcement/model"
"github.com/pkg/errors"
"go.uber.org/zap"
)
func Gorm(ctx context.Context) {
err := global.GVA_DB.WithContext(ctx).AutoMigrate(
new(model.Info),
)
if err != nil {
err = errors.Wrap(err, "注册表失败!")
zap.L().Error(fmt.Sprintf("%+v", err))
}
}

View File

@@ -0,0 +1,23 @@
package initialize
import (
"context"
model "git.echol.cn/loser/st/server/model/system"
"git.echol.cn/loser/st/server/plugin/plugin-tool/utils"
)
func Menu(ctx context.Context) {
entities := []model.SysBaseMenu{
{
ParentId: 9,
Path: "anInfo",
Name: "anInfo",
Hidden: false,
Component: "plugin/announcement/view/info.vue",
Sort: 5,
Meta: model.Meta{Title: "公告管理", Icon: "box"},
},
}
utils.RegisterMenus(entities...)
}

View File

@@ -0,0 +1,15 @@
package initialize
import (
"git.echol.cn/loser/st/server/global"
"git.echol.cn/loser/st/server/middleware"
"git.echol.cn/loser/st/server/plugin/announcement/router"
"github.com/gin-gonic/gin"
)
func Router(engine *gin.Engine) {
public := engine.Group(global.GVA_CONFIG.System.RouterPrefix).Group("")
private := engine.Group(global.GVA_CONFIG.System.RouterPrefix).Group("")
private.Use(middleware.JWTAuth()).Use(middleware.CasbinHandler())
router.Router.Info.Init(public, private)
}

View File

@@ -0,0 +1,18 @@
package initialize
import (
"fmt"
"git.echol.cn/loser/st/server/global"
"git.echol.cn/loser/st/server/plugin/announcement/plugin"
"github.com/pkg/errors"
"go.uber.org/zap"
)
func Viper() {
err := global.GVA_VP.UnmarshalKey("announcement", &plugin.Config)
if err != nil {
err = errors.Wrap(err, "初始化配置文件失败!")
zap.L().Error(fmt.Sprintf("%+v", err))
}
}

View File

@@ -0,0 +1,20 @@
package model
import (
"git.echol.cn/loser/st/server/global"
"gorm.io/datatypes"
)
// Info 公告 结构体
type Info struct {
global.GVA_MODEL
Title string `json:"title" form:"title" gorm:"column:title;comment:公告标题;"` //标题
Content string `json:"content" form:"content" gorm:"column:content;comment:公告内容;type:text;"` //内容
UserID *int `json:"userID" form:"userID" gorm:"column:user_id;comment:发布者;"` //作者
Attachments datatypes.JSON `json:"attachments" form:"attachments" gorm:"column:attachments;comment:相关附件;" swaggertype:"array,object"` //附件
}
// TableName 公告 Info自定义表名 gva_announcements_info
func (Info) TableName() string {
return "gva_announcements_info"
}

View File

@@ -0,0 +1,13 @@
package request
import (
"time"
"git.echol.cn/loser/st/server/model/common/request"
)
type InfoSearch struct {
StartCreatedAt *time.Time `json:"startCreatedAt" form:"startCreatedAt"`
EndCreatedAt *time.Time `json:"endCreatedAt" form:"endCreatedAt"`
request.PageInfo
}

View File

@@ -0,0 +1,33 @@
package announcement
import (
"context"
"git.echol.cn/loser/st/server/plugin/announcement/initialize"
interfaces "git.echol.cn/loser/st/server/utils/plugin/v2"
"github.com/gin-gonic/gin"
)
var _ interfaces.Plugin = (*plugin)(nil)
var Plugin = new(plugin)
type plugin struct{}
func init() {
interfaces.Register(Plugin)
}
func (p *plugin) Register(group *gin.Engine) {
ctx := context.Background()
// 如果需要配置文件请到config.Config中填充配置结构且到下方发放中填入其在config.yaml中的key
// initialize.Viper()
// 安装插件时候自动注册的api数据请到下方法.Api方法中实现
initialize.Api(ctx)
// 安装插件时候自动注册的Menu数据请到下方法.Menu方法中实现
initialize.Menu(ctx)
// 安装插件时候自动注册的Dictionary数据请到下方法.Dictionary方法中实现
initialize.Dictionary(ctx)
initialize.Gorm(ctx)
initialize.Router(group)
}

View File

@@ -0,0 +1,5 @@
package plugin
import "git.echol.cn/loser/st/server/plugin/announcement/config"
var Config config.Config

View File

@@ -0,0 +1,10 @@
package router
import "git.echol.cn/loser/st/server/plugin/announcement/api"
var (
Router = new(router)
apiInfo = api.Api.Info
)
type router struct{ Info info }

View File

@@ -0,0 +1,31 @@
package router
import (
"git.echol.cn/loser/st/server/middleware"
"github.com/gin-gonic/gin"
)
var Info = new(info)
type info struct{}
// Init 初始化 公告 路由信息
func (r *info) Init(public *gin.RouterGroup, private *gin.RouterGroup) {
{
group := private.Group("info").Use(middleware.OperationRecord())
group.POST("createInfo", apiInfo.CreateInfo) // 新建公告
group.DELETE("deleteInfo", apiInfo.DeleteInfo) // 删除公告
group.DELETE("deleteInfoByIds", apiInfo.DeleteInfoByIds) // 批量删除公告
group.PUT("updateInfo", apiInfo.UpdateInfo) // 更新公告
}
{
group := private.Group("info")
group.GET("findInfo", apiInfo.FindInfo) // 根据ID获取公告
group.GET("getInfoList", apiInfo.GetInfoList) // 获取公告列表
}
{
group := public.Group("info")
group.GET("getInfoDataSource", apiInfo.GetInfoDataSource) // 获取公告数据源
group.GET("getInfoPublic", apiInfo.GetInfoPublic) // 获取公告列表
}
}

View File

@@ -0,0 +1,5 @@
package service
var Service = new(service)
type service struct{ Info info }

View File

@@ -0,0 +1,78 @@
package service
import (
"git.echol.cn/loser/st/server/global"
"git.echol.cn/loser/st/server/plugin/announcement/model"
"git.echol.cn/loser/st/server/plugin/announcement/model/request"
)
var Info = new(info)
type info struct{}
// CreateInfo 创建公告记录
// Author [piexlmax](https://github.com/piexlmax)
func (s *info) CreateInfo(info *model.Info) (err error) {
err = global.GVA_DB.Create(info).Error
return err
}
// DeleteInfo 删除公告记录
// Author [piexlmax](https://github.com/piexlmax)
func (s *info) DeleteInfo(ID string) (err error) {
err = global.GVA_DB.Delete(&model.Info{}, "id = ?", ID).Error
return err
}
// DeleteInfoByIds 批量删除公告记录
// Author [piexlmax](https://github.com/piexlmax)
func (s *info) DeleteInfoByIds(IDs []string) (err error) {
err = global.GVA_DB.Delete(&[]model.Info{}, "id in ?", IDs).Error
return err
}
// UpdateInfo 更新公告记录
// Author [piexlmax](https://github.com/piexlmax)
func (s *info) UpdateInfo(info model.Info) (err error) {
err = global.GVA_DB.Model(&model.Info{}).Where("id = ?", info.ID).Updates(&info).Error
return err
}
// GetInfo 根据ID获取公告记录
// Author [piexlmax](https://github.com/piexlmax)
func (s *info) GetInfo(ID string) (info model.Info, err error) {
err = global.GVA_DB.Where("id = ?", ID).First(&info).Error
return
}
// GetInfoInfoList 分页获取公告记录
// Author [piexlmax](https://github.com/piexlmax)
func (s *info) GetInfoInfoList(info request.InfoSearch) (list []model.Info, total int64, err error) {
limit := info.PageSize
offset := info.PageSize * (info.Page - 1)
// 创建db
db := global.GVA_DB.Model(&model.Info{})
var infos []model.Info
// 如果有条件搜索 下方会自动创建搜索语句
if info.StartCreatedAt != nil && info.EndCreatedAt != nil {
db = db.Where("created_at BETWEEN ? AND ?", info.StartCreatedAt, info.EndCreatedAt)
}
err = db.Count(&total).Error
if err != nil {
return
}
if limit != 0 {
db = db.Limit(limit).Offset(offset)
}
err = db.Find(&infos).Error
return infos, total, err
}
func (s *info) GetInfoDataSource() (res map[string][]map[string]any, err error) {
res = make(map[string][]map[string]any)
userID := make([]map[string]any, 0)
global.GVA_DB.Table("sys_users").Select("nick_name as label,id as value").Scan(&userID)
res["userID"] = userID
return
}

View File

@@ -1 +1,5 @@
package plugin
import (
_ "git.echol.cn/loser/st/server/plugin/announcement"
)

View File

@@ -22,10 +22,12 @@ func (s *ApiRouter) InitApiRouter(Router *gin.RouterGroup, RouterPub *gin.Router
apiRouter.POST("getApiById", apiRouterApi.GetApiById) // 获取单条Api消息
apiRouter.POST("updateApi", apiRouterApi.UpdateApi) // 更新api
apiRouter.DELETE("deleteApisByIds", apiRouterApi.DeleteApisByIds) // 删除选中api
apiRouter.POST("setApiRoles", apiRouterApi.SetApiRoles) // 全量覆盖API关联角色
}
{
apiRouterWithoutRecord.POST("getAllApis", apiRouterApi.GetAllApis) // 获取所有api
apiRouterWithoutRecord.POST("getApiList", apiRouterApi.GetApiList) // 获取Api列表
apiRouterWithoutRecord.POST("getAllApis", apiRouterApi.GetAllApis) // 获取所有api
apiRouterWithoutRecord.POST("getApiList", apiRouterApi.GetApiList) // 获取Api列表
apiRouterWithoutRecord.GET("getApiRoles", apiRouterApi.GetApiRoles) // 获取API关联角色ID列表
}
{
apiPublicRouterWithoutRecord.GET("freshCasbin", apiRouterApi.FreshCasbin) // 刷新casbin权限

View File

@@ -16,8 +16,10 @@ func (s *AuthorityRouter) InitAuthorityRouter(Router *gin.RouterGroup) {
authorityRouter.PUT("updateAuthority", authorityApi.UpdateAuthority) // 更新角色
authorityRouter.POST("copyAuthority", authorityApi.CopyAuthority) // 拷贝角色
authorityRouter.POST("setDataAuthority", authorityApi.SetDataAuthority) // 设置角色资源权限
authorityRouter.POST("setRoleUsers", authorityApi.SetRoleUsers) // 全量覆盖角色关联用户
}
{
authorityRouterWithoutRecord.POST("getAuthorityList", authorityApi.GetAuthorityList) // 获取角色列表
authorityRouterWithoutRecord.POST("getAuthorityList", authorityApi.GetAuthorityList) // 获取角色列表
authorityRouterWithoutRecord.GET("getUsersByAuthority", authorityApi.GetUsersByAuthority) // 获取角色关联用户ID列表
}
}

View File

@@ -15,6 +15,7 @@ func (s *MenuRouter) InitMenuRouter(Router *gin.RouterGroup) (R gin.IRoutes) {
menuRouter.POST("addMenuAuthority", authorityMenuApi.AddMenuAuthority) // 增加menu和角色关联关系
menuRouter.POST("deleteBaseMenu", authorityMenuApi.DeleteBaseMenu) // 删除菜单
menuRouter.POST("updateBaseMenu", authorityMenuApi.UpdateBaseMenu) // 更新菜单
menuRouter.POST("setMenuRoles", authorityMenuApi.SetMenuRoles) // 全量覆盖菜单关联角色
}
{
menuRouterWithoutRecord.POST("getMenu", authorityMenuApi.GetMenu) // 获取菜单树
@@ -22,6 +23,7 @@ func (s *MenuRouter) InitMenuRouter(Router *gin.RouterGroup) (R gin.IRoutes) {
menuRouterWithoutRecord.POST("getBaseMenuTree", authorityMenuApi.GetBaseMenuTree) // 获取用户动态路由
menuRouterWithoutRecord.POST("getMenuAuthority", authorityMenuApi.GetMenuAuthority) // 获取指定角色menu
menuRouterWithoutRecord.POST("getBaseMenuById", authorityMenuApi.GetBaseMenuById) // 根据id获取菜单
menuRouterWithoutRecord.GET("getMenuRoles", authorityMenuApi.GetMenuRoles) // 获取菜单关联角色ID列表
}
return menuRouter
}

View File

@@ -4,13 +4,15 @@ import "github.com/gin-gonic/gin"
type SkillsRouter struct{}
func (s *SkillsRouter) InitSkillsRouter(Router *gin.RouterGroup) {
func (s *SkillsRouter) InitSkillsRouter(Router *gin.RouterGroup, pubRouter *gin.RouterGroup) {
skillsRouter := Router.Group("skills")
skillsRouterPub := pubRouter.Group("skills")
{
skillsRouter.GET("getTools", skillsApi.GetTools)
skillsRouter.POST("getSkillList", skillsApi.GetSkillList)
skillsRouter.POST("getSkillDetail", skillsApi.GetSkillDetail)
skillsRouter.POST("saveSkill", skillsApi.SaveSkill)
skillsRouter.POST("deleteSkill", skillsApi.DeleteSkill)
skillsRouter.POST("createScript", skillsApi.CreateScript)
skillsRouter.POST("getScript", skillsApi.GetScript)
skillsRouter.POST("saveScript", skillsApi.SaveScript)
@@ -25,5 +27,9 @@ func (s *SkillsRouter) InitSkillsRouter(Router *gin.RouterGroup) {
skillsRouter.POST("saveTemplate", skillsApi.SaveTemplate)
skillsRouter.POST("getGlobalConstraint", skillsApi.GetGlobalConstraint)
skillsRouter.POST("saveGlobalConstraint", skillsApi.SaveGlobalConstraint)
skillsRouter.POST("packageSkill", skillsApi.PackageSkill)
}
{
skillsRouterPub.POST("downloadOnlineSkill", skillsApi.DownloadOnlineSkill)
}
}

View File

@@ -427,14 +427,23 @@ func (s *CharacterService) processRegexScriptsFromExtensions(userID, characterID
OwnerCharID: &characterID,
}
// 提取字段
if name, ok := scriptData["name"].(string); ok {
// 提取字段 - 兼容 SillyTavern 的字段名
// 脚本名称:优先使用 scriptName其次 name
if scriptName, ok := scriptData["scriptName"].(string); ok {
script.Name = scriptName
} else if name, ok := scriptData["name"].(string); ok {
script.Name = name
}
// 查找正则表达式
if findRegex, ok := scriptData["findRegex"].(string); ok {
script.FindRegex = findRegex
}
if replaceWith, ok := scriptData["replaceWith"].(string); ok {
// 替换字符串:优先使用 replaceString其次 replaceWith
if replaceString, ok := scriptData["replaceString"].(string); ok {
script.ReplaceWith = replaceString
} else if replaceWith, ok := scriptData["replaceWith"].(string); ok {
script.ReplaceWith = replaceWith
}
if placement, ok := scriptData["placement"].(float64); ok {

File diff suppressed because it is too large Load Diff

View File

@@ -3,8 +3,12 @@ package app
import (
"encoding/json"
"errors"
"fmt"
"math/rand"
"regexp"
"strconv"
"strings"
"time"
"git.echol.cn/loser/st/server/global"
"git.echol.cn/loser/st/server/model/app"
@@ -65,6 +69,10 @@ func (s *RegexScriptService) GetRegexScriptList(userID uint, req *request.GetReg
db = db.Where("scope = ?", *req.Scope)
}
if req.OwnerCharID != nil {
db = db.Where("owner_char_id = ?", *req.OwnerCharID)
}
if err := db.Count(&total).Error; err != nil {
return nil, 0, err
}
@@ -218,7 +226,13 @@ func (s *RegexScriptService) ExecuteScript(script *app.RegexScript, text string,
global.GVA_LOG.Warn("正则表达式编译失败", zap.String("pattern", script.FindRegex), zap.Error(err))
return text, err
}
result = re.ReplaceAllString(result, script.ReplaceWith)
// 特殊处理:如果正则匹配 <Status_block>,需要提取 YAML 并注入到 HTML 模板中
if strings.Contains(script.FindRegex, "Status_block") {
result = s.replaceStatusBlockWithHTML(result, script.ReplaceWith, re)
} else {
result = re.ReplaceAllString(result, script.ReplaceWith)
}
}
// 3. 修剪字符串
@@ -233,35 +247,199 @@ func (s *RegexScriptService) ExecuteScript(script *app.RegexScript, text string,
return result, nil
}
// replaceStatusBlockWithHTML 替换 <Status_block> 为 HTML 模板,并注入 YAML 数据
func (s *RegexScriptService) replaceStatusBlockWithHTML(text string, htmlTemplate string, statusBlockRegex *regexp.Regexp) string {
return statusBlockRegex.ReplaceAllStringFunc(text, func(match string) string {
// 提取 YAML 数据
yamlRegex := regexp.MustCompile(`<Status_block>\s*([\s\S]*?)\s*</Status_block>`)
yamlMatches := yamlRegex.FindStringSubmatch(match)
if len(yamlMatches) < 2 {
global.GVA_LOG.Warn("无法提取 Status_block 中的 YAML 数据")
return match
}
yamlData := strings.TrimSpace(yamlMatches[1])
// 在 HTML 模板中查找 <script id="yaml-data-source" type="text/yaml"></script>
// 并将 YAML 数据注入其中
injectedHTML := strings.Replace(
htmlTemplate,
`<script id="yaml-data-source" type="text/yaml"></script>`,
fmt.Sprintf(`<script id="yaml-data-source" type="text/yaml">%s</script>`, yamlData),
1,
)
global.GVA_LOG.Info(fmt.Sprintf("[正则脚本] 已将 Status_block YAML 数据注入到 HTML 模板YAML 长度: %d", len(yamlData)))
return injectedHTML
})
}
// substituteMacros 替换宏变量
func (s *RegexScriptService) substituteMacros(text string, userName string, charName string) string {
result := text
// 保存原始文本
result = strings.ReplaceAll(result, "{{original}}", text)
// 用户名变量
if userName != "" {
result = strings.ReplaceAll(result, "{{user}}", userName)
result = strings.ReplaceAll(result, "{{User}}", userName)
}
// 角色名变量
if charName != "" {
result = strings.ReplaceAll(result, "{{char}}", charName)
result = strings.ReplaceAll(result, "{{Char}}", charName)
}
// 时间变量
now := time.Now()
result = strings.ReplaceAll(result, "{{time}}", now.Format("15:04:05"))
result = strings.ReplaceAll(result, "{{date}}", now.Format("2006-01-02"))
result = strings.ReplaceAll(result, "{{datetime}}", now.Format("2006-01-02 15:04:05"))
result = strings.ReplaceAll(result, "{{timestamp}}", fmt.Sprintf("%d", now.Unix()))
result = strings.ReplaceAll(result, "{{time_12h}}", now.Format("03:04:05 PM"))
result = strings.ReplaceAll(result, "{{date_short}}", now.Format("01/02/06"))
result = strings.ReplaceAll(result, "{{weekday}}", now.Weekday().String())
result = strings.ReplaceAll(result, "{{month}}", now.Month().String())
result = strings.ReplaceAll(result, "{{year}}", fmt.Sprintf("%d", now.Year()))
// 随机数变量
result = regexp.MustCompile(`\{\{random:(\d+)-(\d+)\}\}`).ReplaceAllStringFunc(result, func(match string) string {
re := regexp.MustCompile(`\{\{random:(\d+)-(\d+)\}\}`)
matches := re.FindStringSubmatch(match)
if len(matches) == 3 {
min, _ := strconv.Atoi(matches[1])
max, _ := strconv.Atoi(matches[2])
if max > min {
return fmt.Sprintf("%d", rand.Intn(max-min+1)+min)
}
}
return match
})
// 简单随机数 {{random}}
result = regexp.MustCompile(`\{\{random\}\}`).ReplaceAllStringFunc(result, func(match string) string {
return fmt.Sprintf("%d", rand.Intn(100))
})
// 随机选择 {{pick:option1|option2|option3}}
result = regexp.MustCompile(`\{\{pick:([^}]+)\}\}`).ReplaceAllStringFunc(result, func(match string) string {
re := regexp.MustCompile(`\{\{pick:([^}]+)\}\}`)
matches := re.FindStringSubmatch(match)
if len(matches) == 2 {
options := strings.Split(matches[1], "|")
if len(options) > 0 {
return options[rand.Intn(len(options))]
}
}
return match
})
// 换行符变量
result = strings.ReplaceAll(result, "{{newline}}", "\n")
result = strings.ReplaceAll(result, "{{tab}}", "\t")
result = strings.ReplaceAll(result, "{{space}}", " ")
// 空值变量
result = strings.ReplaceAll(result, "{{empty}}", "")
return result
}
// ExtractSetVars 从文本中提取 {{setvar::key::value}} 并返回变量映射和清理后的文本
func (s *RegexScriptService) ExtractSetVars(text string) (map[string]string, string) {
vars := make(map[string]string)
// 匹配 {{setvar::key::value}}
re := regexp.MustCompile(`\{\{setvar::([^:]+)::([^}]*)\}\}`)
matches := re.FindAllStringSubmatch(text, -1)
for _, match := range matches {
if len(match) == 3 {
key := strings.TrimSpace(match[1])
value := match[2]
vars[key] = value
}
}
// 从文本中移除所有 {{setvar::}} 标记
cleanText := re.ReplaceAllString(text, "")
return vars, cleanText
}
// SubstituteGetVars 替换文本中的 {{getvar::key}} 为实际变量值
func (s *RegexScriptService) SubstituteGetVars(text string, variables map[string]string) string {
result := text
// 匹配 {{getvar::key}}
re := regexp.MustCompile(`\{\{getvar::([^}]+)\}\}`)
result = re.ReplaceAllStringFunc(result, func(match string) string {
matches := re.FindStringSubmatch(match)
if len(matches) == 2 {
key := strings.TrimSpace(matches[1])
if value, ok := variables[key]; ok {
return value
}
}
return "" // 如果变量不存在,返回空字符串
})
return result
}
// ExtractStatusBlock 提取 <Status_block> 中的 YAML 数据
func (s *RegexScriptService) ExtractStatusBlock(text string) (string, string) {
// 匹配 <Status_block>...</Status_block>
re := regexp.MustCompile(`(?s)<Status_block>\s*(.*?)\s*</Status_block>`)
matches := re.FindStringSubmatch(text)
if len(matches) == 2 {
statusBlock := strings.TrimSpace(matches[1])
cleanText := re.ReplaceAllString(text, "")
return statusBlock, cleanText
}
return "", text
}
// ExtractMaintext 提取 <maintext> 中的内容
func (s *RegexScriptService) ExtractMaintext(text string) (string, string) {
// 匹配 <maintext>...</maintext>
re := regexp.MustCompile(`(?s)<maintext>\s*(.*?)\s*</maintext>`)
matches := re.FindStringSubmatch(text)
if len(matches) == 2 {
maintext := strings.TrimSpace(matches[1])
cleanText := re.ReplaceAllString(text, "")
return maintext, cleanText
}
return "", text
}
// GetScriptsForPlacement 获取指定阶段的脚本
func (s *RegexScriptService) GetScriptsForPlacement(userID uint, placement int, charID *uint, presetID *uint) ([]app.RegexScript, error) {
var scripts []app.RegexScript
db := global.GVA_DB.Where("user_id = ? AND placement = ? AND disabled = ?", userID, placement, false)
// markdownOnly=true 脚本只在前端显示层执行,后端不应用
db := global.GVA_DB.Where("user_id = ? AND placement = ? AND disabled = ? AND markdown_only = ?", userID, placement, false, false)
// 作用域过滤:全局(0) 或 角色(1) 或 预设(2)
scopeCondition := "scope = 0" // 全局
if charID != nil {
scopeCondition += " OR (scope = 1 AND owner_char_id = " + string(rune(*charID)) + ")"
// 使用参数化查询避免 SQL 注入
if charID != nil && presetID != nil {
db = db.Where("scope = 0 OR (scope = 1 AND owner_char_id = ?) OR (scope = 2 AND owner_preset_id = ?)", *charID, *presetID)
} else if charID != nil {
db = db.Where("scope = 0 OR (scope = 1 AND owner_char_id = ?)", *charID)
} else if presetID != nil {
db = db.Where("scope = 0 OR (scope = 2 AND owner_preset_id = ?)", *presetID)
} else {
db = db.Where("scope = 0")
}
if presetID != nil {
scopeCondition += " OR (scope = 2 AND owner_preset_id = " + string(rune(*presetID)) + ")"
}
db = db.Where(scopeCondition)
if err := db.Order("\"order\" ASC").Find(&scripts).Error; err != nil {
return nil, err

View File

@@ -331,3 +331,82 @@ func (authorityService *AuthorityService) GetParentAuthorityID(authorityID uint)
}
return *authority.ParentId, nil
}
// GetUserIdsByAuthorityId 获取拥有指定角色的所有用户ID
func (authorityService *AuthorityService) GetUserIdsByAuthorityId(authorityId uint) (userIds []uint, err error) {
var records []system.SysUserAuthority
err = global.GVA_DB.Where("sys_authority_authority_id = ?", authorityId).Find(&records).Error
if err != nil {
return nil, err
}
for _, r := range records {
userIds = append(userIds, r.SysUserId)
}
return userIds, nil
}
// SetRoleUsers 全量覆盖某角色关联的用户列表
// 入参角色ID + 目标用户ID列表保存时将该角色的关联关系完全替换为传入列表
func (authorityService *AuthorityService) SetRoleUsers(authorityId uint, userIds []uint) error {
return global.GVA_DB.Transaction(func(tx *gorm.DB) error {
// 1. 查出当前拥有该角色的所有用户ID
var existingRecords []system.SysUserAuthority
if err := tx.Where("sys_authority_authority_id = ?", authorityId).Find(&existingRecords).Error; err != nil {
return err
}
currentSet := make(map[uint]struct{})
for _, r := range existingRecords {
currentSet[r.SysUserId] = struct{}{}
}
targetSet := make(map[uint]struct{})
for _, id := range userIds {
targetSet[id] = struct{}{}
}
// 2. 删除该角色所有已有的用户关联
if err := tx.Delete(&system.SysUserAuthority{}, "sys_authority_authority_id = ?", authorityId).Error; err != nil {
return err
}
// 3. 对被移除的用户:若该角色是其主角色,则将主角色切换为其剩余的其他角色
for userId := range currentSet {
if _, ok := targetSet[userId]; ok {
continue // 仍在目标列表中,不处理
}
var user system.SysUser
if err := tx.First(&user, "id = ?", userId).Error; err != nil {
continue
}
if user.AuthorityId == authorityId {
// 从剩余关联(已删除当前角色后)中找另一个角色作为主角色
var another system.SysUserAuthority
if err := tx.Where("sys_user_id = ?", userId).First(&another).Error; err != nil {
// 没有其他角色,主角色保持不变,不做处理
continue
}
if err := tx.Model(&system.SysUser{}).Where("id = ?", userId).
Update("authority_id", another.SysAuthorityAuthorityId).Error; err != nil {
return err
}
}
}
// 4. 批量插入新的关联记录
if len(userIds) > 0 {
newRecords := make([]system.SysUserAuthority, 0, len(userIds))
for _, userId := range userIds {
newRecords = append(newRecords, system.SysUserAuthority{
SysUserId: userId,
SysAuthorityAuthorityId: authorityId,
})
}
if err := tx.Create(&newRecords).Error; err != nil {
return err
}
}
return nil
})
}

View File

@@ -171,3 +171,45 @@ func (casbinService *CasbinService) FreshCasbin() (err error) {
err = e.LoadPolicy()
return err
}
// GetAuthoritiesByApi 获取拥有指定API权限的所有角色ID
func (casbinService *CasbinService) GetAuthoritiesByApi(path, method string) (authorityIds []uint, err error) {
var rules []gormadapter.CasbinRule
err = global.GVA_DB.Where("ptype = 'p' AND v1 = ? AND v2 = ?", path, method).Find(&rules).Error
if err != nil {
return nil, err
}
for _, r := range rules {
id, e := strconv.Atoi(r.V0)
if e == nil {
authorityIds = append(authorityIds, uint(id))
}
}
return authorityIds, nil
}
// SetApiAuthorities 全量覆盖某API关联的角色列表
func (casbinService *CasbinService) SetApiAuthorities(path, method string, authorityIds []uint) error {
return global.GVA_DB.Transaction(func(tx *gorm.DB) error {
// 1. 删除该API所有已有的角色关联
if err := tx.Where("ptype = 'p' AND v1 = ? AND v2 = ?", path, method).Delete(&gormadapter.CasbinRule{}).Error; err != nil {
return err
}
// 2. 批量插入新的关联记录
if len(authorityIds) > 0 {
newRules := make([]gormadapter.CasbinRule, 0, len(authorityIds))
for _, authorityId := range authorityIds {
newRules = append(newRules, gormadapter.CasbinRule{
Ptype: "p",
V0: strconv.Itoa(int(authorityId)),
V1: path,
V2: method,
})
}
if err := tx.Create(&newRules).Error; err != nil {
return err
}
}
return nil
})
}

View File

@@ -315,6 +315,65 @@ func (menuService *MenuService) GetMenuAuthority(info *request.GetAuthorityId) (
return menus, err
}
// GetAuthoritiesByMenuId 获取拥有指定菜单的所有角色ID
func (menuService *MenuService) GetAuthoritiesByMenuId(menuId uint) (authorityIds []uint, err error) {
var records []system.SysAuthorityMenu
err = global.GVA_DB.Where("sys_base_menu_id = ?", menuId).Find(&records).Error
if err != nil {
return nil, err
}
for _, r := range records {
id, e := strconv.Atoi(r.AuthorityId)
if e == nil {
authorityIds = append(authorityIds, uint(id))
}
}
return authorityIds, nil
}
// GetDefaultRouterAuthorityIds 获取将指定菜单设为首页的角色ID列表
func (menuService *MenuService) GetDefaultRouterAuthorityIds(menuId uint) (authorityIds []uint, err error) {
var menu system.SysBaseMenu
err = global.GVA_DB.First(&menu, menuId).Error
if err != nil {
return nil, err
}
var authorities []system.SysAuthority
err = global.GVA_DB.Where("default_router = ?", menu.Name).Find(&authorities).Error
if err != nil {
return nil, err
}
for _, auth := range authorities {
authorityIds = append(authorityIds, auth.AuthorityId)
}
return authorityIds, nil
}
// SetMenuAuthorities 全量覆盖某菜单关联的角色列表
func (menuService *MenuService) SetMenuAuthorities(menuId uint, authorityIds []uint) error {
return global.GVA_DB.Transaction(func(tx *gorm.DB) error {
// 1. 删除该菜单所有已有的角色关联
if err := tx.Where("sys_base_menu_id = ?", menuId).Delete(&system.SysAuthorityMenu{}).Error; err != nil {
return err
}
// 2. 批量插入新的关联记录
if len(authorityIds) > 0 {
menuIdStr := strconv.Itoa(int(menuId))
newRecords := make([]system.SysAuthorityMenu, 0, len(authorityIds))
for _, authorityId := range authorityIds {
newRecords = append(newRecords, system.SysAuthorityMenu{
MenuId: menuIdStr,
AuthorityId: strconv.Itoa(int(authorityId)),
})
}
if err := tx.Create(&newRecords).Error; err != nil {
return err
}
}
return nil
})
}
// UserAuthorityDefaultRouter 用户角色默认路由检查
//
// Author [SliverHorn](https://github.com/SliverHorn)

View File

@@ -1,10 +1,15 @@
package system
import (
"archive/zip"
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"io/fs"
"net/http"
"os"
"path/filepath"
"sort"
@@ -158,6 +163,107 @@ func (s *SkillsService) Save(_ context.Context, req request.SkillSaveRequest) er
return nil
}
func (s *SkillsService) Delete(_ context.Context, req request.SkillDeleteRequest) error {
if strings.TrimSpace(req.Tool) == "" {
return errors.New("工具类型不能为空")
}
if !isSafeName(req.Skill) {
return errors.New("技能名称不合法")
}
skillDir, err := s.skillDir(req.Tool, req.Skill)
if err != nil {
return err
}
info, err := os.Stat(skillDir)
if err != nil {
if os.IsNotExist(err) {
return errors.New("技能不存在")
}
return err
}
if !info.IsDir() {
return errors.New("技能目录异常")
}
return os.RemoveAll(skillDir)
}
func (s *SkillsService) Package(_ context.Context, req request.SkillPackageRequest) (string, []byte, error) {
if strings.TrimSpace(req.Tool) == "" {
return "", nil, errors.New("工具类型不能为空")
}
if !isSafeName(req.Skill) {
return "", nil, errors.New("技能名称不合法")
}
skillDir, err := s.skillDir(req.Tool, req.Skill)
if err != nil {
return "", nil, err
}
info, err := os.Stat(skillDir)
if err != nil {
if os.IsNotExist(err) {
return "", nil, errors.New("技能不存在")
}
return "", nil, err
}
if !info.IsDir() {
return "", nil, errors.New("技能目录异常")
}
buf := bytes.NewBuffer(nil)
zw := zip.NewWriter(buf)
walkErr := filepath.WalkDir(skillDir, func(path string, d fs.DirEntry, walkErr error) error {
if walkErr != nil {
return walkErr
}
rel, err := filepath.Rel(skillDir, path)
if err != nil {
return err
}
if rel == "." {
return nil
}
zipName := filepath.ToSlash(rel)
if d.IsDir() {
_, err = zw.Create(strings.TrimSuffix(zipName, "/") + "/")
return err
}
fileInfo, err := d.Info()
if err != nil {
return err
}
header, err := zip.FileInfoHeader(fileInfo)
if err != nil {
return err
}
header.Name = zipName
header.Method = zip.Deflate
writer, err := zw.CreateHeader(header)
if err != nil {
return err
}
content, err := os.ReadFile(path)
if err != nil {
return err
}
_, err = writer.Write(content)
return err
})
if walkErr != nil {
_ = zw.Close()
return "", nil, walkErr
}
if err = zw.Close(); err != nil {
return "", nil, err
}
return req.Skill + ".zip", buf.Bytes(), nil
}
func (s *SkillsService) CreateScript(_ context.Context, req request.SkillScriptCreateRequest) (string, string, error) {
if !isSafeName(req.Skill) {
return "", "", errors.New("技能名称不合法")
@@ -279,6 +385,136 @@ func (s *SkillsService) SaveGlobalConstraint(_ context.Context, req request.Skil
return nil
}
func (s *SkillsService) DownloadOnlineSkill(_ context.Context, req request.DownloadOnlineSkillReq) error {
skillsDir, err := s.toolSkillsDir(req.Tool)
if err != nil {
return err
}
body, err := json.Marshal(map[string]interface{}{
"plugin_id": req.ID,
"version": req.Version,
})
if err != nil {
return fmt.Errorf("构建下载请求失败: %w", err)
}
downloadReq, err := http.NewRequest(http.MethodPost, "https://plugin.gin-vue-admin.com/api/shopPlugin/downloadSkill", bytes.NewReader(body))
if err != nil {
return fmt.Errorf("构建下载请求失败: %w", err)
}
downloadReq.Header.Set("Content-Type", "application/json")
downloadResp, err := http.DefaultClient.Do(downloadReq)
if err != nil {
return fmt.Errorf("下载技能失败: %w", err)
}
defer downloadResp.Body.Close()
if downloadResp.StatusCode != http.StatusOK {
return fmt.Errorf("下载技能失败, HTTP状态码: %d", downloadResp.StatusCode)
}
metaBody, err := io.ReadAll(downloadResp.Body)
if err != nil {
return fmt.Errorf("读取下载结果失败: %w", err)
}
var meta struct {
Data struct {
URL string `json:"url"`
} `json:"data"`
}
if err = json.Unmarshal(metaBody, &meta); err != nil {
return fmt.Errorf("解析下载结果失败: %w", err)
}
realDownloadURL := strings.TrimSpace(meta.Data.URL)
if realDownloadURL == "" {
return errors.New("下载结果缺少 url")
}
zipResp, err := http.Get(realDownloadURL)
if err != nil {
return fmt.Errorf("下载压缩包失败: %w", err)
}
defer zipResp.Body.Close()
if zipResp.StatusCode != http.StatusOK {
return fmt.Errorf("下载压缩包失败, HTTP状态码: %d", zipResp.StatusCode)
}
tmpFile, err := os.CreateTemp("", "gva-skill-*.zip")
if err != nil {
return fmt.Errorf("创建临时文件失败: %w", err)
}
tmpPath := tmpFile.Name()
defer os.Remove(tmpPath)
if _, err = io.Copy(tmpFile, zipResp.Body); err != nil {
tmpFile.Close()
return fmt.Errorf("保存技能包失败: %w", err)
}
tmpFile.Close()
if err = extractZipToDir(tmpPath, skillsDir); err != nil {
return fmt.Errorf("解压技能包失败: %w", err)
}
return nil
}
func extractZipToDir(zipPath, destDir string) error {
r, err := zip.OpenReader(zipPath)
if err != nil {
return err
}
defer r.Close()
for _, f := range r.File {
name := filepath.FromSlash(f.Name)
if strings.Contains(name, "..") {
continue
}
target := filepath.Join(destDir, name)
if !strings.HasPrefix(filepath.Clean(target), filepath.Clean(destDir)) {
continue
}
if f.FileInfo().IsDir() {
if err := os.MkdirAll(target, os.ModePerm); err != nil {
return err
}
continue
}
if err := os.MkdirAll(filepath.Dir(target), os.ModePerm); err != nil {
return err
}
rc, err := f.Open()
if err != nil {
return err
}
out, err := os.OpenFile(target, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
if err != nil {
rc.Close()
return err
}
_, err = io.Copy(out, rc)
rc.Close()
out.Close()
if err != nil {
return err
}
}
return nil
}
func (s *SkillsService) toolSkillsDir(tool string) (string, error) {
toolDir, ok := skillToolDirs[tool]
if !ok {

View File

@@ -109,7 +109,25 @@ func (userService *UserService) GetUserInfoList(info systemReq.GetUserList) (lis
if err != nil {
return
}
err = db.Limit(limit).Offset(offset).Preload("Authorities").Preload("Authority").Find(&userList).Error
orderStr := "id desc"
if info.OrderKey != "" {
allowedOrders := map[string]bool{
"id": true,
"username": true,
"nick_name": true,
"phone": true,
"email": true,
}
if allowedOrders[info.OrderKey] {
orderStr = info.OrderKey
if info.Desc {
orderStr = info.OrderKey + " desc"
}
}
}
err = db.Limit(limit).Offset(offset).Order(orderStr).Preload("Authorities").Preload("Authority").Find(&userList).Error
return userList, total, err
}

View File

@@ -0,0 +1,26 @@
server {
listen 8080;
server_name localhost;
#charset koi8-r;
#access_log logs/host.access.log main;
location / {
root /usr/share/nginx/html;
add_header Cache-Control 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0';
try_files $uri $uri/ /index.html;
}
location /api {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
rewrite ^/api/(.*)$ /$1 break; #重写
proxy_pass http://177.7.0.12:8888; # 设置代理服务器的协议和地址
}
location /api/swagger/index.html {
proxy_pass http://127.0.0.1:8888/swagger/index.html;
}
}

View File

@@ -0,0 +1,32 @@
server {
listen 80;
server_name localhost;
#charset koi8-r;
#access_log logs/host.access.log main;
location / {
root /usr/share/nginx/html/dist;
add_header Cache-Control 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0';
try_files $uri $uri/ /index.html;
}
location /api {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
rewrite ^/api/(.*)$ /$1 break; #重写
proxy_pass http://127.0.0.1:8888; # 设置代理服务器的协议和地址
}
location /form-generator {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://127.0.0.1:8888;
}
location /api/swagger/index.html {
proxy_pass http://127.0.0.1:8888/swagger/index.html;
}
}

1
web-admin/.dockerignore Normal file
View File

@@ -0,0 +1 @@
node_modules/

View File

@@ -0,0 +1,11 @@
ENV = 'development'
VITE_CLI_PORT = 8080
VITE_SERVER_PORT = 8888
VITE_BASE_API = /api
VITE_FILE_API = /api
VITE_BASE_PATH = http://127.0.0.1
VITE_POSITION = open
VITE_EDITOR = code
// VITE_EDITOR = webstorm 如果使用webstorm开发且要使用dom定位到代码行功能 请先自定添加 webstorm到环境变量 再将VITE_EDITOR值修改为webstorm
// 如果使用docker-compose开发模式设置为下面的地址或本机主机IP
//VITE_BASE_PATH = http://177.7.0.12

View File

@@ -0,0 +1,7 @@
ENV = 'production'
#下方为上线需要用到的程序代理前缀一般用于nginx代理转发
VITE_BASE_API = /api
VITE_FILE_API = /api
#下方修改为你的线上ip如果需要在线使用表单构建工具时使用其余情况无需使用以下环境变量
VITE_BASE_PATH = https://demo.gin-vue-admin.com

5
web-admin/.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
node_modules/*
package-lock.json
yarn.lock
bun.lockb
config.yaml

12
web-admin/.prettierrc Normal file
View File

@@ -0,0 +1,12 @@
{
"printWidth": 80,
"tabWidth": 2,
"useTabs": false,
"semi": false,
"singleQuote": true,
"trailingComma": "none",
"bracketSpacing": true,
"arrowParens": "always",
"vueIndentScriptAndStyle": true,
"endOfLine": "lf"
}

25
web-admin/Dockerfile Normal file
View File

@@ -0,0 +1,25 @@
# 如果需要用 cicd ,请设置环境变量:
# variables:
# DOCKER_BUILDKIT: 1
FROM node:20-slim AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
COPY . /app
WORKDIR /app
FROM base AS prod-deps
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod
FROM base AS build
COPY --from=prod-deps /app/node_modules /app/node_modules
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install && pnpm run build
FROM nginx:alpine
LABEL MAINTAINER="bypanghu@163.com"
COPY --from=base /app/.docker-compose/nginx/conf.d/my.conf /etc/nginx/conf.d/my.conf
COPY --from=build /app/dist /usr/share/nginx/html
RUN ls -al /usr/share/nginx/html

106
web-admin/README.md Normal file
View File

@@ -0,0 +1,106 @@
# gin-vue-admin web
## Project setup
```
npm install
```
### Compiles and hot-reloads for development
```
npm run serve
```
### Compiles and minifies for production
```
npm run build
```
### Run your tests
```
npm run test
```
### Lints and fixes files
```
npm run lint
```
整理代码结构
```lua
web
├── babel.config.js
├── Dockerfile
├── favicon.ico
├── index.html -- 主页面
├── limit.js -- 助手代码
├── package.json -- 包管理器代码
├── src -- 源代码
├── api -- api 组
├── App.vue -- 主页面
├── assets -- 静态资源
├── components -- 全局组件
├── core -- gva 组件包
├── config.js -- gva网站配置文件
├── gin-vue-admin.js -- 注册欢迎文件
└── global.js -- 统一导入文件
├── directive -- v-auth 注册文件
├── main.js -- 主文件
├── permission.js -- 路由中间件
├── pinia -- pinia 状态管理器取代vuex
├── index.js -- 入口文件
└── modules -- modules
├── dictionary.js
├── router.js
└── user.js
├── router -- 路由声明文件
└── index.js
├── style -- 全局样式
├── base.scss
├── basics.scss
├── element_visiable.scss -- 此处可以全局覆盖 element-plus 样式
├── iconfont.css -- 顶部几个icon的样式文件
├── main.scss
├── mobile.scss
└── newLogin.scss
├── utils -- 方法包库
├── asyncRouter.js -- 动态路由相关
├── bus.js -- 全局mitt声明文件
├── date.js -- 日期相关
├── dictionary.js -- 获取字典方法
├── downloadImg.js -- 下载图片方法
├── format.js -- 格式整理相关
├── image.js -- 图片相关方法
├── page.js -- 设置页面标题
├── request.js -- 请求
└── stringFun.js -- 字符串文件
| ├── view -- 主要view代码
| | ├── about -- 关于我们
| | ├── dashboard -- 面板
| | ├── error -- 错误
| | ├── example --上传案例
| | ├── iconList -- icon列表
| | ├── init -- 初始化数据
| | | ├── index -- 新版本
| | | ├── init -- 旧版本
| | ├── layout -- layout约束页面
| | | ├── aside
| | | ├── bottomInfo -- bottomInfo
| | | ├── screenfull -- 全屏设置
| | | ├── setting -- 系统设置
| | | └── index.vue -- base 约束
| | ├── login --登录
| | ├── person --个人中心
| | ├── superAdmin -- 超级管理员操作
| | ├── system -- 系统检测页面
| | ├── systemTools -- 系统配置相关页面
| | └── routerHolder.vue -- page 入口页面
├── vite.config.js -- vite 配置文件
└── yarn.lock
```

View File

@@ -0,0 +1,4 @@
module.exports = {
presets: [],
plugins: []
}

View File

@@ -0,0 +1,29 @@
import js from '@eslint/js'
import pluginVue from 'eslint-plugin-vue'
import globals from 'globals'
export default [
js.configs.recommended,
...pluginVue.configs['flat/essential'],
{
name: 'app/files-to-lint',
files: ['**/*.{js,mjs,jsx,vue}'],
languageOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
globals: globals.node
},
rules: {
'vue/max-attributes-per-line': 0,
'vue/no-v-model-argument': 0,
'vue/multi-word-component-names': 'off',
'no-lone-blocks': 'off',
'no-extend-native': 'off',
'no-unused-vars': ['error', { argsIgnorePattern: '^_' }]
}
},
{
name: 'app/files-to-ignore',
ignores: ['**/dist/**', '**/build/*.js', '**/src/assets/**', '**/public/**']
}
]

115
web-admin/index.html Normal file
View File

@@ -0,0 +1,115 @@
<!doctype html>
<html lang="zh-cn" class="transition-colors">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<meta
content="Gin,Vue,Admin.Gin-Vue-Admin,GVA,gin-vue-admin,后台管理框架,vue后台管理框架,gin-vue-admin文档,gin-vue-admin首页,gin-vue-admin"
name="keywords"
/>
<link rel="icon" href="/favicon.ico" />
<title></title>
<style>
.transition-colors {
transition-property: color, background-color, border-color,
text-decoration-color, fill, stroke;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
body {
margin: 0;
--64f90c3645474bd5: #409eff;
}
#gva-loading-box {
position: relative;
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
width: 100vw;
}
#loading-text {
position: absolute;
bottom: calc(50% - 100px);
left: 0;
width: 100%;
text-align: center;
color: #666;
font-size: 14px;
}
#loading {
position: absolute;
top: calc(50% - 20px);
left: calc(50% - 20px);
}
@keyframes loader {
0% {
left: -100px;
}
100% {
left: 110%;
}
}
#box {
width: 50px;
height: 50px;
background: var(--64f90c3645474bd5);
animation: animate 0.5s linear infinite;
position: absolute;
top: 0;
left: 0;
border-radius: 3px;
}
@keyframes animate {
17% {
border-bottom-right-radius: 3px;
}
25% {
transform: translateY(9px) rotate(22.5deg);
}
50% {
transform: translateY(18px) scale(1, 0.9) rotate(45deg);
border-bottom-right-radius: 40px;
}
75% {
transform: translateY(9px) rotate(67.5deg);
}
100% {
transform: translateY(0) rotate(90deg);
}
}
#shadow {
width: 50px;
height: 5px;
background: #000;
opacity: 0.1;
position: absolute;
top: 59px;
left: 0;
border-radius: 50%;
animation: shadow 0.5s linear infinite;
}
.dark #shadow {
background: #fff;
}
@keyframes shadow {
50% {
transform: scale(1.2, 1);
}
}
</style>
</head>
<body>
<div id="gva-loading-box">
<div id="loading">
<div id="shadow"></div>
<div id="box"></div>
</div>
<div id="loading-text">系统正在加载中,请稍候...</div>
</div>
<div id="app"></div>
<script type="module" src="./src/main.js"></script>
</body>
</html>

10
web-admin/jsconfig.json Normal file
View File

@@ -0,0 +1,10 @@
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@/*": ["src/*"]
}
},
"exclude": ["node_modules", "dist"],
"include": ["src/**/*"]
}

38
web-admin/limit.js Normal file
View File

@@ -0,0 +1,38 @@
// 运行项目前通过node执行此脚本 (此脚本与 node_modules 目录同级)
import fs from 'fs'
import path from 'path'
const wfPath = path.resolve(__dirname, './node_modules/.bin')
fs.readdir(wfPath, (err, files) => {
if (err) {
console.log(err)
} else {
if (files.length !== 0) {
files.forEach((item) => {
if (item.split('.')[1] === 'cmd') {
replaceStr(`${wfPath}/${item}`, /"%_prog%"/, '%_prog%')
}
})
}
}
})
// 参数:[文件路径、 需要修改的字符串、修改后的字符串] (替换对应文件内字符串的公共函数)
function replaceStr(filePath, sourceRegx, targetSrt) {
fs.readFile(filePath, (err, data) => {
if (err) {
console.log(err)
} else {
let str = data.toString()
str = str.replace(sourceRegx, targetSrt)
fs.writeFile(filePath, str, (err) => {
if (err) {
console.log(err)
} else {
console.log('\x1B[42m%s\x1B[0m', '文件修改成功')
}
})
}
})
}

20
web-admin/openDocument.js Normal file
View File

@@ -0,0 +1,20 @@
/*
此文件受版权保护,未经授权禁止修改!如果您尚未获得授权,请通过微信(shouzi_1994)联系我们以购买授权。在未授权状态下,只需保留此代码,不会影响任何正常使用。
未经授权的商用使用可能会被我们的资产搜索引擎爬取,并可能导致后续索赔。索赔金额将不低于高级授权费的十倍。请您遵守版权法律法规,尊重知识产权。
*/
import child_process from 'child_process'
var url = 'https://www.gin-vue-admin.com'
var cmd = ''
switch (process.platform) {
case 'win32':
cmd = 'start'
child_process.exec(cmd + ' ' + url)
break
case 'darwin':
cmd = 'open'
child_process.exec(cmd + ' ' + url)
break
}

87
web-admin/package.json Normal file
View File

@@ -0,0 +1,87 @@
{
"name": "gin-vue-admin",
"version": "2.9.0",
"private": true,
"scripts": {
"dev": "node openDocument.js && vite --host --mode development",
"serve": "node openDocument.js && vite --host --mode development",
"build": "vite build --mode production",
"limit-build": "npm install increase-memory-limit-fixbug cross-env -g && npm run fix-memory-limit && node ./limit && npm run build",
"preview": "vite preview",
"fix-memory-limit": "cross-env LIMIT=4096 increase-memory-limit"
},
"type": "module",
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"@form-create/designer": "^3.2.6",
"@form-create/element-ui": "^3.2.10",
"@iconify/vue": "^5.0.0",
"@unocss/transformer-directives": "^66.4.2",
"@vue-office/docx": "^1.6.2",
"@vue-office/excel": "^1.7.11",
"@vue-office/pdf": "^2.0.2",
"@vueuse/core": "^11.0.3",
"@vueuse/integrations": "^12.0.0",
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.12",
"ace-builds": "^1.36.4",
"axios": "1.8.2",
"chokidar": "^4.0.0",
"core-js": "^3.38.1",
"echarts": "5.5.1",
"element-plus": "^2.10.2",
"highlight.js": "^11.10.0",
"install": "^0.13.0",
"marked": "14.1.1",
"marked-highlight": "^2.1.4",
"mitt": "^3.0.1",
"npm": "^11.3.0",
"nprogress": "^0.2.0",
"path": "^0.12.7",
"pinia": "^2.2.2",
"qs": "^6.13.0",
"screenfull": "^6.0.2",
"sortablejs": "^1.15.3",
"spark-md5": "^3.0.2",
"universal-cookie": "^7",
"vform3-builds": "^3.0.10",
"vite-auto-import-svg": "^2.1.0",
"vue": "^3.5.7",
"vue-cropper": "^1.1.4",
"vue-echarts": "^7.0.3",
"vue-qr": "^4.0.9",
"vue-router": "^4.4.3",
"vue3-ace-editor": "^2.2.4",
"vue3-sfc-loader": "^0.9.5",
"vuedraggable": "^4.1.0"
},
"devDependencies": {
"@babel/eslint-parser": "^7.25.1",
"@eslint/js": "^8.56.0",
"@unocss/extractor-svelte": "^66.4.2",
"@unocss/preset-wind3": "^66.4.2",
"@unocss/vite": "^66.5.0",
"@vitejs/plugin-legacy": "^6.0.0",
"@vitejs/plugin-vue": "^5.0.3",
"@vue/cli-plugin-babel": "~5.0.8",
"@vue/cli-plugin-eslint": "~5.0.8",
"@vue/cli-plugin-router": "~5.0.8",
"@vue/cli-plugin-vuex": "~5.0.8",
"@vue/cli-service": "~5.0.8",
"@vue/compiler-sfc": "^3.5.1",
"autoprefixer": "^10.4.20",
"babel-plugin-import": "^1.13.8",
"chalk": "^5.3.0",
"dotenv": "^16.4.5",
"eslint": "^8.57.0",
"eslint-plugin-vue": "^9.19.2",
"globals": "^16.3.0",
"sass": "^1.78.0",
"terser": "^5.31.6",
"vite": "^6.2.3",
"vite-check-multiple-dom": "0.2.1",
"vite-plugin-banner": "^0.8.0",
"vite-plugin-importer": "^0.2.5",
"vite-plugin-vue-devtools": "^7.0.16"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
web-admin/public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

46
web-admin/src/App.vue Normal file
View File

@@ -0,0 +1,46 @@
<template>
<div
id="app"
class="bg-gray-50 text-slate-700 !dark:text-slate-500 dark:bg-slate-800"
>
<el-config-provider :locale="zhCn" :size="appStore.config.global_size">
<router-view />
<Application />
</el-config-provider>
</div>
</template>
<script setup>
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
import Application from '@/components/application/index.vue'
import {useAppStore} from '@/pinia'
const appStore = useAppStore()
defineOptions({
name: 'App'
})
</script>
<style lang="scss">
// 引入初始化样式
#app {
height: 100vh;
overflow: hidden;
font-weight: 400 !important;
}
.el-button {
font-weight: 400 !important;
}
.gva-body-h {
min-height: calc(100% - 3rem);
}
.gva-container {
height: calc(100% - 2.5rem);
}
.gva-container2 {
height: calc(100% - 4.5rem);
}
</style>

206
web-admin/src/api/api.js Normal file
View File

@@ -0,0 +1,206 @@
import service from '@/utils/request'
// @Tags api
// @Summary 分页获取角色列表
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body modelInterface.PageInfo true "分页获取用户列表"
// @Success 200 {string} json "{"success":true,"data":{},"msg":"获取成功"}"
// @Router /api/getApiList [post]
// {
// page int
// pageSize int
// }
export const getApiList = (data) => {
return service({
url: '/api/getApiList',
method: 'post',
data
})
}
// @Tags Api
// @Summary 创建基础api
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body api.CreateApiParams true "创建api"
// @Success 200 {string} json "{"success":true,"data":{},"msg":"获取成功"}"
// @Router /api/createApi [post]
export const createApi = (data) => {
return service({
url: '/api/createApi',
method: 'post',
data
})
}
// @Tags menu
// @Summary 根据id获取菜单
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body api.GetById true "根据id获取菜单"
// @Success 200 {string} json "{"success":true,"data":{},"msg":"获取成功"}"
// @Router /menu/getApiById [post]
export const getApiById = (data) => {
return service({
url: '/api/getApiById',
method: 'post',
data
})
}
// @Tags Api
// @Summary 更新api
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body api.CreateApiParams true "更新api"
// @Success 200 {string} json "{"success":true,"data":{},"msg":"更新成功"}"
// @Router /api/updateApi [post]
export const updateApi = (data) => {
return service({
url: '/api/updateApi',
method: 'post',
data
})
}
// @Tags Api
// @Summary 更新api
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body api.CreateApiParams true "更新api"
// @Success 200 {string} json "{"success":true,"data":{},"msg":"更新成功"}"
// @Router /api/setAuthApi [post]
export const setAuthApi = (data) => {
return service({
url: '/api/setAuthApi',
method: 'post',
data
})
}
// @Tags Api
// @Summary 获取所有的Api 不分页
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Success 200 {string} json "{"success":true,"data":{},"msg":"获取成功"}"
// @Router /api/getAllApis [post]
export const getAllApis = (data) => {
return service({
url: '/api/getAllApis',
method: 'post',
data
})
}
// @Tags Api
// @Summary 删除指定api
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body dbModel.Api true "删除api"
// @Success 200 {string} json "{"success":true,"data":{},"msg":"删除成功"}"
// @Router /api/deleteApi [post]
export const deleteApi = (data) => {
return service({
url: '/api/deleteApi',
method: 'post',
data
})
}
// @Tags SysApi
// @Summary 删除选中Api
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body request.IdsReq true "ID"
// @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}"
// @Router /api/deleteApisByIds [delete]
export const deleteApisByIds = (data) => {
return service({
url: '/api/deleteApisByIds',
method: 'delete',
data
})
}
// FreshCasbin
// @Tags SysApi
// @Summary 刷新casbin缓存
// @accept application/json
// @Produce application/json
// @Success 200 {object} response.Response{msg=string} "刷新成功"
// @Router /api/freshCasbin [get]
export const freshCasbin = () => {
return service({
url: '/api/freshCasbin',
method: 'get'
})
}
export const syncApi = () => {
return service({
url: '/api/syncApi',
method: 'get'
})
}
export const getApiGroups = () => {
return service({
url: '/api/getApiGroups',
method: 'get'
})
}
export const ignoreApi = (data) => {
return service({
url: '/api/ignoreApi',
method: 'post',
data
})
}
export const enterSyncApi = (data) => {
return service({
url: '/api/enterSyncApi',
method: 'post',
data
})
}
/**
* 获取拥有指定API权限的角色ID列表
* @param {string} path API路径
* @param {string} method 请求方法
* @returns {Promise<number[]>} 角色ID数组
*/
export const getApiRoles = (path, method) => {
return service({
url: '/api/getApiRoles',
method: 'get',
params: { path, method }
})
}
/**
* 全量覆盖某API关联的角色列表
* @param {Object} data
* @param {string} data.path API路径
* @param {string} data.method 请求方法
* @param {number[]} data.authorityIds 角色ID列表
* @returns {Promise}
*/
export const setApiRoles = (data) => {
return service({
url: '/api/setApiRoles',
method: 'post',
data
})
}

View File

@@ -0,0 +1,26 @@
import service from '@/utils/request'
// 分类列表
export const getCategoryList = () => {
return service({
url: '/attachmentCategory/getCategoryList',
method: 'get',
})
}
// 添加/编辑分类
export const addCategory = (data) => {
return service({
url: '/attachmentCategory/addCategory',
method: 'post',
data
})
}
// 删除分类
export const deleteCategory = (data) => {
return service({
url: '/attachmentCategory/deleteCategory',
method: 'post',
data
})
}

View File

@@ -0,0 +1,113 @@
import service from '@/utils/request'
// @Router /authority/getAuthorityList [post]
export const getAuthorityList = (data) => {
return service({
url: '/authority/getAuthorityList',
method: 'post',
data
})
}
// @Summary 删除角色
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body {authorityId uint} true "删除角色"
// @Success 200 {string} json "{"success":true,"data":{},"msg":"获取成功"}"
// @Router /authority/deleteAuthority [post]
export const deleteAuthority = (data) => {
return service({
url: '/authority/deleteAuthority',
method: 'post',
data
})
}
// @Summary 创建角色
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body api.CreateAuthorityPatams true "创建角色"
// @Success 200 {string} json "{"success":true,"data":{},"msg":"获取成功"}"
// @Router /authority/createAuthority [post]
export const createAuthority = (data) => {
return service({
url: '/authority/createAuthority',
method: 'post',
data
})
}
// @Tags authority
// @Summary 拷贝角色
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body api.CreateAuthorityPatams true "拷贝角色"
// @Success 200 {string} json "{"success":true,"data":{},"msg":"拷贝成功"}"
// @Router /authority/copyAuthority [post]
export const copyAuthority = (data) => {
return service({
url: '/authority/copyAuthority',
method: 'post',
data
})
}
// @Summary 设置角色资源权限
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body sysModel.SysAuthority true "设置角色资源权限"
// @Success 200 {string} string "{"success":true,"data":{},"msg":"设置成功"}"
// @Router /authority/setDataAuthority [post]
export const setDataAuthority = (data) => {
return service({
url: '/authority/setDataAuthority',
method: 'post',
data
})
}
// @Summary 修改角色
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body model.SysAuthority true "修改角色"
// @Success 200 {string} string "{"success":true,"data":{},"msg":"设置成功"}"
// @Router /authority/setDataAuthority [post]
export const updateAuthority = (data) => {
return service({
url: '/authority/updateAuthority',
method: 'put',
data
})
}
/**
* 获取拥有指定角色的用户ID列表
* @param {number} authorityId 角色ID
* @returns {Promise<number[]>} 用户ID数组
*/
export const getUsersByAuthorityId = (authorityId) => {
return service({
url: '/authority/getUsersByAuthority',
method: 'get',
params: { authorityId }
})
}
/**
* 全量覆盖某角色关联的用户列表
* @param {Object} data
* @param {number} data.authorityId 角色ID
* @param {number[]} data.userIds 用户ID列表
* @returns {Promise}
*/
export const setRoleUsers = (data) => {
return service({
url: '/authority/setRoleUsers',
method: 'post',
data
})
}

View File

@@ -0,0 +1,25 @@
import service from '@/utils/request'
export const getAuthorityBtnApi = (data) => {
return service({
url: '/authorityBtn/getAuthorityBtn',
method: 'post',
data
})
}
export const setAuthorityBtnApi = (data) => {
return service({
url: '/authorityBtn/setAuthorityBtn',
method: 'post',
data
})
}
export const canRemoveAuthorityBtnApi = (params) => {
return service({
url: '/authorityBtn/canRemoveAuthorityBtn',
method: 'post',
params
})
}

View File

@@ -0,0 +1,242 @@
import service from '@/utils/request'
export const preview = (data) => {
return service({
url: '/autoCode/preview',
method: 'post',
data
})
}
export const createTemp = (data) => {
return service({
url: '/autoCode/createTemp',
method: 'post',
data
})
}
// @Tags SysApi
// @Summary 获取当前所有数据库
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Success 200 {string} string "{"success":true,"data":{},"msg":"创建成功"}"
// @Router /autoCode/getDatabase [get]
export const getDB = (params) => {
return service({
url: '/autoCode/getDB',
method: 'get',
params
})
}
// @Tags SysApi
// @Summary 获取当前数据库所有表
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Success 200 {string} string "{"success":true,"data":{},"msg":"创建成功"}"
// @Router /autoCode/getTables [get]
export const getTable = (params) => {
return service({
url: '/autoCode/getTables',
method: 'get',
params
})
}
// @Tags SysApi
// @Summary 获取当前数据库所有表
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Success 200 {string} string "{"success":true,"data":{},"msg":"创建成功"}"
// @Router /autoCode/getColumn [get]
export const getColumn = (params) => {
return service({
url: '/autoCode/getColumn',
method: 'get',
params
})
}
export const getSysHistory = (data) => {
return service({
url: '/autoCode/getSysHistory',
method: 'post',
data
})
}
export const rollback = (data) => {
return service({
url: '/autoCode/rollback',
method: 'post',
data
})
}
export const getMeta = (data) => {
return service({
url: '/autoCode/getMeta',
method: 'post',
data
})
}
export const delSysHistory = (data) => {
return service({
url: '/autoCode/delSysHistory',
method: 'post',
data
})
}
export const createPackageApi = (data) => {
return service({
url: '/autoCode/createPackage',
method: 'post',
data
})
}
export const getPackageApi = () => {
return service({
url: '/autoCode/getPackage',
method: 'post'
})
}
export const deletePackageApi = (data) => {
return service({
url: '/autoCode/delPackage',
method: 'post',
data
})
}
export const getTemplatesApi = () => {
return service({
url: '/autoCode/getTemplates',
method: 'get'
})
}
export const installPlug = (data) => {
return service({
url: '/autoCode/installPlug',
method: 'post',
data
})
}
export const pubPlug = (params) => {
return service({
url: '/autoCode/pubPlug',
method: 'post',
params
})
}
export const llmAuto = (data) => {
return service({
url: '/autoCode/llmAuto',
method: 'post',
data: { ...data },
timeout: 1000 * 60 * 10,
loadingOption: {
lock: true,
fullscreen: true,
text: `小淼正在思考,请稍候...`
}
})
}
export const addFunc = (data) => {
return service({
url: '/autoCode/addFunc',
method: 'post',
data
})
}
export const initMenu = (data) => {
return service({
url: '/autoCode/initMenu',
method: 'post',
data
})
}
export const initAPI = (data) => {
return service({
url: '/autoCode/initAPI',
method: 'post',
data
})
}
export const initDictionary = (data) => {
return service({
url: '/autoCode/initDictionary',
method: 'post',
data
})
}
export const mcp = (data) => {
return service({
url: '/autoCode/mcp',
method: 'post',
data
})
}
export const mcpList = (data) => {
return service({
url: '/autoCode/mcpList',
method: 'post',
data
})
}
export const mcpTest = (data) => {
return service({
url: '/autoCode/mcpTest',
method: 'post',
data
})
}
// @Tags SysApi
// @Summary 获取插件列表
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}"
// @Router /autoCode/getPluginList [get]
export const getPluginList = (params) => {
return service({
url: '/autoCode/getPluginList',
method: 'get',
params
})
}
// @Tags SysApi
// @Summary 删除插件
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}"
// @Router /autoCode/removePlugin [post]
export const removePlugin = (params) => {
return service({
url: '/autoCode/removePlugin',
method: 'post',
params
})
}

View File

@@ -0,0 +1,43 @@
import service from '@/utils/request'
// @Summary 设置角色资源权限
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body sysModel.SysAuthority true "设置角色资源权限"
// @Success 200 {string} string "{"success":true,"data":{},"msg":"设置成功"}"
// @Router /authority/setDataAuthority [post]
export const findFile = (params) => {
return service({
url: '/fileUploadAndDownload/findFile',
method: 'get',
params
})
}
export const breakpointContinue = (data) => {
return service({
url: '/fileUploadAndDownload/breakpointContinue',
method: 'post',
donNotShowLoading: true,
headers: { 'Content-Type': 'multipart/form-data' },
data
})
}
export const breakpointContinueFinish = (params) => {
return service({
url: '/fileUploadAndDownload/breakpointContinueFinish',
method: 'post',
params
})
}
export const removeChunk = (data, params) => {
return service({
url: '/fileUploadAndDownload/removeChunk',
method: 'post',
data,
params
})
}

View File

@@ -0,0 +1,32 @@
import service from '@/utils/request'
// @Tags authority
// @Summary 更改角色api权限
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body api.CreateAuthorityPatams true "更改角色api权限"
// @Success 200 {string} json "{"success":true,"data":{},"msg":"获取成功"}"
// @Router /casbin/UpdateCasbin [post]
export const UpdateCasbin = (data) => {
return service({
url: '/casbin/updateCasbin',
method: 'post',
data
})
}
// @Tags casbin
// @Summary 获取权限列表
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body api.CreateAuthorityPatams true "获取权限列表"
// @Success 200 {string} json "{"success":true,"data":{},"msg":"获取成功"}"
// @Router /casbin/getPolicyPathByAuthorityId [post]
export const getPolicyPathByAuthorityId = (data) => {
return service({
url: '/casbin/getPolicyPathByAuthorityId',
method: 'post',
data
})
}

View File

@@ -0,0 +1,80 @@
import service from '@/utils/request'
// @Tags SysApi
// @Summary 删除客户
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body dbModel.ExaCustomer true "删除客户"
// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}"
// @Router /customer/customer [post]
export const createExaCustomer = (data) => {
return service({
url: '/customer/customer',
method: 'post',
data
})
}
// @Tags SysApi
// @Summary 更新客户信息
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body dbModel.ExaCustomer true "更新客户信息"
// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}"
// @Router /customer/customer [put]
export const updateExaCustomer = (data) => {
return service({
url: '/customer/customer',
method: 'put',
data
})
}
// @Tags SysApi
// @Summary 创建客户
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body dbModel.ExaCustomer true "创建客户"
// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}"
// @Router /customer/customer [delete]
export const deleteExaCustomer = (data) => {
return service({
url: '/customer/customer',
method: 'delete',
data
})
}
// @Tags SysApi
// @Summary 获取单一客户信息
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body dbModel.ExaCustomer true "获取单一客户信息"
// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}"
// @Router /customer/customer [get]
export const getExaCustomer = (params) => {
return service({
url: '/customer/customer',
method: 'get',
params
})
}
// @Tags SysApi
// @Summary 获取权限客户列表
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body modelInterface.PageInfo true "获取权限客户列表"
// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}"
// @Router /customer/customerList [get]
export const getExaCustomerList = (params) => {
return service({
url: '/customer/customerList',
method: 'get',
params
})
}

View File

@@ -0,0 +1,14 @@
import service from '@/utils/request'
// @Tags email
// @Summary 发送测试邮件
// @Security ApiKeyAuth
// @Produce application/json
// @Success 200 {string} string "{"success":true,"data":{},"msg":"返回成功"}"
// @Router /email/emailTest [post]
export const emailTest = (data) => {
return service({
url: '/email/emailTest',
method: 'post',
data
})
}

View File

@@ -0,0 +1,145 @@
import service from '@/utils/request'
// @Tags SysExportTemplate
// @Summary 创建导出模板
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body model.SysExportTemplate true "创建导出模板"
// @Success 200 {string} string "{"success":true,"data":{},"msg":"创建成功"}"
// @Router /sysExportTemplate/createSysExportTemplate [post]
export const createSysExportTemplate = (data) => {
return service({
url: '/sysExportTemplate/createSysExportTemplate',
method: 'post',
data
})
}
// @Tags SysExportTemplate
// @Summary 删除导出模板
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body model.SysExportTemplate true "删除导出模板"
// @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}"
// @Router /sysExportTemplate/deleteSysExportTemplate [delete]
export const deleteSysExportTemplate = (data) => {
return service({
url: '/sysExportTemplate/deleteSysExportTemplate',
method: 'delete',
data
})
}
// @Tags SysExportTemplate
// @Summary 批量删除导出模板
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body request.IdsReq true "批量删除导出模板"
// @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}"
// @Router /sysExportTemplate/deleteSysExportTemplate [delete]
export const deleteSysExportTemplateByIds = (data) => {
return service({
url: '/sysExportTemplate/deleteSysExportTemplateByIds',
method: 'delete',
data
})
}
// @Tags SysExportTemplate
// @Summary 更新导出模板
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body model.SysExportTemplate true "更新导出模板"
// @Success 200 {string} string "{"success":true,"data":{},"msg":"更新成功"}"
// @Router /sysExportTemplate/updateSysExportTemplate [put]
export const updateSysExportTemplate = (data) => {
return service({
url: '/sysExportTemplate/updateSysExportTemplate',
method: 'put',
data
})
}
// @Tags SysExportTemplate
// @Summary 用id查询导出模板
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data query model.SysExportTemplate true "用id查询导出模板"
// @Success 200 {string} string "{"success":true,"data":{},"msg":"查询成功"}"
// @Router /sysExportTemplate/findSysExportTemplate [get]
export const findSysExportTemplate = (params) => {
return service({
url: '/sysExportTemplate/findSysExportTemplate',
method: 'get',
params
})
}
// @Tags SysExportTemplate
// @Summary 分页获取导出模板列表
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data query request.PageInfo true "分页获取导出模板列表"
// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}"
// @Router /sysExportTemplate/getSysExportTemplateList [get]
export const getSysExportTemplateList = (params) => {
return service({
url: '/sysExportTemplate/getSysExportTemplateList',
method: 'get',
params
})
}
// ExportExcel 导出表格token
// @Tags SysExportTemplate
// @Summary 导出表格
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Router /sysExportTemplate/exportExcel [get]
export const exportExcel = (params) => {
return service({
url: '/sysExportTemplate/exportExcel',
method: 'get',
params
})
}
// ExportTemplate 导出表格模板
// @Tags SysExportTemplate
// @Summary 导出表格模板
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Router /sysExportTemplate/exportTemplate [get]
export const exportTemplate = (params) => {
return service({
url: '/sysExportTemplate/exportTemplate',
method: 'get',
params
})
}
// PreviewSQL 预览最终生成的SQL
// @Tags SysExportTemplate
// @Summary 预览最终生成的SQL
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Router /sysExportTemplate/previewSQL [get]
// @Param templateID query string true "导出模板ID"
// @Param params query string false "查询参数编码字符串,参考 ExportExcel 组件"
export const previewSQL = (params) => {
return service({
url: '/sysExportTemplate/previewSQL',
method: 'get',
params
})
}

View File

@@ -0,0 +1,67 @@
import service from '@/utils/request'
// @Tags FileUploadAndDownload
// @Summary 分页文件列表
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body modelInterface.PageInfo true "分页获取文件户列表"
// @Success 200 {string} json "{"success":true,"data":{},"msg":"获取成功"}"
// @Router /fileUploadAndDownload/getFileList [post]
export const getFileList = (data) => {
return service({
url: '/fileUploadAndDownload/getFileList',
method: 'post',
data
})
}
// @Tags FileUploadAndDownload
// @Summary 删除文件
// @Security ApiKeyAuth
// @Produce application/json
// @Param data body dbModel.FileUploadAndDownload true "传入文件里面id即可"
// @Success 200 {string} json "{"success":true,"data":{},"msg":"返回成功"}"
// @Router /fileUploadAndDownload/deleteFile [post]
export const deleteFile = (data) => {
return service({
url: '/fileUploadAndDownload/deleteFile',
method: 'post',
data
})
}
/**
* 编辑文件名或者备注
* @param data
* @returns {*}
*/
export const editFileName = (data) => {
return service({
url: '/fileUploadAndDownload/editFileName',
method: 'post',
data
})
}
/**
* 导入URL
* @param data
* @returns {*}
*/
export const importURL = (data) => {
return service({
url: '/fileUploadAndDownload/importURL',
method: 'post',
data
})
}
// 上传文件 暂时用于头像上传
export const uploadFile = (data) => {
return service({
url: "/fileUploadAndDownload/upload",
method: "post",
data,
});
};

View File

@@ -0,0 +1,19 @@
import axios from 'axios'
const service = axios.create()
export function Commits(page) {
return service({
url:
'https://api.github.com/repos/flipped-aurora/gin-vue-admin/commits?page=' +
page,
method: 'get'
})
}
export function Members() {
return service({
url: 'https://api.github.com/orgs/FLIPPED-AURORA/members',
method: 'get'
})
}

View File

@@ -0,0 +1,27 @@
import service from '@/utils/request'
// @Tags InitDB
// @Summary 初始化用户数据库
// @Produce application/json
// @Param data body request.InitDB true "初始化数据库参数"
// @Success 200 {string} string "{"code":0,"data":{},"msg":"自动创建数据库成功"}"
// @Router /init/initdb [post]
export const initDB = (data) => {
return service({
url: '/init/initdb',
method: 'post',
data,
donNotShowLoading: true
})
}
// @Tags CheckDB
// @Summary 初始化用户数据库
// @Produce application/json
// @Success 200 {string} string "{"code":0,"data":{},"msg":"探测完成"}"
// @Router /init/checkdb [post]
export const checkDB = () => {
return service({
url: '/init/checkdb',
method: 'post'
})
}

14
web-admin/src/api/jwt.js Normal file
View File

@@ -0,0 +1,14 @@
import service from '@/utils/request'
// @Tags jwt
// @Summary jwt加入黑名单
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Success 200 {string} string "{"success":true,"data":{},"msg":"拉黑成功"}"
// @Router /jwt/jsonInBlacklist [post]
export const jsonInBlacklist = () => {
return service({
url: '/jwt/jsonInBlacklist',
method: 'post'
})
}

Some files were not shown because too many files have changed in this diff Show More