feat: comprehensive review optimizations and Chinese model support
- Add fallback pricing for 10 Chinese AI providers (DeepSeek, Qwen, GLM, Moonshot, Doubao, MiniMax, ERNIE, Spark, Hunyuan, Yi) with 38 test cases - Add 32 Chinese model entries to pricing JSON (qwen-long, qwq-32b, glm-4-air, spark-max/pro/lite, hunyuan-pro/standard/lite, etc.) - Add 6 Chinese API domains to SSRF allowlist - Fix XSS vulnerability in HomeView.vue with DOMPurify sanitization - Change DB SSL default from 'disable' to 'prefer' with security comment - Replace hardcoded admin password in config.example.yaml - Enable @typescript-eslint/no-explicit-any as warning - Add vite-plugin-compression for gzip production builds - Add Prettier and EditorConfig configurations - Fix missing context import in sticky_session_test.go
This commit is contained in:
288
CHINESE_MODELS_AUDIT.md
Normal file
288
CHINESE_MODELS_AUDIT.md
Normal file
@@ -0,0 +1,288 @@
|
||||
# Sub2API 国内模型支持审查报告
|
||||
|
||||
**审查日期**: 2026/04/12
|
||||
**项目路径**: D:/project/sub2api
|
||||
|
||||
---
|
||||
|
||||
## 一、国内模型支持现状
|
||||
|
||||
### 1.1 已实现的国内模型支持
|
||||
|
||||
#### 前端模型白名单 (`frontend/src/composables/useModelWhitelist.ts`)
|
||||
|
||||
| 厂商 | 模型系列 | 状态 |
|
||||
|------|----------|------|
|
||||
| **智谱 GLM** | glm-4, glm-4v, glm-4-plus, glm-4-air, glm-4-long, glm-4-flash, glm-4.5, glm-4.6, glm-3-turbo, chatglm_*, cogview-3, cogvideo | ✅ 已添加 |
|
||||
| **阿里通义** | qwen-turbo, qwen-plus, qwen-max, qwen-long, qwen2-*, qwen2.5-*, qwen3-*, qwq-32b | ✅ 已添加 |
|
||||
| **DeepSeek** | deepseek-chat, deepseek-coder, deepseek-reasoner, deepseek-v3, deepseek-r1, deepseek-r1-distill-* | ✅ 已添加 |
|
||||
| **月之暗面** | moonshot-v1-8k/32k/128k, kimi-latest | ✅ 已添加 |
|
||||
| **字节豆包** | doubao-pro-*, doubao-lite-*, doubao-vision-*, doubao-1.5-* | ✅ 已添加 |
|
||||
| **MiniMax** | abab6.5-chat, abab6.5s-chat, abab6-chat, abab5.5-chat | ✅ 已添加 |
|
||||
| **百度文心** | ernie-4.0-*, ernie-3.5-*, ernie-speed-*, ernie-lite-*, ernie-tiny | ✅ 已添加 |
|
||||
| **讯飞星火** | spark-desk, spark-lite, spark-pro, spark-max, spark-ultra | ✅ 已添加 |
|
||||
| **腾讯混元** | hunyuan-lite, hunyuan-standard, hunyuan-pro, hunyuan-turbo, hunyuan-large, hunyuan-vision, hunyuan-code | ✅ 已添加 |
|
||||
| **零一万物** | yi-large, yi-medium, yi-spark, yi-vision, yi-1.5-* | ✅ 已添加 |
|
||||
|
||||
#### 前端模型图标 (`frontend/src/components/common/ModelIcon.vue`)
|
||||
|
||||
| 厂商 | 图标支持 |
|
||||
|------|----------|
|
||||
| 阿里通义 (qwen) | ✅ |
|
||||
| DeepSeek | ✅ |
|
||||
| 月之暗面 (moonshot/kimi) | ✅ |
|
||||
| MiniMax | ✅ |
|
||||
| 智谱 GLM | ❌ 缺失 |
|
||||
| 字节豆包 | ❌ 缺失 |
|
||||
| 百度文心 | ❌ 缺失 |
|
||||
| 讯飞星火 | ❌ 缺失 |
|
||||
| 腾讯混元 | ❌ 缺失 |
|
||||
| 零一万物 Yi | ❌ 缺失 |
|
||||
|
||||
#### 后端URL白名单 (`backend/internal/config/config.go`)
|
||||
|
||||
```go
|
||||
viper.SetDefault("security.url_allowlist.upstream_hosts", []string{
|
||||
"api.openai.com",
|
||||
"api.anthropic.com",
|
||||
"api.kimi.com", // ✅ 月之暗面
|
||||
"open.bigmodel.cn", // ✅ 智谱
|
||||
"api.minimaxi.com", // ✅ MiniMax
|
||||
// ❌ 缺失: 阿里云、字节、百度、讯飞、腾讯、零一万物
|
||||
})
|
||||
```
|
||||
|
||||
#### 后端定价数据 (`backend/resources/model-pricing/model_prices_and_context_window.json`)
|
||||
|
||||
| 厂商 | 定价数据 |
|
||||
|------|----------|
|
||||
| DeepSeek | ✅ 已包含 (7处) |
|
||||
| 其他国内模型 | ❌ **全部缺失** |
|
||||
|
||||
#### 后端计费服务 (`backend/internal/service/billing_service.go`)
|
||||
|
||||
```go
|
||||
// initFallbackPricing() - 回退定价
|
||||
// ❌ 完全缺失国内模型的回退定价
|
||||
|
||||
// getFallbackPricing() - 模型匹配
|
||||
// ❌ 仅支持 Claude/Gemini/GPT,无国内模型匹配逻辑
|
||||
```
|
||||
|
||||
#### 后端Kimi兼容 (`backend/internal/service/gateway_service.go`)
|
||||
|
||||
```go
|
||||
// ✅ 支持 Kimi 风格的 cached_tokens 响应处理
|
||||
// 兼容 Kimi cached_tokens → cache_read_input_tokens
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 二、缺失功能清单
|
||||
|
||||
### 2.1 高优先级(影响核心功能)
|
||||
|
||||
| 问题 | 位置 | 影响 |
|
||||
|------|------|------|
|
||||
| **计费服务无国内模型定价** | `billing_service.go` | 国内模型请求**无法正确计费** |
|
||||
| **定价数据缺失** | `model_prices_and_context_window.json` | 动态定价无法获取国内模型价格 |
|
||||
| **回退定价缺失** | `billing_service.go:initFallbackPricing()` | 动态定价不可用时计费失败 |
|
||||
|
||||
### 2.2 中优先级(影响用户体验)
|
||||
|
||||
| 问题 | 位置 | 影响 |
|
||||
|------|------|------|
|
||||
| URL白名单不完整 | `config.go` | 部分国内API无法访问 |
|
||||
| 模型图标缺失 | `ModelIcon.vue` | 部分模型无品牌图标 |
|
||||
|
||||
### 2.3 低优先级
|
||||
|
||||
| 问题 | 位置 | 影响 |
|
||||
|------|------|------|
|
||||
| 无国内模型特殊处理 | `gateway_service.go` | 部分模型可能有特殊响应格式 |
|
||||
|
||||
---
|
||||
|
||||
## 三、具体改进建议
|
||||
|
||||
### 3.1 计费服务改进 (`billing_service.go`)
|
||||
|
||||
**需要添加的回退定价**:
|
||||
|
||||
```go
|
||||
// DeepSeek
|
||||
s.fallbackPrices["deepseek-chat"] = &ModelPricing{
|
||||
InputPricePerToken: 2.8e-7, // $0.28 per MTok
|
||||
OutputPricePerToken: 4.2e-7, // $0.42 per MTok
|
||||
CacheReadPricePerToken: 2.8e-8, // $0.028 per MTok
|
||||
}
|
||||
s.fallbackPrices["deepseek-reasoner"] = &ModelPricing{
|
||||
InputPricePerToken: 2.8e-7,
|
||||
OutputPricePerToken: 4.2e-7,
|
||||
CacheReadPricePerToken: 2.8e-8,
|
||||
}
|
||||
|
||||
// 通义千问
|
||||
s.fallbackPrices["qwen-turbo"] = &ModelPricing{...}
|
||||
s.fallbackPrices["qwen-plus"] = &ModelPricing{...}
|
||||
s.fallbackPrices["qwen-max"] = &ModelPricing{...}
|
||||
|
||||
// 智谱 GLM
|
||||
s.fallbackPrices["glm-4"] = &ModelPricing{...}
|
||||
// ... 其他模型
|
||||
```
|
||||
|
||||
**需要添加的匹配逻辑** (`getFallbackPricing`):
|
||||
|
||||
```go
|
||||
// DeepSeek
|
||||
if strings.Contains(modelLower, "deepseek") {
|
||||
if strings.Contains(modelLower, "reasoner") || strings.Contains(modelLower, "r1") {
|
||||
return s.fallbackPrices["deepseek-reasoner"]
|
||||
}
|
||||
return s.fallbackPrices["deepseek-chat"]
|
||||
}
|
||||
|
||||
// 通义千问
|
||||
if strings.Contains(modelLower, "qwen") || strings.Contains(modelLower, "qwq") {
|
||||
if strings.Contains(modelLower, "max") {
|
||||
return s.fallbackPrices["qwen-max"]
|
||||
}
|
||||
if strings.Contains(modelLower, "plus") {
|
||||
return s.fallbackPrices["qwen-plus"]
|
||||
}
|
||||
return s.fallbackPrices["qwen-turbo"]
|
||||
}
|
||||
|
||||
// 智谱 GLM
|
||||
if strings.Contains(modelLower, "glm-") || strings.Contains(modelLower, "chatglm") {
|
||||
return s.fallbackPrices["glm-4"]
|
||||
}
|
||||
|
||||
// ... 其他厂商
|
||||
```
|
||||
|
||||
### 3.2 定价数据补充 (`model_prices_and_context_window.json`)
|
||||
|
||||
需要添加以下模型的定价数据:
|
||||
|
||||
- qwen-turbo, qwen-plus, qwen-max, qwen-long, qwen2.5-*, qwen3-*
|
||||
- glm-4, glm-4-plus, glm-4-air, glm-4-flash, glm-4.5, glm-4.6
|
||||
- moonshot-v1-*, kimi-latest
|
||||
- doubao-pro-*, doubao-lite-*, doubao-1.5-*
|
||||
- abab6.5-chat, abab6.5s-chat
|
||||
- ernie-4.0-*, ernie-3.5-*
|
||||
- spark-desk-*, spark-*
|
||||
- hunyuan-*
|
||||
- yi-large, yi-medium, yi-*
|
||||
|
||||
### 3.3 URL白名单补充 (`config.go`)
|
||||
|
||||
```go
|
||||
viper.SetDefault("security.url_allowlist.upstream_hosts", []string{
|
||||
// 现有
|
||||
"api.openai.com",
|
||||
"api.anthropic.com",
|
||||
"api.kimi.com",
|
||||
"open.bigmodel.cn",
|
||||
"api.minimaxi.com",
|
||||
// 需要添加
|
||||
"dashscope.aliyuncs.com", // 阿里云通义
|
||||
"ark.cn-beijing.volces.com", // 字节豆包
|
||||
"aip.baidubce.com", // 百度文心
|
||||
"spark-api.xf-yun.com", // 讯飞星火
|
||||
"hunyuan.tencentcloudapi.com", // 腾讯混元
|
||||
"api.lingyiwanwu.com", // 零一万物
|
||||
})
|
||||
```
|
||||
|
||||
### 3.4 前端图标补充 (`ModelIcon.vue`)
|
||||
|
||||
```typescript
|
||||
const providerIcons: Record<string, ProviderIcon> = {
|
||||
// 现有
|
||||
qwen: { ... },
|
||||
deepseek: { ... },
|
||||
moonshot: { ... },
|
||||
minimax: { ... },
|
||||
// 需要添加
|
||||
zhipu: { /* 智谱图标 */ },
|
||||
baidu: { /* 百度图标 */ },
|
||||
spark: { /* 讯飞图标 */ },
|
||||
hunyuan: { /* 腾讯图标 */ },
|
||||
yi: { /* 零一万物图标 */ },
|
||||
doubao: { /* 字节图标 */ },
|
||||
}
|
||||
|
||||
// 模型匹配逻辑补充
|
||||
if (modelLower.includes('glm') || modelLower.includes('chatglm')) return 'zhipu'
|
||||
if (modelLower.includes('ernie')) return 'baidu'
|
||||
if (modelLower.includes('spark')) return 'spark'
|
||||
if (modelLower.includes('hunyuan')) return 'hunyuan'
|
||||
if (modelLower.includes('yi-') || modelLower.includes('yi-large')) return 'yi'
|
||||
if (modelLower.includes('doubao')) return 'doubao'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、测试建议
|
||||
|
||||
### 4.1 计费测试
|
||||
|
||||
```go
|
||||
// billing_service_test.go
|
||||
func TestGetFallbackPricing_ChineseModels(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
model string
|
||||
expectNilPricing bool
|
||||
}{
|
||||
{"deepseek chat", "deepseek-chat", false},
|
||||
{"deepseek reasoner", "deepseek-reasoner", false},
|
||||
{"deepseek r1", "deepseek-r1", false},
|
||||
{"qwen max", "qwen-max", false},
|
||||
{"qwen turbo", "qwen-turbo", false},
|
||||
{"glm-4", "glm-4", false},
|
||||
{"moonshot v1", "moonshot-v1-8k", false},
|
||||
{"doubao pro", "doubao-pro-256k", false},
|
||||
{"abab chat", "abab6.5-chat", false},
|
||||
{"ernie 4.0", "ernie-4.0-8k", false},
|
||||
{"spark max", "spark-max", false},
|
||||
{"hunyuan pro", "hunyuan-pro", false},
|
||||
{"yi large", "yi-large", false},
|
||||
}
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、总结
|
||||
|
||||
### 国内模型支持完成度
|
||||
|
||||
| 模块 | 完成度 | 说明 |
|
||||
|------|--------|------|
|
||||
| 前端模型白名单 | 100% | 所有主流国内模型已添加 |
|
||||
| 前端模型图标 | 40% | 仅4家厂商有图标 |
|
||||
| 后端URL白名单 | 30% | 仅3家厂商在白名单 |
|
||||
| 后端定价数据 | 10% | 仅DeepSeek有定价 |
|
||||
| 后端计费服务 | 0% | **完全缺失** |
|
||||
|
||||
### 核心问题
|
||||
|
||||
**国内模型支持目前处于"前端可用、后端不可用"的状态**:
|
||||
|
||||
1. ✅ 前端可以显示和选择国内模型
|
||||
2. ❌ 后端无法正确计费(会导致计费失败或免费使用)
|
||||
3. ❌ 部分国内API因URL白名单限制无法访问
|
||||
|
||||
### 建议优先级
|
||||
|
||||
1. **立即修复**:添加计费服务的国内模型定价支持
|
||||
2. **尽快完成**:补充定价数据文件
|
||||
3. **逐步完善**:URL白名单、模型图标
|
||||
|
||||
---
|
||||
|
||||
**审查完成时间**: 2026/04/12
|
||||
382
REVIEW_REPORT_2026-04-12.md
Normal file
382
REVIEW_REPORT_2026-04-12.md
Normal file
@@ -0,0 +1,382 @@
|
||||
# Sub2API 项目综合审查报告
|
||||
|
||||
**审查日期**: 2026/04/12
|
||||
**项目路径**: D:/project/sub2api
|
||||
**远程仓库**: https://www.tksea.top/pham/tokens-reef
|
||||
**审查工具**: Claude Code 专业审查工具
|
||||
|
||||
---
|
||||
|
||||
## 一、项目概述
|
||||
|
||||
### 1.1 项目简介
|
||||
|
||||
**Sub2API** 是一个 AI API 网关平台,用于分发和管理 AI 产品订阅的 API 配额。用户通过平台生成的 API Key 访问上游 AI 服务,平台负责认证、计费、负载均衡和请求转发。
|
||||
|
||||
### 1.2 技术栈
|
||||
|
||||
| 组件 | 技术 | 版本 |
|
||||
|------|------|------|
|
||||
| 后端 | Go | 1.26.1 |
|
||||
| Web框架 | Gin | 1.9.1 |
|
||||
| ORM | Ent | 0.14.5 |
|
||||
| 前端 | Vue 3 | 3.4+ |
|
||||
| 构建工具 | Vite | 5.0+ |
|
||||
| 状态管理 | Pinia | 2.1.7 |
|
||||
| CSS框架 | TailwindCSS | 3.4.0 |
|
||||
| 数据库 | PostgreSQL | 15+ |
|
||||
| 缓存/队列 | Redis | 7+ |
|
||||
| 容器化 | Docker | Ready |
|
||||
|
||||
### 1.3 项目结构
|
||||
|
||||
```
|
||||
sub2api/
|
||||
├── backend/ # Go 后端服务
|
||||
│ ├── cmd/server/ # 应用入口
|
||||
│ ├── ent/ # Ent ORM 实体定义
|
||||
│ ├── internal/ # 内部模块
|
||||
│ │ ├── config/ # 配置管理
|
||||
│ │ ├── handler/ # HTTP 处理器
|
||||
│ │ ├── service/ # 业务逻辑
|
||||
│ │ ├── repository/ # 数据访问
|
||||
│ │ ├── middleware/ # 中间件
|
||||
│ │ ├── pkg/ # 内部工具包
|
||||
│ │ └── util/ # 工具函数
|
||||
│ └── migrations/ # 数据库迁移
|
||||
│
|
||||
├── frontend/ # Vue 3 前端
|
||||
│ └── src/
|
||||
│ ├── api/ # API 调用封装
|
||||
│ ├── components/ # 可复用组件
|
||||
│ ├── composables/ # 组合式函数
|
||||
│ ├── stores/ # Pinia 状态管理
|
||||
│ ├── router/ # 路由配置
|
||||
│ ├── types/ # TypeScript 类型
|
||||
│ ├── utils/ # 工具函数
|
||||
│ └── views/ # 页面组件
|
||||
│
|
||||
├── deploy/ # 部署配置
|
||||
│ ├── docker-compose.yml # Docker Compose
|
||||
│ ├── .env.example # 环境变量示例
|
||||
│ └── config.example.yaml # 配置文件示例
|
||||
│
|
||||
└── .github/workflows/ # CI/CD 配置
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 二、后端代码审查
|
||||
|
||||
### 2.1 架构设计
|
||||
|
||||
| 方面 | 评分 | 说明 |
|
||||
|------|------|------|
|
||||
| 分层设计 | 优秀 | Handler/Service/Repository 三层分离清晰 |
|
||||
| 依赖注入 | 优秀 | 使用 Google Wire 编译时依赖注入 |
|
||||
| ORM使用 | 良好 | Ent ORM 类型安全,Schema定义规范 |
|
||||
| 模块化 | 优秀 | internal/pkg 组织合理,职责明确 |
|
||||
|
||||
### 2.2 安全性
|
||||
|
||||
| 方面 | 评分 | 说明 |
|
||||
|------|------|------|
|
||||
| SQL注入防护 | 优秀 | Ent ORM + 参数化查询,无注入风险 |
|
||||
| 认证实现 | 优秀 | JWT + TokenVersion + bcrypt + TOTP 2FA |
|
||||
| 授权机制 | 优秀 | IP白名单/黑名单,API Key状态验证 |
|
||||
| 敏感数据 | 良好 | AES-256-GCM加密,日志脱敏完善 |
|
||||
| CORS配置 | 良好 | 自动处理通配符+凭证冲突 |
|
||||
| 安全头 | 优秀 | CSP nonce, X-Frame-Options, Referrer-Policy |
|
||||
|
||||
### 2.3 API设计
|
||||
|
||||
| 方面 | 评分 | 说明 |
|
||||
|------|------|------|
|
||||
| RESTful规范 | 良好 | 资源导向URL,正确使用HTTP方法 |
|
||||
| 错误处理 | 优秀 | 结构化错误响应,统一错误码 |
|
||||
| 请求验证 | 良好 | Gin binding验证,类型安全 |
|
||||
|
||||
### 2.4 并发安全
|
||||
|
||||
| 方面 | 评分 | 说明 |
|
||||
|------|------|------|
|
||||
| Goroutine使用 | 良好 | 后台任务、信号处理使用合理 |
|
||||
| Channel使用 | 良好 | 优雅关闭协调正确 |
|
||||
| 锁机制 | 优秀 | sync.Once, atomic, RWMutex 使用得当 |
|
||||
|
||||
### 2.5 性能考虑
|
||||
|
||||
| 方面 | 评分 | 说明 |
|
||||
|------|------|------|
|
||||
| 数据库查询 | 良好 | 批量查询、索引优化、连接池配置 |
|
||||
| 缓存策略 | 优秀 | Redis + Ristretto + go-cache 多层缓存 |
|
||||
| HTTP客户端 | 良好 | 连接池隔离策略,可配置参数 |
|
||||
|
||||
### 2.6 后端问题汇总
|
||||
|
||||
**中等严重度**:
|
||||
1. `context.Background()` 使用较多(50+处),可能导致资源泄漏
|
||||
2. 部分查询使用 `LIMIT 10000`,大数据集可能内存溢出
|
||||
|
||||
**低严重度**:
|
||||
1. 部分错误信息可能泄露内部实现细节
|
||||
2. 部分魔法数字应移至配置
|
||||
|
||||
---
|
||||
|
||||
## 三、前端代码审查
|
||||
|
||||
### 3.1 架构设计
|
||||
|
||||
| 方面 | 评分 | 说明 |
|
||||
|------|------|------|
|
||||
| 组件设计 | 良好 | Composition API,功能模块划分清晰 |
|
||||
| 状态管理 | 优秀 | Pinia setup store,职责划分合理 |
|
||||
| 路由设计 | 优秀 | 懒加载、导航守卫、chunk失败处理 |
|
||||
| API封装 | 优秀 | Axios拦截器、Token刷新、取消请求 |
|
||||
|
||||
### 3.2 安全性
|
||||
|
||||
| 方面 | 评分 | 说明 |
|
||||
|------|------|------|
|
||||
| XSS防护 | 良好 | DOMPurify净化,但部分v-html需加强 |
|
||||
| CSRF防护 | 良好 | JWT Bearer Token天然防范 |
|
||||
| 敏感数据 | 良好 | Token存储在localStorage,日志忽略敏感信息 |
|
||||
| 认证状态 | 优秀 | 完整Token生命周期,自动刷新机制 |
|
||||
|
||||
**XSS风险点**:
|
||||
| 文件 | 行号 | 风险 | 建议 |
|
||||
|------|------|------|------|
|
||||
| HomeView.vue | 12 | 中 | v-html需添加DOMPurify净化 |
|
||||
| KeyUsageView.vue | 260 | 中 | 确认数据来源,必要时净化 |
|
||||
|
||||
### 3.3 代码质量
|
||||
|
||||
| 方面 | 评分 | 说明 |
|
||||
|------|------|------|
|
||||
| TypeScript | 良好 | 严格模式,完整类型定义 |
|
||||
| 组件复用 | 优秀 | 通用组件库 + Composables |
|
||||
| 错误处理 | 优秀 | 统一错误处理,Toast通知 |
|
||||
| 代码规范 | 良好 | ESLint配置,建议添加Prettier |
|
||||
|
||||
### 3.4 性能
|
||||
|
||||
| 方面 | 评分 | 说明 |
|
||||
|------|------|------|
|
||||
| 懒加载 | 优秀 | 所有路由组件动态导入 |
|
||||
| 虚拟滚动 | 优秀 | @tanstack/vue-virtual 实现 |
|
||||
| 构建优化 | 良好 | manualChunks分离,建议添加压缩 |
|
||||
|
||||
### 3.5 前端问题汇总
|
||||
|
||||
**高优先级**:
|
||||
1. `HomeView.vue` 的 `v-html` 需添加 DOMPurify 净化
|
||||
2. TypeScript `no-explicit-any` 建议改为 `warn`
|
||||
|
||||
**中优先级**:
|
||||
1. 大型组件(如 `AppSidebar.vue` 685行)建议拆分
|
||||
2. 添加构建压缩插件
|
||||
|
||||
---
|
||||
|
||||
## 四、安全配置审查
|
||||
|
||||
### 4.1 配置文件安全
|
||||
|
||||
| 检查项 | 状态 | 说明 |
|
||||
|--------|------|------|
|
||||
| 敏感信息占位符 | 通过 | 无真实密钥硬编码 |
|
||||
| 默认密码警告 | 警告 | `admin_password: "admin123"` 弱密码示例 |
|
||||
| JWT密钥说明 | 通过 | 明确提示必须设置固定密钥 |
|
||||
| TOTP密钥说明 | 通过 | 提示不设置将导致2FA失效 |
|
||||
|
||||
### 4.2 认证安全
|
||||
|
||||
| 检查项 | 状态 | 说明 |
|
||||
|--------|------|------|
|
||||
| JWT算法限制 | 通过 | 仅允许HS256/HS384/HS512 |
|
||||
| Token版本控制 | 通过 | 密码修改后旧Token失效 |
|
||||
| bcrypt密码哈希 | 通过 | 使用golang.org/x/crypto/bcrypt |
|
||||
| TOTP加密存储 | 通过 | AES-256-GCM加密 |
|
||||
| 恒定时间比较 | 通过 | subtle.ConstantTimeCompare |
|
||||
|
||||
### 4.3 网络安全
|
||||
|
||||
| 检查项 | 状态 | 说明 |
|
||||
|--------|------|------|
|
||||
| CORS处理 | 通过 | 自动禁用通配符+凭证冲突 |
|
||||
| CSP策略 | 优秀 | nonce支持,完整安全头 |
|
||||
| HTTPS配置 | 警告 | 默认禁用SSL,生产需启用 |
|
||||
| 代理配置 | 通过 | 默认禁用TLS验证跳过 |
|
||||
|
||||
### 4.4 敏感信息保护
|
||||
|
||||
| 检查项 | 状态 | 说明 |
|
||||
|--------|------|------|
|
||||
| 日志脱敏 | 优秀 | 自动识别并脱敏多种敏感格式 |
|
||||
| 错误信息 | 良好 | 认证错误模糊化处理 |
|
||||
| API密钥存储 | 良好 | L1/L2双层缓存,支持负缓存 |
|
||||
|
||||
### 4.5 CI/CD安全
|
||||
|
||||
| 检查项 | 状态 | 说明 |
|
||||
|--------|------|------|
|
||||
| 权限最小化 | 通过 | contents: read 基础权限 |
|
||||
| 密钥注入 | 通过 | 使用GitHub Secrets |
|
||||
| 安全扫描 | 通过 | govulncheck + pnpm audit |
|
||||
| 镜像安全 | 通过 | 非root用户,alpine基础镜像 |
|
||||
|
||||
### 4.6 安全配置问题汇总
|
||||
|
||||
**高优先级**:
|
||||
| 问题 | 位置 | 建议 |
|
||||
|------|------|------|
|
||||
| 默认允许HTTP URL | .env.example:327 | 生产环境设为false |
|
||||
| 默认允许私有IP | .env.example:331 | 评估SSRF风险 |
|
||||
| 示例弱密码 | config.example.yaml:831 | 使用占位符替代 |
|
||||
| 默认禁用SSL | docker-compose.standalone.yml:50 | 生产使用require |
|
||||
|
||||
---
|
||||
|
||||
## 五、评分汇总
|
||||
|
||||
### 5.1 后端评分
|
||||
|
||||
| 类别 | 评分 |
|
||||
|------|------|
|
||||
| 架构设计 | 90/100 |
|
||||
| 安全性 | 92/100 |
|
||||
| API设计 | 88/100 |
|
||||
| 并发安全 | 90/100 |
|
||||
| 性能 | 85/100 |
|
||||
| **后端总分** | **89/100** |
|
||||
|
||||
### 5.2 前端评分
|
||||
|
||||
| 类别 | 评分 |
|
||||
|------|------|
|
||||
| 架构设计 | 88/100 |
|
||||
| 安全性 | 85/100 |
|
||||
| 代码质量 | 87/100 |
|
||||
| 性能 | 86/100 |
|
||||
| 可维护性 | 90/100 |
|
||||
| **前端总分** | **87/100** |
|
||||
|
||||
### 5.3 安全配置评分
|
||||
|
||||
| 类别 | 评分 |
|
||||
|------|------|
|
||||
| 配置文件安全 | 85/100 |
|
||||
| 认证安全 | 95/100 |
|
||||
| 网络安全 | 90/100 |
|
||||
| 敏感信息保护 | 95/100 |
|
||||
| CI/CD安全 | 95/100 |
|
||||
| **安全总分** | **92/100** |
|
||||
|
||||
### 5.4 综合评分
|
||||
|
||||
| 模块 | 权重 | 评分 | 加权分 |
|
||||
|------|------|------|--------|
|
||||
| 后端 | 35% | 89 | 31.15 |
|
||||
| 前端 | 30% | 87 | 26.10 |
|
||||
| 安全配置 | 35% | 92 | 32.20 |
|
||||
| **综合评分** | 100% | - | **89.45/100** |
|
||||
|
||||
---
|
||||
|
||||
## 六、国内模型支持审查
|
||||
|
||||
### 6.1 支持现状
|
||||
|
||||
| 模块 | 完成度 | 说明 |
|
||||
|------|--------|------|
|
||||
| 前端模型白名单 | **100%** | 已添加智谱、通义、DeepSeek、月之暗面、豆包、MiniMax、百度、讯飞、腾讯、零一万物等 |
|
||||
| 前端模型图标 | **40%** | 仅 qwen/deepseek/moonshot/minimax 有图标 |
|
||||
| 后端URL白名单 | **30%** | 仅 kimi/bigmodel/minimaxi 在白名单 |
|
||||
| 后端定价数据 | **10%** | 仅 DeepSeek 有定价数据 |
|
||||
| 后端计费服务 | **0%** | **完全缺失国内模型定价** |
|
||||
|
||||
### 6.2 核心问题
|
||||
|
||||
**国内模型支持目前处于"前端可用、后端不可用"的状态**:
|
||||
|
||||
1. ✅ 前端可以显示和选择国内模型
|
||||
2. ❌ **后端无法正确计费**(会导致计费失败或免费使用)
|
||||
3. ❌ 部分国内API因URL白名单限制无法访问
|
||||
|
||||
### 6.3 需要修复的问题
|
||||
|
||||
| 优先级 | 问题 | 位置 | 影响 |
|
||||
|--------|------|------|------|
|
||||
| **紧急** | 计费服务无国内模型定价 | `billing_service.go` | 国内模型请求无法正确计费 |
|
||||
| **紧急** | 定价数据缺失 | `model_prices_and_context_window.json` | 动态定价无法获取价格 |
|
||||
| **高** | URL白名单不完整 | `config.go` | 部分国内API无法访问 |
|
||||
| **中** | 模型图标缺失 | `ModelIcon.vue` | 部分模型无品牌图标 |
|
||||
|
||||
### 6.4 详细报告
|
||||
|
||||
完整的国内模型支持审查报告见: `CHINESE_MODELS_AUDIT.md`
|
||||
|
||||
---
|
||||
|
||||
## 七、改进建议优先级
|
||||
|
||||
### 高优先级(需立即处理)
|
||||
|
||||
1. **【紧急】国内模型计费支持**: 添加计费服务的国内模型定价
|
||||
2. **【紧急】国内模型定价数据**: 补充 `model_prices_and_context_window.json`
|
||||
3. **前端XSS防护**: `HomeView.vue` 添加 DOMPurify 净化
|
||||
4. **生产环境配置**: 禁用HTTP URL和私有IP访问
|
||||
5. **SSL配置**: 生产环境启用数据库TLS
|
||||
6. **示例密码**: 移除弱密码示例
|
||||
|
||||
### 中优先级(建议近期处理)
|
||||
|
||||
1. **国内模型URL白名单**: 添加阿里云、字节、百度、讯飞、腾讯、零一万物API域名
|
||||
2. **后端Context**: 减少 `context.Background()` 使用
|
||||
3. **查询优化**: 大数据集使用游标分页
|
||||
4. **前端组件拆分**: 大型组件模块化
|
||||
5. **TypeScript配置**: 启用 `no-explicit-any` 警告
|
||||
6. **构建优化**: 添加压缩插件
|
||||
|
||||
### 低优先级(可后续处理)
|
||||
|
||||
1. **国内模型图标**: 补充智谱、百度、讯飞、腾讯、零一万物、字节图标
|
||||
2. **代码规范**: 添加 Prettier 和 .editorconfig
|
||||
3. **文档完善**: 添加架构说明和变更日志
|
||||
4. **监控增强**: 添加请求追踪和可观测性
|
||||
|
||||
---
|
||||
|
||||
## 八、项目优势总结
|
||||
|
||||
1. **架构成熟**: 清晰的分层设计,依赖注入,模块化组织
|
||||
2. **安全完善**: 多层安全防护,日志脱敏,CSP策略
|
||||
3. **性能优化**: 多层缓存,连接池隔离,虚拟滚动
|
||||
4. **代码质量**: TypeScript严格模式,完整的类型定义
|
||||
5. **部署友好**: Docker化,CI/CD自动化,多种部署方式
|
||||
|
||||
---
|
||||
|
||||
## 九、结论
|
||||
|
||||
Sub2API 是一个**架构成熟、安全完善、代码质量较高**的 AI API 网关项目。
|
||||
|
||||
**主要发现**:
|
||||
- 后端采用现代Go实践(Wire DI、Ent ORM),安全措施全面
|
||||
- 前端使用Vue3 Composition API,状态管理规范
|
||||
- 安全配置整体良好,生产部署需注意配置调整
|
||||
- **国内模型支持不完整**:前端已添加白名单,但后端计费服务缺失定价支持
|
||||
|
||||
**建议重点改进**:
|
||||
1. **【紧急】添加国内模型计费支持** - 当前会导致计费失败
|
||||
2. 前端XSS防护加强
|
||||
3. 生产环境安全配置检查
|
||||
4. 大数据查询优化
|
||||
|
||||
**总体评价**: 项目代码质量优秀,适合生产环境部署。但**国内模型支持功能不完整**,需要补充后端计费逻辑后才能正常使用。
|
||||
|
||||
---
|
||||
|
||||
**审查完成时间**: 2026/04/12
|
||||
**审查人员**: Claude Code AI
|
||||
@@ -1008,6 +1008,7 @@ func load(allowMissingJWTSecret bool) (*Config, error) {
|
||||
// 配置文件不存在时使用默认值
|
||||
}
|
||||
|
||||
|
||||
var cfg Config
|
||||
if err := viper.Unmarshal(&cfg); err != nil {
|
||||
return nil, fmt.Errorf("unmarshal config error: %w", err)
|
||||
@@ -1157,6 +1158,12 @@ func setDefaults() {
|
||||
"api.kimi.com",
|
||||
"open.bigmodel.cn",
|
||||
"api.minimaxi.com",
|
||||
"dashscope.aliyuncs.com",
|
||||
"ark.cn-beijing.volces.com",
|
||||
"aip.baidubce.com",
|
||||
"spark-api.xf-yun.com",
|
||||
"hunyuan.tencentcloudapi.com",
|
||||
"api.lingyiwanwu.com",
|
||||
"generativelanguage.googleapis.com",
|
||||
"cloudcode-pa.googleapis.com",
|
||||
"*.openai.azure.com",
|
||||
|
||||
@@ -266,6 +266,141 @@ func (s *BillingService) initFallbackPricing() {
|
||||
SupportsCacheBreakdown: false,
|
||||
}
|
||||
s.fallbackPrices["gpt-5.3-codex"] = s.fallbackPrices["gpt-5.1-codex"]
|
||||
|
||||
// ── Chinese model providers ──────────────────────────────────────────
|
||||
|
||||
// DeepSeek (uniform pricing for all models)
|
||||
s.fallbackPrices["deepseek-chat"] = &ModelPricing{
|
||||
InputPricePerToken: 2.8e-7, // ¥1/MTok ≈ $0.14/MTok → $0.00000014/token (using $0.28/MTok avg)
|
||||
OutputPricePerToken: 4.2e-7,
|
||||
CacheReadPricePerToken: 2.8e-8,
|
||||
}
|
||||
|
||||
// Alibaba Qwen
|
||||
s.fallbackPrices["qwen-turbo"] = &ModelPricing{
|
||||
InputPricePerToken: 3e-8, // ¥0.3/MTok
|
||||
OutputPricePerToken: 6e-8,
|
||||
}
|
||||
s.fallbackPrices["qwen-plus"] = &ModelPricing{
|
||||
InputPricePerToken: 1e-7, // ¥0.8/MTok
|
||||
OutputPricePerToken: 2e-7,
|
||||
}
|
||||
s.fallbackPrices["qwen-max"] = &ModelPricing{
|
||||
InputPricePerToken: 4e-6, // ¥20/MTok
|
||||
OutputPricePerToken: 8e-6,
|
||||
}
|
||||
s.fallbackPrices["qwen-long"] = &ModelPricing{
|
||||
InputPricePerToken: 5e-7, // ¥0.5/MTok
|
||||
OutputPricePerToken: 1e-6,
|
||||
}
|
||||
s.fallbackPrices["qwq-32b"] = &ModelPricing{
|
||||
InputPricePerToken: 1.2e-6, // ¥8/MTok
|
||||
OutputPricePerToken: 1.2e-6,
|
||||
}
|
||||
|
||||
// Zhipu GLM
|
||||
s.fallbackPrices["glm-4"] = &ModelPricing{
|
||||
InputPricePerToken: 1.5e-6, // ¥100/MTok → ~$0.0015/token (conservative avg)
|
||||
OutputPricePerToken: 1.5e-6,
|
||||
}
|
||||
s.fallbackPrices["glm-4-plus"] = &ModelPricing{
|
||||
InputPricePerToken: 5e-6,
|
||||
OutputPricePerToken: 5e-6,
|
||||
}
|
||||
s.fallbackPrices["glm-4-air"] = &ModelPricing{
|
||||
InputPricePerToken: 1e-7,
|
||||
OutputPricePerToken: 1e-7,
|
||||
}
|
||||
s.fallbackPrices["glm-4-flash"] = &ModelPricing{
|
||||
InputPricePerToken: 1e-8, // ¥1/MTok
|
||||
OutputPricePerToken: 1e-8,
|
||||
}
|
||||
s.fallbackPrices["glm-4-long"] = &ModelPricing{
|
||||
InputPricePerToken: 1e-6,
|
||||
OutputPricePerToken: 1e-6,
|
||||
}
|
||||
|
||||
// Moonshot / Kimi
|
||||
s.fallbackPrices["moonshot-v1"] = &ModelPricing{
|
||||
InputPricePerToken: 1.2e-6, // ¥12/MTok
|
||||
OutputPricePerToken: 1.2e-6,
|
||||
}
|
||||
|
||||
// ByteDance Doubao
|
||||
s.fallbackPrices["doubao-pro"] = &ModelPricing{
|
||||
InputPricePerToken: 5e-7,
|
||||
OutputPricePerToken: 9e-7,
|
||||
}
|
||||
s.fallbackPrices["doubao-lite"] = &ModelPricing{
|
||||
InputPricePerToken: 3e-8, // ¥0.3/MTok
|
||||
OutputPricePerToken: 6e-8,
|
||||
}
|
||||
|
||||
// MiniMax
|
||||
s.fallbackPrices["abab6.5-chat"] = &ModelPricing{
|
||||
InputPricePerToken: 3e-7,
|
||||
OutputPricePerToken: 3e-7,
|
||||
}
|
||||
s.fallbackPrices["abab6.5s-chat"] = &ModelPricing{
|
||||
InputPricePerToken: 2e-8, // ¥0.2/MTok
|
||||
OutputPricePerToken: 2e-8,
|
||||
}
|
||||
|
||||
// Baidu Wenxin
|
||||
s.fallbackPrices["ernie-4.0"] = &ModelPricing{
|
||||
InputPricePerToken: 1.2e-5, // ¥120/MTok
|
||||
OutputPricePerToken: 1.2e-5,
|
||||
}
|
||||
s.fallbackPrices["ernie-3.5"] = &ModelPricing{
|
||||
InputPricePerToken: 1.2e-6,
|
||||
OutputPricePerToken: 1.2e-6,
|
||||
}
|
||||
s.fallbackPrices["ernie-speed"] = &ModelPricing{
|
||||
InputPricePerToken: 4e-9, // free → nominal value
|
||||
OutputPricePerToken: 8e-9,
|
||||
}
|
||||
|
||||
// iFlytek Spark
|
||||
s.fallbackPrices["spark-max"] = &ModelPricing{
|
||||
InputPricePerToken: 1.8e-5, // ¥120/MTok
|
||||
OutputPricePerToken: 1.8e-5,
|
||||
}
|
||||
s.fallbackPrices["spark-pro"] = &ModelPricing{
|
||||
InputPricePerToken: 3.6e-6,
|
||||
OutputPricePerToken: 3.6e-6,
|
||||
}
|
||||
s.fallbackPrices["spark-lite"] = &ModelPricing{
|
||||
InputPricePerToken: 1.8e-7,
|
||||
OutputPricePerToken: 1.8e-7,
|
||||
}
|
||||
|
||||
// Tencent Hunyuan
|
||||
s.fallbackPrices["hunyuan-pro"] = &ModelPricing{
|
||||
InputPricePerToken: 3.2e-6,
|
||||
OutputPricePerToken: 3.2e-6,
|
||||
}
|
||||
s.fallbackPrices["hunyuan-standard"] = &ModelPricing{
|
||||
InputPricePerToken: 8e-8,
|
||||
OutputPricePerToken: 8e-8,
|
||||
}
|
||||
s.fallbackPrices["hunyuan-lite"] = &ModelPricing{
|
||||
InputPricePerToken: 0.0, // free
|
||||
OutputPricePerToken: 0.0,
|
||||
}
|
||||
|
||||
// Yi (01.AI)
|
||||
s.fallbackPrices["yi-large"] = &ModelPricing{
|
||||
InputPricePerToken: 2e-6,
|
||||
OutputPricePerToken: 2e-6,
|
||||
}
|
||||
s.fallbackPrices["yi-medium"] = &ModelPricing{
|
||||
InputPricePerToken: 1e-7,
|
||||
OutputPricePerToken: 1e-7,
|
||||
}
|
||||
s.fallbackPrices["yi-spark"] = &ModelPricing{
|
||||
InputPricePerToken: 1e-8,
|
||||
OutputPricePerToken: 1e-8,
|
||||
}
|
||||
}
|
||||
|
||||
// getFallbackPricing 根据模型系列获取回退价格
|
||||
@@ -325,6 +460,125 @@ func (s *BillingService) getFallbackPricing(model string) *ModelPricing {
|
||||
}
|
||||
}
|
||||
|
||||
// Chinese model providers
|
||||
if p := s.getChineseModelFallbackPricing(modelLower); p != nil {
|
||||
return p
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getChineseModelFallbackPricing handles Chinese model provider family matching.
|
||||
// Returns nil if the model does not match any known Chinese provider family.
|
||||
func (s *BillingService) getChineseModelFallbackPricing(modelLower string) *ModelPricing {
|
||||
// DeepSeek – all variants share the same pricing
|
||||
if strings.Contains(modelLower, "deepseek") {
|
||||
return s.fallbackPrices["deepseek-chat"]
|
||||
}
|
||||
|
||||
// Alibaba Qwen / QwQ
|
||||
if strings.Contains(modelLower, "qwen") || strings.Contains(modelLower, "qwq") {
|
||||
if strings.Contains(modelLower, "qwq") {
|
||||
return s.fallbackPrices["qwq-32b"]
|
||||
}
|
||||
if strings.Contains(modelLower, "max") {
|
||||
return s.fallbackPrices["qwen-max"]
|
||||
}
|
||||
if strings.Contains(modelLower, "long") {
|
||||
return s.fallbackPrices["qwen-long"]
|
||||
}
|
||||
if strings.Contains(modelLower, "plus") {
|
||||
return s.fallbackPrices["qwen-plus"]
|
||||
}
|
||||
// qwen-turbo, qwen2.*, qwen2.5.*, qwen3.* all fall back to turbo
|
||||
return s.fallbackPrices["qwen-turbo"]
|
||||
}
|
||||
|
||||
// Zhipu GLM / chatglm
|
||||
if strings.Contains(modelLower, "glm") || strings.Contains(modelLower, "chatglm") {
|
||||
if strings.Contains(modelLower, "plus") || strings.Contains(modelLower, "4.5") || strings.Contains(modelLower, "4.6") {
|
||||
return s.fallbackPrices["glm-4-plus"]
|
||||
}
|
||||
if strings.Contains(modelLower, "flash") {
|
||||
return s.fallbackPrices["glm-4-flash"]
|
||||
}
|
||||
if strings.Contains(modelLower, "air") {
|
||||
return s.fallbackPrices["glm-4-air"]
|
||||
}
|
||||
if strings.Contains(modelLower, "long") {
|
||||
return s.fallbackPrices["glm-4-long"]
|
||||
}
|
||||
if strings.Contains(modelLower, "3-turbo") {
|
||||
return s.fallbackPrices["glm-4"] // glm-3-turbo price similar
|
||||
}
|
||||
return s.fallbackPrices["glm-4"]
|
||||
}
|
||||
|
||||
// Moonshot / Kimi
|
||||
if strings.Contains(modelLower, "moonshot") || strings.Contains(modelLower, "kimi") {
|
||||
return s.fallbackPrices["moonshot-v1"]
|
||||
}
|
||||
|
||||
// ByteDance Doubao
|
||||
if strings.Contains(modelLower, "doubao") {
|
||||
if strings.Contains(modelLower, "lite") {
|
||||
return s.fallbackPrices["doubao-lite"]
|
||||
}
|
||||
return s.fallbackPrices["doubao-pro"]
|
||||
}
|
||||
|
||||
// MiniMax abab*
|
||||
if strings.Contains(modelLower, "abab") || strings.Contains(modelLower, "minimax") {
|
||||
if strings.Contains(modelLower, "6.5s") {
|
||||
return s.fallbackPrices["abab6.5s-chat"]
|
||||
}
|
||||
return s.fallbackPrices["abab6.5-chat"]
|
||||
}
|
||||
|
||||
// Baidu Wenxin (ERNIE)
|
||||
if strings.Contains(modelLower, "ernie") {
|
||||
if strings.Contains(modelLower, "4.0") {
|
||||
return s.fallbackPrices["ernie-4.0"]
|
||||
}
|
||||
if strings.Contains(modelLower, "speed") || strings.Contains(modelLower, "lite") || strings.Contains(modelLower, "tiny") {
|
||||
return s.fallbackPrices["ernie-speed"]
|
||||
}
|
||||
return s.fallbackPrices["ernie-3.5"]
|
||||
}
|
||||
|
||||
// iFlytek Spark (must come after yi since yi-spark contains "spark")
|
||||
if strings.Contains(modelLower, "spark") && !strings.HasPrefix(modelLower, "yi") {
|
||||
if strings.Contains(modelLower, "max") || strings.Contains(modelLower, "ultra") {
|
||||
return s.fallbackPrices["spark-max"]
|
||||
}
|
||||
if strings.Contains(modelLower, "pro") || strings.Contains(modelLower, "desk") {
|
||||
return s.fallbackPrices["spark-pro"]
|
||||
}
|
||||
return s.fallbackPrices["spark-lite"]
|
||||
}
|
||||
|
||||
// Tencent Hunyuan
|
||||
if strings.Contains(modelLower, "hunyuan") {
|
||||
if strings.Contains(modelLower, "pro") || strings.Contains(modelLower, "large") {
|
||||
return s.fallbackPrices["hunyuan-pro"]
|
||||
}
|
||||
if strings.Contains(modelLower, "standard") || strings.Contains(modelLower, "turbo") {
|
||||
return s.fallbackPrices["hunyuan-standard"]
|
||||
}
|
||||
return s.fallbackPrices["hunyuan-lite"]
|
||||
}
|
||||
|
||||
// Yi (01.AI)
|
||||
if strings.HasPrefix(modelLower, "yi-") || strings.HasPrefix(modelLower, "yi ") {
|
||||
if strings.Contains(modelLower, "large") {
|
||||
return s.fallbackPrices["yi-large"]
|
||||
}
|
||||
if strings.Contains(modelLower, "medium") || strings.Contains(modelLower, "spark") || strings.Contains(modelLower, "vision") {
|
||||
return s.fallbackPrices["yi-medium"]
|
||||
}
|
||||
return s.fallbackPrices["yi-spark"]
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -240,7 +240,57 @@ func TestGetFallbackPricing_FamilyMatching(t *testing.T) {
|
||||
{name: "openai gpt5.1 codex max alias", model: "gpt-5.1-codex-max", expectedInput: 1.5e-6},
|
||||
{name: "openai codex mini latest alias", model: "codex-mini-latest", expectedInput: 1.5e-6},
|
||||
{name: "openai unknown no fallback", model: "gpt-unknown-model", expectNilPricing: true},
|
||||
{name: "non supported family", model: "qwen-max", expectNilPricing: true},
|
||||
{name: "non supported family", model: "some-unknown-model", expectNilPricing: true},
|
||||
|
||||
// ── Chinese model providers ──
|
||||
// DeepSeek
|
||||
{name: "deepseek chat", model: "deepseek-chat", expectedInput: 2.8e-7},
|
||||
{name: "deepseek coder", model: "deepseek-coder", expectedInput: 2.8e-7},
|
||||
{name: "deepseek reasoner", model: "deepseek-reasoner", expectedInput: 2.8e-7},
|
||||
{name: "deepseek v3", model: "deepseek-v3", expectedInput: 2.8e-7},
|
||||
{name: "deepseek r1", model: "deepseek-r1", expectedInput: 2.8e-7},
|
||||
{name: "deepseek r1 distill qwen", model: "deepseek-r1-distill-qwen-32b", expectedInput: 2.8e-7},
|
||||
// Alibaba Qwen
|
||||
{name: "qwen turbo", model: "qwen-turbo", expectedInput: 3e-8},
|
||||
{name: "qwen plus", model: "qwen-plus", expectedInput: 1e-7},
|
||||
{name: "qwen max", model: "qwen-max", expectedInput: 4e-6},
|
||||
{name: "qwen long", model: "qwen-long", expectedInput: 5e-7},
|
||||
{name: "qwen2.5 72b", model: "qwen2.5-72b-instruct", expectedInput: 3e-8},
|
||||
{name: "qwq 32b", model: "qwq-32b", expectedInput: 1.2e-6},
|
||||
// Zhipu GLM
|
||||
{name: "glm 4", model: "glm-4", expectedInput: 1.5e-6},
|
||||
{name: "glm 4 plus", model: "glm-4-plus", expectedInput: 5e-6},
|
||||
{name: "glm 4 air", model: "glm-4-air", expectedInput: 1e-7},
|
||||
{name: "glm 4 flash", model: "glm-4-flash", expectedInput: 1e-8},
|
||||
{name: "glm 4 long", model: "glm-4-long", expectedInput: 1e-6},
|
||||
{name: "glm 3 turbo", model: "glm-3-turbo", expectedInput: 1.5e-6},
|
||||
// Moonshot / Kimi
|
||||
{name: "moonshot v1 8k", model: "moonshot-v1-8k", expectedInput: 1.2e-6},
|
||||
{name: "moonshot v1 32k", model: "moonshot-v1-32k", expectedInput: 1.2e-6},
|
||||
{name: "kimi latest", model: "kimi-latest", expectedInput: 1.2e-6},
|
||||
// ByteDance Doubao
|
||||
{name: "doubao pro 256k", model: "doubao-pro-256k", expectedInput: 5e-7},
|
||||
{name: "doubao lite 4k", model: "doubao-lite-4k", expectedInput: 3e-8},
|
||||
{name: "doubao 1.5 pro", model: "doubao-1.5-pro-256k", expectedInput: 5e-7},
|
||||
// MiniMax
|
||||
{name: "abab6.5 chat", model: "abab6.5-chat", expectedInput: 3e-7},
|
||||
{name: "abab6.5s chat", model: "abab6.5s-chat", expectedInput: 2e-8},
|
||||
// Baidu Wenxin
|
||||
{name: "ernie 4.0", model: "ernie-4.0-8k", expectedInput: 1.2e-5},
|
||||
{name: "ernie 3.5", model: "ernie-3.5-8k", expectedInput: 1.2e-6},
|
||||
{name: "ernie speed", model: "ernie-speed-8k", expectedInput: 4e-9},
|
||||
// iFlytek Spark
|
||||
{name: "spark max", model: "spark-max", expectedInput: 1.8e-5},
|
||||
{name: "spark pro", model: "spark-pro", expectedInput: 3.6e-6},
|
||||
{name: "spark lite", model: "spark-lite", expectedInput: 1.8e-7},
|
||||
// Tencent Hunyuan
|
||||
{name: "hunyuan pro", model: "hunyuan-pro", expectedInput: 3.2e-6},
|
||||
{name: "hunyuan standard", model: "hunyuan-standard", expectedInput: 8e-8},
|
||||
{name: "hunyuan lite", model: "hunyuan-lite", expectedInput: 0.0},
|
||||
// Yi (01.AI)
|
||||
{name: "yi large", model: "yi-large", expectedInput: 2e-6},
|
||||
{name: "yi medium", model: "yi-medium", expectedInput: 1e-7},
|
||||
{name: "yi spark", model: "yi-spark", expectedInput: 1e-7},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
|
||||
@@ -792,6 +792,402 @@
|
||||
"supports_reasoning": true,
|
||||
"supports_tool_choice": true
|
||||
},
|
||||
"qwen-turbo": {
|
||||
"input_cost_per_token": 3e-08,
|
||||
"litellm_provider": "openai",
|
||||
"max_input_tokens": 131072,
|
||||
"max_output_tokens": 8192,
|
||||
"max_tokens": 8192,
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 6e-08,
|
||||
"source": "https://help.aliyun.com/document_detail/2712576.html",
|
||||
"supports_function_calling": true,
|
||||
"supports_native_streaming": true,
|
||||
"supports_prompt_caching": true,
|
||||
"supports_tool_choice": true
|
||||
},
|
||||
"qwen-plus": {
|
||||
"input_cost_per_token": 1e-07,
|
||||
"litellm_provider": "openai",
|
||||
"max_input_tokens": 131072,
|
||||
"max_output_tokens": 8192,
|
||||
"max_tokens": 8192,
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 2e-07,
|
||||
"source": "https://help.aliyun.com/document_detail/2712576.html",
|
||||
"supports_function_calling": true,
|
||||
"supports_native_streaming": true,
|
||||
"supports_prompt_caching": true,
|
||||
"supports_tool_choice": true
|
||||
},
|
||||
"qwen-max": {
|
||||
"input_cost_per_token": 4e-06,
|
||||
"litellm_provider": "openai",
|
||||
"max_input_tokens": 32768,
|
||||
"max_output_tokens": 8192,
|
||||
"max_tokens": 8192,
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 8e-06,
|
||||
"source": "https://help.aliyun.com/document_detail/2712576.html",
|
||||
"supports_function_calling": true,
|
||||
"supports_native_streaming": true,
|
||||
"supports_prompt_caching": true,
|
||||
"supports_tool_choice": true
|
||||
},
|
||||
"qwen-long": {
|
||||
"input_cost_per_token": 5e-07,
|
||||
"litellm_provider": "openai",
|
||||
"max_input_tokens": 10000000,
|
||||
"max_output_tokens": 6000,
|
||||
"max_tokens": 6000,
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 1e-06,
|
||||
"source": "https://help.aliyun.com/document_detail/2712576.html",
|
||||
"supports_function_calling": false,
|
||||
"supports_native_streaming": true,
|
||||
"supports_prompt_caching": true
|
||||
},
|
||||
"glm-4": {
|
||||
"input_cost_per_token": 1.5e-06,
|
||||
"litellm_provider": "openai",
|
||||
"max_input_tokens": 128000,
|
||||
"max_output_tokens": 4096,
|
||||
"max_tokens": 4096,
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 1.5e-06,
|
||||
"source": "https://open.bigmodel.cn/dev/howuse/model",
|
||||
"supports_function_calling": true,
|
||||
"supports_native_streaming": true,
|
||||
"supports_tool_choice": true
|
||||
},
|
||||
"glm-4-plus": {
|
||||
"input_cost_per_token": 5e-06,
|
||||
"litellm_provider": "openai",
|
||||
"max_input_tokens": 128000,
|
||||
"max_output_tokens": 4096,
|
||||
"max_tokens": 4096,
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 5e-06,
|
||||
"source": "https://open.bigmodel.cn/dev/howuse/model",
|
||||
"supports_function_calling": true,
|
||||
"supports_native_streaming": true,
|
||||
"supports_tool_choice": true
|
||||
},
|
||||
"glm-4-flash": {
|
||||
"input_cost_per_token": 1e-08,
|
||||
"litellm_provider": "openai",
|
||||
"max_input_tokens": 128000,
|
||||
"max_output_tokens": 4096,
|
||||
"max_tokens": 4096,
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 1e-08,
|
||||
"source": "https://open.bigmodel.cn/dev/howuse/model",
|
||||
"supports_function_calling": true,
|
||||
"supports_native_streaming": true,
|
||||
"supports_tool_choice": true
|
||||
},
|
||||
"moonshot-v1-8k": {
|
||||
"input_cost_per_token": 1.2e-06,
|
||||
"litellm_provider": "openai",
|
||||
"max_input_tokens": 8192,
|
||||
"max_output_tokens": 4096,
|
||||
"max_tokens": 4096,
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 1.2e-06,
|
||||
"source": "https://platform.moonshot.cn/docs/pricing",
|
||||
"supports_function_calling": true,
|
||||
"supports_native_streaming": true
|
||||
},
|
||||
"moonshot-v1-32k": {
|
||||
"input_cost_per_token": 1.2e-06,
|
||||
"litellm_provider": "openai",
|
||||
"max_input_tokens": 32768,
|
||||
"max_output_tokens": 4096,
|
||||
"max_tokens": 4096,
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 1.2e-06,
|
||||
"source": "https://platform.moonshot.cn/docs/pricing",
|
||||
"supports_function_calling": true,
|
||||
"supports_native_streaming": true
|
||||
},
|
||||
"moonshot-v1-128k": {
|
||||
"input_cost_per_token": 1.2e-06,
|
||||
"litellm_provider": "openai",
|
||||
"max_input_tokens": 131072,
|
||||
"max_output_tokens": 4096,
|
||||
"max_tokens": 4096,
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 1.2e-06,
|
||||
"source": "https://platform.moonshot.cn/docs/pricing",
|
||||
"supports_function_calling": true,
|
||||
"supports_native_streaming": true
|
||||
},
|
||||
"kimi-latest": {
|
||||
"input_cost_per_token": 1.2e-06,
|
||||
"litellm_provider": "openai",
|
||||
"max_input_tokens": 131072,
|
||||
"max_output_tokens": 8192,
|
||||
"max_tokens": 8192,
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 1.2e-06,
|
||||
"source": "https://platform.moonshot.cn/docs/pricing",
|
||||
"supports_function_calling": true,
|
||||
"supports_native_streaming": true,
|
||||
"supports_tool_choice": true
|
||||
},
|
||||
"doubao-pro-256k": {
|
||||
"input_cost_per_token": 5e-07,
|
||||
"litellm_provider": "openai",
|
||||
"max_input_tokens": 256000,
|
||||
"max_output_tokens": 4096,
|
||||
"max_tokens": 4096,
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 9e-07,
|
||||
"source": "https://www.volcengine.com/docs/82379/1099475",
|
||||
"supports_function_calling": true,
|
||||
"supports_native_streaming": true,
|
||||
"supports_tool_choice": true
|
||||
},
|
||||
"doubao-lite-4k": {
|
||||
"input_cost_per_token": 3e-08,
|
||||
"litellm_provider": "openai",
|
||||
"max_input_tokens": 4096,
|
||||
"max_output_tokens": 4096,
|
||||
"max_tokens": 4096,
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 6e-08,
|
||||
"source": "https://www.volcengine.com/docs/82379/1099475",
|
||||
"supports_function_calling": true,
|
||||
"supports_native_streaming": true
|
||||
},
|
||||
"abab6.5-chat": {
|
||||
"input_cost_per_token": 3e-07,
|
||||
"litellm_provider": "openai",
|
||||
"max_input_tokens": 131072,
|
||||
"max_output_tokens": 8192,
|
||||
"max_tokens": 8192,
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 3e-07,
|
||||
"source": "https://www.minimaxi.com/document/guides/chat-model/chat",
|
||||
"supports_function_calling": true,
|
||||
"supports_native_streaming": true
|
||||
},
|
||||
"ernie-4.0-8k": {
|
||||
"input_cost_per_token": 1.2e-05,
|
||||
"litellm_provider": "openai",
|
||||
"max_input_tokens": 8192,
|
||||
"max_output_tokens": 4096,
|
||||
"max_tokens": 4096,
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 1.2e-05,
|
||||
"source": "https://cloud.baidu.com/doc/WENXINWORKSHOP/s/Fm2vr9u53",
|
||||
"supports_function_calling": true,
|
||||
"supports_native_streaming": true,
|
||||
"supports_tool_choice": true
|
||||
},
|
||||
"ernie-3.5-8k": {
|
||||
"input_cost_per_token": 1.2e-06,
|
||||
"litellm_provider": "openai",
|
||||
"max_input_tokens": 8192,
|
||||
"max_output_tokens": 4096,
|
||||
"max_tokens": 4096,
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 1.2e-06,
|
||||
"source": "https://cloud.baidu.com/doc/WENXINWORKSHOP/s/Fm2vr9u53",
|
||||
"supports_function_calling": true,
|
||||
"supports_native_streaming": true,
|
||||
"supports_tool_choice": true
|
||||
},
|
||||
"yi-large": {
|
||||
"input_cost_per_token": 2e-06,
|
||||
"litellm_provider": "openai",
|
||||
"max_input_tokens": 32768,
|
||||
"max_output_tokens": 4096,
|
||||
"max_tokens": 4096,
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 2e-06,
|
||||
"source": "https://platform.lingyiwanwu.com/docs# pricing",
|
||||
"supports_function_calling": false,
|
||||
"supports_native_streaming": true
|
||||
},
|
||||
"qwen-long": {
|
||||
"input_cost_per_token": 5e-07,
|
||||
"litellm_provider": "openai",
|
||||
"max_input_tokens": 10000000,
|
||||
"max_output_tokens": 6000,
|
||||
"max_tokens": 6000,
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 1e-06,
|
||||
"source": "https://help.aliyun.com/document_detail/2712576.html",
|
||||
"supports_function_calling": false,
|
||||
"supports_native_streaming": true,
|
||||
"supports_prompt_caching": true
|
||||
},
|
||||
"qwq-32b": {
|
||||
"input_cost_per_token": 1.2e-06,
|
||||
"litellm_provider": "openai",
|
||||
"max_input_tokens": 131072,
|
||||
"max_output_tokens": 16384,
|
||||
"max_tokens": 16384,
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 1.2e-06,
|
||||
"source": "https://help.aliyun.com/document_detail/2712576.html",
|
||||
"supports_function_calling": true,
|
||||
"supports_native_streaming": true,
|
||||
"supports_prompt_caching": true,
|
||||
"supports_reasoning": true,
|
||||
"supports_tool_choice": true
|
||||
},
|
||||
"glm-4-air": {
|
||||
"input_cost_per_token": 1e-07,
|
||||
"litellm_provider": "openai",
|
||||
"max_input_tokens": 128000,
|
||||
"max_output_tokens": 4096,
|
||||
"max_tokens": 4096,
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 1e-07,
|
||||
"source": "https://open.bigmodel.cn/dev/howuse/model",
|
||||
"supports_function_calling": true,
|
||||
"supports_native_streaming": true,
|
||||
"supports_tool_choice": true
|
||||
},
|
||||
"glm-4-long": {
|
||||
"input_cost_per_token": 1e-06,
|
||||
"litellm_provider": "openai",
|
||||
"max_input_tokens": 1048576,
|
||||
"max_output_tokens": 4096,
|
||||
"max_tokens": 4096,
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 1e-06,
|
||||
"source": "https://open.bigmodel.cn/dev/howuse/model",
|
||||
"supports_function_calling": true,
|
||||
"supports_native_streaming": true
|
||||
},
|
||||
"kimi-latest": {
|
||||
"input_cost_per_token": 1.2e-06,
|
||||
"litellm_provider": "openai",
|
||||
"max_input_tokens": 131072,
|
||||
"max_output_tokens": 8192,
|
||||
"max_tokens": 8192,
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 1.2e-06,
|
||||
"source": "https://platform.moonshot.cn/docs/pricing",
|
||||
"supports_function_calling": true,
|
||||
"supports_native_streaming": true,
|
||||
"supports_tool_choice": true
|
||||
},
|
||||
"doubao-1.5-pro-256k": {
|
||||
"input_cost_per_token": 5e-07,
|
||||
"litellm_provider": "openai",
|
||||
"max_input_tokens": 256000,
|
||||
"max_output_tokens": 4096,
|
||||
"max_tokens": 4096,
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 9e-07,
|
||||
"source": "https://www.volcengine.com/docs/82379/1099475",
|
||||
"supports_function_calling": true,
|
||||
"supports_native_streaming": true,
|
||||
"supports_reasoning": true,
|
||||
"supports_tool_choice": true
|
||||
},
|
||||
"abab6.5s-chat": {
|
||||
"input_cost_per_token": 2e-08,
|
||||
"litellm_provider": "openai",
|
||||
"max_input_tokens": 131072,
|
||||
"max_output_tokens": 8192,
|
||||
"max_tokens": 8192,
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 2e-08,
|
||||
"source": "https://www.minimaxi.com/document/guides/chat-model/chat",
|
||||
"supports_function_calling": true,
|
||||
"supports_native_streaming": true
|
||||
},
|
||||
"ernie-speed-8k": {
|
||||
"input_cost_per_token": 4e-09,
|
||||
"litellm_provider": "openai",
|
||||
"max_input_tokens": 8192,
|
||||
"max_output_tokens": 4096,
|
||||
"max_tokens": 4096,
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 8e-09,
|
||||
"source": "https://cloud.baidu.com/doc/WENXINWORKSHOP/s/Fm2vr9u53",
|
||||
"supports_function_calling": true,
|
||||
"supports_native_streaming": true
|
||||
},
|
||||
"spark-max": {
|
||||
"input_cost_per_token": 1.8e-05,
|
||||
"litellm_provider": "openai",
|
||||
"max_input_tokens": 32768,
|
||||
"max_output_tokens": 4096,
|
||||
"max_tokens": 4096,
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 1.8e-05,
|
||||
"source": "https://www.xfyun.cn/doc/spark/Web.html",
|
||||
"supports_function_calling": true,
|
||||
"supports_native_streaming": true,
|
||||
"supports_tool_choice": true
|
||||
},
|
||||
"spark-pro": {
|
||||
"input_cost_per_token": 3.6e-06,
|
||||
"litellm_provider": "openai",
|
||||
"max_input_tokens": 32768,
|
||||
"max_output_tokens": 4096,
|
||||
"max_tokens": 4096,
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 3.6e-06,
|
||||
"source": "https://www.xfyun.cn/doc/spark/Web.html",
|
||||
"supports_function_calling": true,
|
||||
"supports_native_streaming": true
|
||||
},
|
||||
"spark-lite": {
|
||||
"input_cost_per_token": 1.8e-07,
|
||||
"litellm_provider": "openai",
|
||||
"max_input_tokens": 32768,
|
||||
"max_output_tokens": 4096,
|
||||
"max_tokens": 4096,
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 1.8e-07,
|
||||
"source": "https://www.xfyun.cn/doc/spark/Web.html",
|
||||
"supports_function_calling": true,
|
||||
"supports_native_streaming": true
|
||||
},
|
||||
"hunyuan-pro": {
|
||||
"input_cost_per_token": 3.2e-06,
|
||||
"litellm_provider": "openai",
|
||||
"max_input_tokens": 32768,
|
||||
"max_output_tokens": 4096,
|
||||
"max_tokens": 4096,
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 3.2e-06,
|
||||
"source": "https://cloud.tencent.com/document/product/1729/97731",
|
||||
"supports_function_calling": true,
|
||||
"supports_native_streaming": true,
|
||||
"supports_tool_choice": true
|
||||
},
|
||||
"hunyuan-standard": {
|
||||
"input_cost_per_token": 8e-08,
|
||||
"litellm_provider": "openai",
|
||||
"max_input_tokens": 32768,
|
||||
"max_output_tokens": 4096,
|
||||
"max_tokens": 4096,
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 8e-08,
|
||||
"source": "https://cloud.tencent.com/document/product/1729/97731",
|
||||
"supports_function_calling": true,
|
||||
"supports_native_streaming": true
|
||||
},
|
||||
"hunyuan-lite": {
|
||||
"input_cost_per_token": 0.0,
|
||||
"litellm_provider": "openai",
|
||||
"max_input_tokens": 32768,
|
||||
"max_output_tokens": 4096,
|
||||
"max_tokens": 4096,
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 0.0,
|
||||
"source": "https://cloud.tencent.com/document/product/1729/97731",
|
||||
"supports_function_calling": true,
|
||||
"supports_native_streaming": true
|
||||
},
|
||||
"gemini-1.0-pro": {
|
||||
"input_cost_per_character": 1.25e-07,
|
||||
"input_cost_per_image": 0.0025,
|
||||
|
||||
@@ -828,7 +828,7 @@ default:
|
||||
# Initial admin account (created on first run)
|
||||
# 初始管理员账户(首次运行时创建)
|
||||
admin_email: "admin@example.com"
|
||||
admin_password: "admin123"
|
||||
admin_password: "REPLACE_WITH_SECURE_PASSWORD"
|
||||
|
||||
# Default settings for new users
|
||||
# 新用户默认设置
|
||||
|
||||
@@ -47,7 +47,8 @@ services:
|
||||
- DATABASE_USER=${DATABASE_USER:-sub2api}
|
||||
- DATABASE_PASSWORD=${DATABASE_PASSWORD:?DATABASE_PASSWORD is required}
|
||||
- DATABASE_DBNAME=${DATABASE_DBNAME:-sub2api}
|
||||
- DATABASE_SSLMODE=${DATABASE_SSLMODE:-disable}
|
||||
# SECURITY: 'disable' sends DB credentials in cleartext. Use 'prefer' or higher in production.
|
||||
- DATABASE_SSLMODE=${DATABASE_SSLMODE:-prefer}
|
||||
- DATABASE_MAX_OPEN_CONNS=${DATABASE_MAX_OPEN_CONNS:-50}
|
||||
- DATABASE_MAX_IDLE_CONNS=${DATABASE_MAX_IDLE_CONNS:-10}
|
||||
- DATABASE_CONN_MAX_LIFETIME_MINUTES=${DATABASE_CONN_MAX_LIFETIME_MINUTES:-30}
|
||||
|
||||
12
frontend/.editorconfig
Normal file
12
frontend/.editorconfig
Normal file
@@ -0,0 +1,12 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
@@ -29,7 +29,7 @@ module.exports = {
|
||||
],
|
||||
"@typescript-eslint/ban-types": "off",
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-explicit-any": "warn",
|
||||
"vue/multi-word-component-names": "off",
|
||||
"vue/no-use-v-if-with-v-for": "off",
|
||||
},
|
||||
|
||||
12
frontend/.prettierrc
Normal file
12
frontend/.prettierrc
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100,
|
||||
"bracketSpacing": true,
|
||||
"arrowParens": "always",
|
||||
"endOfLine": "lf",
|
||||
"vueIndentScriptAndStyle": false,
|
||||
"htmlWhitespaceSensitivity": "ignore"
|
||||
}
|
||||
7429
frontend/package-lock.json
generated
Normal file
7429
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -53,6 +53,7 @@
|
||||
"typescript": "~5.6.0",
|
||||
"vite": "^5.0.10",
|
||||
"vite-plugin-checker": "^0.9.1",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vitest": "^2.1.9",
|
||||
"vue-tsc": "^2.2.0"
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
class="h-screen w-full border-0"
|
||||
allowfullscreen
|
||||
></iframe>
|
||||
<!-- HTML mode - SECURITY: homeContent is admin-only setting, XSS risk is acceptable -->
|
||||
<div v-else v-html="homeContent"></div>
|
||||
<!-- HTML mode - sanitized with DOMPurify -->
|
||||
<div v-else v-html="sanitizedHomeContent"></div>
|
||||
</div>
|
||||
|
||||
<!-- Default Home Page -->
|
||||
@@ -407,6 +407,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import DOMPurify from 'dompurify'
|
||||
import { useAuthStore, useAppStore } from '@/stores'
|
||||
import LocaleSwitcher from '@/components/common/LocaleSwitcher.vue'
|
||||
import Icon from '@/components/icons/Icon.vue'
|
||||
@@ -429,6 +430,15 @@ const isHomeContentUrl = computed(() => {
|
||||
return content.startsWith('http://') || content.startsWith('https://')
|
||||
})
|
||||
|
||||
// Sanitize HTML content with DOMPurify to prevent XSS
|
||||
const sanitizedHomeContent = computed(() => {
|
||||
if (!homeContent.value || isHomeContentUrl.value) return ''
|
||||
return DOMPurify.sanitize(homeContent.value, {
|
||||
ADD_TAGS: ['iframe'],
|
||||
ADD_ATTR: ['allow', 'allowfullscreen', 'frameborder', 'scrolling']
|
||||
})
|
||||
})
|
||||
|
||||
// Theme
|
||||
const isDark = ref(document.documentElement.classList.contains('dark'))
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { defineConfig, loadEnv, Plugin } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import checker from 'vite-plugin-checker'
|
||||
import viteCompression from 'vite-plugin-compression'
|
||||
import { resolve } from 'path'
|
||||
|
||||
/**
|
||||
@@ -47,7 +48,12 @@ export default defineConfig(({ mode }) => {
|
||||
typescript: true,
|
||||
vueTsc: true
|
||||
}),
|
||||
injectPublicSettings(backendUrl)
|
||||
injectPublicSettings(backendUrl),
|
||||
viteCompression({
|
||||
algorithm: 'gzip',
|
||||
threshold: 10240,
|
||||
deleteOriginFile: false
|
||||
})
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
|
||||
Reference in New Issue
Block a user