Compare commits

...

6 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
315 changed files with 48841 additions and 2261 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/)

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

View File

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

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

@@ -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

@@ -3,6 +3,7 @@ package app
import (
"bufio"
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
@@ -371,21 +372,15 @@ func (s *ConversationService) SendMessage(userID, conversationID uint, req *requ
return nil, err
}
// 获取对话历史(最近10条
// 获取完整对话历史(context 管理由 callAIService 内部处理
var messages []app.Message
err = global.GVA_DB.Where("conversation_id = ?", conversationID).
Order("created_at DESC").
Limit(10).
Order("created_at ASC").
Find(&messages).Error
if err != nil {
return nil, err
}
// 反转消息顺序(从旧到新)
for i, j := 0, len(messages)-1; i < j; i, j = i+1, j-1 {
messages[i], messages[j] = messages[j], messages[i]
}
// 调用 AI 服务获取回复
aiResponse, err := s.callAIService(conversation, character, messages)
if err != nil {
@@ -527,47 +522,26 @@ func (s *ConversationService) callAIService(conversation app.Conversation, chara
}
}
// 构建系统提示词(如果预设有系统提示词,则追加到角色卡提示词后
systemPrompt := s.buildSystemPrompt(character)
if preset != nil && preset.SystemPrompt != "" {
systemPrompt = systemPrompt + "\n\n" + preset.SystemPrompt
global.GVA_LOG.Info("已追加预设的系统提示词")
// 构建消息列表(含 context 预算管理
var presetSysPrompt string
if preset != nil {
presetSysPrompt = preset.SystemPrompt
}
wbEngine := &WorldbookEngine{}
apiMessages := s.buildAPIMessagesWithContextManagement(
messages, character, presetSysPrompt, wbEngine, conversation, &aiConfig, preset,
)
// 集成世界书触发引擎
if conversation.WorldbookEnabled && conversation.WorldbookID != nil {
global.GVA_LOG.Info(fmt.Sprintf("世界书已启用ID: %d", *conversation.WorldbookID))
// 提取消息内容用于扫描
var messageContents []string
for _, msg := range messages {
messageContents = append(messageContents, msg.Content)
}
// 使用世界书引擎扫描并触发条目
engine := &WorldbookEngine{}
triggered, err := engine.ScanAndTrigger(*conversation.WorldbookID, messageContents)
if err != nil {
global.GVA_LOG.Warn(fmt.Sprintf("世界书触发失败: %v", err))
} else if len(triggered) > 0 {
global.GVA_LOG.Info(fmt.Sprintf("触发了 %d 个世界书条目", len(triggered)))
// 将触发的世界书内容注入到系统提示词
systemPrompt = engine.BuildPromptWithWorldbook(systemPrompt, triggered)
} else {
global.GVA_LOG.Info("没有触发任何世界书条目")
}
// 从 apiMessages 中提取 systemPrompt供 Anthropic 独立参数使用
systemPrompt := ""
if len(apiMessages) > 0 && apiMessages[0]["role"] == "system" {
systemPrompt = apiMessages[0]["content"]
}
// 构建消息列表
apiMessages := s.buildAPIMessages(messages, systemPrompt)
// 打印发送给AI的完整内容
global.GVA_LOG.Info("========== 发送给AI的完整内容 ==========")
global.GVA_LOG.Info(fmt.Sprintf("系统提示词: %s", systemPrompt))
global.GVA_LOG.Info("消息列表:")
for i, msg := range apiMessages {
global.GVA_LOG.Info(fmt.Sprintf(" [%d] Role: %s, Content: %s", i, msg["role"], msg["content"]))
}
global.GVA_LOG.Info(fmt.Sprintf("系统提示词长度: %d 字符", len(systemPrompt)))
global.GVA_LOG.Info(fmt.Sprintf("历史消息条数: %d", len(apiMessages)-1))
global.GVA_LOG.Info("==========================================")
// 确定使用的模型如果用户在设置中指定了AI配置则使用该配置的默认模型
@@ -735,7 +709,7 @@ func (s *ConversationService) getWeekdayInChinese(weekday time.Weekday) string {
}
// SendMessageStream 流式发送消息并获取 AI 回复
func (s *ConversationService) SendMessageStream(userID, conversationID uint, req *request.SendMessageRequest, streamChan chan string, doneChan chan bool) error {
func (s *ConversationService) SendMessageStream(ctx context.Context, userID, conversationID uint, req *request.SendMessageRequest, streamChan chan string, doneChan chan bool) error {
defer close(streamChan)
defer close(doneChan)
@@ -796,21 +770,15 @@ func (s *ConversationService) SendMessageStream(userID, conversationID uint, req
return err
}
// 获取对话历史(最近10条
// 获取完整对话历史(context 管理由 buildAPIMessagesWithContextManagement 处理
var messages []app.Message
err = global.GVA_DB.Where("conversation_id = ?", conversationID).
Order("created_at DESC").
Limit(10).
Order("created_at ASC").
Find(&messages).Error
if err != nil {
return err
}
// 反转消息顺序(从旧到新)
for i, j := 0, len(messages)-1; i < j; i, j = i+1, j-1 {
messages[i], messages[j] = messages[j], messages[i]
}
// 获取 AI 配置
var aiConfig app.AIConfig
var configID uint
@@ -857,42 +825,26 @@ func (s *ConversationService) SendMessageStream(userID, conversationID uint, req
}
}
// 构建系统提示词(应用预设
systemPrompt := s.buildSystemPrompt(character)
if streamPreset != nil && streamPreset.SystemPrompt != "" {
systemPrompt = systemPrompt + "\n\n" + streamPreset.SystemPrompt
// 构建消息列表(含 context 预算管理
var streamPresetSysPrompt string
if streamPreset != nil {
streamPresetSysPrompt = streamPreset.SystemPrompt
}
streamWbEngine := &WorldbookEngine{}
apiMessages := s.buildAPIMessagesWithContextManagement(
messages, character, streamPresetSysPrompt, streamWbEngine, conversation, &aiConfig, streamPreset,
)
// 集成世界书触发引擎(流式传输)
if conversation.WorldbookEnabled && conversation.WorldbookID != nil {
global.GVA_LOG.Info(fmt.Sprintf("[流式传输] 世界书已启用ID: %d", *conversation.WorldbookID))
var messageContents []string
for _, msg := range messages {
messageContents = append(messageContents, msg.Content)
}
engine := &WorldbookEngine{}
triggeredEntries, wbErr := engine.ScanAndTrigger(*conversation.WorldbookID, messageContents)
if wbErr != nil {
global.GVA_LOG.Warn(fmt.Sprintf("[流式传输] 世界书触发失败: %v", wbErr))
} else if len(triggeredEntries) > 0 {
global.GVA_LOG.Info(fmt.Sprintf("[流式传输] 触发了 %d 个世界书条目", len(triggeredEntries)))
systemPrompt = engine.BuildPromptWithWorldbook(systemPrompt, triggeredEntries)
} else {
global.GVA_LOG.Info("[流式传输] 没有触发任何世界书条目")
}
// 从 apiMessages 中提取 systemPrompt供 Anthropic 独立参数使用
systemPrompt := ""
if len(apiMessages) > 0 && apiMessages[0]["role"] == "system" {
systemPrompt = apiMessages[0]["content"]
}
apiMessages := s.buildAPIMessages(messages, systemPrompt)
// 打印发送给AI的完整内容流式传输
global.GVA_LOG.Info("========== [流式传输] 发送给AI的完整内容 ==========")
global.GVA_LOG.Info(fmt.Sprintf("系统提示词: %s", systemPrompt))
global.GVA_LOG.Info("消息列表:")
for i, msg := range apiMessages {
global.GVA_LOG.Info(fmt.Sprintf(" [%d] Role: %s, Content: %s", i, msg["role"], msg["content"]))
}
global.GVA_LOG.Info(fmt.Sprintf("[流式传输] 系统提示词长度: %d 字符", len(systemPrompt)))
global.GVA_LOG.Info(fmt.Sprintf("[流式传输] 历史消息条数: %d", len(apiMessages)-1))
global.GVA_LOG.Info("==========================================")
// 确定使用的模型
@@ -910,15 +862,21 @@ func (s *ConversationService) SendMessageStream(userID, conversationID uint, req
var fullContent string
switch aiConfig.Provider {
case "openai", "custom":
fullContent, err = s.callOpenAIAPIStream(&aiConfig, model, apiMessages, streamPreset, streamChan)
fullContent, err = s.callOpenAIAPIStream(ctx, &aiConfig, model, apiMessages, streamPreset, streamChan)
case "anthropic":
fullContent, err = s.callAnthropicAPIStream(&aiConfig, model, apiMessages, systemPrompt, streamPreset, streamChan)
fullContent, err = s.callAnthropicAPIStream(ctx, &aiConfig, model, apiMessages, systemPrompt, streamPreset, streamChan)
default:
return fmt.Errorf("不支持的 AI 提供商: %s", aiConfig.Provider)
}
if err != nil {
global.GVA_LOG.Error(fmt.Sprintf("========== [流式传输] AI返回错误 ==========\n%v\n==========================================", err))
// AI 调用失败,回滚已写入的用户消息,避免孤立记录残留在数据库
if delErr := global.GVA_DB.Delete(&userMessage).Error; delErr != nil {
global.GVA_LOG.Error(fmt.Sprintf("[流式传输] 回滚用户消息失败: %v", delErr))
} else {
global.GVA_LOG.Info("[流式传输] 已回滚用户消息")
}
return err
}
@@ -962,8 +920,9 @@ func (s *ConversationService) SendMessageStream(userID, conversationID uint, req
}
// callOpenAIAPIStream 调用 OpenAI API 流式传输
func (s *ConversationService) callOpenAIAPIStream(config *app.AIConfig, model string, messages []map[string]string, preset *app.AIPreset, streamChan chan string) (string, error) {
client := &http.Client{Timeout: 120 * time.Second}
func (s *ConversationService) callOpenAIAPIStream(ctx context.Context, config *app.AIConfig, model string, messages []map[string]string, preset *app.AIPreset, streamChan chan string) (string, error) {
// 不设 Timeout生命周期由调用方传入的 ctx 控制(客户端断连时自动取消)
client := &http.Client{}
if model == "" {
model = config.DefaultModel
@@ -1025,7 +984,7 @@ func (s *ConversationService) callOpenAIAPIStream(config *app.AIConfig, model st
}
endpoint := config.BaseURL + "/chat/completions"
req, err := http.NewRequest("POST", endpoint, bytes.NewBuffer(bodyBytes))
req, err := http.NewRequestWithContext(ctx, "POST", endpoint, bytes.NewBuffer(bodyBytes))
if err != nil {
return "", fmt.Errorf("创建请求失败: %v", err)
}
@@ -1035,6 +994,10 @@ func (s *ConversationService) callOpenAIAPIStream(config *app.AIConfig, model st
resp, err := client.Do(req)
if err != nil {
// 客户端主动断开时 ctx 被取消,不算真正的错误
if ctx.Err() != nil {
return "", nil
}
return "", fmt.Errorf("请求失败: %v", err)
}
defer resp.Body.Close()
@@ -1050,49 +1013,51 @@ func (s *ConversationService) callOpenAIAPIStream(config *app.AIConfig, model st
for {
line, err := reader.ReadString('\n')
// 先处理本次读到的数据EOF 时可能仍携带最后一行内容)
if line != "" {
trimmed := strings.TrimSpace(line)
if trimmed != "" && trimmed != "data: [DONE]" && strings.HasPrefix(trimmed, "data: ") {
data := strings.TrimPrefix(trimmed, "data: ")
var streamResp struct {
Choices []struct {
Delta struct {
Content string `json:"content"`
} `json:"delta"`
} `json:"choices"`
}
if jsonErr := json.Unmarshal([]byte(data), &streamResp); jsonErr == nil {
if len(streamResp.Choices) > 0 {
content := streamResp.Choices[0].Delta.Content
if content != "" {
fullContent.WriteString(content)
streamChan <- content
}
}
}
}
}
// 再检查读取错误
if err != nil {
if err == io.EOF {
break
}
// ctx 被取消(客户端断开)时不算真正的流读取错误
if ctx.Err() != nil {
return fullContent.String(), nil
}
return "", fmt.Errorf("读取流失败: %v", err)
}
line = strings.TrimSpace(line)
if line == "" || line == "data: [DONE]" {
continue
}
if strings.HasPrefix(line, "data: ") {
data := strings.TrimPrefix(line, "data: ")
var streamResp struct {
Choices []struct {
Delta struct {
Content string `json:"content"`
} `json:"delta"`
} `json:"choices"`
}
if err := json.Unmarshal([]byte(data), &streamResp); err != nil {
continue
}
if len(streamResp.Choices) > 0 {
content := streamResp.Choices[0].Delta.Content
if content != "" {
fullContent.WriteString(content)
streamChan <- content
}
}
}
}
return fullContent.String(), nil
}
// callAnthropicAPIStream 调用 Anthropic API 流式传输
func (s *ConversationService) callAnthropicAPIStream(config *app.AIConfig, model string, messages []map[string]string, systemPrompt string, preset *app.AIPreset, streamChan chan string) (string, error) {
client := &http.Client{Timeout: 120 * time.Second}
func (s *ConversationService) callAnthropicAPIStream(ctx context.Context, config *app.AIConfig, model string, messages []map[string]string, systemPrompt string, preset *app.AIPreset, streamChan chan string) (string, error) {
// 不设 Timeout生命周期由调用方传入的 ctx 控制(客户端断连时自动取消)
client := &http.Client{}
if model == "" {
model = config.DefaultModel
@@ -1152,7 +1117,7 @@ func (s *ConversationService) callAnthropicAPIStream(config *app.AIConfig, model
}
endpoint := config.BaseURL + "/messages"
req, err := http.NewRequest("POST", endpoint, bytes.NewBuffer(bodyBytes))
req, err := http.NewRequestWithContext(ctx, "POST", endpoint, bytes.NewBuffer(bodyBytes))
if err != nil {
return "", fmt.Errorf("创建请求失败: %v", err)
}
@@ -1163,6 +1128,10 @@ func (s *ConversationService) callAnthropicAPIStream(config *app.AIConfig, model
resp, err := client.Do(req)
if err != nil {
// 客户端主动断开时 ctx 被取消,不算真正的错误
if ctx.Err() != nil {
return "", nil
}
return "", fmt.Errorf("请求失败: %v", err)
}
defer resp.Body.Close()
@@ -1178,38 +1147,39 @@ func (s *ConversationService) callAnthropicAPIStream(config *app.AIConfig, model
for {
line, err := reader.ReadString('\n')
// 先处理本次读到的数据EOF 时可能仍携带最后一行内容)
if line != "" {
trimmed := strings.TrimSpace(line)
if trimmed != "" && strings.HasPrefix(trimmed, "data: ") {
data := strings.TrimPrefix(trimmed, "data: ")
var streamResp struct {
Type string `json:"type"`
Delta struct {
Type string `json:"type"`
Text string `json:"text"`
} `json:"delta"`
}
if jsonErr := json.Unmarshal([]byte(data), &streamResp); jsonErr == nil {
if streamResp.Type == "content_block_delta" && streamResp.Delta.Text != "" {
fullContent.WriteString(streamResp.Delta.Text)
streamChan <- streamResp.Delta.Text
}
}
}
}
// 再检查读取错误
if err != nil {
if err == io.EOF {
break
}
// ctx 被取消(客户端断开)时不算真正的流读取错误
if ctx.Err() != nil {
return fullContent.String(), nil
}
return "", fmt.Errorf("读取流失败: %v", err)
}
line = strings.TrimSpace(line)
if line == "" {
continue
}
if strings.HasPrefix(line, "data: ") {
data := strings.TrimPrefix(line, "data: ")
var streamResp struct {
Type string `json:"type"`
Delta struct {
Type string `json:"type"`
Text string `json:"text"`
} `json:"delta"`
}
if err := json.Unmarshal([]byte(data), &streamResp); err != nil {
continue
}
if streamResp.Type == "content_block_delta" && streamResp.Delta.Text != "" {
fullContent.WriteString(streamResp.Delta.Text)
streamChan <- streamResp.Delta.Text
}
}
}
return fullContent.String(), nil
@@ -1243,19 +1213,16 @@ func (s *ConversationService) RegenerateMessage(userID, conversationID uint) (*r
})
}
// 获取删除后的消息历史
// 获取删除后的完整消息历史context 管理由 callAIService 内部处理)
var messages []app.Message
err = global.GVA_DB.Where("conversation_id = ?", conversationID).
Order("created_at DESC").Limit(10).Find(&messages).Error
Order("created_at ASC").Find(&messages).Error
if err != nil {
return nil, err
}
if len(messages) == 0 {
return nil, errors.New("没有可用的消息历史")
}
for i, j := 0, len(messages)-1; i < j; i, j = i+1, j-1 {
messages[i], messages[j] = messages[j], messages[i]
}
aiResponse, err := s.callAIService(conversation, character, messages)
if err != nil {
@@ -1282,7 +1249,7 @@ func (s *ConversationService) RegenerateMessage(userID, conversationID uint) (*r
}
// RegenerateMessageStream 流式重新生成最后一条 AI 回复
func (s *ConversationService) RegenerateMessageStream(userID, conversationID uint, streamChan chan string, doneChan chan bool) error {
func (s *ConversationService) RegenerateMessageStream(ctx context.Context, userID, conversationID uint, streamChan chan string, doneChan chan bool) error {
defer close(streamChan)
defer close(doneChan)
@@ -1312,19 +1279,16 @@ func (s *ConversationService) RegenerateMessageStream(userID, conversationID uin
})
}
// 获取删除后的消息历史
// 获取删除后的完整消息历史context 管理由 buildAPIMessagesWithContextManagement 处理)
var messages []app.Message
err = global.GVA_DB.Where("conversation_id = ?", conversationID).
Order("created_at DESC").Limit(10).Find(&messages).Error
Order("created_at ASC").Find(&messages).Error
if err != nil {
return err
}
if len(messages) == 0 {
return errors.New("没有可用的消息历史")
}
for i, j := 0, len(messages)-1; i < j; i, j = i+1, j-1 {
messages[i], messages[j] = messages[j], messages[i]
}
// 获取 AI 配置
var aiConfig app.AIConfig
@@ -1367,11 +1331,21 @@ func (s *ConversationService) RegenerateMessageStream(userID, conversationID uin
}
}
systemPrompt := s.buildSystemPrompt(character)
if preset != nil && preset.SystemPrompt != "" {
systemPrompt = systemPrompt + "\n\n" + preset.SystemPrompt
// 构建消息列表(含 context 预算管理)
var regenPresetSysPrompt string
if preset != nil {
regenPresetSysPrompt = preset.SystemPrompt
}
regenWbEngine := &WorldbookEngine{}
apiMessages := s.buildAPIMessagesWithContextManagement(
messages, character, regenPresetSysPrompt, regenWbEngine, conversation, &aiConfig, preset,
)
// 从 apiMessages 中提取 systemPrompt供 Anthropic 独立参数使用
systemPrompt := ""
if len(apiMessages) > 0 && apiMessages[0]["role"] == "system" {
systemPrompt = apiMessages[0]["content"]
}
apiMessages := s.buildAPIMessages(messages, systemPrompt)
model := aiConfig.DefaultModel
if model == "" {
@@ -1384,14 +1358,27 @@ func (s *ConversationService) RegenerateMessageStream(userID, conversationID uin
var fullContent string
switch aiConfig.Provider {
case "openai", "custom":
fullContent, err = s.callOpenAIAPIStream(&aiConfig, model, apiMessages, preset, streamChan)
fullContent, err = s.callOpenAIAPIStream(ctx, &aiConfig, model, apiMessages, preset, streamChan)
case "anthropic":
fullContent, err = s.callAnthropicAPIStream(&aiConfig, model, apiMessages, systemPrompt, preset, streamChan)
fullContent, err = s.callAnthropicAPIStream(ctx, &aiConfig, model, apiMessages, systemPrompt, preset, streamChan)
default:
return fmt.Errorf("不支持的 AI 提供商: %s", aiConfig.Provider)
}
if err != nil {
// AI 调用失败,恢复刚才删除的 assistant 消息,避免数据永久丢失
if lastAssistantMsg.ID > 0 {
if restoreErr := global.GVA_DB.Unscoped().Model(&lastAssistantMsg).Update("deleted_at", nil).Error; restoreErr != nil {
global.GVA_LOG.Error(fmt.Sprintf("[重新生成] 恢复 assistant 消息失败: %v", restoreErr))
} else {
// 回滚 conversation 统计
global.GVA_DB.Model(&conversation).Updates(map[string]interface{}{
"message_count": gorm.Expr("message_count + 1"),
"token_count": gorm.Expr("token_count + ?", lastAssistantMsg.TokenCount),
})
global.GVA_LOG.Info("[重新生成] 已恢复 assistant 消息")
}
}
return err
}
@@ -1437,9 +1424,333 @@ func (s *ConversationService) buildAPIMessages(messages []app.Message, systemPro
return apiMessages
}
// estimateTokens 粗略估算文本的 token 数(字符数 / 3适用于中英混合文本
func estimateTokens(text string) int {
if text == "" {
return 0
}
// 中文字符约 1 char = 1 token英文约 4 chars = 1 token
// 取中间值 1 char ≈ 0.75 token即 chars * 4 / 3 的倒数 ≈ chars / 1.5
// 保守估算用 chars / 2 防止超出
n := len([]rune(text))
return (n + 1) / 2
}
// contextConfig 保存从 AIConfig.Settings 中解析出的上下文配置
type contextConfig struct {
contextLength int // 模型上下文窗口大小token 数)
maxTokens int // 最大输出 token 数
}
// getContextConfig 从 AIConfig 中读取上下文配置,如果没有配置则使用默认值
func getContextConfig(aiConfig *app.AIConfig, preset *app.AIPreset) contextConfig {
cfg := contextConfig{
contextLength: 200000, // 保守默认值
maxTokens: 2000,
}
// 从 preset 读取 max_tokens
if preset != nil && preset.MaxTokens > 0 {
cfg.maxTokens = preset.MaxTokens
}
// 从 AIConfig.Settings 读取 context_length
if len(aiConfig.Settings) > 0 {
var settings map[string]interface{}
if err := json.Unmarshal(aiConfig.Settings, &settings); err == nil {
if cl, ok := settings["context_length"].(float64); ok && cl > 0 {
cfg.contextLength = int(cl)
}
}
}
return cfg
}
// buildContextManagedSystemPrompt 按优先级构建 system prompt超出 budget 时截断低优先级内容
// 优先级(从高到低):
// 1. 核心人设Name/Description/Personality/Scenario/SystemPrompt
// 2. Preset.SystemPrompt
// 3. Worldbook 触发条目
// 4. CharacterBook 内嵌条目
// 5. MesExample对话示例最容易被截断
//
// 返回构建好的 systemPrompt 以及消耗的 token 数
func (s *ConversationService) buildContextManagedSystemPrompt(
character app.AICharacter,
presetSystemPrompt string,
worldbookEngine *WorldbookEngine,
conversation app.Conversation,
messageContents []string,
budget int,
) (string, int) {
used := 0
// ── 优先级1核心人设 ─────────────────────────────────────────────
core := fmt.Sprintf("你是 %s。", character.Name)
if character.Description != "" {
core += fmt.Sprintf("\n\n描述%s", character.Description)
}
if character.Personality != "" {
core += fmt.Sprintf("\n\n性格%s", character.Personality)
}
if character.Scenario != "" {
core += fmt.Sprintf("\n\n场景%s", character.Scenario)
}
if character.SystemPrompt != "" {
core += fmt.Sprintf("\n\n系统提示%s", character.SystemPrompt)
}
core += "\n\n请根据以上设定进行角色扮演保持角色的性格和说话方式。"
core = s.applyMacroVariables(core, character)
coreTokens := estimateTokens(core)
if coreTokens >= budget {
// 极端情况:核心人设本身就超出 budget截断到 budget
runes := []rune(core)
limit := budget * 2
if limit > len(runes) {
limit = len(runes)
}
core = string(runes[:limit])
global.GVA_LOG.Warn(fmt.Sprintf("[context] 核心人设超出 budget已截断至 %d chars", limit))
return core, budget
}
used += coreTokens
prompt := core
// ── 优先级2Preset.SystemPrompt ────────────────────────────────
if presetSystemPrompt != "" {
tokens := estimateTokens(presetSystemPrompt)
if used+tokens <= budget {
prompt += "\n\n" + presetSystemPrompt
used += tokens
} else {
// 尝试部分插入
remaining := budget - used
if remaining > 50 {
runes := []rune(presetSystemPrompt)
limit := remaining * 2
if limit > len(runes) {
limit = len(runes)
}
prompt += "\n\n" + string(runes[:limit])
used = budget
}
global.GVA_LOG.Warn(fmt.Sprintf("[context] Preset.SystemPrompt 因 budget 不足被截断(需要 %d tokens剩余 %d", tokens, budget-used))
}
}
if used >= budget {
return prompt, used
}
// ── 优先级3世界书触发条目 ──────────────────────────────────────
if conversation.WorldbookEnabled && conversation.WorldbookID != nil && worldbookEngine != nil {
triggeredEntries, wbErr := worldbookEngine.ScanAndTrigger(*conversation.WorldbookID, messageContents)
if wbErr != nil {
global.GVA_LOG.Warn(fmt.Sprintf("[context] 世界书触发失败: %v", wbErr))
} else if len(triggeredEntries) > 0 {
wbHeader := "\n\n[World Information]"
wbSection := wbHeader
for _, te := range triggeredEntries {
if te.Entry == nil || te.Entry.Content == "" {
continue
}
line := fmt.Sprintf("\n- %s", te.Entry.Content)
lineTokens := estimateTokens(line)
if used+estimateTokens(wbSection)+lineTokens <= budget {
wbSection += line
used += lineTokens
} else {
global.GVA_LOG.Warn(fmt.Sprintf("[context] 世界书条目 (id=%d) 因 budget 不足被跳过", te.Entry.ID))
break
}
}
if wbSection != wbHeader {
prompt += wbSection
}
}
}
if used >= budget {
return prompt, used
}
// ── 优先级4CharacterBook 内嵌条目 ──────────────────────────────
if len(character.CharacterBook) > 0 {
var characterBook map[string]interface{}
if err := json.Unmarshal(character.CharacterBook, &characterBook); err == nil {
if entries, ok := characterBook["entries"].([]interface{}); ok && len(entries) > 0 {
cbSection := "\n\n世界设定"
addedAny := false
for _, entry := range entries {
entryMap, ok := entry.(map[string]interface{})
if !ok {
continue
}
enabled := true
if enabledVal, ok := entryMap["enabled"].(bool); ok {
enabled = enabledVal
}
if !enabled {
continue
}
content, ok := entryMap["content"].(string)
if !ok || content == "" {
continue
}
line := fmt.Sprintf("\n- %s", content)
lineTokens := estimateTokens(line)
if used+estimateTokens(cbSection)+lineTokens <= budget {
cbSection += line
used += lineTokens
addedAny = true
} else {
global.GVA_LOG.Warn("[context] CharacterBook 条目因 budget 不足被跳过")
break
}
}
if addedAny {
prompt += cbSection
}
}
}
}
if used >= budget {
return prompt, used
}
// ── 优先级5MesExample对话示例最低优先级──────────────────
if character.MesExample != "" {
mesTokens := estimateTokens(character.MesExample)
prefix := "\n\n对话示例\n"
prefixTokens := estimateTokens(prefix)
if used+prefixTokens+mesTokens <= budget {
prompt += prefix + character.MesExample
used += prefixTokens + mesTokens
} else {
// 尝试截断 MesExample
remaining := budget - used - prefixTokens
if remaining > 100 {
runes := []rune(character.MesExample)
limit := remaining * 2
if limit > len(runes) {
limit = len(runes)
}
prompt += prefix + string(runes[:limit])
used = budget
global.GVA_LOG.Warn(fmt.Sprintf("[context] MesExample 被截断(原始 %d tokens保留约 %d tokens", mesTokens, remaining))
} else {
global.GVA_LOG.Warn("[context] MesExample 因 budget 不足被完全跳过")
}
}
}
return prompt, used
}
// trimMessagesToBudget 从历史消息中按 token 预算选取最近的消息
// 优先保留最新的消息,从后往前丢弃旧消息直到 token 数在 budget 内
func trimMessagesToBudget(messages []app.Message, budget int) []app.Message {
if budget <= 0 {
return nil
}
// messages 已经是从旧到新的顺序
// 从最新消息开始往前累加,直到超出 budget
selected := make([]app.Message, 0, len(messages))
used := 0
for i := len(messages) - 1; i >= 0; i-- {
msg := messages[i]
if msg.Role == "system" {
continue
}
t := estimateTokens(msg.Content)
if used+t > budget {
global.GVA_LOG.Warn(fmt.Sprintf("[context] 历史消息已截断,保留最近 %d 条(共 %d 条),使用 %d tokens", len(selected), len(messages), used))
break
}
used += t
selected = append([]app.Message{msg}, selected...) // 保持时序
}
return selected
}
// buildAPIMessagesWithContextManagement 整合 context 管理,构建最终的 messages 列表
// 返回 apiMessages 及各部分 token 统计日志
func (s *ConversationService) buildAPIMessagesWithContextManagement(
allMessages []app.Message,
character app.AICharacter,
presetSystemPrompt string,
worldbookEngine *WorldbookEngine,
conversation app.Conversation,
aiConfig *app.AIConfig,
preset *app.AIPreset,
) []map[string]string {
cfg := getContextConfig(aiConfig, preset)
// 安全边际:为输出保留 max_tokens另加 200 token 缓冲
inputBudget := cfg.contextLength - cfg.maxTokens - 200
if inputBudget <= 0 {
inputBudget = cfg.contextLength / 2
}
// 为历史消息分配预算system prompt 最多占用 60% 的 input budget
systemBudget := inputBudget * 60 / 100
historyBudget := inputBudget - systemBudget
// 提取消息内容用于世界书扫描
var messageContents []string
for _, msg := range allMessages {
messageContents = append(messageContents, msg.Content)
}
// 构建 system prompt含 worldbook 注入,按优先级截断)
systemPrompt, systemTokens := s.buildContextManagedSystemPrompt(
character,
presetSystemPrompt,
worldbookEngine,
conversation,
messageContents,
systemBudget,
)
// 如果 system prompt 实际用量比预算少,把节省的预算让给历史消息
if systemTokens < systemBudget {
historyBudget += systemBudget - systemTokens
}
global.GVA_LOG.Info(fmt.Sprintf("[context] 配置context_length=%d, max_tokens=%d, input_budget=%d, system=%d tokens, history_budget=%d",
cfg.contextLength, cfg.maxTokens, inputBudget, systemTokens, historyBudget))
// 按 token 预算裁剪历史消息
trimmedMessages := trimMessagesToBudget(allMessages, historyBudget)
// 构建最终 messages
apiMessages := make([]map[string]string, 0, len(trimmedMessages)+1)
apiMessages = append(apiMessages, map[string]string{
"role": "system",
"content": systemPrompt,
})
for _, msg := range trimmedMessages {
if msg.Role == "system" {
continue
}
apiMessages = append(apiMessages, map[string]string{
"role": msg.Role,
"content": msg.Content,
})
}
return apiMessages
}
// callOpenAIAPI 调用 OpenAI API
func (s *ConversationService) callOpenAIAPI(config *app.AIConfig, model string, messages []map[string]string, preset *app.AIPreset) (string, error) {
client := &http.Client{Timeout: 120 * time.Second}
client := &http.Client{Timeout: 10 * time.Minute}
// 使用配置的模型或默认模型
if model == "" {
@@ -1559,7 +1870,7 @@ func (s *ConversationService) callOpenAIAPI(config *app.AIConfig, model string,
// callAnthropicAPI 调用 Anthropic API
func (s *ConversationService) callAnthropicAPI(config *app.AIConfig, model string, messages []map[string]string, systemPrompt string, preset *app.AIPreset) (string, error) {
client := &http.Client{Timeout: 120 * time.Second}
client := &http.Client{Timeout: 10 * time.Minute}
// 使用配置的模型或默认模型
if model == "" {

View File

@@ -69,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
}
@@ -422,7 +426,8 @@ func (s *RegexScriptService) ExtractMaintext(text string) (string, string) {
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)
// 使用参数化查询避免 SQL 注入

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'
})
}

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

@@ -0,0 +1,141 @@
import service from '@/utils/request'
// @Summary 用户登录 获取动态路由
// @Produce application/json
// @Param 可以什么都不填 调一下即可
// @Router /menu/getMenu [post]
export const asyncMenu = () => {
return service({
url: '/menu/getMenu',
method: 'post'
})
}
// @Summary 获取menu列表
// @Produce application/json
// @Param {
// page int
// pageSize int
// }
// @Router /menu/getMenuList [post]
export const getMenuList = (data) => {
return service({
url: '/menu/getMenuList',
method: 'post',
data
})
}
// @Summary 新增基础menu
// @Produce application/json
// @Param menu Object
// @Router /menu/getMenuList [post]
export const addBaseMenu = (data) => {
return service({
url: '/menu/addBaseMenu',
method: 'post',
data
})
}
// @Summary 获取基础路由列表
// @Produce application/json
// @Param 可以什么都不填 调一下即可
// @Router /menu/getBaseMenuTree [post]
export const getBaseMenuTree = () => {
return service({
url: '/menu/getBaseMenuTree',
method: 'post'
})
}
// @Summary 添加用户menu关联关系
// @Produce application/json
// @Param menus Object authorityId string
// @Router /menu/getMenuList [post]
export const addMenuAuthority = (data) => {
return service({
url: '/menu/addMenuAuthority',
method: 'post',
data
})
}
// @Summary 获取用户menu关联关系
// @Produce application/json
// @Param authorityId string
// @Router /menu/getMenuAuthority [post]
export const getMenuAuthority = (data) => {
return service({
url: '/menu/getMenuAuthority',
method: 'post',
data
})
}
// @Summary 删除menu
// @Produce application/json
// @Param ID float64
// @Router /menu/deleteBaseMenu [post]
export const deleteBaseMenu = (data) => {
return service({
url: '/menu/deleteBaseMenu',
method: 'post',
data
})
}
// @Summary 修改menu列表
// @Produce application/json
// @Param menu Object
// @Router /menu/updateBaseMenu [post]
export const updateBaseMenu = (data) => {
return service({
url: '/menu/updateBaseMenu',
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/getBaseMenuById [post]
export const getBaseMenuById = (data) => {
return service({
url: '/menu/getBaseMenuById',
method: 'post',
data
})
}
/**
* 获取拥有指定菜单的角色ID列表
* @param {number} menuId 菜单ID
* @returns {Promise<number[]>} 角色ID数组
*/
export const getMenuRoles = (menuId) => {
return service({
url: '/menu/getMenuRoles',
method: 'get',
params: { menuId }
})
}
/**
* 全量覆盖某菜单关联的角色列表
* @param {Object} data
* @param {number} data.menuId 菜单ID
* @param {number[]} data.authorityIds 角色ID列表
* @returns {Promise}
*/
export const setMenuRoles = (data) => {
return service({
url: '/menu/setMenuRoles',
method: 'post',
data
})
}

View File

@@ -0,0 +1,10 @@
import service from '@/utils/request'
export const getShopPluginList = (params) => {
return service({
baseURL: "plugin",
url: '/shopPlugin/getShopPluginList',
method: 'get',
params
})
}

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