forked from xiaohei/taiji-AI-PAD
更新超级管理员
This commit is contained in:
@@ -7,7 +7,7 @@ pydantic-settings==2.1.0
|
||||
# Database
|
||||
sqlalchemy==2.0.23
|
||||
asyncpg==0.29.0
|
||||
psycopg2
|
||||
psycopg2-binary==2.9.9
|
||||
|
||||
# Redis and cache
|
||||
redis==5.0.1
|
||||
|
||||
@@ -31,7 +31,7 @@ router = APIRouter(prefix="/api/admin", tags=["超级管理员"])
|
||||
def _verify_admin_permission(principal: dict):
|
||||
"""验证超级管理员权限"""
|
||||
role = principal.get("claims", {}).get("role")
|
||||
if role != "super_admin":
|
||||
if role not in ["super_admin", "admin"]:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="需要超级管理员权限"
|
||||
|
||||
@@ -61,10 +61,13 @@ async def _ensure_test_user(db: AsyncSession) -> User:
|
||||
if test_user is None:
|
||||
test_user = User(
|
||||
id=TEST_USER_ID,
|
||||
name="测试用户", # 添加name字段
|
||||
username="test_user",
|
||||
email="test@taiji-ai.com",
|
||||
password_hash="", # 添加password_hash字段
|
||||
hashed_password="",
|
||||
full_name="测试用户",
|
||||
role="user", # 添加role字段
|
||||
is_active=True,
|
||||
is_admin=False,
|
||||
)
|
||||
|
||||
@@ -424,10 +424,47 @@ async def channel_login(payload: Dict[str, str], db: AsyncSession = Depends(get_
|
||||
password = payload.get("password") or "temp-pass"
|
||||
if not email:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="email is required")
|
||||
|
||||
# 查找或创建渠道用户
|
||||
user = await ensure_user(email, password, db)
|
||||
if user.hashed_password and not verify_password(password, user.hashed_password):
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="invalid credentials")
|
||||
token = create_access_token({"sub": str(user.id), "email": email, "role": "channel_admin"})
|
||||
|
||||
# 如果用户没有channel_id,尝试查找或创建对应的渠道
|
||||
channel_id = user.channel_id
|
||||
if not channel_id:
|
||||
# 查找是否有对应的渠道
|
||||
from models import Channel
|
||||
result = await db.execute(select(Channel).where(Channel.email == email))
|
||||
channel = result.scalar_one_or_none()
|
||||
|
||||
if not channel:
|
||||
# 创建一个默认渠道
|
||||
channel = Channel(
|
||||
name=f"渠道-{email.split('@')[0]}",
|
||||
email=email,
|
||||
password_hash=user.password_hash,
|
||||
commission_rate=10.0,
|
||||
channel_credit=0.0,
|
||||
custom_agent_cpu=2.0,
|
||||
custom_agent_memory=4.0,
|
||||
status="active"
|
||||
)
|
||||
db.add(channel)
|
||||
await db.flush()
|
||||
|
||||
# 更新用户的channel_id
|
||||
user.channel_id = channel.id
|
||||
await db.commit()
|
||||
await db.refresh(user)
|
||||
channel_id = channel.id
|
||||
|
||||
token = create_access_token({
|
||||
"sub": str(user.id),
|
||||
"email": email,
|
||||
"role": "channel_admin",
|
||||
"channelId": str(channel_id)
|
||||
})
|
||||
return {"token": token, "tokenType": "bearer", "email": email, "expiresIn": 60 * 60}
|
||||
|
||||
|
||||
|
||||
@@ -86,7 +86,16 @@ class AgentActivity(BaseModel):
|
||||
|
||||
class GatewaySelectRequest(BaseModel):
|
||||
"""选择网关请求"""
|
||||
gatewayType: str = Field(..., pattern="^(MCP|A2A|API)$")
|
||||
gatewayType: str
|
||||
|
||||
@validator('gatewayType')
|
||||
def validate_gateway_type(cls, v):
|
||||
"""验证并转换gateway类型为大写"""
|
||||
if v:
|
||||
v = v.upper()
|
||||
if v not in {'MCP', 'A2A', 'API'}:
|
||||
raise ValueError('gatewayType must be MCP, A2A, or API')
|
||||
return v
|
||||
|
||||
|
||||
class CreateAPIRequest(BaseModel):
|
||||
@@ -100,13 +109,22 @@ class GenerateToolRequest(BaseModel):
|
||||
"""生成工具请求"""
|
||||
name: str
|
||||
description: str
|
||||
frameworkTemplate: str = Field(..., pattern="^(MCP|A2A|API)$")
|
||||
frameworkTemplate: str
|
||||
gateway: str
|
||||
agentCount: int
|
||||
cpu: float
|
||||
memory: float
|
||||
maxScale: int
|
||||
model: str
|
||||
|
||||
@validator('frameworkTemplate', 'gateway')
|
||||
def validate_gateway_fields(cls, v):
|
||||
"""验证并转换gateway相关字段为大写"""
|
||||
if v:
|
||||
v = v.upper()
|
||||
if v not in {'MCP', 'A2A', 'API'}:
|
||||
raise ValueError('Must be MCP, A2A, or API')
|
||||
return v
|
||||
|
||||
|
||||
class CreateDataTemplateRequest(BaseModel):
|
||||
@@ -132,7 +150,16 @@ class DeployAgentRequest(BaseModel):
|
||||
agentId: str
|
||||
instances: int
|
||||
model: str
|
||||
gateway: str = Field(..., pattern="^(MCP|A2A|API)$")
|
||||
gateway: str
|
||||
|
||||
@validator('gateway')
|
||||
def validate_gateway(cls, v):
|
||||
"""验证并转换gateway为大写"""
|
||||
if v:
|
||||
v = v.upper()
|
||||
if v not in {'MCP', 'A2A', 'API'}:
|
||||
raise ValueError('gateway must be MCP, A2A, or API')
|
||||
return v
|
||||
|
||||
|
||||
class WorkflowNode(BaseModel):
|
||||
@@ -147,9 +174,18 @@ class CreateWorkflowRequest(BaseModel):
|
||||
"""创建工作流请求"""
|
||||
name: str
|
||||
description: str
|
||||
gateway: str = Field(..., pattern="^(MCP|A2A|API)$")
|
||||
gateway: str
|
||||
nodes: List[WorkflowNode] = Field(..., max_length=3)
|
||||
|
||||
@validator('gateway')
|
||||
def validate_gateway(cls, v):
|
||||
"""验证并转换gateway为大写"""
|
||||
if v:
|
||||
v = v.upper()
|
||||
if v not in {'MCP', 'A2A', 'API'}:
|
||||
raise ValueError('gateway must be MCP, A2A, or API')
|
||||
return v
|
||||
|
||||
@validator('nodes')
|
||||
def validate_nodes(cls, v):
|
||||
if len(v) > 3:
|
||||
|
||||
@@ -10,13 +10,37 @@ from sqlalchemy.orm import sessionmaker
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.engine import make_url
|
||||
import logging
|
||||
from urllib.parse import urlparse, parse_qs, urlencode, urlunparse
|
||||
|
||||
from config import settings
|
||||
from models import Base
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
database_url = settings.database_url
|
||||
|
||||
def prepare_database_url(url: str) -> str:
|
||||
"""
|
||||
处理数据库 URL,移除 asyncpg 不支持的参数(如 sslmode)
|
||||
"""
|
||||
if not url or "asyncpg" not in url:
|
||||
return url
|
||||
|
||||
parsed = urlparse(url)
|
||||
query_params = parse_qs(parsed.query)
|
||||
|
||||
# 移除 sslmode 参数(asyncpg 不支持,需要用 ssl connect_args 替代)
|
||||
if "sslmode" in query_params:
|
||||
del query_params["sslmode"]
|
||||
|
||||
# 重新构建查询字符串
|
||||
new_query = urlencode(query_params, doseq=True)
|
||||
new_parsed = parsed._replace(query=new_query)
|
||||
|
||||
return urlunparse(new_parsed)
|
||||
|
||||
|
||||
# 处理数据库 URL
|
||||
database_url = prepare_database_url(settings.database_url)
|
||||
database_url_obj = make_url(database_url)
|
||||
|
||||
engine_kwargs = {
|
||||
@@ -33,7 +57,9 @@ else:
|
||||
"pool_recycle": 3600,
|
||||
})
|
||||
|
||||
if database_url_obj.host and database_url_obj.host.endswith("postgres.database.azure.com"):
|
||||
# Azure Database for PostgreSQL 或任何包含 sslmode 的连接都需要 TLS
|
||||
if (database_url_obj.host and database_url_obj.host.endswith("postgres.database.azure.com")) or \
|
||||
"sslmode" in settings.database_url:
|
||||
# Azure Database for PostgreSQL requires TLS; provide a default SSL context.
|
||||
ssl_context = ssl.create_default_context()
|
||||
existing_connect_args = engine_kwargs.get("connect_args") or {}
|
||||
|
||||
@@ -9,6 +9,7 @@ pydantic-settings==2.1.0
|
||||
# 数据库
|
||||
sqlalchemy==2.0.25
|
||||
asyncpg==0.29.0
|
||||
aiosqlite==0.19.0
|
||||
alembic==1.13.1
|
||||
psycopg2-binary==2.9.9
|
||||
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
# 数据库管理脚本
|
||||
|
||||
本目录包含 taiji-AI-PAD 项目的数据库管理脚本。
|
||||
|
||||
## 📁 脚本列表
|
||||
|
||||
| 脚本 | 说明 |
|
||||
|------|------|
|
||||
| `init_database.py` | 数据库初始化脚本,创建表结构和初始数据 |
|
||||
| `verify_database.py` | 数据库验证脚本,检查数据库状态和数据完整性 |
|
||||
| `init_test_accounts.py` | 创建测试账号脚本 |
|
||||
| `check_accounts.py` | 检查账号配置脚本(无需数据库连接) |
|
||||
|
||||
---
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 1. 准备环境
|
||||
|
||||
```bash
|
||||
cd services/mcp-server
|
||||
|
||||
# 创建虚拟环境(如果没有)
|
||||
python3 -m venv venv
|
||||
|
||||
# 激活虚拟环境
|
||||
source venv/bin/activate # Linux/Mac
|
||||
# 或 venv\Scripts\activate # Windows
|
||||
|
||||
# 安装依赖
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### 2. 初始化数据库
|
||||
|
||||
```bash
|
||||
python scripts/init_database.py
|
||||
```
|
||||
|
||||
**可选参数:**
|
||||
- `--skip-sample-data`: 只创建表结构,不创建初始数据
|
||||
- `--force`: 强制重新创建所有数据(⚠️ 会删除现有数据)
|
||||
|
||||
### 3. 验证数据库
|
||||
|
||||
```bash
|
||||
python scripts/verify_database.py
|
||||
```
|
||||
|
||||
**可选参数:**
|
||||
- `-v, --verbose`: 显示详细信息
|
||||
- `--json`: 以JSON格式输出结果
|
||||
|
||||
---
|
||||
|
||||
## 👤 初始账户
|
||||
|
||||
### 管理员账户
|
||||
|
||||
| 角色 | 邮箱 | 密码 |
|
||||
|------|------|------|
|
||||
| 超级管理员 | superadmin@taiji-ai.com | Admin@123456 |
|
||||
| 系统管理员 | admin@taiji-ai.com | Admin@123456 |
|
||||
|
||||
### 测试用户
|
||||
|
||||
| 名称 | 邮箱 | 密码 | 订阅级别 |
|
||||
|------|------|------|----------|
|
||||
| 测试用户1 | testuser1@taiji-ai.com | Test@123456 | basic |
|
||||
| 测试用户2 | testuser2@taiji-ai.com | Test@123456 | professional |
|
||||
|
||||
---
|
||||
|
||||
## 🔐 登录测试
|
||||
|
||||
### 超级管理员登录
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8000/api/auth/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"email":"superadmin@taiji-ai.com","password":"Admin@123456"}'
|
||||
```
|
||||
|
||||
### 测试用户登录
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8000/api/auth/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"email":"testuser1@taiji-ai.com","password":"Test@123456"}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 创建的初始数据
|
||||
|
||||
### 渠道
|
||||
- 默认渠道 (default-channel@taiji-ai.com)
|
||||
|
||||
### 工具 (5个)
|
||||
| 名称 | 类别 | 说明 |
|
||||
|------|------|------|
|
||||
| web_search | api | 网络搜索工具 |
|
||||
| text_completion | llm | 文本补全工具 |
|
||||
| weather_api | api | 天气查询API |
|
||||
| code_executor | sandbox | 代码执行工具 |
|
||||
| document_parser | integration | 文档解析工具 |
|
||||
|
||||
### 平台Agent (3个)
|
||||
| 名称 | 类别 | 说明 |
|
||||
|------|------|------|
|
||||
| 通用助手 | general | 通用AI助手 |
|
||||
| 代码助手 | development | 专业代码助手 |
|
||||
| 数据分析师 | analytics | 数据分析专家 |
|
||||
|
||||
---
|
||||
|
||||
## 📋 数据库表结构
|
||||
|
||||
初始化脚本会创建以下表:
|
||||
|
||||
- `users` - 用户表
|
||||
- `channels` - 渠道表
|
||||
- `agents` - Agent表
|
||||
- `tools` - 工具表
|
||||
- `sessions` - 会话表
|
||||
- `executions` - 执行记录表
|
||||
- `api_keys` - API密钥表
|
||||
- `billing` - 计费详情表
|
||||
- `balances` - 用户余额表
|
||||
- `billing_records` - 计费记录表
|
||||
- `recharge_records` - 充值记录表
|
||||
- `audit_logs` - 审计日志表
|
||||
- `model_providers` - 模型供应商表
|
||||
- `resource_allocations` - 资源分配表
|
||||
- `applications` - 申请审批表
|
||||
- `workflows` - 工作流表
|
||||
- `channel_agent_quotas` - 渠道Agent配额表
|
||||
- `provider_models` - 模型提供商表
|
||||
- `gateway_apis` - 网关API表
|
||||
- `data_templates` - 数据模板表
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 注意事项
|
||||
|
||||
1. **首次运行**: 确保数据库连接配置正确(检查 `config.py` 或环境变量)
|
||||
2. **生产环境**: 不要在生产环境使用 `--force` 参数
|
||||
3. **密码安全**: 生产环境请修改默认密码
|
||||
4. **API密钥**: 初始化时生成的API密钥只显示一次,请妥善保管
|
||||
|
||||
---
|
||||
|
||||
## 🔧 故障排除
|
||||
|
||||
### 连接失败
|
||||
|
||||
1. 检查数据库服务是否运行
|
||||
2. 验证数据库连接字符串
|
||||
3. 确认网络连接正常
|
||||
|
||||
### 表已存在
|
||||
|
||||
脚本默认不会覆盖已存在的数据,如需重新初始化:
|
||||
```bash
|
||||
python scripts/init_database.py --force
|
||||
```
|
||||
|
||||
### 依赖问题
|
||||
|
||||
```bash
|
||||
pip install --upgrade -r requirements.txt
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**版本**: v1.0
|
||||
**更新日期**: 2025年12月25日
|
||||
|
||||
@@ -0,0 +1,674 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
数据库初始化脚本
|
||||
|
||||
功能:
|
||||
1. 创建所有数据库表(基于 models.py 中的定义)
|
||||
2. 创建初始管理员用户
|
||||
3. 创建默认渠道
|
||||
4. 创建示例工具
|
||||
5. 创建测试用户
|
||||
|
||||
使用方法:
|
||||
cd services/mcp-server
|
||||
python scripts/init_database.py
|
||||
|
||||
可选参数:
|
||||
--skip-sample-data 只创建表结构,不创建初始数据
|
||||
--force 强制重新创建所有数据(会删除现有数据)
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
import os
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
import logging
|
||||
|
||||
# 添加项目根目录到Python路径
|
||||
project_root = Path(__file__).parent.parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, text
|
||||
from passlib.context import CryptContext
|
||||
import uuid
|
||||
|
||||
# 延迟导入,确保路径已添加
|
||||
from database import AsyncSessionLocal, engine, init_db, check_db_connection
|
||||
from models import (
|
||||
Base, User, Channel, Agent, Tool, Session, APIKey,
|
||||
ModelProvider, AuditLog, Balance
|
||||
)
|
||||
from config import settings
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s - %(levelname)s - %(message)s"
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
|
||||
|
||||
# ============== 初始数据定义 ==============
|
||||
|
||||
# 默认管理员账户
|
||||
DEFAULT_ADMINS = [
|
||||
{
|
||||
"name": "超级管理员",
|
||||
"email": "superadmin@taiji-ai.com",
|
||||
"password": "Admin@123456",
|
||||
"role": "super_admin",
|
||||
"subscription_tier": "enterprise",
|
||||
"balance": 100000.0,
|
||||
"credit_limit": 500000.0,
|
||||
"status": "active",
|
||||
"is_admin": True,
|
||||
"is_active": True,
|
||||
},
|
||||
{
|
||||
"name": "系统管理员",
|
||||
"email": "admin@taiji-ai.com",
|
||||
"password": "Admin@123456",
|
||||
"role": "admin",
|
||||
"subscription_tier": "enterprise",
|
||||
"balance": 50000.0,
|
||||
"credit_limit": 200000.0,
|
||||
"status": "active",
|
||||
"is_admin": True,
|
||||
"is_active": True,
|
||||
},
|
||||
]
|
||||
|
||||
# 默认渠道
|
||||
DEFAULT_CHANNELS = [
|
||||
{
|
||||
"name": "默认渠道",
|
||||
"email": "default-channel@taiji-ai.com",
|
||||
"password": "Channel@123456",
|
||||
"commission_rate": 0.1,
|
||||
"channel_credit": 100000.0,
|
||||
"custom_agent_cpu": 4.0,
|
||||
"custom_agent_memory": 8.0,
|
||||
"status": "active",
|
||||
},
|
||||
]
|
||||
|
||||
# 示例工具
|
||||
SAMPLE_TOOLS = [
|
||||
{
|
||||
"name": "web_search",
|
||||
"description": "网络搜索工具,支持全网搜索并返回相关结果",
|
||||
"category": "api",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {"type": "string", "description": "搜索查询关键词"},
|
||||
"limit": {"type": "integer", "description": "返回结果数量限制", "default": 10},
|
||||
"language": {"type": "string", "description": "结果语言", "default": "zh"}
|
||||
},
|
||||
"required": ["query"]
|
||||
},
|
||||
"endpoint": "https://api.example.com/search",
|
||||
"method": "POST",
|
||||
"auth_type": "api_key",
|
||||
"rate_limit": 100,
|
||||
"cost_per_call": 0.01,
|
||||
"is_public": True,
|
||||
"is_active": True,
|
||||
},
|
||||
{
|
||||
"name": "text_completion",
|
||||
"description": "文本补全工具,基于LLM生成文本内容",
|
||||
"category": "llm",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"prompt": {"type": "string", "description": "输入提示文本"},
|
||||
"max_tokens": {"type": "integer", "description": "最大生成token数", "default": 150},
|
||||
"temperature": {"type": "number", "description": "生成温度参数", "default": 0.7},
|
||||
"model": {"type": "string", "description": "使用的模型", "default": "gpt-4"}
|
||||
},
|
||||
"required": ["prompt"]
|
||||
},
|
||||
"rate_limit": 60,
|
||||
"cost_per_call": 0.05,
|
||||
"is_public": True,
|
||||
"is_active": True,
|
||||
},
|
||||
{
|
||||
"name": "weather_api",
|
||||
"description": "天气查询API,获取指定城市的天气信息",
|
||||
"category": "api",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"city": {"type": "string", "description": "城市名称"},
|
||||
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"], "default": "celsius"}
|
||||
},
|
||||
"required": ["city"]
|
||||
},
|
||||
"endpoint": "https://api.openweathermap.org/data/2.5/weather",
|
||||
"method": "GET",
|
||||
"auth_type": "api_key",
|
||||
"rate_limit": 1000,
|
||||
"cost_per_call": 0.001,
|
||||
"is_public": True,
|
||||
"is_active": True,
|
||||
},
|
||||
{
|
||||
"name": "code_executor",
|
||||
"description": "代码执行工具,在安全沙箱中执行代码",
|
||||
"category": "sandbox",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": {"type": "string", "description": "要执行的代码"},
|
||||
"language": {"type": "string", "enum": ["python", "javascript", "bash"], "default": "python"},
|
||||
"timeout": {"type": "integer", "description": "执行超时时间(秒)", "default": 30}
|
||||
},
|
||||
"required": ["code"]
|
||||
},
|
||||
"rate_limit": 30,
|
||||
"cost_per_call": 0.1,
|
||||
"timeout": 60,
|
||||
"is_public": True,
|
||||
"is_active": True,
|
||||
},
|
||||
{
|
||||
"name": "document_parser",
|
||||
"description": "文档解析工具,支持PDF、Word等格式",
|
||||
"category": "integration",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"file_url": {"type": "string", "description": "文档URL"},
|
||||
"format": {"type": "string", "enum": ["pdf", "docx", "txt", "html"], "default": "pdf"},
|
||||
"extract_images": {"type": "boolean", "description": "是否提取图片", "default": False}
|
||||
},
|
||||
"required": ["file_url"]
|
||||
},
|
||||
"rate_limit": 50,
|
||||
"cost_per_call": 0.02,
|
||||
"is_public": True,
|
||||
"is_active": True,
|
||||
},
|
||||
]
|
||||
|
||||
# 平台Agent
|
||||
PLATFORM_AGENTS = [
|
||||
{
|
||||
"name": "通用助手",
|
||||
"type": "platform",
|
||||
"description": "通用AI助手,可处理多种任务",
|
||||
"category": "general",
|
||||
"role": "通用助手",
|
||||
"goal": "帮助用户完成各种任务,包括问答、信息检索、文本生成等",
|
||||
"config": {"max_iterations": 10, "verbose": True},
|
||||
"tools": ["web_search", "text_completion"],
|
||||
"capabilities": ["chat", "search", "summarize"],
|
||||
"cpu": 2.0,
|
||||
"memory": 4.0,
|
||||
"max_instances": 100,
|
||||
"status": "active",
|
||||
"version": "1.0.0",
|
||||
},
|
||||
{
|
||||
"name": "代码助手",
|
||||
"type": "platform",
|
||||
"description": "专业代码助手,帮助编写和调试代码",
|
||||
"category": "development",
|
||||
"role": "代码助手",
|
||||
"goal": "帮助用户编写、调试、优化代码",
|
||||
"config": {"max_iterations": 20, "verbose": True},
|
||||
"tools": ["code_executor", "text_completion"],
|
||||
"capabilities": ["code_generation", "code_review", "debugging"],
|
||||
"cpu": 4.0,
|
||||
"memory": 8.0,
|
||||
"max_instances": 50,
|
||||
"status": "active",
|
||||
"version": "1.0.0",
|
||||
},
|
||||
{
|
||||
"name": "数据分析师",
|
||||
"type": "platform",
|
||||
"description": "数据分析专家,处理数据分析任务",
|
||||
"category": "analytics",
|
||||
"role": "数据分析师",
|
||||
"goal": "帮助用户进行数据分析、可视化和报告生成",
|
||||
"config": {"max_iterations": 15, "verbose": True},
|
||||
"tools": ["code_executor", "document_parser"],
|
||||
"capabilities": ["data_analysis", "visualization", "reporting"],
|
||||
"cpu": 4.0,
|
||||
"memory": 8.0,
|
||||
"max_instances": 30,
|
||||
"status": "active",
|
||||
"version": "1.0.0",
|
||||
},
|
||||
]
|
||||
|
||||
# 测试用户
|
||||
TEST_USERS = [
|
||||
{
|
||||
"name": "测试用户1",
|
||||
"email": "testuser1@taiji-ai.com",
|
||||
"password": "Test@123456",
|
||||
"role": "user",
|
||||
"subscription_tier": "basic",
|
||||
"balance": 1000.0,
|
||||
"credit_limit": 5000.0,
|
||||
"status": "active",
|
||||
},
|
||||
{
|
||||
"name": "测试用户2",
|
||||
"email": "testuser2@taiji-ai.com",
|
||||
"password": "Test@123456",
|
||||
"role": "user",
|
||||
"subscription_tier": "professional",
|
||||
"balance": 5000.0,
|
||||
"credit_limit": 20000.0,
|
||||
"status": "active",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
# ============== 数据库操作函数 ==============
|
||||
|
||||
async def create_tables():
|
||||
"""创建所有数据库表"""
|
||||
logger.info("正在创建数据库表...")
|
||||
|
||||
try:
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
logger.info("✅ 数据库表创建成功")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"❌ 创建数据库表失败: {e}")
|
||||
return False
|
||||
|
||||
|
||||
async def drop_all_tables():
|
||||
"""删除所有数据库表(危险操作!)"""
|
||||
logger.warning("⚠️ 正在删除所有数据库表...")
|
||||
|
||||
try:
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.drop_all)
|
||||
logger.info("✅ 所有表已删除")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"❌ 删除表失败: {e}")
|
||||
return False
|
||||
|
||||
|
||||
async def create_channels(session: AsyncSession) -> list:
|
||||
"""创建默认渠道"""
|
||||
logger.info("正在创建默认渠道...")
|
||||
created = []
|
||||
|
||||
for channel_data in DEFAULT_CHANNELS:
|
||||
# 检查是否已存在
|
||||
result = await session.execute(
|
||||
select(Channel).where(Channel.email == channel_data["email"])
|
||||
)
|
||||
existing = result.scalar_one_or_none()
|
||||
|
||||
if existing:
|
||||
logger.info(f" 渠道 '{channel_data['name']}' 已存在,跳过")
|
||||
created.append(existing)
|
||||
continue
|
||||
|
||||
password = channel_data.pop("password")
|
||||
channel = Channel(
|
||||
**channel_data,
|
||||
password_hash=pwd_context.hash(password)
|
||||
)
|
||||
session.add(channel)
|
||||
created.append(channel)
|
||||
logger.info(f" ✅ 创建渠道: {channel_data['name']}")
|
||||
|
||||
await session.flush()
|
||||
return created
|
||||
|
||||
|
||||
async def create_admins(session: AsyncSession, channels: list) -> list:
|
||||
"""创建管理员用户"""
|
||||
logger.info("正在创建管理员用户...")
|
||||
created = []
|
||||
|
||||
for admin_data in DEFAULT_ADMINS:
|
||||
# 检查是否已存在
|
||||
result = await session.execute(
|
||||
select(User).where(User.email == admin_data["email"])
|
||||
)
|
||||
existing = result.scalar_one_or_none()
|
||||
|
||||
if existing:
|
||||
logger.info(f" 用户 '{admin_data['name']}' 已存在,跳过")
|
||||
created.append(existing)
|
||||
continue
|
||||
|
||||
password = admin_data.pop("password")
|
||||
user = User(
|
||||
**admin_data,
|
||||
password_hash=pwd_context.hash(password),
|
||||
channel_id=channels[0].id if channels else None
|
||||
)
|
||||
session.add(user)
|
||||
created.append(user)
|
||||
logger.info(f" ✅ 创建管理员: {admin_data['name']} ({admin_data['role']})")
|
||||
|
||||
await session.flush()
|
||||
return created
|
||||
|
||||
|
||||
async def create_tools(session: AsyncSession) -> list:
|
||||
"""创建示例工具"""
|
||||
logger.info("正在创建示例工具...")
|
||||
created = []
|
||||
|
||||
for tool_data in SAMPLE_TOOLS:
|
||||
# 检查是否已存在
|
||||
result = await session.execute(
|
||||
select(Tool).where(Tool.name == tool_data["name"])
|
||||
)
|
||||
existing = result.scalar_one_or_none()
|
||||
|
||||
if existing:
|
||||
logger.info(f" 工具 '{tool_data['name']}' 已存在,跳过")
|
||||
created.append(existing)
|
||||
continue
|
||||
|
||||
tool = Tool(**tool_data)
|
||||
session.add(tool)
|
||||
created.append(tool)
|
||||
logger.info(f" ✅ 创建工具: {tool_data['name']} ({tool_data['category']})")
|
||||
|
||||
await session.flush()
|
||||
return created
|
||||
|
||||
|
||||
async def create_platform_agents(session: AsyncSession, admins: list) -> list:
|
||||
"""创建平台Agent"""
|
||||
logger.info("正在创建平台Agent...")
|
||||
created = []
|
||||
|
||||
# 使用第一个管理员作为owner
|
||||
owner = admins[0] if admins else None
|
||||
|
||||
for agent_data in PLATFORM_AGENTS:
|
||||
# 检查是否已存在
|
||||
result = await session.execute(
|
||||
select(Agent).where(Agent.name == agent_data["name"])
|
||||
)
|
||||
existing = result.scalar_one_or_none()
|
||||
|
||||
if existing:
|
||||
logger.info(f" Agent '{agent_data['name']}' 已存在,跳过")
|
||||
created.append(existing)
|
||||
continue
|
||||
|
||||
agent = Agent(
|
||||
**agent_data,
|
||||
owner_id=owner.id if owner else None
|
||||
)
|
||||
session.add(agent)
|
||||
created.append(agent)
|
||||
logger.info(f" ✅ 创建Agent: {agent_data['name']} ({agent_data['type']})")
|
||||
|
||||
await session.flush()
|
||||
return created
|
||||
|
||||
|
||||
async def create_test_users(session: AsyncSession, channels: list) -> list:
|
||||
"""创建测试用户"""
|
||||
logger.info("正在创建测试用户...")
|
||||
created = []
|
||||
|
||||
for user_data in TEST_USERS:
|
||||
# 检查是否已存在
|
||||
result = await session.execute(
|
||||
select(User).where(User.email == user_data["email"])
|
||||
)
|
||||
existing = result.scalar_one_or_none()
|
||||
|
||||
if existing:
|
||||
logger.info(f" 用户 '{user_data['name']}' 已存在,跳过")
|
||||
created.append(existing)
|
||||
continue
|
||||
|
||||
password = user_data.pop("password")
|
||||
user = User(
|
||||
**user_data,
|
||||
password_hash=pwd_context.hash(password),
|
||||
channel_id=channels[0].id if channels else None
|
||||
)
|
||||
session.add(user)
|
||||
created.append(user)
|
||||
logger.info(f" ✅ 创建用户: {user_data['name']} ({user_data['role']})")
|
||||
|
||||
await session.flush()
|
||||
return created
|
||||
|
||||
|
||||
async def create_api_keys(session: AsyncSession, users: list) -> list:
|
||||
"""为管理员创建API密钥"""
|
||||
logger.info("正在创建API密钥...")
|
||||
created = []
|
||||
|
||||
for user in users[:2]: # 只为前两个管理员创建
|
||||
# 检查是否已有API密钥
|
||||
result = await session.execute(
|
||||
select(APIKey).where(APIKey.user_id == user.id)
|
||||
)
|
||||
existing = result.scalar_one_or_none()
|
||||
|
||||
if existing:
|
||||
logger.info(f" 用户 '{user.name}' 已有API密钥,跳过")
|
||||
continue
|
||||
|
||||
# 生成API密钥 - 注意 api_key_prefix 字段只有 10 字符
|
||||
api_key = f"sk-{uuid.uuid4().hex[:28]}" # 总长度32字符
|
||||
api_key_hash = pwd_context.hash(api_key)
|
||||
api_key_prefix = api_key[:10] # 保留前10字符作为前缀
|
||||
|
||||
key = APIKey(
|
||||
user_id=user.id,
|
||||
api_key_hash=api_key_hash,
|
||||
api_key_prefix=api_key_prefix,
|
||||
name=f"{user.name}的API密钥",
|
||||
is_active=True,
|
||||
)
|
||||
session.add(key)
|
||||
created.append((key, api_key, user.name))
|
||||
logger.info(f" ✅ 为 {user.name} 创建API密钥")
|
||||
|
||||
await session.flush()
|
||||
return created
|
||||
|
||||
|
||||
async def log_audit(session: AsyncSession, action: str, details: dict):
|
||||
"""记录审计日志"""
|
||||
try:
|
||||
audit = AuditLog(
|
||||
action=action,
|
||||
resource_type="system",
|
||||
resource_id="init",
|
||||
details=details,
|
||||
success=True,
|
||||
)
|
||||
session.add(audit)
|
||||
await session.flush()
|
||||
except Exception as e:
|
||||
logger.warning(f"记录审计日志失败: {e}")
|
||||
|
||||
|
||||
# ============== 主函数 ==============
|
||||
|
||||
async def init_database(skip_sample_data: bool = False, force: bool = False):
|
||||
"""初始化数据库的主函数"""
|
||||
|
||||
print("=" * 70)
|
||||
print(" taiji-AI-PAD 数据库初始化工具")
|
||||
print("=" * 70)
|
||||
print(f" 时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
print(f" 数据库: {settings.database_url[:50]}...")
|
||||
print("=" * 70)
|
||||
print()
|
||||
|
||||
# 1. 检查数据库连接
|
||||
logger.info("步骤 1/7: 检查数据库连接...")
|
||||
if not await check_db_connection():
|
||||
logger.error("❌ 数据库连接失败!请检查数据库配置和网络连接。")
|
||||
return False
|
||||
logger.info("✅ 数据库连接正常")
|
||||
print()
|
||||
|
||||
# 2. 是否强制重新创建
|
||||
if force:
|
||||
logger.warning("⚠️ 强制模式:将删除所有现有数据!")
|
||||
confirm = input("确认删除所有数据?(输入 'yes' 确认): ")
|
||||
if confirm.lower() != 'yes':
|
||||
logger.info("操作已取消")
|
||||
return False
|
||||
await drop_all_tables()
|
||||
print()
|
||||
|
||||
# 3. 创建数据库表
|
||||
logger.info("步骤 2/7: 创建数据库表...")
|
||||
if not await create_tables():
|
||||
return False
|
||||
print()
|
||||
|
||||
if skip_sample_data:
|
||||
logger.info("⏭️ 跳过初始数据创建(--skip-sample-data)")
|
||||
return True
|
||||
|
||||
# 4-7. 创建初始数据
|
||||
async with AsyncSessionLocal() as session:
|
||||
try:
|
||||
# 4. 创建渠道
|
||||
logger.info("步骤 3/7: 创建默认渠道...")
|
||||
channels = await create_channels(session)
|
||||
print()
|
||||
|
||||
# 5. 创建管理员
|
||||
logger.info("步骤 4/7: 创建管理员用户...")
|
||||
admins = await create_admins(session, channels)
|
||||
print()
|
||||
|
||||
# 6. 创建工具
|
||||
logger.info("步骤 5/7: 创建示例工具...")
|
||||
tools = await create_tools(session)
|
||||
print()
|
||||
|
||||
# 7. 创建平台Agent
|
||||
logger.info("步骤 6/7: 创建平台Agent...")
|
||||
agents = await create_platform_agents(session, admins)
|
||||
print()
|
||||
|
||||
# 8. 创建测试用户
|
||||
logger.info("步骤 7/7: 创建测试用户和API密钥...")
|
||||
test_users = await create_test_users(session, channels)
|
||||
api_keys = await create_api_keys(session, admins)
|
||||
print()
|
||||
|
||||
# 记录审计日志
|
||||
await log_audit(session, "database_init", {
|
||||
"channels_created": len([c for c in channels if c.id]),
|
||||
"admins_created": len(admins),
|
||||
"tools_created": len(tools),
|
||||
"agents_created": len(agents),
|
||||
"test_users_created": len(test_users),
|
||||
})
|
||||
|
||||
# 提交所有更改
|
||||
await session.commit()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ 创建初始数据失败: {e}")
|
||||
await session.rollback()
|
||||
return False
|
||||
|
||||
# 打印结果摘要
|
||||
print()
|
||||
print("=" * 70)
|
||||
print(" ✅ 数据库初始化完成!")
|
||||
print("=" * 70)
|
||||
print()
|
||||
|
||||
# 打印管理员账户信息
|
||||
print("【管理员账户】")
|
||||
print("-" * 70)
|
||||
print(f"{'角色':<15} {'邮箱':<35} {'密码':<15}")
|
||||
print("-" * 70)
|
||||
for admin in DEFAULT_ADMINS:
|
||||
print(f"{admin['role']:<15} {admin['email']:<35} Admin@123456")
|
||||
print()
|
||||
|
||||
# 打印API密钥
|
||||
if api_keys:
|
||||
print("【API密钥】")
|
||||
print("-" * 70)
|
||||
for key, api_key, user_name in api_keys:
|
||||
print(f"{user_name}: {api_key}")
|
||||
print("-" * 70)
|
||||
print("⚠️ 请妥善保管API密钥,密钥只显示一次!")
|
||||
print()
|
||||
|
||||
# 打印测试用户
|
||||
print("【测试用户】")
|
||||
print("-" * 70)
|
||||
for user in TEST_USERS:
|
||||
print(f"{user['name']}: {user['email']} / Test@123456")
|
||||
print()
|
||||
|
||||
# 打印登录测试命令
|
||||
print("【登录测试命令】")
|
||||
print("-" * 70)
|
||||
print("# 超级管理员登录")
|
||||
print('curl -X POST http://localhost:8000/api/auth/login \\')
|
||||
print(' -H "Content-Type: application/json" \\')
|
||||
print(' -d \'{"email":"superadmin@taiji-ai.com","password":"Admin@123456"}\'')
|
||||
print()
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def main():
|
||||
"""命令行入口"""
|
||||
parser = argparse.ArgumentParser(description="taiji-AI-PAD 数据库初始化工具")
|
||||
parser.add_argument(
|
||||
"--skip-sample-data",
|
||||
action="store_true",
|
||||
help="只创建表结构,不创建初始数据"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--force",
|
||||
action="store_true",
|
||||
help="强制重新创建所有数据(会删除现有数据)"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
success = asyncio.run(init_database(
|
||||
skip_sample_data=args.skip_sample_data,
|
||||
force=args.force
|
||||
))
|
||||
sys.exit(0 if success else 1)
|
||||
except KeyboardInterrupt:
|
||||
print("\n操作已取消")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
logger.error(f"初始化失败: {e}", exc_info=True)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -0,0 +1,582 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
数据库验证脚本
|
||||
|
||||
功能:
|
||||
1. 验证数据库连接
|
||||
2. 验证所有表结构是否正确创建
|
||||
3. 验证初始数据是否存在
|
||||
4. 验证用户登录功能
|
||||
5. 生成验证报告
|
||||
|
||||
使用方法:
|
||||
cd services/mcp-server
|
||||
python scripts/verify_database.py
|
||||
|
||||
可选参数:
|
||||
--verbose, -v 显示详细信息
|
||||
--json 以JSON格式输出结果
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
import os
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
import logging
|
||||
|
||||
# 添加项目根目录到Python路径
|
||||
project_root = Path(__file__).parent.parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, text, inspect
|
||||
from passlib.context import CryptContext
|
||||
|
||||
# 延迟导入
|
||||
from database import AsyncSessionLocal, engine, check_db_connection
|
||||
from models import (
|
||||
Base, User, Channel, Agent, Tool, Session, APIKey,
|
||||
Execution, Billing, AuditLog, Balance, ModelProvider,
|
||||
ResourceAllocation, Application, Workflow, BillingRecord,
|
||||
RechargeRecord, ChannelAgentQuota, ProviderModel,
|
||||
GatewayAPI, DataTemplate
|
||||
)
|
||||
from config import settings
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s - %(levelname)s - %(message)s"
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
|
||||
|
||||
# ============== 验证结果类 ==============
|
||||
|
||||
class VerificationResult:
|
||||
"""验证结果封装"""
|
||||
|
||||
def __init__(self):
|
||||
self.checks = []
|
||||
self.passed = 0
|
||||
self.failed = 0
|
||||
self.warnings = 0
|
||||
|
||||
def add_check(self, name: str, passed: bool, message: str = "", warning: bool = False):
|
||||
"""添加检查结果"""
|
||||
status = "✅" if passed else ("⚠️" if warning else "❌")
|
||||
self.checks.append({
|
||||
"name": name,
|
||||
"passed": passed,
|
||||
"warning": warning,
|
||||
"message": message,
|
||||
"status": status
|
||||
})
|
||||
|
||||
if passed:
|
||||
self.passed += 1
|
||||
elif warning:
|
||||
self.warnings += 1
|
||||
else:
|
||||
self.failed += 1
|
||||
|
||||
def summary(self) -> dict:
|
||||
"""返回摘要"""
|
||||
return {
|
||||
"total": len(self.checks),
|
||||
"passed": self.passed,
|
||||
"failed": self.failed,
|
||||
"warnings": self.warnings,
|
||||
"success_rate": f"{(self.passed / len(self.checks) * 100):.1f}%" if self.checks else "N/A"
|
||||
}
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""转换为字典"""
|
||||
return {
|
||||
"summary": self.summary(),
|
||||
"checks": self.checks,
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
def print_report(self, verbose: bool = False):
|
||||
"""打印报告"""
|
||||
print()
|
||||
print("=" * 70)
|
||||
print(" 数据库验证报告")
|
||||
print("=" * 70)
|
||||
print(f" 时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
print(f" 数据库: {settings.database_url[:50]}...")
|
||||
print("=" * 70)
|
||||
print()
|
||||
|
||||
for check in self.checks:
|
||||
if verbose or not check["passed"] or check["warning"]:
|
||||
print(f" {check['status']} {check['name']}")
|
||||
if check["message"]:
|
||||
print(f" {check['message']}")
|
||||
|
||||
print()
|
||||
print("-" * 70)
|
||||
summary = self.summary()
|
||||
print(f" 总计: {summary['total']} 项检查")
|
||||
print(f" 通过: {summary['passed']} ✅")
|
||||
print(f" 警告: {summary['warnings']} ⚠️")
|
||||
print(f" 失败: {summary['failed']} ❌")
|
||||
print(f" 成功率: {summary['success_rate']}")
|
||||
print("-" * 70)
|
||||
|
||||
|
||||
# ============== 验证函数 ==============
|
||||
|
||||
async def verify_connection(result: VerificationResult):
|
||||
"""验证数据库连接"""
|
||||
try:
|
||||
connected = await check_db_connection()
|
||||
result.add_check(
|
||||
"数据库连接",
|
||||
connected,
|
||||
"连接正常" if connected else "无法连接到数据库"
|
||||
)
|
||||
return connected
|
||||
except Exception as e:
|
||||
result.add_check("数据库连接", False, str(e))
|
||||
return False
|
||||
|
||||
|
||||
async def verify_tables(result: VerificationResult):
|
||||
"""验证表结构"""
|
||||
expected_tables = [
|
||||
"users", "channels", "agents", "tools", "sessions",
|
||||
"executions", "api_keys", "billing", "audit_logs",
|
||||
"balances", "model_providers", "resource_allocations",
|
||||
"applications", "workflows", "billing_records",
|
||||
"recharge_records", "channel_agent_quotas", "provider_models",
|
||||
"gateway_apis", "data_templates"
|
||||
]
|
||||
|
||||
try:
|
||||
async with engine.connect() as conn:
|
||||
# 获取实际存在的表
|
||||
def get_tables(connection):
|
||||
inspector = inspect(connection)
|
||||
return inspector.get_table_names()
|
||||
|
||||
actual_tables = await conn.run_sync(get_tables)
|
||||
|
||||
# 检查每个预期的表
|
||||
for table in expected_tables:
|
||||
exists = table in actual_tables
|
||||
result.add_check(
|
||||
f"表 '{table}'",
|
||||
exists,
|
||||
"存在" if exists else "不存在",
|
||||
warning=not exists
|
||||
)
|
||||
|
||||
# 检查是否有额外的表(可能是LiteLLM等创建的)
|
||||
extra_tables = set(actual_tables) - set(expected_tables)
|
||||
if extra_tables:
|
||||
result.add_check(
|
||||
"额外的表",
|
||||
True,
|
||||
f"发现额外表: {', '.join(extra_tables)}",
|
||||
warning=True
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
result.add_check("表结构检查", False, str(e))
|
||||
return False
|
||||
|
||||
|
||||
async def verify_admin_users(result: VerificationResult):
|
||||
"""验证管理员用户"""
|
||||
admin_emails = [
|
||||
"superadmin@taiji-ai.com",
|
||||
"admin@taiji-ai.com"
|
||||
]
|
||||
|
||||
try:
|
||||
async with AsyncSessionLocal() as session:
|
||||
for email in admin_emails:
|
||||
query = select(User).where(User.email == email)
|
||||
res = await session.execute(query)
|
||||
user = res.scalar_one_or_none()
|
||||
|
||||
if user:
|
||||
result.add_check(
|
||||
f"管理员 '{email}'",
|
||||
True,
|
||||
f"存在, 角色: {user.role}, 状态: {user.status}"
|
||||
)
|
||||
else:
|
||||
result.add_check(
|
||||
f"管理员 '{email}'",
|
||||
False,
|
||||
"用户不存在"
|
||||
)
|
||||
|
||||
# 统计管理员数量
|
||||
query = select(User).where(User.role.in_(["super_admin", "admin"]))
|
||||
res = await session.execute(query)
|
||||
admins = res.scalars().all()
|
||||
|
||||
result.add_check(
|
||||
"管理员数量",
|
||||
len(admins) >= 1,
|
||||
f"共 {len(admins)} 个管理员"
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
result.add_check("管理员用户检查", False, str(e))
|
||||
return False
|
||||
|
||||
|
||||
async def verify_channels(result: VerificationResult):
|
||||
"""验证渠道"""
|
||||
try:
|
||||
async with AsyncSessionLocal() as session:
|
||||
query = select(Channel)
|
||||
res = await session.execute(query)
|
||||
channels = res.scalars().all()
|
||||
|
||||
if channels:
|
||||
result.add_check(
|
||||
"默认渠道",
|
||||
True,
|
||||
f"共 {len(channels)} 个渠道"
|
||||
)
|
||||
|
||||
# 验证渠道状态
|
||||
active_channels = [c for c in channels if c.status == "active"]
|
||||
result.add_check(
|
||||
"活跃渠道",
|
||||
len(active_channels) > 0,
|
||||
f"共 {len(active_channels)} 个活跃渠道"
|
||||
)
|
||||
else:
|
||||
result.add_check(
|
||||
"默认渠道",
|
||||
False,
|
||||
"没有渠道数据",
|
||||
warning=True
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
result.add_check("渠道检查", False, str(e))
|
||||
return False
|
||||
|
||||
|
||||
async def verify_tools(result: VerificationResult):
|
||||
"""验证工具"""
|
||||
expected_tools = ["web_search", "text_completion", "weather_api"]
|
||||
|
||||
try:
|
||||
async with AsyncSessionLocal() as session:
|
||||
query = select(Tool)
|
||||
res = await session.execute(query)
|
||||
tools = res.scalars().all()
|
||||
|
||||
if tools:
|
||||
result.add_check(
|
||||
"示例工具",
|
||||
True,
|
||||
f"共 {len(tools)} 个工具"
|
||||
)
|
||||
|
||||
# 检查每个预期的工具
|
||||
tool_names = [t.name for t in tools]
|
||||
for tool_name in expected_tools:
|
||||
exists = tool_name in tool_names
|
||||
if not exists:
|
||||
result.add_check(
|
||||
f"工具 '{tool_name}'",
|
||||
False,
|
||||
"工具不存在",
|
||||
warning=True
|
||||
)
|
||||
else:
|
||||
result.add_check(
|
||||
"示例工具",
|
||||
False,
|
||||
"没有工具数据",
|
||||
warning=True
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
result.add_check("工具检查", False, str(e))
|
||||
return False
|
||||
|
||||
|
||||
async def verify_agents(result: VerificationResult):
|
||||
"""验证Agent"""
|
||||
try:
|
||||
async with AsyncSessionLocal() as session:
|
||||
query = select(Agent)
|
||||
res = await session.execute(query)
|
||||
agents = res.scalars().all()
|
||||
|
||||
if agents:
|
||||
result.add_check(
|
||||
"平台Agent",
|
||||
True,
|
||||
f"共 {len(agents)} 个Agent"
|
||||
)
|
||||
|
||||
# 统计类型
|
||||
platform_agents = [a for a in agents if a.type == "platform"]
|
||||
custom_agents = [a for a in agents if a.type == "custom"]
|
||||
|
||||
result.add_check(
|
||||
"Agent类型分布",
|
||||
True,
|
||||
f"平台: {len(platform_agents)}, 自定义: {len(custom_agents)}"
|
||||
)
|
||||
else:
|
||||
result.add_check(
|
||||
"平台Agent",
|
||||
False,
|
||||
"没有Agent数据",
|
||||
warning=True
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
result.add_check("Agent检查", False, str(e))
|
||||
return False
|
||||
|
||||
|
||||
async def verify_password_auth(result: VerificationResult):
|
||||
"""验证密码认证功能"""
|
||||
test_cases = [
|
||||
("superadmin@taiji-ai.com", "Admin@123456", True),
|
||||
("superadmin@taiji-ai.com", "wrong_password", False),
|
||||
]
|
||||
|
||||
try:
|
||||
async with AsyncSessionLocal() as session:
|
||||
for email, password, should_pass in test_cases:
|
||||
query = select(User).where(User.email == email)
|
||||
res = await session.execute(query)
|
||||
user = res.scalar_one_or_none()
|
||||
|
||||
if not user:
|
||||
result.add_check(
|
||||
f"密码验证 ({email})",
|
||||
False,
|
||||
"用户不存在",
|
||||
warning=True
|
||||
)
|
||||
continue
|
||||
|
||||
# 验证密码
|
||||
is_valid = pwd_context.verify(password, user.password_hash)
|
||||
expected_result = is_valid == should_pass
|
||||
|
||||
result.add_check(
|
||||
f"密码验证 ({email}, {'正确密码' if should_pass else '错误密码'})",
|
||||
expected_result,
|
||||
"验证通过" if expected_result else "验证失败"
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
result.add_check("密码认证检查", False, str(e))
|
||||
return False
|
||||
|
||||
|
||||
async def verify_data_integrity(result: VerificationResult):
|
||||
"""验证数据完整性"""
|
||||
try:
|
||||
async with AsyncSessionLocal() as session:
|
||||
# 检查用户-渠道关联
|
||||
query = select(User).where(User.channel_id.isnot(None))
|
||||
res = await session.execute(query)
|
||||
users_with_channel = res.scalars().all()
|
||||
|
||||
for user in users_with_channel:
|
||||
# 验证渠道是否存在
|
||||
channel_query = select(Channel).where(Channel.id == user.channel_id)
|
||||
channel_res = await session.execute(channel_query)
|
||||
channel = channel_res.scalar_one_or_none()
|
||||
|
||||
if not channel:
|
||||
result.add_check(
|
||||
f"用户 '{user.email}' 的渠道关联",
|
||||
False,
|
||||
f"关联的渠道 {user.channel_id} 不存在"
|
||||
)
|
||||
|
||||
result.add_check(
|
||||
"用户-渠道关联完整性",
|
||||
True,
|
||||
f"已验证 {len(users_with_channel)} 个用户的渠道关联"
|
||||
)
|
||||
|
||||
# 检查Agent-用户关联
|
||||
query = select(Agent).where(Agent.owner_id.isnot(None))
|
||||
res = await session.execute(query)
|
||||
agents_with_owner = res.scalars().all()
|
||||
|
||||
for agent in agents_with_owner:
|
||||
owner_query = select(User).where(User.id == agent.owner_id)
|
||||
owner_res = await session.execute(owner_query)
|
||||
owner = owner_res.scalar_one_or_none()
|
||||
|
||||
if not owner:
|
||||
result.add_check(
|
||||
f"Agent '{agent.name}' 的所有者关联",
|
||||
False,
|
||||
f"关联的用户 {agent.owner_id} 不存在"
|
||||
)
|
||||
|
||||
result.add_check(
|
||||
"Agent-用户关联完整性",
|
||||
True,
|
||||
f"已验证 {len(agents_with_owner)} 个Agent的所有者关联"
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
result.add_check("数据完整性检查", False, str(e))
|
||||
return False
|
||||
|
||||
|
||||
async def get_table_stats(result: VerificationResult) -> dict:
|
||||
"""获取表统计信息"""
|
||||
stats = {}
|
||||
tables = [
|
||||
("users", User),
|
||||
("channels", Channel),
|
||||
("agents", Agent),
|
||||
("tools", Tool),
|
||||
("sessions", Session),
|
||||
("api_keys", APIKey),
|
||||
]
|
||||
|
||||
try:
|
||||
async with AsyncSessionLocal() as session:
|
||||
for table_name, model in tables:
|
||||
try:
|
||||
query = select(model)
|
||||
res = await session.execute(query)
|
||||
count = len(res.scalars().all())
|
||||
stats[table_name] = count
|
||||
except Exception:
|
||||
stats[table_name] = "N/A"
|
||||
|
||||
return stats
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取统计信息失败: {e}")
|
||||
return stats
|
||||
|
||||
|
||||
# ============== 主函数 ==============
|
||||
|
||||
async def verify_database(verbose: bool = False, output_json: bool = False):
|
||||
"""验证数据库的主函数"""
|
||||
result = VerificationResult()
|
||||
|
||||
# 1. 验证连接
|
||||
if not await verify_connection(result):
|
||||
if output_json:
|
||||
print(json.dumps(result.to_dict(), indent=2, ensure_ascii=False))
|
||||
else:
|
||||
result.print_report(verbose)
|
||||
return False
|
||||
|
||||
# 2. 验证表结构
|
||||
await verify_tables(result)
|
||||
|
||||
# 3. 验证管理员用户
|
||||
await verify_admin_users(result)
|
||||
|
||||
# 4. 验证渠道
|
||||
await verify_channels(result)
|
||||
|
||||
# 5. 验证工具
|
||||
await verify_tools(result)
|
||||
|
||||
# 6. 验证Agent
|
||||
await verify_agents(result)
|
||||
|
||||
# 7. 验证密码认证
|
||||
await verify_password_auth(result)
|
||||
|
||||
# 8. 验证数据完整性
|
||||
await verify_data_integrity(result)
|
||||
|
||||
# 获取统计信息
|
||||
stats = await get_table_stats(result)
|
||||
|
||||
# 输出结果
|
||||
if output_json:
|
||||
output = result.to_dict()
|
||||
output["statistics"] = stats
|
||||
print(json.dumps(output, indent=2, ensure_ascii=False))
|
||||
else:
|
||||
result.print_report(verbose)
|
||||
|
||||
# 打印统计信息
|
||||
print()
|
||||
print("【数据统计】")
|
||||
print("-" * 70)
|
||||
for table, count in stats.items():
|
||||
print(f" {table:<20}: {count}")
|
||||
print("-" * 70)
|
||||
|
||||
return result.failed == 0
|
||||
|
||||
|
||||
def main():
|
||||
"""命令行入口"""
|
||||
parser = argparse.ArgumentParser(description="taiji-AI-PAD 数据库验证工具")
|
||||
parser.add_argument(
|
||||
"-v", "--verbose",
|
||||
action="store_true",
|
||||
help="显示详细信息"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--json",
|
||||
action="store_true",
|
||||
help="以JSON格式输出结果"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
success = asyncio.run(verify_database(
|
||||
verbose=args.verbose,
|
||||
output_json=args.json
|
||||
))
|
||||
sys.exit(0 if success else 1)
|
||||
except KeyboardInterrupt:
|
||||
print("\n操作已取消")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
if args.json:
|
||||
print(json.dumps({"error": str(e)}, indent=2))
|
||||
else:
|
||||
logger.error(f"验证失败: {e}", exc_info=True)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
Reference in New Issue
Block a user