🎉 初始化项目

This commit is contained in:
2026-03-03 06:05:51 +08:00
commit e1c70fe218
241 changed files with 148285 additions and 0 deletions

230
server/model/app/README.md Normal file
View File

@@ -0,0 +1,230 @@
# AI 中转代理系统 - App 模块
## 已完成的功能模块
按照 server 目录的标准结构,已将 AI 中转代理项目的代码编写到各个 `app` 目录下:
### 1. Model 层 (server/model/app/)
**核心模型:**
- `ai_preset.go` - AI 预设模板模型(支持 SillyTavern 格式)
- `ai_provider.go` - AI 服务提供商模型
- `ai_preset_binding.go` - 用户-预设-提供商绑定模型
- `ai_request_log.go` - AI 请求日志模型
- `ai_preset_types.go` - 预设相关的自定义类型
**Request 结构体 (server/model/app/request/)**
- `ai_preset.go` - 预设相关请求(创建、更新、导入)
- `ai_provider.go` - 提供商相关请求(创建、更新)
- `ai_proxy.go` - 代理请求(聊天补全、消息、角色卡片)
**Response 结构体 (server/model/app/response/)**
- `ai_proxy.go` - 代理响应OpenAI 兼容格式)
### 2. Service 层 (server/service/app/)
- `ai_preset.go` - 预设服务CRUD、导入导出
- `ai_provider.go` - 提供商服务CRUD、连接测试
- `ai_proxy.go` - 代理服务(核心预设注入引擎、转发到上游 AI
- `enter.go` - Service 统一入口
### 3. API 层 (server/api/v1/app/)
- `ai_preset.go` - 预设管理 API
- `ai_provider.go` - 提供商管理 API
- `ai_proxy.go` - 代理转发 APIOpenAI 兼容接口)
- `enter.go` - API 统一入口
### 4. Router 层 (server/router/app/)
- `ai_preset.go` - 预设路由
- `ai_provider.go` - 提供商路由
- `ai_proxy.go` - 代理路由
- `enter.go` - Router 统一入口
## 核心功能特性
### 1. AI 预设管理
- ✅ 创建、更新、删除预设
- ✅ 获取预设列表和详情
- ✅ 导入 SillyTavern 格式预设
- ✅ 导出预设为 JSON
- ✅ 支持公开/私有预设
- ✅ 完整的 Prompt 注入配置
- ✅ 正则脚本处理
### 2. AI 服务提供商管理
- ✅ 创建、更新、删除提供商
- ✅ 获取提供商列表和详情
- ✅ 支持自定义配置
- ✅ 启用/禁用状态管理
### 3. AI 代理服务
- ✅ OpenAI 兼容的聊天补全接口
- ✅ 预设注入引擎
- ✅ 变量替换({{user}}, {{char}}
- ✅ 正则脚本处理(输入/输出)
- ✅ 转发到上游 AI 服务
- ✅ 请求日志记录
- ⏳ 流式响应(待实现)
## API 端点
### 预设管理
```
POST /app/preset # 创建预设
PUT /app/preset # 更新预设
DELETE /app/preset/:id # 删除预设
GET /app/preset/:id # 获取预设详情
GET /app/preset/list # 获取预设列表
POST /app/preset/import # 导入预设
GET /app/preset/:id/export # 导出预设
```
### 提供商管理
```
POST /app/provider # 创建提供商
PUT /app/provider # 更新提供商
DELETE /app/provider/:id # 删除提供商
GET /app/provider/:id # 获取提供商详情
GET /app/provider/list # 获取提供商列表
```
### 代理接口OpenAI 兼容)
```
POST /v1/chat/completions # 聊天补全
```
## 数据模型说明
### AiPreset预设模板
```go
- UserID: 用户ID
- Name: 预设名称
- Description: 预设描述
- Prompts: 提示词数组支持 SillyTavern 格式
- RegexScripts: 正则脚本数组
- Temperature, TopP, MaxTokens: 模型参数
- StreamEnabled: 是否启用流式
- IsDefault: 是否默认
- IsPublic: 是否公开
```
### Prompt提示词
```go
- Name: 提示词名称
- Role: 角色system/user/assistant
- Content: 提示词内容
- Identifier: 标识符
- InjectionPosition: 注入位置
- InjectionDepth: 注入深度
- InjectionOrder: 注入顺序
- Marker: 是否为占位符
```
### RegexScript正则脚本
```go
- ScriptName: 脚本名称
- FindRegex: 查找正则
- ReplaceString: 替换字符串
- Placement: 应用位置1=输入, 2=输出
- MinDepth, MaxDepth: 深度限制
```
## 预设注入流程
1. 接收用户请求
2. 加载预设配置
3. 按 injection_order 排序 prompts
4. 根据 injection_depth 插入到对话历史中
5. 替换变量({{user}}, {{char}}
6. 应用正则脚本placement=1 处理输入)
7. 转发到上游 AI
8. 应用正则脚本placement=2 处理输出)
9. 记录日志
10. 返回响应
## 待完善功能
1. **完整的预设注入引擎**
- 完整实现 injection_depth 逻辑
- 完整实现正则脚本处理
- 支持更多变量替换
2. **流式响应**
- 实现 SSE 流式输出
- 支持流式日志记录
3. **预设绑定管理**
- 实现 binding_key 机制
- 支持多预设切换
4. **导入导出**
- 完善 SillyTavern JSON 解析
- 支持批量导入导出
## 使用示例
### 1. 创建 AI 提供商
```bash
POST /app/provider
{
"name": "OpenAI",
"baseUrl": "https://api.openai.com/v1",
"apiKey": "sk-xxx",
"model": "gpt-4",
"isActive": true
}
```
### 2. 导入 SillyTavern 预设
```bash
POST /app/preset/import
{
"name": "TG角色扮演",
"data": { ... } // TGbreak.json 内容
}
```
### 3. 发送聊天请求
```bash
POST /v1/chat/completions
{
"messages": [
{"role": "user", "content": "你好"}
],
"presetId": 1,
"characterCard": {
"name": "小美",
"description": "活泼的女孩"
}
}
```
## 项目结构
```
server/
├── model/app/ # 数据模型
│ ├── ai_preset.go
│ ├── ai_provider.go
│ ├── ai_preset_binding.go
│ ├── ai_request_log.go
│ ├── request/ # 请求结构体
│ └── response/ # 响应结构体
├── service/app/ # 业务逻辑
│ ├── ai_preset.go
│ ├── ai_provider.go
│ ├── ai_proxy.go
│ └── enter.go
├── api/v1/app/ # API 处理器
│ ├── ai_preset.go
│ ├── ai_provider.go
│ ├── ai_proxy.go
│ └── enter.go
└── router/app/ # 路由注册
├── ai_preset.go
├── ai_provider.go
├── ai_proxy.go
└── enter.go
```

