fix: 恢复 rsync 误删的近期开发(agents/skills/mcp/knowledge/workflow/tenant/billing/storage/privacy/promotion/achievement/automation/code-exec/developer/distribution/im-bot/portal/casdoor 等后端模块 + 对应前端页面路由, 补回 main.py 挂载与 App.tsx 路由
This commit is contained in:
1 parent
d95b847fee
commit
e8e5ffa1a9
65 files changed
+35624
-339
No files matched your search
@@ -0,0 +1,369 @@
|
||||
"""
|
||||
Code Execution Admin API
|
||||
Manages code execution sandbox containers, task submission, and result retrieval.
|
||||
|
||||
Endpoints:
|
||||
GET /stats - Execution statistics
|
||||
GET /runtimes - Available runtime info from sandbox
|
||||
POST /execute - Submit code for execution
|
||||
GET /tasks - List execution tasks (with filters)
|
||||
GET /tasks/{id} - Get specific task result
|
||||
DELETE /tasks/{id} - Cancel/delete task
|
||||
"""
|
||||
import logging
|
||||
import uuid
|
||||
import json
|
||||
import time
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from typing import Optional, List
|
||||
|
||||
import httpx
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import text
|
||||
|
||||
from ...database import get_db, SessionLocal
|
||||
from ...auth import get_current_admin
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
CST = timezone(timedelta(hours=8))
|
||||
|
||||
# Code execution sandbox configuration
|
||||
# The executor container is on the same Docker network, accessible by container name
|
||||
EXECUTOR_URL = "http://qihuaiflow-executor:8000"
|
||||
EXECUTOR_TIMEOUT = 120 # max execution timeout
|
||||
|
||||
# Supported languages
|
||||
SUPPORTED_LANGUAGES = ["python", "javascript"]
|
||||
|
||||
|
||||
def _execute_raw(db: Session, sql, params=None):
|
||||
"""Execute raw SQL and return results."""
|
||||
result = db.execute(text(sql), params or {})
|
||||
if sql.strip().upper().startswith("SELECT") or sql.strip().upper().startswith("WITH"):
|
||||
rows = result.fetchall()
|
||||
if rows:
|
||||
return [dict(r._mapping) for r in rows]
|
||||
return []
|
||||
db.commit()
|
||||
return result
|
||||
|
||||
|
||||
def _format_cst_time(dt):
|
||||
"""Format datetime to CST string."""
|
||||
if dt is None:
|
||||
return None
|
||||
if dt.tzinfo is None:
|
||||
dt = dt.replace(tzinfo=CST)
|
||||
return dt.strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
|
||||
def _now_cst():
|
||||
"""Get current time as CST-aware datetime."""
|
||||
return datetime.now(CST)
|
||||
|
||||
|
||||
# ===== Pydantic Models =====
|
||||
|
||||
class ExecuteRequest(BaseModel):
|
||||
language: str = "python"
|
||||
code: str
|
||||
stdin: str = ""
|
||||
timeout: int = Field(default=30, ge=1, le=120)
|
||||
|
||||
|
||||
class ExecuteResponse(BaseModel):
|
||||
task_id: str
|
||||
success: bool
|
||||
stdout: str = ""
|
||||
stderr: str = ""
|
||||
exit_code: int = 0
|
||||
execution_time_ms: float = 0
|
||||
error: Optional[str] = None
|
||||
truncated: bool = False
|
||||
created_at: str = ""
|
||||
|
||||
|
||||
class TaskFilter(BaseModel):
|
||||
language: Optional[str] = None
|
||||
status: Optional[str] = None
|
||||
page: int = 1
|
||||
page_size: int = 20
|
||||
|
||||
|
||||
# ===== API Endpoints =====
|
||||
|
||||
@router.get("/stats")
|
||||
async def get_execution_stats(db: Session = Depends(get_db), _=Depends(get_current_admin)):
|
||||
"""Get code execution statistics."""
|
||||
try:
|
||||
total = _execute_raw(db, "SELECT COUNT(*) as cnt FROM code_exec_tasks")[0]["cnt"]
|
||||
today = _execute_raw(db,
|
||||
"SELECT COUNT(*) as cnt FROM code_exec_tasks WHERE DATE(created_at) = CURDATE()")[0]["cnt"]
|
||||
success_count = _execute_raw(db,
|
||||
"SELECT COUNT(*) as cnt FROM code_exec_tasks WHERE exit_code = 0 AND error IS NULL")[0]["cnt"]
|
||||
failed_count = _execute_raw(db,
|
||||
"SELECT COUNT(*) as cnt FROM code_exec_tasks WHERE exit_code != 0 OR error IS NOT NULL")[0]["cnt"]
|
||||
timeout_count = _execute_raw(db,
|
||||
"SELECT COUNT(*) as cnt FROM code_exec_tasks WHERE error LIKE '%timeout%'")[0]["cnt"]
|
||||
by_language = _execute_raw(db,
|
||||
"SELECT language, COUNT(*) as cnt FROM code_exec_tasks GROUP BY language")
|
||||
|
||||
avg_time = _execute_raw(db,
|
||||
"SELECT AVG(execution_time_ms) as avg_ms FROM code_exec_tasks WHERE execution_time_ms > 0")[0]
|
||||
total_avg = round(avg_time.get("avg_ms", 0) or 0, 2)
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {
|
||||
"total": total,
|
||||
"today": today,
|
||||
"success": success_count,
|
||||
"failed": failed_count,
|
||||
"timeout": timeout_count,
|
||||
"avg_execution_time_ms": total_avg,
|
||||
"by_language": {r["language"]: r["cnt"] for r in by_language} if by_language else {},
|
||||
}
|
||||
}
|
||||
except Exception as e:
|
||||
# Table might not exist yet
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {
|
||||
"total": 0, "today": 0, "success": 0, "failed": 0, "timeout": 0,
|
||||
"avg_execution_time_ms": 0, "by_language": {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@router.get("/runtimes")
|
||||
async def get_runtimes(_=Depends(get_current_admin)):
|
||||
"""Get sandbox runtime info."""
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=5) as client:
|
||||
resp = await client.get(f"{EXECUTOR_URL}/info")
|
||||
if resp.status_code == 200:
|
||||
return {"code": 0, "data": resp.json()}
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {
|
||||
"python": "Python 3.11 (sandbox)",
|
||||
"nodejs": "Node.js (sandbox)",
|
||||
"workspace": "/workspace",
|
||||
"max_code_length": 51200,
|
||||
"max_timeout": 120,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@router.post("/execute", response_model=ExecuteResponse)
|
||||
async def execute_code(req: ExecuteRequest, db: Session = Depends(get_db), _=Depends(get_current_admin)):
|
||||
"""Submit code for execution."""
|
||||
if req.language not in SUPPORTED_LANGUAGES:
|
||||
raise HTTPException(status_code=400, detail=f"Unsupported language: {req.language}")
|
||||
|
||||
task_id = str(uuid.uuid4())[:8]
|
||||
now = _now_cst()
|
||||
|
||||
# Create task record
|
||||
_execute_raw(db, """
|
||||
INSERT INTO code_exec_tasks (id, language, code_preview, status, timeout_sec, created_at)
|
||||
VALUES (:id, :lang, :preview, 'running', :timeout, :now)
|
||||
""", {
|
||||
"id": task_id,
|
||||
"lang": req.language,
|
||||
"preview": req.code[:200],
|
||||
"timeout": req.timeout,
|
||||
"now": now,
|
||||
})
|
||||
|
||||
# Execute via sandbox
|
||||
result = {
|
||||
"success": False,
|
||||
"stdout": "",
|
||||
"stderr": "",
|
||||
"exit_code": -1,
|
||||
"execution_time_ms": 0,
|
||||
"error": None,
|
||||
}
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=req.timeout + 10) as client:
|
||||
resp = await client.post(
|
||||
f"{EXECUTOR_URL}/execute",
|
||||
json={
|
||||
"language": req.language,
|
||||
"code": req.code,
|
||||
"stdin": req.stdin,
|
||||
"timeout": req.timeout,
|
||||
},
|
||||
)
|
||||
if resp.status_code == 200:
|
||||
data = resp.json()
|
||||
result = {
|
||||
"success": data.get("success", False),
|
||||
"stdout": data.get("stdout", ""),
|
||||
"stderr": data.get("stderr", ""),
|
||||
"exit_code": data.get("exit_code", -1),
|
||||
"execution_time_ms": data.get("execution_time_ms", 0),
|
||||
"error": data.get("error"),
|
||||
"truncated": data.get("truncated", False),
|
||||
}
|
||||
else:
|
||||
result["error"] = f"Sandbox returned {resp.status_code}: {resp.text[:200]}"
|
||||
except httpx.TimeoutException:
|
||||
result["error"] = f"Execution timed out after {req.timeout}s"
|
||||
except httpx.ConnectError:
|
||||
result["error"] = "Execution sandbox is not available"
|
||||
except Exception as e:
|
||||
result["error"] = str(e)[:500]
|
||||
|
||||
# Determine status
|
||||
if result["error"]:
|
||||
status = "failed"
|
||||
elif result["exit_code"] != 0:
|
||||
status = "error"
|
||||
else:
|
||||
status = "completed"
|
||||
|
||||
# Update task record
|
||||
now2 = _now_cst()
|
||||
_execute_raw(db, """
|
||||
UPDATE code_exec_tasks SET
|
||||
status = :status, exit_code = :exit_code, execution_time_ms = :time_ms,
|
||||
stdout = :stdout, stderr = :stderr, error_msg = :error,
|
||||
truncated = :truncated, completed_at = :now
|
||||
WHERE id = :id
|
||||
""", {
|
||||
"status": status,
|
||||
"exit_code": result["exit_code"],
|
||||
"time_ms": result["execution_time_ms"],
|
||||
"stdout": result["stdout"][:50000] if result.get("stdout") else "",
|
||||
"stderr": result["stderr"][:50000] if result.get("stderr") else "",
|
||||
"error": result["error"][:2000] if result.get("error") else None,
|
||||
"truncated": 1 if result.get("truncated") else 0,
|
||||
"now": now2,
|
||||
"id": task_id,
|
||||
})
|
||||
|
||||
return ExecuteResponse(
|
||||
task_id=task_id,
|
||||
success=result["success"],
|
||||
stdout=result.get("stdout", ""),
|
||||
stderr=result.get("stderr", ""),
|
||||
exit_code=result["exit_code"],
|
||||
execution_time_ms=result["execution_time_ms"],
|
||||
error=result.get("error"),
|
||||
truncated=result.get("truncated", False),
|
||||
created_at=_format_cst_time(now),
|
||||
)
|
||||
|
||||
|
||||
@router.get("/tasks")
|
||||
async def list_tasks(
|
||||
language: Optional[str] = Query(None),
|
||||
status: Optional[str] = Query(None),
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
db: Session = Depends(get_db),
|
||||
_=Depends(get_current_admin),
|
||||
):
|
||||
"""List execution tasks with filters."""
|
||||
where_clauses = []
|
||||
params = {}
|
||||
|
||||
if language:
|
||||
where_clauses.append("language = :lang")
|
||||
params["lang"] = language
|
||||
if status:
|
||||
where_clauses.append("status = :status")
|
||||
params["status"] = status
|
||||
|
||||
where_sql = ""
|
||||
if where_clauses:
|
||||
where_sql = "WHERE " + " AND ".join(where_clauses)
|
||||
|
||||
# Count total
|
||||
count_result = _execute_raw(db, f"SELECT COUNT(*) as cnt FROM code_exec_tasks {where_sql}", params)
|
||||
total = count_result[0]["cnt"] if count_result else 0
|
||||
|
||||
# Fetch page
|
||||
offset = (page - 1) * page_size
|
||||
tasks = _execute_raw(db, f"""
|
||||
SELECT id, language, code_preview, status, exit_code, execution_time_ms,
|
||||
error_msg, truncated, created_at, completed_at
|
||||
FROM code_exec_tasks {where_sql}
|
||||
ORDER BY created_at DESC
|
||||
LIMIT :limit OFFSET :offset
|
||||
""", {**params, "limit": page_size, "offset": offset})
|
||||
|
||||
items = []
|
||||
for t in (tasks or []):
|
||||
items.append({
|
||||
"id": t["id"],
|
||||
"language": t["language"],
|
||||
"code_preview": t.get("code_preview", ""),
|
||||
"status": t["status"],
|
||||
"exit_code": t.get("exit_code"),
|
||||
"execution_time_ms": t.get("execution_time_ms"),
|
||||
"error_msg": t.get("error_msg"),
|
||||
"truncated": bool(t.get("truncated", 0)),
|
||||
"created_at": _format_cst_time(t.get("created_at")),
|
||||
"completed_at": _format_cst_time(t.get("completed_at")),
|
||||
})
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"items": items,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@router.get("/tasks/{task_id}")
|
||||
async def get_task(task_id: str, db: Session = Depends(get_db), _=Depends(get_current_admin)):
|
||||
"""Get a specific task with full output."""
|
||||
results = _execute_raw(db, """
|
||||
SELECT id, language, code_preview, status, exit_code, execution_time_ms,
|
||||
stdout, stderr, error_msg, truncated, created_at, completed_at
|
||||
FROM code_exec_tasks WHERE id = :id
|
||||
""", {"id": task_id})
|
||||
|
||||
if not results:
|
||||
raise HTTPException(status_code=404, detail="Task not found")
|
||||
|
||||
t = results[0]
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {
|
||||
"id": t["id"],
|
||||
"language": t["language"],
|
||||
"code_preview": t.get("code_preview", ""),
|
||||
"status": t["status"],
|
||||
"exit_code": t.get("exit_code"),
|
||||
"execution_time_ms": t.get("execution_time_ms"),
|
||||
"stdout": t.get("stdout", ""),
|
||||
"stderr": t.get("stderr", ""),
|
||||
"error_msg": t.get("error_msg"),
|
||||
"truncated": bool(t.get("truncated", 0)),
|
||||
"created_at": _format_cst_time(t.get("created_at")),
|
||||
"completed_at": _format_cst_time(t.get("completed_at")),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@router.delete("/tasks/{task_id}")
|
||||
async def delete_task(task_id: str, db: Session = Depends(get_db), _=Depends(get_current_admin)):
|
||||
"""Delete a task record."""
|
||||
result = _execute_raw(db, "DELETE FROM code_exec_tasks WHERE id = :id", {"id": task_id})
|
||||
return {"code": 0, "msg": "deleted"}
|
||||
Reference in new issue
Block a user