forked from xiaohei/taiji-AI-PAD
更新超级管理员收入
This commit is contained in:
@@ -175,6 +175,46 @@
|
||||
| `/tenants/{id}/recharge` | POST | 租户充值 | 为租户充值 EU |
|
||||
| `/tenants/{id}/credit` | PUT | 设置授信额度 | 设置租户授信额度 |
|
||||
|
||||
| `/tenants/{id}/password` | PUT | 重置/修改租户密码 | 管理员为租户重置或设置新密码(超级管理员需提供 `channel_id` 查询参数,其他渠道管理员使用 token 中的 `channelId`) |
|
||||
|
||||
#### 管理员为租户重置密码
|
||||
|
||||
接口: `PUT /api/channel/tenants/{tenant_id}/password`
|
||||
|
||||
描述: 渠道管理员或超级管理员为指定租户设置一个新的登录密码。该接口由渠道内有 `manage:tenants` 权限的管理员调用。
|
||||
|
||||
权限:
|
||||
- 需要 `manage:tenants` 权限(角色示例: `channel_admin`, `billing_admin`, `super_admin`)
|
||||
- 超级管理员调用时必须在查询参数中提供 `channel_id`,且只能操作该渠道下的租户;渠道管理员使用其 Token 中的 `channelId` 自动确定渠道。
|
||||
|
||||
请求体示例 (JSON):
|
||||
|
||||
```json
|
||||
{
|
||||
"newPassword": "NewSecurePass123"
|
||||
}
|
||||
```
|
||||
|
||||
返回示例 (成功):
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"tenantId": "<tenant-id>",
|
||||
"name": "Tenant Name"
|
||||
},
|
||||
"message": "租户密码已重置",
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
错误与注意事项:
|
||||
- 如果 `tenant_id` 格式非法,返回 400 错误("无效的租户ID格式")。
|
||||
- 如果租户不存在或不属于该渠道,返回 404 错误("租户不存在或不属于该渠道")。
|
||||
- `newPassword` 目前只做最小长度校验(>= 8),建议前端/后端强制更严格的密码复杂度策略(包含大写、小写、数字、特殊字符)以提升安全。
|
||||
- 建议在重置密码操作中记录审计日志(操作者、时间、原因),并可选择触发邮件通知租户以提示安全变更。
|
||||
|
||||
|
||||
### 3.4 租户模型分配(LiteLLM 集成)
|
||||
|
||||
| 接口 | 方法 | 功能描述 | 业务逻辑 |
|
||||
|
||||
@@ -0,0 +1,516 @@
|
||||
# 自由注册 API 接口文档
|
||||
|
||||
> 生成时间: 2026-01-11
|
||||
>
|
||||
> 本文档描述了 Taiji AI-PAD 平台的用户自由注册功能,包括邮箱验证码发送和用户注册接口。
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
1. [接口概览](#1-接口概览)
|
||||
2. [发送验证码接口](#2-发送验证码接口)
|
||||
3. [用户注册接口](#3-用户注册接口)
|
||||
4. [错误码说明](#4-错误码说明)
|
||||
5. [使用示例](#5-使用示例)
|
||||
6. [注意事项](#6-注意事项)
|
||||
|
||||
---
|
||||
|
||||
## 1. 接口概览
|
||||
|
||||
### 基础信息
|
||||
|
||||
- **Base URL**: `/api/auth`
|
||||
- **认证方式**: 无需认证(公开接口)
|
||||
- **数据格式**: JSON
|
||||
|
||||
### 接口列表
|
||||
|
||||
| 接口 | 方法 | 路径 | 说明 |
|
||||
|------|------|------|------|
|
||||
| 发送验证码 | POST | `/api/auth/register/send-code` | 发送邮箱验证码 |
|
||||
| 用户注册 | POST | `/api/auth/register` | 用户注册并自动分配资源 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 发送验证码接口
|
||||
|
||||
### 接口信息
|
||||
|
||||
- **路径**: `/api/auth/register/send-code`
|
||||
- **方法**: `POST`
|
||||
- **说明**: 向指定邮箱发送6位数字验证码,验证码有效期为10分钟
|
||||
|
||||
### 请求参数
|
||||
|
||||
#### Query Parameters
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 | 示例 |
|
||||
|--------|------|------|------|------|
|
||||
| email | string | 是 | 邮箱地址 | `user@example.com` |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```bash
|
||||
POST /api/auth/register/send-code?email=user@example.com
|
||||
```
|
||||
|
||||
### 响应格式
|
||||
|
||||
#### 成功响应
|
||||
|
||||
**状态码**: `200 OK`
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "验证码已发送到您的邮箱,请查收",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
**状态码**: `400 Bad Request`
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": {
|
||||
"code": "EMAIL_ALREADY_REGISTERED",
|
||||
"message": "该邮箱已被注册"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 错误情况
|
||||
|
||||
| 状态码 | 错误信息 | 说明 |
|
||||
|--------|----------|------|
|
||||
| 400 | 该邮箱已被注册 | 邮箱已存在于系统中 |
|
||||
| 500 | 验证码发送失败,请稍后重试 | 邮件服务异常或 Redis 连接失败 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 用户注册接口
|
||||
|
||||
### 接口信息
|
||||
|
||||
- **路径**: `/api/auth/register`
|
||||
- **方法**: `POST`
|
||||
- **说明**: 用户注册新账户,注册成功后自动分配默认资源并返回登录 Token
|
||||
|
||||
### 请求参数
|
||||
|
||||
#### Request Body
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 | 示例 |
|
||||
|--------|------|------|------|------|
|
||||
| username | string | 是 | 用户名,3-50个字符 | `"john_doe"` |
|
||||
| email | string | 是 | 邮箱地址,需符合邮箱格式 | `"user@example.com"` |
|
||||
| password | string | 是 | 密码,至少8个字符 | `"password123"` |
|
||||
| verification_code | string | 是 | 邮箱验证码,6位数字 | `"123456"` |
|
||||
| full_name | string | 否 | 全名/显示名称 | `"John Doe"` |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```bash
|
||||
POST /api/auth/register
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"username": "john_doe",
|
||||
"email": "user@example.com",
|
||||
"password": "password123",
|
||||
"verification_code": "123456",
|
||||
"full_name": "John Doe"
|
||||
}
|
||||
```
|
||||
|
||||
### 响应格式
|
||||
|
||||
#### 成功响应
|
||||
|
||||
**状态码**: `200 OK`
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "注册成功",
|
||||
"data": {
|
||||
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||
"user": {
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"name": "John Doe",
|
||||
"email": "user@example.com",
|
||||
"username": "john_doe",
|
||||
"role": "user",
|
||||
"channelId": "b415e70b-8d37-481c-b229-bc3b7871607b"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
**状态码**: `400 Bad Request`
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": {
|
||||
"code": "VERIFICATION_CODE_INVALID",
|
||||
"message": "验证码错误或已过期"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 错误情况
|
||||
|
||||
| 状态码 | 错误信息 | 说明 |
|
||||
|--------|----------|------|
|
||||
| 400 | 验证码错误或已过期 | 验证码不正确或超过10分钟有效期 |
|
||||
| 400 | 该邮箱已被注册 | 邮箱已存在于系统中 |
|
||||
| 400 | 该用户名已被使用 | 用户名已被其他用户使用 |
|
||||
| 500 | 渠道不存在,请联系管理员 | 系统配置错误 |
|
||||
| 500 | 注册失败: {错误详情} | 资源分配失败或其他系统错误 |
|
||||
|
||||
### 自动分配的资源
|
||||
|
||||
注册成功后,系统会自动为新用户分配以下资源:
|
||||
|
||||
#### 1. 渠道分配
|
||||
- **渠道ID**: `b415e70b-8d37-481c-b229-bc3b7871607b` (taiji 渠道)
|
||||
- 用户将自动归属到该渠道
|
||||
|
||||
#### 2. 自定义 Agent 配额
|
||||
- **CPU 配额**: 2 核
|
||||
- **内存配额**: 2 GB
|
||||
- 用户可以在配额内创建多个自定义 Agent
|
||||
|
||||
#### 3. 平台 Agent 配额
|
||||
- 为每个可用的平台 Agent 模板分配 **1 个 Pod 配额**
|
||||
- 平台 Agent 模板由系统动态获取
|
||||
|
||||
#### 4. 供应商模型
|
||||
- 为所有活跃供应商的所有模型创建 LiteLLM Key
|
||||
- 每个模型分配默认配额:
|
||||
- **RPM**: 60(或供应商默认值)
|
||||
- **TPM**: 10000(或供应商默认值)
|
||||
- **最大预算**: $500/月
|
||||
|
||||
#### 5. 账户余额
|
||||
- **初始余额**: 20 元(EU)
|
||||
- 可用于 Agent 运行和模型调用
|
||||
|
||||
---
|
||||
|
||||
## 4. 错误码说明
|
||||
|
||||
### 通用错误码
|
||||
|
||||
| 错误码 | HTTP 状态码 | 说明 |
|
||||
|--------|-------------|------|
|
||||
| `EMAIL_ALREADY_REGISTERED` | 400 | 邮箱已被注册 |
|
||||
| `USERNAME_ALREADY_EXISTS` | 400 | 用户名已被使用 |
|
||||
| `VERIFICATION_CODE_INVALID` | 400 | 验证码错误或已过期 |
|
||||
| `VERIFICATION_CODE_EXPIRED` | 400 | 验证码已过期 |
|
||||
| `CHANNEL_NOT_FOUND` | 500 | 渠道不存在 |
|
||||
| `RESOURCE_ALLOCATION_FAILED` | 500 | 资源分配失败 |
|
||||
| `EMAIL_SEND_FAILED` | 500 | 邮件发送失败 |
|
||||
| `INTERNAL_SERVER_ERROR` | 500 | 服务器内部错误 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 使用示例
|
||||
|
||||
### 完整注册流程
|
||||
|
||||
#### 步骤 1: 发送验证码
|
||||
|
||||
```bash
|
||||
curl -X POST "https://api.taiji-ai.com/api/auth/register/send-code?email=user@example.com"
|
||||
```
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "验证码已发送到您的邮箱,请查收"
|
||||
}
|
||||
```
|
||||
|
||||
#### 步骤 2: 查看邮箱获取验证码
|
||||
|
||||
用户收到邮件,验证码为:`123456`
|
||||
|
||||
#### 步骤 3: 提交注册
|
||||
|
||||
```bash
|
||||
curl -X POST "https://api.taiji-ai.com/api/auth/register" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"username": "john_doe",
|
||||
"email": "user@example.com",
|
||||
"password": "SecurePass123!",
|
||||
"verification_code": "123456",
|
||||
"full_name": "John Doe"
|
||||
}'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "注册成功",
|
||||
"data": {
|
||||
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||
"user": {
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"name": "John Doe",
|
||||
"email": "user@example.com",
|
||||
"username": "john_doe",
|
||||
"role": "user",
|
||||
"channelId": "b415e70b-8d37-481c-b229-bc3b7871607b"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 步骤 4: 使用 Token 访问其他接口
|
||||
|
||||
```bash
|
||||
curl -X GET "https://api.taiji-ai.com/api/user/profile" \
|
||||
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
||||
```
|
||||
|
||||
### JavaScript/TypeScript 示例
|
||||
|
||||
```typescript
|
||||
// 1. 发送验证码
|
||||
async function sendVerificationCode(email: string) {
|
||||
const response = await fetch(
|
||||
`https://api.taiji-ai.com/api/auth/register/send-code?email=${encodeURIComponent(email)}`,
|
||||
{ method: 'POST' }
|
||||
);
|
||||
const data = await response.json();
|
||||
return data;
|
||||
}
|
||||
|
||||
// 2. 注册用户
|
||||
async function register(userData: {
|
||||
username: string;
|
||||
email: string;
|
||||
password: string;
|
||||
verification_code: string;
|
||||
full_name?: string;
|
||||
}) {
|
||||
const response = await fetch('https://api.taiji-ai.com/api/auth/register', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(userData),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
// 保存 token 到本地存储
|
||||
localStorage.setItem('token', data.data.token);
|
||||
localStorage.setItem('refreshToken', data.data.refreshToken);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
// 使用示例
|
||||
async function registerFlow() {
|
||||
const email = 'user@example.com';
|
||||
|
||||
// 发送验证码
|
||||
await sendVerificationCode(email);
|
||||
|
||||
// 等待用户输入验证码(实际应用中从表单获取)
|
||||
const verificationCode = '123456';
|
||||
|
||||
// 注册
|
||||
const result = await register({
|
||||
username: 'john_doe',
|
||||
email: email,
|
||||
password: 'SecurePass123!',
|
||||
verification_code: verificationCode,
|
||||
full_name: 'John Doe',
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
console.log('注册成功!', result.data.user);
|
||||
} else {
|
||||
console.error('注册失败:', result.error.message);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Python 示例
|
||||
|
||||
```python
|
||||
import requests
|
||||
|
||||
BASE_URL = "https://api.taiji-ai.com"
|
||||
|
||||
# 1. 发送验证码
|
||||
def send_verification_code(email: str):
|
||||
url = f"{BASE_URL}/api/auth/register/send-code"
|
||||
params = {"email": email}
|
||||
response = requests.post(url, params=params)
|
||||
return response.json()
|
||||
|
||||
# 2. 注册用户
|
||||
def register(username: str, email: str, password: str,
|
||||
verification_code: str, full_name: str = None):
|
||||
url = f"{BASE_URL}/api/auth/register"
|
||||
data = {
|
||||
"username": username,
|
||||
"email": email,
|
||||
"password": password,
|
||||
"verification_code": verification_code,
|
||||
}
|
||||
if full_name:
|
||||
data["full_name"] = full_name
|
||||
|
||||
response = requests.post(url, json=data)
|
||||
return response.json()
|
||||
|
||||
# 使用示例
|
||||
if __name__ == "__main__":
|
||||
email = "user@example.com"
|
||||
|
||||
# 发送验证码
|
||||
result = send_verification_code(email)
|
||||
print("验证码发送结果:", result)
|
||||
|
||||
# 等待用户输入验证码
|
||||
verification_code = input("请输入验证码: ")
|
||||
|
||||
# 注册
|
||||
result = register(
|
||||
username="john_doe",
|
||||
email=email,
|
||||
password="SecurePass123!",
|
||||
verification_code=verification_code,
|
||||
full_name="John Doe"
|
||||
)
|
||||
|
||||
if result.get("success"):
|
||||
print("注册成功!")
|
||||
print("Token:", result["data"]["token"])
|
||||
print("用户信息:", result["data"]["user"])
|
||||
else:
|
||||
print("注册失败:", result.get("error", {}).get("message"))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 注意事项
|
||||
|
||||
### 验证码相关
|
||||
|
||||
1. **有效期**: 验证码有效期为 **10 分钟**,过期后需要重新发送
|
||||
2. **一次性使用**: 验证码验证成功后会自动删除,不能重复使用
|
||||
3. **发送频率**: 建议限制同一邮箱的验证码发送频率,避免滥用
|
||||
4. **邮件延迟**: 邮件发送可能存在延迟,请耐心等待
|
||||
|
||||
### 注册相关
|
||||
|
||||
1. **用户名规则**:
|
||||
- 长度:3-50 个字符
|
||||
- 建议使用字母、数字、下划线
|
||||
- 不能与已存在的用户名重复
|
||||
|
||||
2. **密码规则**:
|
||||
- 长度:至少 8 个字符
|
||||
- 建议包含大小写字母、数字和特殊字符
|
||||
- 不要使用常见密码
|
||||
|
||||
3. **邮箱规则**:
|
||||
- 必须符合标准邮箱格式
|
||||
- 不能与已注册的邮箱重复
|
||||
- 建议使用常用邮箱服务商
|
||||
|
||||
4. **资源分配**:
|
||||
- 资源分配是异步进行的,某些资源分配失败不会影响用户注册
|
||||
- 如果平台 Agent 模板获取失败,会跳过平台 Agent 分配,但用户仍可注册成功
|
||||
- 如果 LiteLLM 服务不可用,会跳过模型分配,但用户仍可注册成功
|
||||
|
||||
5. **Token 使用**:
|
||||
- 注册成功后返回的 Token 可以直接用于后续 API 调用
|
||||
- Token 有效期:24 小时
|
||||
- 建议将 Token 存储在安全的地方(如 localStorage 或 secure cookie)
|
||||
|
||||
### 安全建议
|
||||
|
||||
1. **HTTPS**: 所有 API 调用必须使用 HTTPS
|
||||
2. **密码安全**: 不要在客户端存储明文密码
|
||||
3. **Token 安全**: 不要在 URL 中传递 Token,使用 Authorization Header
|
||||
4. **验证码安全**: 验证码不应在前端显示,应通过邮件发送
|
||||
5. **错误处理**: 不要向用户暴露详细的错误信息,避免信息泄露
|
||||
|
||||
### 常见问题
|
||||
|
||||
#### Q: 验证码收不到怎么办?
|
||||
A:
|
||||
1. 检查邮箱的垃圾邮件文件夹
|
||||
2. 确认邮箱地址输入正确
|
||||
3. 等待几分钟后重试
|
||||
4. 如果仍然收不到,请联系客服
|
||||
|
||||
#### Q: 验证码输入错误怎么办?
|
||||
A: 验证码输入错误后,需要重新发送验证码
|
||||
|
||||
#### Q: 注册后资源没有分配怎么办?
|
||||
A:
|
||||
1. 检查账户余额和配额是否已分配
|
||||
2. 如果资源未分配,请联系客服或管理员
|
||||
3. 管理员可以通过后台手动分配资源
|
||||
|
||||
#### Q: 可以修改默认分配的资源吗?
|
||||
A: 注册时的默认资源分配是系统预设的,注册后可以通过渠道管理员或超级管理员调整资源配额
|
||||
|
||||
---
|
||||
|
||||
## 7. 更新日志
|
||||
|
||||
### 2026-01-11
|
||||
- 初始版本发布
|
||||
- 支持邮箱验证码注册
|
||||
- 支持自动资源分配
|
||||
|
||||
---
|
||||
|
||||
## 8. 相关接口
|
||||
|
||||
注册成功后,用户可以使用以下相关接口:
|
||||
|
||||
- **用户信息**: `GET /api/user/profile` - 获取用户信息
|
||||
- **余额查询**: `GET /api/user/billing/balance` - 查询账户余额
|
||||
- **Agent 管理**: `GET /api/user/agents` - 查看 Agent 列表
|
||||
- **平台 Agent**: `GET /api/user/platform-agents/available` - 查看可用的平台 Agent
|
||||
|
||||
更多接口请参考 [API 接口完整清单](./API接口完整清单-详细版.md)
|
||||
|
||||
---
|
||||
|
||||
## 9. 技术支持
|
||||
|
||||
如有问题或建议,请联系:
|
||||
|
||||
- **技术支持邮箱**: supportagnet@taijiaicloud.com
|
||||
- **文档更新**: 本文档会随系统更新而更新,请关注最新版本
|
||||
|
||||
---
|
||||
|
||||
**文档版本**: v1.0
|
||||
**最后更新**: 2026-01-11
|
||||
|
||||
@@ -339,7 +339,8 @@
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| tenant_id | string | 是 | 租户ID |
|
||||
|
||||
curl -i -X DELETE "http://localhost:8002/api/channel/tenants/2c45b96d-c158-466f-b5fc-3eccdc24a8bb?channel_id=6e6fc470-76f8-4bb1-8ea4-625dc5b12bc6" \
|
||||
-H "Authorization: Bearer $SUPER_ADMIN_TOKEN"
|
||||
**响应字段**:
|
||||
```json
|
||||
{
|
||||
|
||||
@@ -89,6 +89,11 @@ services:
|
||||
- LITELLM_MASTER_KEY=${LITELLM_MASTER_KEY:-sk-1f06b8f0d2e34c9b8a9f3d75a1c4e9b7-7e3a2c6bd9f441d8}
|
||||
- AGENT_MANAGER_URL=${AGENT_MANAGER_URL:-http://host.docker.internal:8000}
|
||||
- ENVIRONMENT=production
|
||||
- ENABLE_TEST_MODE=true
|
||||
- SMTP_SERVER=${SMTP_SERVER:-smtp.office365.com}
|
||||
- SMTP_PORT=${SMTP_PORT:-587}
|
||||
- SMTP_EMAIL=${SMTP_EMAIL:-supportagnet@taijiaicloud.com}
|
||||
- SMTP_PASSWORD=${SMTP_PASSWORD:-l5YYL7TOK2WvRKtf}
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
volumes:
|
||||
|
||||
@@ -0,0 +1,269 @@
|
||||
"""
|
||||
邮箱验证码功能
|
||||
"""
|
||||
|
||||
import os
|
||||
import random
|
||||
import string
|
||||
import smtplib
|
||||
from email.mime.text import MIMEText
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from typing import Optional
|
||||
import structlog
|
||||
from app.state import get_state
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
# 邮箱配置 - 从环境变量读取
|
||||
SMTP_SERVER = os.getenv("SMTP_SERVER", "smtp.office365.com")
|
||||
SMTP_PORT = int(os.getenv("SMTP_PORT", "587"))
|
||||
SMTP_EMAIL = os.getenv("SMTP_EMAIL", "supportagnet@taijiaicloud.com")
|
||||
# 注意:生产环境应该通过环境变量设置SMTP_PASSWORD,不要硬编码密码
|
||||
# 这里使用默认值仅用于开发测试,生产环境必须通过环境变量配置
|
||||
SMTP_PASSWORD = os.getenv("SMTP_PASSWORD", "l5YYL7TOK2WvRKtf")
|
||||
|
||||
# 验证码配置
|
||||
VERIFICATION_CODE_LENGTH = 6
|
||||
VERIFICATION_CODE_EXPIRE_SECONDS = 600 # 10分钟
|
||||
|
||||
|
||||
def _send_email_sync(msg: MIMEMultipart) -> None:
|
||||
"""同步发送邮件(在 executor 中运行)"""
|
||||
if not SMTP_PASSWORD:
|
||||
raise ValueError("SMTP_PASSWORD环境变量未设置,无法发送邮件")
|
||||
|
||||
server = smtplib.SMTP(SMTP_SERVER, SMTP_PORT)
|
||||
try:
|
||||
server.starttls()
|
||||
server.login(SMTP_EMAIL, SMTP_PASSWORD)
|
||||
server.send_message(msg)
|
||||
logger.info("邮件发送成功", to=msg['To'])
|
||||
except smtplib.SMTPAuthenticationError as e:
|
||||
logger.error("SMTP认证失败", error=str(e), smtp_server=SMTP_SERVER, smtp_email=SMTP_EMAIL)
|
||||
raise
|
||||
except smtplib.SMTPException as e:
|
||||
logger.error("SMTP发送失败", error=str(e))
|
||||
raise
|
||||
finally:
|
||||
server.quit()
|
||||
|
||||
|
||||
def generate_verification_code() -> str:
|
||||
"""生成6位数字验证码"""
|
||||
return ''.join(random.choices(string.digits, k=VERIFICATION_CODE_LENGTH))
|
||||
|
||||
|
||||
async def send_verification_code(email: str, code: str) -> bool:
|
||||
"""
|
||||
发送验证码邮件
|
||||
|
||||
Args:
|
||||
email: 收件人邮箱
|
||||
code: 验证码
|
||||
|
||||
Returns:
|
||||
是否发送成功
|
||||
"""
|
||||
if not SMTP_PASSWORD:
|
||||
logger.error("SMTP密码未配置,无法发送邮件", email=email)
|
||||
return False
|
||||
|
||||
try:
|
||||
# 创建邮件
|
||||
msg = MIMEMultipart()
|
||||
msg['From'] = SMTP_EMAIL
|
||||
msg['To'] = email
|
||||
msg['Subject'] = "Taiji AI-PAD 注册验证码"
|
||||
|
||||
# 邮件正文
|
||||
body = f"""
|
||||
尊敬的用户:
|
||||
|
||||
您的注册验证码是:{code}
|
||||
|
||||
验证码有效期为10分钟,请勿泄露给他人。
|
||||
|
||||
如果您没有进行注册操作,请忽略此邮件。
|
||||
|
||||
此邮件由系统自动发送,请勿回复。
|
||||
|
||||
---
|
||||
Taiji AI-PAD 团队
|
||||
"""
|
||||
msg.attach(MIMEText(body, 'plain', 'utf-8'))
|
||||
|
||||
# 发送邮件(使用同步方式,因为 smtplib 不支持异步)
|
||||
import asyncio
|
||||
loop = asyncio.get_event_loop()
|
||||
await loop.run_in_executor(None, _send_email_sync, msg)
|
||||
|
||||
logger.info("验证码邮件发送成功", email=email)
|
||||
return True
|
||||
except smtplib.SMTPAuthenticationError as e:
|
||||
logger.error(
|
||||
"SMTP认证失败,请检查SMTP_PASSWORD是否正确",
|
||||
email=email,
|
||||
error=str(e),
|
||||
smtp_server=SMTP_SERVER,
|
||||
smtp_email=SMTP_EMAIL,
|
||||
hint="Office365可能需要使用应用专用密码(App Password)而不是普通密码"
|
||||
)
|
||||
return False
|
||||
except smtplib.SMTPException as e:
|
||||
logger.error("SMTP发送失败", email=email, error=str(e), smtp_server=SMTP_SERVER)
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error("验证码邮件发送失败", email=email, error=str(e), error_type=type(e).__name__)
|
||||
return False
|
||||
|
||||
|
||||
async def store_verification_code(email: str, code: str) -> bool:
|
||||
"""
|
||||
存储验证码到Redis
|
||||
|
||||
Args:
|
||||
email: 邮箱地址
|
||||
code: 验证码
|
||||
|
||||
Returns:
|
||||
是否存储成功
|
||||
"""
|
||||
try:
|
||||
state = get_state()
|
||||
if not state.redis_client:
|
||||
logger.warning("Redis未连接,无法存储验证码")
|
||||
return False
|
||||
|
||||
key = f"verification_code:{email}"
|
||||
await state.redis_client.setex(
|
||||
key,
|
||||
VERIFICATION_CODE_EXPIRE_SECONDS,
|
||||
code
|
||||
)
|
||||
logger.info("验证码已存储", email=email)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error("验证码存储失败", email=email, error=str(e))
|
||||
return False
|
||||
|
||||
|
||||
async def verify_code(email: str, code: str) -> bool:
|
||||
"""
|
||||
验证验证码
|
||||
|
||||
Args:
|
||||
email: 邮箱地址
|
||||
code: 验证码
|
||||
|
||||
Returns:
|
||||
是否验证成功
|
||||
"""
|
||||
try:
|
||||
state = get_state()
|
||||
if not state.redis_client:
|
||||
logger.warning("Redis未连接,无法验证验证码")
|
||||
return False
|
||||
|
||||
key = f"verification_code:{email}"
|
||||
|
||||
# 处理Redis集群的MOVED重定向
|
||||
max_retries = 3
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
stored_code = await state.redis_client.get(key)
|
||||
break
|
||||
except Exception as e:
|
||||
error_str = str(e)
|
||||
if "MOVED" in error_str and attempt < max_retries - 1:
|
||||
# Redis集群重定向,等待后重试
|
||||
import asyncio
|
||||
await asyncio.sleep(0.1)
|
||||
logger.debug("Redis集群重定向,重试中", attempt=attempt+1, error=error_str)
|
||||
continue
|
||||
else:
|
||||
raise
|
||||
|
||||
if not stored_code:
|
||||
logger.warning("验证码不存在或已过期", email=email, provided_code=code)
|
||||
return False
|
||||
|
||||
# 确保都是字符串类型进行比较
|
||||
stored_code = str(stored_code).strip()
|
||||
code = str(code).strip()
|
||||
|
||||
if stored_code != code:
|
||||
logger.warning(
|
||||
"验证码错误",
|
||||
email=email,
|
||||
provided_code=code,
|
||||
stored_code=stored_code,
|
||||
provided_type=type(code).__name__,
|
||||
stored_type=type(stored_code).__name__
|
||||
)
|
||||
return False
|
||||
|
||||
# 验证成功后删除验证码(同样处理集群重定向)
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
await state.redis_client.delete(key)
|
||||
break
|
||||
except Exception as e:
|
||||
error_str = str(e)
|
||||
if "MOVED" in error_str and attempt < max_retries - 1:
|
||||
import asyncio
|
||||
await asyncio.sleep(0.1)
|
||||
continue
|
||||
else:
|
||||
logger.warning("删除验证码失败,但验证已成功", email=email, error=error_str)
|
||||
break
|
||||
|
||||
logger.info("验证码验证成功", email=email)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error("验证码验证失败", email=email, code=code, error=str(e), error_type=type(e).__name__)
|
||||
return False
|
||||
|
||||
|
||||
async def send_and_store_verification_code(email: str) -> Optional[str]:
|
||||
"""
|
||||
生成、发送并存储验证码
|
||||
|
||||
Args:
|
||||
email: 邮箱地址
|
||||
|
||||
Returns:
|
||||
验证码(如果成功),None(如果失败)
|
||||
"""
|
||||
import os
|
||||
code = generate_verification_code()
|
||||
|
||||
# 先存储验证码(即使邮件发送失败,验证码也已存储,可以手动查看Redis)
|
||||
store_success = await store_verification_code(email, code)
|
||||
if not store_success:
|
||||
logger.error("验证码存储失败,无法继续", email=email)
|
||||
return None
|
||||
|
||||
# 发送邮件
|
||||
send_success = await send_verification_code(email, code)
|
||||
|
||||
# 测试模式:即使邮件发送失败也返回验证码(仅用于开发/测试环境)
|
||||
test_mode = os.getenv("ENABLE_TEST_MODE", "false").lower() == "true" or os.getenv("DEBUG", "false").lower() == "true"
|
||||
|
||||
if not send_success:
|
||||
if test_mode:
|
||||
# 测试模式:记录验证码到日志(仅测试环境)
|
||||
logger.warning(
|
||||
"测试模式:邮件发送失败,但验证码已存储到Redis",
|
||||
email=email,
|
||||
verification_code=code,
|
||||
hint="验证码已存储到Redis,可通过Redis获取或查看日志(仅测试环境)"
|
||||
)
|
||||
return code
|
||||
else:
|
||||
# 生产模式:邮件发送失败则不返回验证码
|
||||
logger.error("邮件发送失败,验证码已存储但未发送", email=email)
|
||||
return None
|
||||
|
||||
logger.info("验证码已发送并存储", email=email)
|
||||
return code
|
||||
|
||||
@@ -39,11 +39,37 @@ def register_lifecycle_events(app: FastAPI) -> None:
|
||||
# Redis是可选的,连接失败不影响服务启动
|
||||
try:
|
||||
if settings.redis_url:
|
||||
state.redis_client = redis.from_url(
|
||||
settings.redis_url,
|
||||
encoding="utf-8",
|
||||
decode_responses=True,
|
||||
)
|
||||
# 检查是否是集群模式(通过URL或错误信息判断)
|
||||
# Azure Redis Cache集群模式需要使用集群客户端
|
||||
try:
|
||||
# 先尝试普通连接
|
||||
test_client = redis.from_url(
|
||||
settings.redis_url,
|
||||
encoding="utf-8",
|
||||
decode_responses=True,
|
||||
)
|
||||
await test_client.ping()
|
||||
await test_client.aclose()
|
||||
# 普通连接成功,使用普通客户端
|
||||
state.redis_client = redis.from_url(
|
||||
settings.redis_url,
|
||||
encoding="utf-8",
|
||||
decode_responses=True,
|
||||
)
|
||||
except Exception as cluster_test:
|
||||
# 如果普通连接失败,可能是集群模式,尝试使用集群客户端
|
||||
# 注意:redis-py的集群支持需要额外配置
|
||||
# 这里先使用普通连接,但添加重定向处理
|
||||
logger.warning("Redis普通连接失败,尝试集群模式", error=str(cluster_test))
|
||||
# 对于Azure Redis Cache,通常使用普通连接但需要处理MOVED重定向
|
||||
# 使用skip_full_coverage_check=True来允许部分节点连接
|
||||
state.redis_client = redis.from_url(
|
||||
settings.redis_url,
|
||||
encoding="utf-8",
|
||||
decode_responses=True,
|
||||
skip_full_coverage_check=True,
|
||||
)
|
||||
|
||||
await state.redis_client.ping()
|
||||
redis_connections.set(1)
|
||||
logger.info("Redis连接成功")
|
||||
|
||||
@@ -18,7 +18,7 @@ from models import (
|
||||
BillingRecord, Application, ModelProvider,
|
||||
ChannelProviderAccess, ProviderApplication,
|
||||
ChannelCustomAgentQuota, ResourceApplication, PlatformAgentQuota,
|
||||
PlatformAgentTemplateConfig
|
||||
PlatformAgentTemplateConfig, AgentBillingRecord, ModelBillingRecord
|
||||
)
|
||||
from app.auth import require_auth, get_password_hash
|
||||
from app.schemas import (
|
||||
@@ -483,13 +483,25 @@ async def get_admin_dashboard_stats(
|
||||
# 总 Agent 数
|
||||
total_agents = platform_agents_count + custom_agents_count
|
||||
|
||||
# 总调用次数
|
||||
calls_count = await db.execute(select(func.count(BillingRecord.id)))
|
||||
total_calls = calls_count.scalar() or 0
|
||||
# 总调用次数 - 从 AgentBillingRecord 和 ModelBillingRecord 统计
|
||||
agent_calls_count = await db.execute(select(func.count(AgentBillingRecord.id)))
|
||||
agent_calls = agent_calls_count.scalar() or 0
|
||||
|
||||
# 总收入
|
||||
revenue = await db.execute(select(func.sum(BillingRecord.cost)))
|
||||
total_revenue = float(revenue.scalar() or 0)
|
||||
model_calls_count = await db.execute(select(func.count(ModelBillingRecord.id)))
|
||||
model_calls = model_calls_count.scalar() or 0
|
||||
|
||||
total_calls = agent_calls + model_calls
|
||||
|
||||
# 总收入 - 从 AgentBillingRecord 和 ModelBillingRecord 统计
|
||||
# AgentBillingRecord.cost 是 Agent 使用费用
|
||||
agent_revenue = await db.execute(select(func.sum(AgentBillingRecord.cost)))
|
||||
agent_total = float(agent_revenue.scalar() or 0)
|
||||
|
||||
# ModelBillingRecord.total_cost 是模型调用费用
|
||||
model_revenue = await db.execute(select(func.sum(ModelBillingRecord.total_cost)))
|
||||
model_total = float(model_revenue.scalar() or 0)
|
||||
|
||||
total_revenue = agent_total + model_total
|
||||
|
||||
# 从 Agent Manager 获取 K8s 中实际运行的平台端 Agent 资源统计
|
||||
k8s_agents_count = 0
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"""
|
||||
|
||||
from datetime import timedelta
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Query
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
import secrets
|
||||
@@ -11,7 +11,10 @@ import hashlib
|
||||
from typing import Optional
|
||||
|
||||
from database import get_db
|
||||
from models import User, Channel, APIKey
|
||||
from models import (
|
||||
User, Channel, APIKey, Balance, TenantCustomAgentQuota,
|
||||
PlatformAgentQuota, TenantModelKey, ResourceAllocation, ModelProvider
|
||||
)
|
||||
from app.auth import (
|
||||
authenticate_user,
|
||||
create_access_token,
|
||||
@@ -28,7 +31,16 @@ from app.schemas import (
|
||||
RegenerateAPIKeyResponse,
|
||||
UserCreate,
|
||||
)
|
||||
from app.email_verification import verify_code, send_and_store_verification_code
|
||||
from app.agent_manager_client import get_agent_manager_client, AgentManagerError
|
||||
from app.litellm_client import get_litellm_client, LiteLLMClientError
|
||||
from config import settings
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from sqlalchemy import and_
|
||||
import structlog
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/api/auth", tags=["认证"])
|
||||
|
||||
@@ -410,13 +422,60 @@ async def regenerate_api_key(
|
||||
)
|
||||
|
||||
|
||||
@router.post("/register/send-code", response_model=SuccessResponse)
|
||||
async def send_verification_code_endpoint(
|
||||
email: str = Query(..., description="邮箱地址"),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
发送邮箱验证码
|
||||
|
||||
在用户注册前,先调用此接口发送验证码到邮箱
|
||||
"""
|
||||
# 检查邮箱是否已存在
|
||||
result = await db.execute(select(User).where(User.email == email))
|
||||
existing_user = result.scalar_one_or_none()
|
||||
|
||||
if existing_user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="该邮箱已被注册"
|
||||
)
|
||||
|
||||
# 发送验证码
|
||||
code = await send_and_store_verification_code(email)
|
||||
if not code:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="验证码发送失败,请稍后重试"
|
||||
)
|
||||
|
||||
return SuccessResponse(
|
||||
message="验证码已发送到您的邮箱,请查收"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/register", response_model=SuccessResponse)
|
||||
async def register(req: UserCreate, db: AsyncSession = Depends(get_db)):
|
||||
"""
|
||||
用户注册接口
|
||||
|
||||
允许用户自由注册,创建普通用户账户
|
||||
注册成功后自动分配默认资源:
|
||||
- 分配到 taiji 渠道 (channelId: b415e70b-8d37-481c-b229-bc3b7871607b)
|
||||
- 自定义 agent 配额:2 CPU, 2 GB 内存
|
||||
- 平台 agent 各1个
|
||||
- 供应商所有模型
|
||||
- 余额:20元
|
||||
"""
|
||||
# 验证邮箱验证码
|
||||
is_valid = await verify_code(req.email, req.verification_code)
|
||||
if not is_valid:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="验证码错误或已过期"
|
||||
)
|
||||
|
||||
# 检查邮箱是否已存在
|
||||
result = await db.execute(select(User).where(User.email == req.email))
|
||||
existing_user = result.scalar_one_or_none()
|
||||
@@ -427,7 +486,7 @@ async def register(req: UserCreate, db: AsyncSession = Depends(get_db)):
|
||||
detail="该邮箱已被注册"
|
||||
)
|
||||
|
||||
# 检查用户名是否已存在(如果提供了username)
|
||||
# 检查用户名是否已存在
|
||||
if req.username:
|
||||
result = await db.execute(select(User).where(User.username == req.username))
|
||||
existing_username = result.scalar_one_or_none()
|
||||
@@ -437,6 +496,20 @@ async def register(req: UserCreate, db: AsyncSession = Depends(get_db)):
|
||||
detail="该用户名已被使用"
|
||||
)
|
||||
|
||||
# taiji 渠道 ID
|
||||
TAIJI_CHANNEL_ID = uuid.UUID("b415e70b-8d37-481c-b229-bc3b7871607b")
|
||||
|
||||
# 验证渠道存在
|
||||
channel_result = await db.execute(
|
||||
select(Channel).where(Channel.id == TAIJI_CHANNEL_ID)
|
||||
)
|
||||
channel = channel_result.scalar_one_or_none()
|
||||
if not channel:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="渠道不存在,请联系管理员"
|
||||
)
|
||||
|
||||
# 创建新用户
|
||||
password_hash = get_password_hash(req.password)
|
||||
username = req.username or req.email.split("@")[0]
|
||||
@@ -450,6 +523,7 @@ async def register(req: UserCreate, db: AsyncSession = Depends(get_db)):
|
||||
username=username,
|
||||
full_name=req.full_name or name,
|
||||
role="user", # 默认角色为普通用户
|
||||
channel_id=TAIJI_CHANNEL_ID, # 分配到 taiji 渠道
|
||||
status="active",
|
||||
is_active=True,
|
||||
is_admin=False,
|
||||
@@ -460,8 +534,143 @@ async def register(req: UserCreate, db: AsyncSession = Depends(get_db)):
|
||||
)
|
||||
|
||||
db.add(new_user)
|
||||
await db.commit()
|
||||
await db.refresh(new_user)
|
||||
await db.flush() # 获取 user.id
|
||||
|
||||
user_id = new_user.id
|
||||
|
||||
try:
|
||||
# 1. 创建余额记录,初始余额 20 元
|
||||
balance = Balance(
|
||||
user_id=user_id,
|
||||
eu_balance=20.0
|
||||
)
|
||||
db.add(balance)
|
||||
|
||||
# 2. 分配自定义 Agent 配额:2 CPU, 2 GB 内存
|
||||
custom_agent_quota = TenantCustomAgentQuota(
|
||||
tenant_id=user_id,
|
||||
cpu_quota=2.0,
|
||||
memory_quota=2.0,
|
||||
cpu_used=0.0,
|
||||
memory_used=0.0,
|
||||
agent_count=0
|
||||
)
|
||||
db.add(custom_agent_quota)
|
||||
|
||||
# 3. 获取所有平台 Agent 模板,为每个模板分配 1 个配额
|
||||
try:
|
||||
agent_manager_client = get_agent_manager_client()
|
||||
platform_templates = await agent_manager_client.list_platform_templates()
|
||||
|
||||
for template in platform_templates:
|
||||
template_name = template.template
|
||||
# 创建平台 Agent 配额记录
|
||||
platform_quota = PlatformAgentQuota(
|
||||
target_id=user_id,
|
||||
target_type="tenant",
|
||||
template_name=template_name,
|
||||
pod_quota=1, # 每个平台 agent 分配 1 个配额
|
||||
pod_used=0,
|
||||
allocated_by=None, # 系统自动分配
|
||||
allocated_at=datetime.utcnow()
|
||||
)
|
||||
db.add(platform_quota)
|
||||
|
||||
# 同时创建 ResourceAllocation 记录(兼容旧逻辑)
|
||||
allocation = ResourceAllocation(
|
||||
target_id=user_id,
|
||||
target_type="tenant",
|
||||
resource_type="agent",
|
||||
resource_id=template_name,
|
||||
quantity=1
|
||||
)
|
||||
db.add(allocation)
|
||||
except (AgentManagerError, Exception) as e:
|
||||
logger.warning(f"获取平台 Agent 模板失败,跳过平台 Agent 分配: {e}")
|
||||
|
||||
# 4. 分配所有供应商模型
|
||||
providers_result = await db.execute(
|
||||
select(ModelProvider).where(ModelProvider.is_active == True)
|
||||
)
|
||||
providers = providers_result.scalars().all()
|
||||
|
||||
if channel.litellm_team_id:
|
||||
litellm_client = get_litellm_client()
|
||||
|
||||
for provider in providers:
|
||||
# 为每个供应商的每个模型创建 TenantModelKey
|
||||
for model_name in provider.supported_models:
|
||||
try:
|
||||
# 在 LiteLLM 中创建 Key
|
||||
key = await litellm_client.generate_key(
|
||||
team_id=channel.litellm_team_id,
|
||||
models=[model_name],
|
||||
rpm_limit=provider.rpm or 60,
|
||||
tpm_limit=provider.tpm or 10000,
|
||||
max_budget=500.0, # 默认预算
|
||||
budget_duration="monthly",
|
||||
key_name=f"tenant-{user_id}-{model_name}",
|
||||
metadata={
|
||||
"tenant_id": str(user_id),
|
||||
"tenant_name": name,
|
||||
"channel_id": str(TAIJI_CHANNEL_ID),
|
||||
"channel_name": channel.name,
|
||||
"model": model_name,
|
||||
}
|
||||
)
|
||||
|
||||
# 加密存储 Key
|
||||
encrypted_key = litellm_client.encrypt_key(key.key)
|
||||
|
||||
# 保存到数据库
|
||||
tenant_key = TenantModelKey(
|
||||
tenant_id=user_id,
|
||||
channel_id=TAIJI_CHANNEL_ID,
|
||||
model_name=model_name,
|
||||
litellm_key_id=key.key,
|
||||
litellm_key_hash=encrypted_key,
|
||||
rpm_limit=provider.rpm or 60,
|
||||
tpm_limit=provider.tpm or 10000,
|
||||
max_budget=500.0,
|
||||
budget_duration="monthly",
|
||||
status="active",
|
||||
)
|
||||
db.add(tenant_key)
|
||||
|
||||
# 同时记录到 ResourceAllocation
|
||||
model_allocation = ResourceAllocation(
|
||||
target_id=user_id,
|
||||
target_type="tenant",
|
||||
resource_type="model",
|
||||
resource_id=str(provider.id),
|
||||
rpm=provider.rpm or 60,
|
||||
tpm=provider.tpm or 10000,
|
||||
)
|
||||
db.add(model_allocation)
|
||||
except (LiteLLMClientError, Exception) as e:
|
||||
logger.warning(f"为模型 {model_name} 创建 LiteLLM Key 失败: {e}")
|
||||
# 继续处理其他模型
|
||||
else:
|
||||
logger.warning(f"渠道 {TAIJI_CHANNEL_ID} 未配置 LiteLLM team,跳过模型分配")
|
||||
|
||||
# 提交所有更改
|
||||
await db.commit()
|
||||
await db.refresh(new_user)
|
||||
|
||||
logger.info(
|
||||
f"用户注册成功并分配默认资源",
|
||||
user_id=str(user_id),
|
||||
email=req.email,
|
||||
channel_id=str(TAIJI_CHANNEL_ID)
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
await db.rollback()
|
||||
logger.error(f"用户注册失败: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"注册失败: {str(e)}"
|
||||
)
|
||||
|
||||
# 创建JWT token,自动登录
|
||||
token = create_access_token(
|
||||
@@ -470,6 +679,7 @@ async def register(req: UserCreate, db: AsyncSession = Depends(get_db)):
|
||||
"email": new_user.email,
|
||||
"role": new_user.role,
|
||||
"user_id": str(new_user.id),
|
||||
"channelId": str(TAIJI_CHANNEL_ID),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -483,6 +693,7 @@ async def register(req: UserCreate, db: AsyncSession = Depends(get_db)):
|
||||
"email": new_user.email,
|
||||
"username": new_user.username,
|
||||
"role": new_user.role,
|
||||
"channelId": str(TAIJI_CHANNEL_ID),
|
||||
}
|
||||
},
|
||||
message="注册成功"
|
||||
|
||||
@@ -295,11 +295,19 @@ async def allocate_tenant_resources(
|
||||
detail="无法获取渠道ID"
|
||||
)
|
||||
|
||||
# 验证租户存在且属于指定渠道
|
||||
# 验证 tenant_id 格式并确认租户属于指定渠道
|
||||
try:
|
||||
tenant_uuid = uuid.UUID(tenant_id)
|
||||
except ValueError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="无效的租户ID格式"
|
||||
)
|
||||
|
||||
result = await db.execute(
|
||||
select(User).where(
|
||||
and_(
|
||||
User.id == tenant_id,
|
||||
User.id == tenant_uuid,
|
||||
User.channel_id == channel_id
|
||||
)
|
||||
)
|
||||
|
||||
@@ -897,102 +897,8 @@ async def admin_login(payload: Dict[str, str], db: AsyncSession = Depends(get_db
|
||||
return {"token": token, "tokenType": "bearer", "email": email, "expiresIn": 60 * 60}
|
||||
|
||||
|
||||
@router.get("/admin/dashboard/stats")
|
||||
async def admin_dashboard_stats(db: AsyncSession = Depends(get_db)) -> Dict[str, Any]:
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
agent_count = (await db.execute(select(func.count(Agent.id)))).scalar() or 0
|
||||
channels_count = (await db.execute(select(func.count(Channel.id)))).scalar() or 0
|
||||
tenants_count = (await db.execute(select(func.count(User.id)).where(User.role == "user"))).scalar() or 0
|
||||
balances = (await db.execute(select(func.coalesce(func.sum(Balance.eu_balance), 0)))).scalar() or 0
|
||||
|
||||
# 统计平台端 Agent (type='platform')
|
||||
platform_agents = (await db.execute(
|
||||
select(Agent).where(Agent.type == "platform")
|
||||
)).scalars().all()
|
||||
platform_agent_count = len(platform_agents)
|
||||
platform_cpu = sum(float(agent.cpu or 0) for agent in platform_agents)
|
||||
platform_memory = sum(float(agent.memory or 0) for agent in platform_agents)
|
||||
|
||||
# 统计自定义 Agent (type='custom')
|
||||
custom_agents = (await db.execute(
|
||||
select(Agent).where(Agent.type == "custom")
|
||||
)).scalars().all()
|
||||
custom_agent_count = len(custom_agents)
|
||||
custom_cpu = sum(float(agent.cpu or 0) for agent in custom_agents)
|
||||
custom_memory = sum(float(agent.memory or 0) for agent in custom_agents)
|
||||
|
||||
# 从 Agent Manager 获取 K8s 中实际运行的 Agent 资源统计
|
||||
k8s_agents_count = 0
|
||||
k8s_total_cpu = 0.0
|
||||
k8s_total_memory = 0.0
|
||||
|
||||
try:
|
||||
from app.agent_manager_client import get_agent_manager_client
|
||||
client = get_agent_manager_client()
|
||||
|
||||
# 获取所有运行中的 Agent
|
||||
k8s_agents_result = await client.list_agents()
|
||||
k8s_agents = k8s_agents_result.agents # 从 AgentListResult 中获取 agents 列表
|
||||
k8s_agents_count = len(k8s_agents)
|
||||
|
||||
# 获取每个 Agent 的资源配置
|
||||
for agent in k8s_agents:
|
||||
agent_name = agent.get("name")
|
||||
if agent_name:
|
||||
try:
|
||||
metrics = await client.get_agent_metrics(agent_name)
|
||||
# 解析 CPU (如 "500m" -> 0.5 核)
|
||||
cpu_limit = metrics.limits.get("cpu", "0")
|
||||
if cpu_limit.endswith("m"):
|
||||
k8s_total_cpu += float(cpu_limit[:-1]) / 1000
|
||||
else:
|
||||
k8s_total_cpu += float(cpu_limit)
|
||||
|
||||
# 解析内存 (如 "512Mi" -> 0.5 GB)
|
||||
memory_limit = metrics.limits.get("memory", "0")
|
||||
if memory_limit.endswith("Mi"):
|
||||
k8s_total_memory += float(memory_limit[:-2]) / 1024
|
||||
elif memory_limit.endswith("Gi"):
|
||||
k8s_total_memory += float(memory_limit[:-2])
|
||||
elif memory_limit.endswith("Ki"):
|
||||
k8s_total_memory += float(memory_limit[:-2]) / (1024 * 1024)
|
||||
except Exception as e:
|
||||
logger.warning(f"获取 Agent {agent_name} 资源指标失败: {e}")
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"连接 Agent Manager 失败,使用数据库统计: {e}")
|
||||
# 如果 Agent Manager 不可用,使用数据库中的平台端 Agent 统计
|
||||
k8s_total_cpu = platform_cpu
|
||||
k8s_total_memory = platform_memory
|
||||
|
||||
# 总 Agent 数:平台端 + 自定义
|
||||
total_agents = platform_agent_count + custom_agent_count
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": {
|
||||
"totalChannels": channels_count,
|
||||
"totalTenants": tenants_count,
|
||||
"totalAgents": total_agents,
|
||||
"totalCalls": 0,
|
||||
"totalRevenue": round(float(balances), 2),
|
||||
"totalAllocatedCpu": round(k8s_total_cpu + custom_cpu, 2),
|
||||
"totalAllocatedMemory": round(k8s_total_memory + custom_memory, 2),
|
||||
"platformAgents": {
|
||||
"count": k8s_agents_count if k8s_agents_count > 0 else platform_agent_count,
|
||||
"cpu": round(k8s_total_cpu if k8s_total_cpu > 0 else platform_cpu, 2),
|
||||
"memory": round(k8s_total_memory if k8s_total_memory > 0 else platform_memory, 2),
|
||||
},
|
||||
"customAgents": {
|
||||
"count": custom_agent_count,
|
||||
"cpu": round(custom_cpu, 2),
|
||||
"memory": round(custom_memory, 2),
|
||||
},
|
||||
},
|
||||
"message": None,
|
||||
}
|
||||
# 注意: /admin/dashboard/stats 接口已移至 admin.py,避免重复定义
|
||||
# 该接口从 AgentBillingRecord 和 ModelBillingRecord 统计收入和调用次数
|
||||
|
||||
|
||||
@router.get("/admin/channels")
|
||||
|
||||
@@ -51,6 +51,15 @@ class PasswordChangeRequest(BaseModel):
|
||||
new_password: str
|
||||
|
||||
|
||||
class UserCreate(BaseModel):
|
||||
"""创建用户请求(自由注册)"""
|
||||
username: str = Field(..., min_length=3, max_length=50, description="用户名,3-50个字符")
|
||||
email: EmailStr = Field(..., description="邮箱地址")
|
||||
password: str = Field(..., min_length=8, description="密码,至少8个字符")
|
||||
verification_code: str = Field(..., min_length=6, max_length=6, description="邮箱验证码,6位数字")
|
||||
full_name: Optional[str] = Field(None, description="全名/显示名称")
|
||||
|
||||
|
||||
# ============= 密钥管理 =============
|
||||
|
||||
class APIKeyInfo(BaseModel):
|
||||
|
||||
@@ -398,6 +398,7 @@ class UserCreate(BaseModel):
|
||||
email: str = Field(..., pattern=r'^[^@]+@[^@]+\.[^@]+$')
|
||||
password: str = Field(..., min_length=8)
|
||||
full_name: Optional[str] = None
|
||||
verification_code: str = Field(..., min_length=6, max_length=6, description="邮箱验证码")
|
||||
|
||||
|
||||
class UserUpdate(BaseModel):
|
||||
|
||||
Reference in New Issue
Block a user