View File

@@ -0,0 +1,59 @@
package app
import (
"git.echol.cn/loser/ai_proxy/server/global"
)
// AiPreset AI预设模板
type AiPreset struct {
global.GVA_MODEL
UserID uint `json:"userId" gorm:"comment:用户ID;index"`
Name string `json:"name" gorm:"comment:预设名称;size:200;not null"`
Description string `json:"description" gorm:"comment:预设描述;type:text"`
Prompts Prompts `json:"prompts" gorm:"comment:提示词数组;type:jsonb;not null"`
RegexScripts RegexScripts `json:"regexScripts" gorm:"comment:正则脚本;type:jsonb"`
Temperature float64 `json:"temperature" gorm:"comment:温度;default:1.0"`
TopP float64 `json:"topP" gorm:"comment:TopP;default:0.9"`
MaxTokens int `json:"maxTokens" gorm:"comment:最大Token数;default:4096"`
FrequencyPenalty float64 `json:"frequencyPenalty" gorm:"comment:频率惩罚;default:0"`
PresencePenalty float64 `json:"presencePenalty" gorm:"comment:存在惩罚;default:0"`
StreamEnabled bool `json:"streamEnabled" gorm:"comment:是否启用流式;default:true"`
IsDefault bool `json:"isDefault" gorm:"comment:是否默认;default:false"`
IsPublic bool `json:"isPublic" gorm:"comment:是否公开;default:false"`
}
func (AiPreset) TableName() string {
return "ai_presets"
}
// Prompt 提示词结构
type Prompt struct {
Name string `json:"name"`
SystemPrompt bool `json:"system_prompt"`
Role string `json:"role"`
Content string `json:"content"`
Identifier string `json:"identifier"`
InjectionPosition int `json:"injection_position"`
InjectionDepth int `json:"injection_depth"`
InjectionOrder int `json:"injection_order"`
InjectionTrigger []string `json:"injection_trigger"`
Marker bool `json:"marker"`
ForbidOverrides bool `json:"forbid_overrides"`
}
// RegexScript 正则脚本
type RegexScript struct {
ID string `json:"id"`
ScriptName string `json:"scriptName"`
FindRegex string `json:"findRegex"`
ReplaceString string `json:"replaceString"`
TrimStrings []string `json:"trimStrings"`
Placement []int `json:"placement"`
Disabled bool `json:"disabled"`
MarkdownOnly bool `json:"markdownOnly"`
PromptOnly bool `json:"promptOnly"`
RunOnEdit bool `json:"runOnEdit"`
SubstituteRegex int `json:"substituteRegex"`
MinDepth *int `json:"minDepth"`
MaxDepth *int `json:"maxDepth"`
}

