fix: 修复定时任务模块问题
All checks were successful
continuous-integration/drone/push Build is passing

1. 修复 SDK 文档 API 路由顺序问题
   - 将静态路由 /sdk-docs, /test-script, /secrets 移到动态路由 /{task_id} 之前
   - 解决 "请求参数验证失败" 错误

2. 优化错误页面体验
   - 使用 sessionStorage 传递错误信息,URL 保持干净
   - 使用 router.replace 替代 push,浏览器返回不会停留在错误页
   - 记录来源页面,支持正确返回

3. 增强网络错误处理
   - 区分超时、网络错误、服务不可用
   - 后端未启动时显示友好的 "服务暂时不可用" 提示

4. 添加定时任务模块文档
This commit is contained in:
2026-01-28 18:18:04 +08:00
parent 262a1b409f
commit b0f7d1ba9e
4 changed files with 649 additions and 223 deletions

View File

@@ -75,6 +75,191 @@ class TestScriptRequest(BaseModel):
params: Optional[dict] = None
# ==================== Static Routes (must be before dynamic routes) ====================
@router.get("/sdk-docs")
async def get_sdk_docs():
"""获取SDK文档"""
return {
"functions": [
{
"name": "log",
"signature": "log(message: str, level: str = 'INFO')",
"description": "记录日志",
"example": "log('处理完成', 'INFO')"
},
{
"name": "print",
"signature": "print(*args)",
"description": "打印输出",
"example": "print('Hello', 'World')"
},
{
"name": "ai",
"signature": "ai(prompt: str, system: str = None, model: str = None, temperature: float = 0.7)",
"description": "调用AI模型",
"example": "result = ai('生成一段问候语', system='你是友善的助手')"
},
{
"name": "dingtalk",
"signature": "dingtalk(webhook: str, content: str, title: str = None, at_all: bool = False)",
"description": "发送钉钉消息",
"example": "dingtalk(webhook_url, '# 标题\\n内容')"
},
{
"name": "wecom",
"signature": "wecom(webhook: str, content: str, msg_type: str = 'markdown')",
"description": "发送企微消息",
"example": "wecom(webhook_url, '消息内容')"
},
{
"name": "http_get",
"signature": "http_get(url: str, headers: dict = None, params: dict = None)",
"description": "发起GET请求",
"example": "resp = http_get('https://api.example.com/data')"
},
{
"name": "http_post",
"signature": "http_post(url: str, data: any = None, headers: dict = None)",
"description": "发起POST请求",
"example": "resp = http_post('https://api.example.com/submit', {'key': 'value'})"
},
{
"name": "db_query",
"signature": "db_query(sql: str, params: dict = None)",
"description": "执行只读SQL查询",
"example": "rows = db_query('SELECT * FROM users WHERE status = :status', {'status': 1})"
},
{
"name": "get_var",
"signature": "get_var(key: str, default: any = None)",
"description": "获取持久化变量",
"example": "counter = get_var('counter', 0)"
},
{
"name": "set_var",
"signature": "set_var(key: str, value: any)",
"description": "设置持久化变量",
"example": "set_var('counter', counter + 1)"
},
{
"name": "del_var",
"signature": "del_var(key: str)",
"description": "删除持久化变量",
"example": "del_var('temp_data')"
},
{
"name": "get_param",
"signature": "get_param(key: str, default: any = None)",
"description": "获取任务参数",
"example": "prompt = get_param('prompt', '默认提示词')"
},
{
"name": "get_params",
"signature": "get_params()",
"description": "获取所有任务参数",
"example": "params = get_params()"
},
{
"name": "get_tenants",
"signature": "get_tenants(app_code: str = None)",
"description": "获取租户列表",
"example": "tenants = get_tenants('notification-service')"
},
{
"name": "get_tenant_config",
"signature": "get_tenant_config(tenant_id: str, app_code: str, key: str = None)",
"description": "获取租户的应用配置",
"example": "webhook = get_tenant_config('tenant1', 'notification-service', 'dingtalk_webhook')"
},
{
"name": "get_all_tenant_configs",
"signature": "get_all_tenant_configs(app_code: str)",
"description": "获取所有租户的应用配置",
"example": "configs = get_all_tenant_configs('notification-service')"
},
{
"name": "get_secret",
"signature": "get_secret(key: str)",
"description": "获取密钥(优先租户级)",
"example": "api_key = get_secret('api_key')"
}
],
"variables": [
{"name": "task_id", "description": "当前任务ID"},
{"name": "tenant_id", "description": "当前租户ID"},
{"name": "trace_id", "description": "当前执行追踪ID"}
],
"libraries": [
{"name": "json", "description": "JSON处理"},
{"name": "re", "description": "正则表达式"},
{"name": "math", "description": "数学函数"},
{"name": "random", "description": "随机数"},
{"name": "hashlib", "description": "哈希函数"},
{"name": "base64", "description": "Base64编解码"},
{"name": "datetime", "description": "日期时间处理"},
{"name": "timedelta", "description": "时间差"},
{"name": "urlencode/quote/unquote", "description": "URL编码"}
]
}
@router.post("/test-script")
async def test_script(data: TestScriptRequest, db: Session = Depends(get_db)):
"""测试脚本执行"""
executor = ScriptExecutor(db)
result = executor.test_script(
script_content=data.script_content,
task_id=0,
tenant_id=data.tenant_id,
params=data.params
)
return result
@router.get("/secrets")
async def list_secrets(
tenant_id: Optional[str] = None,
db: Session = Depends(get_db)
):
"""获取密钥列表"""
query = db.query(Secret)
if tenant_id:
query = query.filter(Secret.tenant_id == tenant_id)
items = query.order_by(desc(Secret.created_at)).all()
return {
"items": [
{
"id": s.id,
"tenant_id": s.tenant_id,
"secret_key": s.secret_key,
"description": s.description,
"created_at": s.created_at,
"updated_at": s.updated_at
}
for s in items
]
}
@router.post("/secrets")
async def create_secret(data: SecretCreate, db: Session = Depends(get_db)):
"""创建密钥"""
secret = Secret(
tenant_id=data.tenant_id,
secret_key=data.secret_key,
secret_value=data.secret_value,
description=data.description
)
db.add(secret)
db.commit()
db.refresh(secret)
return {"success": True, "id": secret.id}
# ==================== Task CRUD ====================
@router.get("")
@@ -272,194 +457,7 @@ async def get_task_logs(
}
# ==================== Script Testing ====================
@router.post("/test-script")
async def test_script(data: TestScriptRequest, db: Session = Depends(get_db)):
"""测试脚本执行"""
executor = ScriptExecutor(db)
result = executor.test_script(
script_content=data.script_content,
task_id=0,
tenant_id=data.tenant_id,
params=data.params
)
return result
# ==================== SDK Documentation ====================
@router.get("/sdk-docs")
async def get_sdk_docs():
"""获取SDK文档"""
return {
"functions": [
{
"name": "log",
"signature": "log(message: str, level: str = 'INFO')",
"description": "记录日志",
"example": "log('处理完成', 'INFO')"
},
{
"name": "print",
"signature": "print(*args)",
"description": "打印输出",
"example": "print('Hello', 'World')"
},
{
"name": "ai",
"signature": "ai(prompt: str, system: str = None, model: str = None, temperature: float = 0.7)",
"description": "调用AI模型",
"example": "result = ai('生成一段问候语', system='你是友善的助手')"
},
{
"name": "dingtalk",
"signature": "dingtalk(webhook: str, content: str, title: str = None, at_all: bool = False)",
"description": "发送钉钉消息",
"example": "dingtalk(webhook_url, '# 标题\\n内容')"
},
{
"name": "wecom",
"signature": "wecom(webhook: str, content: str, msg_type: str = 'markdown')",
"description": "发送企微消息",
"example": "wecom(webhook_url, '消息内容')"
},
{
"name": "http_get",
"signature": "http_get(url: str, headers: dict = None, params: dict = None)",
"description": "发起GET请求",
"example": "resp = http_get('https://api.example.com/data')"
},
{
"name": "http_post",
"signature": "http_post(url: str, data: any = None, headers: dict = None)",
"description": "发起POST请求",
"example": "resp = http_post('https://api.example.com/submit', {'key': 'value'})"
},
{
"name": "db_query",
"signature": "db_query(sql: str, params: dict = None)",
"description": "执行只读SQL查询",
"example": "rows = db_query('SELECT * FROM users WHERE status = :status', {'status': 1})"
},
{
"name": "get_var",
"signature": "get_var(key: str, default: any = None)",
"description": "获取持久化变量",
"example": "counter = get_var('counter', 0)"
},
{
"name": "set_var",
"signature": "set_var(key: str, value: any)",
"description": "设置持久化变量",
"example": "set_var('counter', counter + 1)"
},
{
"name": "del_var",
"signature": "del_var(key: str)",
"description": "删除持久化变量",
"example": "del_var('temp_data')"
},
{
"name": "get_param",
"signature": "get_param(key: str, default: any = None)",
"description": "获取任务参数",
"example": "prompt = get_param('prompt', '默认提示词')"
},
{
"name": "get_params",
"signature": "get_params()",
"description": "获取所有任务参数",
"example": "params = get_params()"
},
{
"name": "get_tenants",
"signature": "get_tenants(app_code: str = None)",
"description": "获取租户列表",
"example": "tenants = get_tenants('notification-service')"
},
{
"name": "get_tenant_config",
"signature": "get_tenant_config(tenant_id: str, app_code: str, key: str = None)",
"description": "获取租户的应用配置",
"example": "webhook = get_tenant_config('tenant1', 'notification-service', 'dingtalk_webhook')"
},
{
"name": "get_all_tenant_configs",
"signature": "get_all_tenant_configs(app_code: str)",
"description": "获取所有租户的应用配置",
"example": "configs = get_all_tenant_configs('notification-service')"
},
{
"name": "get_secret",
"signature": "get_secret(key: str)",
"description": "获取密钥(优先租户级)",
"example": "api_key = get_secret('api_key')"
}
],
"variables": [
{"name": "task_id", "description": "当前任务ID"},
{"name": "tenant_id", "description": "当前租户ID"},
{"name": "trace_id", "description": "当前执行追踪ID"}
],
"libraries": [
{"name": "json", "description": "JSON处理"},
{"name": "re", "description": "正则表达式"},
{"name": "math", "description": "数学函数"},
{"name": "random", "description": "随机数"},
{"name": "hashlib", "description": "哈希函数"},
{"name": "base64", "description": "Base64编解码"},
{"name": "datetime", "description": "日期时间处理"},
{"name": "timedelta", "description": "时间差"},
{"name": "urlencode/quote/unquote", "description": "URL编码"}
]
}
# ==================== Secrets ====================
@router.get("/secrets")
async def list_secrets(
tenant_id: Optional[str] = None,
db: Session = Depends(get_db)
):
"""获取密钥列表"""
query = db.query(Secret)
if tenant_id:
query = query.filter(Secret.tenant_id == tenant_id)
items = query.order_by(desc(Secret.created_at)).all()
return {
"items": [
{
"id": s.id,
"tenant_id": s.tenant_id,
"secret_key": s.secret_key,
"description": s.description,
"created_at": s.created_at,
"updated_at": s.updated_at
}
for s in items
]
}
@router.post("/secrets")
async def create_secret(data: SecretCreate, db: Session = Depends(get_db)):
"""创建密钥"""
secret = Secret(
tenant_id=data.tenant_id,
secret_key=data.secret_key,
secret_value=data.secret_value,
description=data.description
)
db.add(secret)
db.commit()
db.refresh(secret)
return {"success": True, "id": secret.id}
# ==================== Secrets (dynamic routes) ====================
@router.put("/secrets/{secret_id}")
async def update_secret(secret_id: int, data: SecretUpdate, db: Session = Depends(get_db)):