- 新增 platform_scheduled_tasks, platform_task_logs, platform_script_vars, platform_secrets 数据库表 - 实现 ScriptSDK 提供 AI/通知/DB/HTTP/变量存储/参数获取等功能 - 实现安全的脚本执行器,支持沙箱环境和禁止危险操作 - 实现 APScheduler 调度服务,支持简单时间点和 CRON 表达式 - 新增定时任务 API 路由,包含 CRUD、执行、日志、密钥管理 - 新增定时任务前端页面,支持脚本编辑、测试运行、日志查看
This commit is contained in:
@@ -23,18 +23,6 @@ class ToolItem(BaseModel):
|
||||
path: str
|
||||
|
||||
|
||||
class ConfigSchemaItem(BaseModel):
|
||||
"""配置项定义"""
|
||||
key: str # 配置键
|
||||
label: str # 显示标签
|
||||
type: str # text | radio | select | switch
|
||||
options: Optional[List[str]] = None # radio/select 的选项值
|
||||
option_labels: Optional[dict] = None # 选项显示名称 {"value": "显示名"}
|
||||
default: Optional[str] = None # 默认值
|
||||
placeholder: Optional[str] = None # 输入提示(text类型)
|
||||
required: bool = False # 是否必填
|
||||
|
||||
|
||||
class AppCreate(BaseModel):
|
||||
"""创建应用"""
|
||||
app_code: str
|
||||
@@ -42,7 +30,6 @@ class AppCreate(BaseModel):
|
||||
base_url: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
tools: Optional[List[ToolItem]] = None
|
||||
config_schema: Optional[List[ConfigSchemaItem]] = None
|
||||
require_jssdk: bool = False
|
||||
|
||||
|
||||
@@ -52,7 +39,6 @@ class AppUpdate(BaseModel):
|
||||
base_url: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
tools: Optional[List[ToolItem]] = None
|
||||
config_schema: Optional[List[ConfigSchemaItem]] = None
|
||||
require_jssdk: Optional[bool] = None
|
||||
status: Optional[int] = None
|
||||
|
||||
@@ -133,7 +119,6 @@ async def create_app(
|
||||
base_url=data.base_url,
|
||||
description=data.description,
|
||||
tools=json.dumps([t.model_dump() for t in data.tools], ensure_ascii=False) if data.tools else None,
|
||||
config_schema=json.dumps([c.model_dump() for c in data.config_schema], ensure_ascii=False) if data.config_schema else None,
|
||||
require_jssdk=1 if data.require_jssdk else 0,
|
||||
status=1
|
||||
)
|
||||
@@ -165,13 +150,6 @@ async def update_app(
|
||||
else:
|
||||
update_data['tools'] = None
|
||||
|
||||
# 处理 config_schema JSON
|
||||
if 'config_schema' in update_data:
|
||||
if update_data['config_schema']:
|
||||
update_data['config_schema'] = json.dumps([c.model_dump() if hasattr(c, 'model_dump') else c for c in update_data['config_schema']], ensure_ascii=False)
|
||||
else:
|
||||
update_data['config_schema'] = None
|
||||
|
||||
# 处理 require_jssdk
|
||||
if 'require_jssdk' in update_data:
|
||||
update_data['require_jssdk'] = 1 if update_data['require_jssdk'] else 0
|
||||
@@ -281,21 +259,6 @@ async def get_app_tools(
|
||||
return tools
|
||||
|
||||
|
||||
@router.get("/{app_code}/config-schema")
|
||||
async def get_app_config_schema(
|
||||
app_code: str,
|
||||
user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""获取应用的配置项定义(用于租户订阅时渲染表单)"""
|
||||
app = db.query(App).filter(App.app_code == app_code).first()
|
||||
if not app:
|
||||
raise HTTPException(status_code=404, detail="应用不存在")
|
||||
|
||||
config_schema = json.loads(app.config_schema) if app.config_schema else []
|
||||
return config_schema
|
||||
|
||||
|
||||
def format_app(app: App) -> dict:
|
||||
"""格式化应用数据"""
|
||||
return {
|
||||
@@ -305,7 +268,6 @@ def format_app(app: App) -> dict:
|
||||
"base_url": app.base_url,
|
||||
"description": app.description,
|
||||
"tools": json.loads(app.tools) if app.tools else [],
|
||||
"config_schema": json.loads(app.config_schema) if app.config_schema else [],
|
||||
"require_jssdk": bool(app.require_jssdk),
|
||||
"status": app.status,
|
||||
"created_at": app.created_at,
|
||||
|
||||
@@ -1,325 +0,0 @@
|
||||
"""脚本管理路由"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional, List
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import text
|
||||
from datetime import datetime
|
||||
|
||||
from ..database import get_db
|
||||
from .auth import get_current_user, require_operator
|
||||
from ..models.user import User
|
||||
|
||||
router = APIRouter(prefix="/scripts", tags=["脚本管理"])
|
||||
|
||||
|
||||
# Schemas
|
||||
|
||||
class ScriptCreate(BaseModel):
|
||||
tenant_id: Optional[str] = None
|
||||
name: str
|
||||
filename: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
script_content: str
|
||||
category: Optional[str] = None
|
||||
is_enabled: bool = True
|
||||
|
||||
|
||||
class ScriptUpdate(BaseModel):
|
||||
name: Optional[str] = None
|
||||
filename: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
script_content: Optional[str] = None
|
||||
category: Optional[str] = None
|
||||
is_enabled: Optional[bool] = None
|
||||
|
||||
|
||||
class ScriptRunRequest(BaseModel):
|
||||
tenant_id: Optional[str] = None # 可指定以哪个租户身份运行
|
||||
|
||||
|
||||
# API Endpoints
|
||||
|
||||
@router.get("")
|
||||
async def list_scripts(
|
||||
page: int = Query(1, ge=1),
|
||||
size: int = Query(50, ge=1, le=200),
|
||||
tenant_id: Optional[str] = None,
|
||||
category: Optional[str] = None,
|
||||
keyword: Optional[str] = None,
|
||||
user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""获取脚本列表"""
|
||||
where_clauses = []
|
||||
params = {}
|
||||
|
||||
if tenant_id:
|
||||
where_clauses.append("(tenant_id = :tenant_id OR tenant_id IS NULL)")
|
||||
params["tenant_id"] = tenant_id
|
||||
if category:
|
||||
where_clauses.append("category = :category")
|
||||
params["category"] = category
|
||||
if keyword:
|
||||
where_clauses.append("(name LIKE :keyword OR description LIKE :keyword)")
|
||||
params["keyword"] = f"%{keyword}%"
|
||||
|
||||
where_sql = " AND ".join(where_clauses) if where_clauses else "1=1"
|
||||
|
||||
# 查询总数
|
||||
count_result = db.execute(
|
||||
text(f"SELECT COUNT(*) FROM platform_scripts WHERE {where_sql}"),
|
||||
params
|
||||
)
|
||||
total = count_result.scalar()
|
||||
|
||||
# 查询列表
|
||||
params["offset"] = (page - 1) * size
|
||||
params["limit"] = size
|
||||
result = db.execute(
|
||||
text(f"""
|
||||
SELECT id, tenant_id, name, filename, description, category,
|
||||
is_enabled, last_run_at, last_run_status, created_by, created_at, updated_at,
|
||||
LENGTH(script_content) as content_length
|
||||
FROM platform_scripts
|
||||
WHERE {where_sql}
|
||||
ORDER BY updated_at DESC, id DESC
|
||||
LIMIT :limit OFFSET :offset
|
||||
"""),
|
||||
params
|
||||
)
|
||||
scripts = [dict(row) for row in result.mappings().all()]
|
||||
|
||||
# 获取分类列表
|
||||
cat_result = db.execute(
|
||||
text("SELECT DISTINCT category FROM platform_scripts WHERE category IS NOT NULL AND category != ''")
|
||||
)
|
||||
categories = [row[0] for row in cat_result.fetchall()]
|
||||
|
||||
return {
|
||||
"total": total,
|
||||
"page": page,
|
||||
"size": size,
|
||||
"items": scripts,
|
||||
"categories": categories
|
||||
}
|
||||
|
||||
|
||||
@router.get("/{script_id}")
|
||||
async def get_script(
|
||||
script_id: int,
|
||||
user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""获取脚本详情(包含内容)"""
|
||||
result = db.execute(
|
||||
text("SELECT * FROM platform_scripts WHERE id = :id"),
|
||||
{"id": script_id}
|
||||
)
|
||||
script = result.mappings().first()
|
||||
|
||||
if not script:
|
||||
raise HTTPException(status_code=404, detail="脚本不存在")
|
||||
|
||||
return dict(script)
|
||||
|
||||
|
||||
@router.post("")
|
||||
async def create_script(
|
||||
data: ScriptCreate,
|
||||
user: User = Depends(require_operator),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""创建脚本"""
|
||||
# 自动生成文件名
|
||||
filename = data.filename
|
||||
if not filename and data.name:
|
||||
# 转换为安全的文件名
|
||||
import re
|
||||
safe_name = re.sub(r'[^\w\u4e00-\u9fff]', '_', data.name)
|
||||
filename = f"{safe_name}.py"
|
||||
|
||||
db.execute(
|
||||
text("""
|
||||
INSERT INTO platform_scripts
|
||||
(tenant_id, name, filename, description, script_content, category, is_enabled, created_by)
|
||||
VALUES (:tenant_id, :name, :filename, :description, :script_content, :category, :is_enabled, :created_by)
|
||||
"""),
|
||||
{
|
||||
"tenant_id": data.tenant_id,
|
||||
"name": data.name,
|
||||
"filename": filename,
|
||||
"description": data.description,
|
||||
"script_content": data.script_content,
|
||||
"category": data.category,
|
||||
"is_enabled": 1 if data.is_enabled else 0,
|
||||
"created_by": user.username if hasattr(user, 'username') else None
|
||||
}
|
||||
)
|
||||
db.commit()
|
||||
|
||||
result = db.execute(text("SELECT LAST_INSERT_ID() as id"))
|
||||
script_id = result.scalar()
|
||||
|
||||
return {"id": script_id, "message": "创建成功"}
|
||||
|
||||
|
||||
@router.put("/{script_id}")
|
||||
async def update_script(
|
||||
script_id: int,
|
||||
data: ScriptUpdate,
|
||||
user: User = Depends(require_operator),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""更新脚本"""
|
||||
# 检查是否存在
|
||||
result = db.execute(
|
||||
text("SELECT id FROM platform_scripts WHERE id = :id"),
|
||||
{"id": script_id}
|
||||
)
|
||||
if not result.scalar():
|
||||
raise HTTPException(status_code=404, detail="脚本不存在")
|
||||
|
||||
updates = []
|
||||
params = {"id": script_id}
|
||||
|
||||
if data.name is not None:
|
||||
updates.append("name = :name")
|
||||
params["name"] = data.name
|
||||
if data.filename is not None:
|
||||
updates.append("filename = :filename")
|
||||
params["filename"] = data.filename
|
||||
if data.description is not None:
|
||||
updates.append("description = :description")
|
||||
params["description"] = data.description
|
||||
if data.script_content is not None:
|
||||
updates.append("script_content = :script_content")
|
||||
params["script_content"] = data.script_content
|
||||
if data.category is not None:
|
||||
updates.append("category = :category")
|
||||
params["category"] = data.category
|
||||
if data.is_enabled is not None:
|
||||
updates.append("is_enabled = :is_enabled")
|
||||
params["is_enabled"] = 1 if data.is_enabled else 0
|
||||
|
||||
if updates:
|
||||
db.execute(
|
||||
text(f"UPDATE platform_scripts SET {', '.join(updates)} WHERE id = :id"),
|
||||
params
|
||||
)
|
||||
db.commit()
|
||||
|
||||
return {"message": "更新成功"}
|
||||
|
||||
|
||||
@router.delete("/{script_id}")
|
||||
async def delete_script(
|
||||
script_id: int,
|
||||
user: User = Depends(require_operator),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""删除脚本"""
|
||||
result = db.execute(
|
||||
text("SELECT id FROM platform_scripts WHERE id = :id"),
|
||||
{"id": script_id}
|
||||
)
|
||||
if not result.scalar():
|
||||
raise HTTPException(status_code=404, detail="脚本不存在")
|
||||
|
||||
db.execute(
|
||||
text("DELETE FROM platform_scripts WHERE id = :id"),
|
||||
{"id": script_id}
|
||||
)
|
||||
db.commit()
|
||||
|
||||
return {"message": "删除成功"}
|
||||
|
||||
|
||||
@router.post("/{script_id}/run")
|
||||
async def run_script(
|
||||
script_id: int,
|
||||
data: ScriptRunRequest = None,
|
||||
user: User = Depends(require_operator),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""执行脚本"""
|
||||
from ..services.script_executor import test_script as run_test
|
||||
|
||||
# 获取脚本
|
||||
result = db.execute(
|
||||
text("SELECT * FROM platform_scripts WHERE id = :id"),
|
||||
{"id": script_id}
|
||||
)
|
||||
script = result.mappings().first()
|
||||
|
||||
if not script:
|
||||
raise HTTPException(status_code=404, detail="脚本不存在")
|
||||
|
||||
if not script["script_content"]:
|
||||
raise HTTPException(status_code=400, detail="脚本内容为空")
|
||||
|
||||
# 确定租户ID
|
||||
tenant_id = (data.tenant_id if data else None) or script["tenant_id"] or "system"
|
||||
|
||||
# 执行脚本
|
||||
exec_result = await run_test(
|
||||
tenant_id=tenant_id,
|
||||
script_content=script["script_content"]
|
||||
)
|
||||
|
||||
# 更新执行状态
|
||||
status = "success" if exec_result.success else "failed"
|
||||
db.execute(
|
||||
text("""
|
||||
UPDATE platform_scripts
|
||||
SET last_run_at = NOW(), last_run_status = :status
|
||||
WHERE id = :id
|
||||
"""),
|
||||
{"id": script_id, "status": status}
|
||||
)
|
||||
db.commit()
|
||||
|
||||
return exec_result.to_dict()
|
||||
|
||||
|
||||
@router.post("/{script_id}/copy")
|
||||
async def copy_script(
|
||||
script_id: int,
|
||||
user: User = Depends(require_operator),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""复制脚本"""
|
||||
# 获取原脚本
|
||||
result = db.execute(
|
||||
text("SELECT * FROM platform_scripts WHERE id = :id"),
|
||||
{"id": script_id}
|
||||
)
|
||||
script = result.mappings().first()
|
||||
|
||||
if not script:
|
||||
raise HTTPException(status_code=404, detail="脚本不存在")
|
||||
|
||||
# 创建副本
|
||||
new_name = f"{script['name']} - 副本"
|
||||
db.execute(
|
||||
text("""
|
||||
INSERT INTO platform_scripts
|
||||
(tenant_id, name, filename, description, script_content, category, is_enabled, created_by)
|
||||
VALUES (:tenant_id, :name, :filename, :description, :script_content, :category, 1, :created_by)
|
||||
"""),
|
||||
{
|
||||
"tenant_id": script["tenant_id"],
|
||||
"name": new_name,
|
||||
"filename": None,
|
||||
"description": script["description"],
|
||||
"script_content": script["script_content"],
|
||||
"category": script["category"],
|
||||
"created_by": user.username if hasattr(user, 'username') else None
|
||||
}
|
||||
)
|
||||
db.commit()
|
||||
|
||||
result = db.execute(text("SELECT LAST_INSERT_ID() as id"))
|
||||
new_id = result.scalar()
|
||||
|
||||
return {"id": new_id, "message": "复制成功"}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -17,13 +17,6 @@ router = APIRouter(prefix="/tenant-apps", tags=["应用配置"])
|
||||
|
||||
# Schemas
|
||||
|
||||
class CustomConfigItem(BaseModel):
|
||||
"""自定义配置项"""
|
||||
key: str # 配置键
|
||||
value: str # 配置值
|
||||
remark: Optional[str] = None # 备注说明
|
||||
|
||||
|
||||
class TenantAppCreate(BaseModel):
|
||||
tenant_id: str
|
||||
app_code: str = "tools"
|
||||
@@ -32,7 +25,6 @@ class TenantAppCreate(BaseModel):
|
||||
access_token: Optional[str] = None # 如果不传则自动生成
|
||||
allowed_origins: Optional[List[str]] = None
|
||||
allowed_tools: Optional[List[str]] = None
|
||||
custom_configs: Optional[List[CustomConfigItem]] = None # 自定义配置
|
||||
|
||||
|
||||
class TenantAppUpdate(BaseModel):
|
||||
@@ -41,7 +33,6 @@ class TenantAppUpdate(BaseModel):
|
||||
access_token: Optional[str] = None
|
||||
allowed_origins: Optional[List[str]] = None
|
||||
allowed_tools: Optional[List[str]] = None
|
||||
custom_configs: Optional[List[CustomConfigItem]] = None # 自定义配置
|
||||
status: Optional[int] = None
|
||||
|
||||
|
||||
@@ -120,7 +111,6 @@ async def create_tenant_app(
|
||||
access_token=access_token,
|
||||
allowed_origins=json.dumps(data.allowed_origins) if data.allowed_origins else None,
|
||||
allowed_tools=json.dumps(data.allowed_tools) if data.allowed_tools else None,
|
||||
custom_configs=json.dumps([c.model_dump() for c in data.custom_configs], ensure_ascii=False) if data.custom_configs else None,
|
||||
status=1
|
||||
)
|
||||
db.add(app)
|
||||
@@ -149,14 +139,6 @@ async def update_tenant_app(
|
||||
update_data['allowed_origins'] = json.dumps(update_data['allowed_origins']) if update_data['allowed_origins'] else None
|
||||
if 'allowed_tools' in update_data:
|
||||
update_data['allowed_tools'] = json.dumps(update_data['allowed_tools']) if update_data['allowed_tools'] else None
|
||||
if 'custom_configs' in update_data:
|
||||
if update_data['custom_configs']:
|
||||
update_data['custom_configs'] = json.dumps(
|
||||
[c.model_dump() if hasattr(c, 'model_dump') else c for c in update_data['custom_configs']],
|
||||
ensure_ascii=False
|
||||
)
|
||||
else:
|
||||
update_data['custom_configs'] = None
|
||||
|
||||
for key, value in update_data.items():
|
||||
setattr(app, key, value)
|
||||
@@ -182,27 +164,6 @@ async def delete_tenant_app(
|
||||
return {"success": True}
|
||||
|
||||
|
||||
@router.get("/{app_id}/token")
|
||||
async def get_token(
|
||||
app_id: int,
|
||||
user: User = Depends(require_operator),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""获取真实的 access_token(仅管理员可用)"""
|
||||
app = db.query(TenantApp).filter(TenantApp.id == app_id).first()
|
||||
if not app:
|
||||
raise HTTPException(status_code=404, detail="应用配置不存在")
|
||||
|
||||
# 获取应用的 base_url
|
||||
app_info = db.query(App).filter(App.app_code == app.app_code).first()
|
||||
base_url = app_info.base_url if app_info else ""
|
||||
|
||||
return {
|
||||
"access_token": app.access_token,
|
||||
"base_url": base_url
|
||||
}
|
||||
|
||||
|
||||
@router.post("/{app_id}/regenerate-token")
|
||||
async def regenerate_token(
|
||||
app_id: int,
|
||||
@@ -246,7 +207,6 @@ def format_tenant_app(app: TenantApp, mask_secret: bool = True, db: Session = No
|
||||
"access_token": "******" if mask_secret and app.access_token else app.access_token,
|
||||
"allowed_origins": json.loads(app.allowed_origins) if app.allowed_origins else [],
|
||||
"allowed_tools": json.loads(app.allowed_tools) if app.allowed_tools else [],
|
||||
"custom_configs": json.loads(app.custom_configs) if app.custom_configs else [],
|
||||
"status": app.status,
|
||||
"created_at": app.created_at,
|
||||
"updated_at": app.updated_at
|
||||
|
||||
Reference in New Issue
Block a user