View File

@@ -0,0 +1,22 @@
package app
import (
"git.echol.cn/loser/ai_proxy/server/global"
)
// AiPresetBinding 预设-提供商绑定关系
type AiPresetBinding struct {
global.GVA_MODEL
PresetID uint `json:"presetId" gorm:"comment:预设ID;index"`
ProviderID uint `json:"providerId" gorm:"comment:提供商ID;index"`
Priority int `json:"priority" gorm:"comment:优先级;default:0"` // 多个预设时的执行顺序
IsActive bool `json:"isActive" gorm:"comment:是否启用;default:true"`
// 关联
Preset AiPreset `json:"preset" gorm:"foreignKey:PresetID"`
Provider AiProvider `json:"provider" gorm:"foreignKey:ProviderID"`
}
func (AiPresetBinding) TableName() string {
return "ai_preset_bindings"
}

View File

@@ -0,0 +1,36 @@
package app
import (
"database/sql/driver"
"encoding/json"
)
// Prompts 提示词数组类型
type Prompts []Prompt
func (p Prompts) Value() (driver.Value, error) {
return json.Marshal(p)
}
func (p *Prompts) Scan(value interface{}) error {
bytes, ok := value.([]byte)
if !ok {
return nil
}
return json.Unmarshal(bytes, p)
}
// RegexScripts 正则脚本数组类型
type RegexScripts []RegexScript
func (r RegexScripts) Value() (driver.Value, error) {
return json.Marshal(r)
}
func (r *RegexScripts) Scan(value interface{}) error {
bytes, ok := value.([]byte)
if !ok {
return nil
}
return json.Unmarshal(bytes, r)
}

View File

@@ -0,0 +1,23 @@
package app
import (
"git.echol.cn/loser/ai_proxy/server/global"
)
// AiProvider AI服务提供商配置
type AiProvider struct {
global.GVA_MODEL
Name string `json:"name" gorm:"comment:提供商名称;size:100;not null;uniqueIndex"`
Type string `json:"type" gorm:"comment:提供商类型;size:50;not null"` // openai, claude, gemini 等
BaseURL string `json:"baseUrl" gorm:"comment:API基础地址;size:500;not null"`
Endpoint string `json:"endpoint" gorm:"comment:API端点;size:200"` // 如 /v1/chat/completions
UpstreamKey string `json:"upstreamKey" gorm:"comment:上游API密钥;type:text"` // 上游 AI 的 key
Model string `json:"model" gorm:"comment:默认模型;size:100"`
ProxyKey string `json:"proxyKey" gorm:"comment:代理访问密钥;size:100;uniqueIndex"` // 用户访问本代理时使用的 key
Config map[string]interface{} `json:"config" gorm:"comment:额外配置;type:jsonb;serializer:json"`
IsActive bool `json:"isActive" gorm:"comment:是否启用;default:true"`
}
func (AiProvider) TableName() string {
return "ai_providers"
}

View File

@@ -0,0 +1,26 @@
package app
import (
"git.echol.cn/loser/ai_proxy/server/global"
)
// AiRequestLog AI请求日志
type AiRequestLog struct {
global.GVA_MODEL
UserID *uint `json:"userId" gorm:"comment:用户ID;index"`
PresetID *uint `json:"presetId" gorm:"comment:预设ID"`
ProviderID *uint `json:"providerId" gorm:"comment:提供商ID"`
OriginalMessage string `json:"originalMessage" gorm:"comment:原始消息;type:text"`
InjectedPrompt map[string]interface{} `json:"injectedPrompt" gorm:"comment:注入后的提示词;type:jsonb;serializer:json"`
ResponseText string `json:"responseText" gorm:"comment:响应文本;type:text"`
ResponseTokens int `json:"responseTokens" gorm:"comment:响应Token数"`
PromptTokens int `json:"promptTokens" gorm:"comment:提示Token数"`
TotalTokens int `json:"totalTokens" gorm:"comment:总Token数"`
LatencyMs int `json:"latencyMs" gorm:"comment:延迟(毫秒)"`
Status string `json:"status" gorm:"comment:状态;size:20"` // success/error/timeout
ErrorMessage string `json:"errorMessage" gorm:"comment:错误信息;type:text"`
}
func (AiRequestLog) TableName() string {
return "ai_request_logs"
}

View File

@@ -0,0 +1,42 @@
package request
import "git.echol.cn/loser/ai_proxy/server/model/app"
// CreateAiPresetRequest 创建预设请求
type CreateAiPresetRequest struct {
Name string `json:"name" binding:"required"`
Description string `json:"description"`
Prompts []app.Prompt `json:"prompts" binding:"required"`
RegexScripts []app.RegexScript `json:"regexScripts"`
Temperature float64 `json:"temperature"`
TopP float64 `json:"topP"`
MaxTokens int `json:"maxTokens"`
FrequencyPenalty float64 `json:"frequencyPenalty"`
PresencePenalty float64 `json:"presencePenalty"`
StreamEnabled bool `json:"streamEnabled"`
IsDefault bool `json:"isDefault"`
IsPublic bool `json:"isPublic"`
}
// UpdateAiPresetRequest 更新预设请求
type UpdateAiPresetRequest struct {
ID uint `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Prompts []app.Prompt `json:"prompts"`
RegexScripts []app.RegexScript `json:"regexScripts"`
Temperature float64 `json:"temperature"`
TopP float64 `json:"topP"`
MaxTokens int `json:"maxTokens"`
FrequencyPenalty float64 `json:"frequencyPenalty"`
PresencePenalty float64 `json:"presencePenalty"`
StreamEnabled bool `json:"streamEnabled"`
IsDefault bool `json:"isDefault"`
IsPublic bool `json:"isPublic"`
}
// ImportAiPresetRequest 导入预设请求支持SillyTavern格式
type ImportAiPresetRequest struct {
Name string `json:"name" binding:"required"`
Data interface{} `json:"data" binding:"required"`
}

View File

@@ -0,0 +1,23 @@
package request
// CreateBindingRequest 创建绑定请求
type CreateBindingRequest struct {
PresetID uint `json:"presetId" binding:"required"`
ProviderID uint `json:"providerId" binding:"required"`
Priority int `json:"priority"`
}
// UpdateBindingRequest 更新绑定请求
type UpdateBindingRequest struct {
ID uint `json:"id" binding:"required"`
Priority int `json:"priority"`
IsActive bool `json:"isActive"`
}
// GetBindingListRequest 获取绑定列表请求
type GetBindingListRequest struct {
Page int `json:"page" form:"page"`
PageSize int `json:"pageSize" form:"pageSize"`
ProviderID uint `json:"providerId" form:"providerId"`
PresetID uint `json:"presetId" form:"presetId"`
}

View File

@@ -0,0 +1,28 @@
package request
// CreateAiProviderRequest 创建AI提供商请求
type CreateAiProviderRequest struct {
Name string `json:"name" binding:"required"`
Type string `json:"type" binding:"required"`
BaseURL string `json:"baseUrl" binding:"required,url"`
Endpoint string `json:"endpoint"`
UpstreamKey string `json:"upstreamKey" binding:"required"`
Model string `json:"model"`
ProxyKey string `json:"proxyKey" binding:"required"`
Config map[string]interface{} `json:"config"`
IsActive bool `json:"isActive"`
}
// UpdateAiProviderRequest 更新AI提供商请求
type UpdateAiProviderRequest struct {
ID uint `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
BaseURL string `json:"baseUrl"`
Endpoint string `json:"endpoint"`
UpstreamKey string `json:"upstreamKey"`
Model string `json:"model"`
ProxyKey string `json:"proxyKey"`
Config map[string]interface{} `json:"config"`
IsActive bool `json:"isActive"`
}

View File

@@ -0,0 +1,15 @@
package request
// TestConnectionRequest 测试连接请求
type TestConnectionRequest struct {
BaseURL string `json:"baseUrl" binding:"required"`
UpstreamKey string `json:"upstreamKey" binding:"required"`
Type string `json:"type" binding:"required"` // openai, claude, gemini 等
}
// GetModelsRequest 获取模型列表请求
type GetModelsRequest struct {
BaseURL string `json:"baseUrl" binding:"required"`
UpstreamKey string `json:"upstreamKey" binding:"required"`
Type string `json:"type" binding:"required"`
}

View File

@@ -0,0 +1,31 @@
package request
// Message 消息结构
type Message struct {
Role string `json:"role"`
Content string `json:"content"`
}
// CharacterCard 角色卡片
type CharacterCard struct {
Name string `json:"name"`
Description string `json:"description"`
Personality string `json:"personality"`
Scenario string `json:"scenario"`
}
// ChatCompletionRequest 聊天补全请求OpenAI兼容
type ChatCompletionRequest struct {
Model string `json:"model"`
Messages []Message `json:"messages" binding:"required"`
PresetID uint `json:"presetId"`
BindingKey string `json:"bindingKey"`
CharacterCard *CharacterCard `json:"characterCard"`
Variables map[string]string `json:"variables"`
Temperature *float64 `json:"temperature"`
TopP *float64 `json:"topP"`
MaxTokens *int `json:"maxTokens"`
FrequencyPenalty *float64 `json:"frequencyPenalty"`
PresencePenalty *float64 `json:"presencePenalty"`
Stream bool `json:"stream"`
}

View File

@@ -0,0 +1,16 @@
package response
import "time"
// BindingInfo 绑定信息
type BindingInfo struct {
ID uint `json:"id"`
PresetID uint `json:"presetId"`
PresetName string `json:"presetName"`
ProviderID uint `json:"providerId"`
ProviderName string `json:"providerName"`
Priority int `json:"priority"`
IsActive bool `json:"isActive"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}

View File

@@ -0,0 +1,15 @@
package response
// ModelInfo 模型信息
type ModelInfo struct {
ID string `json:"id"`
Name string `json:"name"`
OwnedBy string `json:"ownedBy"`
}
// TestConnectionResponse 测试连接响应
type TestConnectionResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
Latency int64 `json:"latency"` // 延迟(毫秒)
}

View File

@@ -0,0 +1,38 @@
package response
// ChatCompletionResponse OpenAI兼容的聊天补全响应
type ChatCompletionResponse struct {
ID string `json:"id"`
Object string `json:"object"`
Created int64 `json:"created"`
Model string `json:"model"`
Choices []Choice `json:"choices"`
Usage Usage `json:"usage"`
}
// Choice 选择项
type Choice struct {
Index int `json:"index"`
Message Message `json:"message"`
FinishReason string `json:"finish_reason"`
}
// Message 消息
type Message struct {
Role string `json:"role"`
Content string `json:"content"`
}
// Usage Token使用情况
type Usage struct {
PromptTokens int `json:"prompt_tokens"`
CompletionTokens int `json:"completion_tokens"`
TotalTokens int `json:"total_tokens"`
}
// StreamChunk 流式响应块
type StreamChunk struct {
Content string `json:"content"`
Done bool `json:"done"`
Error error `json:"error,omitempty"`
}