refactor: sync running-container source back to repo for reproducible docker compose build; remove dead code (orphan pipeline.py + 104 stale files from iterative dev) and drop unused app/ mirror dir
This commit is contained in:
1 parent
1346fbbd4b
commit
5d7f32db0a
138 files changed
+12846
-84248
No files matched your search
@@ -1,752 +0,0 @@
|
||||
"""
|
||||
Distribution Builder Admin API - Phase G4
|
||||
发行版构建器:SaaS 产品线管理,配置包生成,许可证管理
|
||||
|
||||
Endpoints:
|
||||
GET / - 发行版列表
|
||||
POST / - 创建发行版
|
||||
GET /{id} - 发行版详情
|
||||
PUT /{id} - 更新发行版
|
||||
DELETE /{id} - 删除发行版
|
||||
POST /{id}/build - 构建配置包
|
||||
GET /{id}/download - 下载配置包
|
||||
POST /{id}/licenses - 生成许可证
|
||||
GET /licenses - 许可证列表
|
||||
GET /licenses/{lid} - 许可证详情
|
||||
POST /licenses/{lid}/revoke - 吊销许可证
|
||||
POST /licenses/validate - 验证许可证
|
||||
GET /stats - 发行版统计
|
||||
GET /templates - 发行版模板
|
||||
"""
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import uuid
|
||||
import yaml
|
||||
import zipfile
|
||||
import io
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from typing import Optional, List
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from fastapi.responses import StreamingResponse
|
||||
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
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
CST = timezone(timedelta(hours=8))
|
||||
LICENSE_SECRET = "qihuaiflow-distro-secret-key-2026"
|
||||
|
||||
# ==================== Pydantic Models ====================
|
||||
|
||||
class DistroCreate(BaseModel):
|
||||
name: str = Field(..., description="发行版名称")
|
||||
description: str = Field("")
|
||||
brand_name: Optional[str] = None
|
||||
brand_logo_url: Optional[str] = None
|
||||
brand_color: str = "#6366f1"
|
||||
system_prompt: str = ""
|
||||
provider: str = "siliconflow"
|
||||
model: str = "deepseek-ai/DeepSeek-V4-Flash"
|
||||
mcp_extensions: Optional[List[dict]] = None
|
||||
workflow_ids: Optional[List[str]] = None
|
||||
knowledge_base_ids: Optional[List[str]] = None
|
||||
agent_ids: Optional[List[str]] = None
|
||||
features: Optional[List[str]] = None
|
||||
version: str = "1.0.0"
|
||||
|
||||
class DistroUpdate(BaseModel):
|
||||
name: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
brand_name: Optional[str] = None
|
||||
brand_logo_url: Optional[str] = None
|
||||
brand_color: Optional[str] = None
|
||||
system_prompt: Optional[str] = None
|
||||
provider: Optional[str] = None
|
||||
model: Optional[str] = None
|
||||
mcp_extensions: Optional[List[dict]] = None
|
||||
workflow_ids: Optional[List[str]] = None
|
||||
knowledge_base_ids: Optional[List[str]] = None
|
||||
agent_ids: Optional[List[str]] = None
|
||||
features: Optional[List[str]] = None
|
||||
version: Optional[str] = None
|
||||
status: Optional[str] = None
|
||||
|
||||
class LicenseCreate(BaseModel):
|
||||
distribution_id: str
|
||||
org_name: str
|
||||
licensee_email: Optional[str] = None
|
||||
features: Optional[List[str]] = None
|
||||
max_users: int = 10
|
||||
max_executions: int = 10000
|
||||
expiry_date: Optional[str] = None # YYYY-MM-DD
|
||||
|
||||
class LicenseValidate(BaseModel):
|
||||
license_key: str
|
||||
distribution_id: Optional[str] = None
|
||||
|
||||
# ==================== Helpers ====================
|
||||
|
||||
def _execute_raw(db, sql, params=None):
|
||||
return db.execute(text(sql), params or {})
|
||||
|
||||
def _generate_id(prefix="distro"):
|
||||
return f"{prefix}-{uuid.uuid4().hex[:8]}"
|
||||
|
||||
def _format_cst_time(dt):
|
||||
if dt is None:
|
||||
return None
|
||||
if dt.tzinfo is None:
|
||||
return dt.replace(tzinfo=CST)
|
||||
return dt.astimezone(CST)
|
||||
|
||||
def _row_to_dict(row, columns):
|
||||
return {col: row[i] for i, col in enumerate(columns)}
|
||||
|
||||
def _serialize_dt(obj):
|
||||
if isinstance(obj, datetime):
|
||||
return _format_cst_time(obj).strftime("%Y-%m-%d %H:%M:%S")
|
||||
return obj
|
||||
|
||||
# ==================== Distribution CRUD ====================
|
||||
|
||||
@router.get("", summary="发行版列表")
|
||||
async def list_distributions(
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
status: Optional[str] = None,
|
||||
search: Optional[str] = None,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
conditions = []
|
||||
params = {}
|
||||
if status:
|
||||
conditions.append("status = :status")
|
||||
params["status"] = status
|
||||
if search:
|
||||
conditions.append("(name LIKE :search OR description LIKE :search)")
|
||||
params["search"] = f"%{search}%"
|
||||
|
||||
where = " AND ".join(conditions) if conditions else "1=1"
|
||||
|
||||
rows = _execute_raw(db, f"SELECT COUNT(*) FROM distributions WHERE {where}", params)
|
||||
total = rows.fetchone()[0]
|
||||
|
||||
offset = (page - 1) * page_size
|
||||
rows = _execute_raw(db,
|
||||
f"SELECT * FROM distributions WHERE {where} ORDER BY created_at DESC LIMIT :limit OFFSET :offset",
|
||||
{**params, "limit": page_size, "offset": offset}
|
||||
)
|
||||
cols = list(rows.keys())
|
||||
items = []
|
||||
for row in rows.fetchall():
|
||||
d = _row_to_dict(row, cols)
|
||||
for key in ["mcp_extensions", "workflow_ids", "knowledge_base_ids", "agent_ids", "features", "config_json"]:
|
||||
if d.get(key) and isinstance(d[key], str):
|
||||
try:
|
||||
d[key] = json.loads(d[key])
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
pass
|
||||
for key in ["created_at", "updated_at"]:
|
||||
d[key] = _serialize_dt(d[key])
|
||||
items.append(d)
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {"total": total, "items": items, "page": page, "page_size": page_size}
|
||||
}
|
||||
|
||||
@router.post("", summary="创建发行版")
|
||||
async def create_distribution(
|
||||
body: DistroCreate,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
distro_id = _generate_id("distro")
|
||||
now = datetime.now()
|
||||
|
||||
config_json = json.dumps({
|
||||
"provider": body.provider,
|
||||
"model": body.model,
|
||||
"mcp_extensions": body.mcp_extensions or [],
|
||||
"workflow_ids": body.workflow_ids or [],
|
||||
"knowledge_base_ids": body.knowledge_base_ids or [],
|
||||
"agent_ids": body.agent_ids or [],
|
||||
"features": body.features or ["chat"],
|
||||
}, ensure_ascii=False)
|
||||
|
||||
_execute_raw(db, """
|
||||
INSERT INTO distributions (id, name, description, brand_name, brand_logo_url, brand_color,
|
||||
system_prompt, provider, model, mcp_extensions, workflow_ids, knowledge_base_ids,
|
||||
agent_ids, features, config_json, status, version, created_at, updated_at)
|
||||
VALUES (:id, :name, :desc, :brand_name, :brand_logo, :brand_color,
|
||||
:system_prompt, :provider, :model, :mcp_ext, :wf_ids, :kb_ids,
|
||||
:agent_ids, :features, :config_json, 'draft', :version, :now, :now)
|
||||
""", {
|
||||
"id": distro_id, "name": body.name, "desc": body.description,
|
||||
"brand_name": body.brand_name, "brand_logo": body.brand_logo_url, "brand_color": body.brand_color,
|
||||
"system_prompt": body.system_prompt, "provider": body.provider, "model": body.model,
|
||||
"mcp_ext": json.dumps(body.mcp_extensions) if body.mcp_extensions else None,
|
||||
"wf_ids": json.dumps(body.workflow_ids) if body.workflow_ids else None,
|
||||
"kb_ids": json.dumps(body.knowledge_base_ids) if body.knowledge_base_ids else None,
|
||||
"agent_ids": json.dumps(body.agent_ids) if body.agent_ids else None,
|
||||
"features": json.dumps(body.features) if body.features else None,
|
||||
"config_json": config_json, "version": body.version, "now": now,
|
||||
})
|
||||
db.commit()
|
||||
|
||||
return {"code": 0, "data": {"id": distro_id}, "message": "发行版创建成功"}
|
||||
|
||||
@router.get("/templates", summary="发行版模板")
|
||||
async def get_templates(
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取可用资源模板:工作流、Agent、知识库、MCP扩展"""
|
||||
# Get available workflows (any status)
|
||||
try:
|
||||
rows = _execute_raw(db, "SELECT id, name, status FROM agent_workflows ORDER BY updated_at DESC LIMIT 20")
|
||||
cols = list(rows.keys())
|
||||
workflows = [_row_to_dict(r, cols) for r in rows.fetchall()]
|
||||
except Exception:
|
||||
workflows = []
|
||||
|
||||
# Get available agents
|
||||
try:
|
||||
rows = _execute_raw(db, "SELECT id, name, description FROM market_apps WHERE type='agent' AND status='published' LIMIT 20")
|
||||
cols = list(rows.keys())
|
||||
agents = [_row_to_dict(r, cols) for r in rows.fetchall()]
|
||||
except Exception:
|
||||
agents = []
|
||||
|
||||
# Get available knowledge bases
|
||||
rows = _execute_raw(db, "SELECT id, name, description FROM knowledge_bases WHERE status='active' LIMIT 20")
|
||||
cols = list(rows.keys())
|
||||
kbs = [_row_to_dict(r, cols) for r in rows.fetchall()]
|
||||
|
||||
# Get available MCP tools
|
||||
try:
|
||||
rows = _execute_raw(db, "SELECT id, app_id as name, server_type FROM mcp_tool_configs WHERE security_status='approved' LIMIT 20")
|
||||
cols = list(rows.keys())
|
||||
mcps = [_row_to_dict(r, cols) for r in rows.fetchall()]
|
||||
except Exception:
|
||||
mcps = []
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {
|
||||
"workflows": workflows,
|
||||
"agents": agents,
|
||||
"knowledge_bases": kbs,
|
||||
"mcp_tools": mcps,
|
||||
}
|
||||
}
|
||||
|
||||
@router.get("/stats", summary="发行版统计")
|
||||
async def get_stats(
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
rows = _execute_raw(db, """
|
||||
SELECT
|
||||
COUNT(*) as total,
|
||||
SUM(CASE WHEN status='draft' THEN 1 ELSE 0 END) as draft,
|
||||
SUM(CASE WHEN status='active' THEN 1 ELSE 0 END) as active,
|
||||
SUM(CASE WHEN status='archived' THEN 1 ELSE 0 END) as archived,
|
||||
SUM(download_count) as total_downloads,
|
||||
(SELECT COUNT(*) FROM distribution_licenses) as total_licenses,
|
||||
(SELECT COUNT(*) FROM distribution_licenses WHERE status='active') as active_licenses
|
||||
FROM distributions
|
||||
""")
|
||||
row = rows.fetchone()
|
||||
cols = list(rows.keys())
|
||||
stats = _row_to_dict(row, cols)
|
||||
|
||||
return {"code": 0, "data": stats}
|
||||
|
||||
# ==================== License Management ====================
|
||||
|
||||
def _generate_license_key(distro_id: str, org_name: str, features: list, expiry: str) -> str:
|
||||
"""Generate HMAC-SHA256 license key"""
|
||||
payload = f"{distro_id}:{org_name}:{json.dumps(sorted(features or []))}:{expiry or 'perpetual'}"
|
||||
return hmac.new(
|
||||
LICENSE_SECRET.encode(),
|
||||
payload.encode(),
|
||||
hashlib.sha256
|
||||
).hexdigest()[:32]
|
||||
|
||||
def _verify_license_signature(license_key: str, payload: str) -> bool:
|
||||
"""Verify license key signature"""
|
||||
expected = hmac.new(
|
||||
LICENSE_SECRET.encode(),
|
||||
payload.encode(),
|
||||
hashlib.sha256
|
||||
).hexdigest()[:32]
|
||||
return hmac.compare_digest(expected, license_key)
|
||||
|
||||
@router.get("/licenses", summary="许可证列表")
|
||||
async def list_licenses(
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
distribution_id: Optional[str] = None,
|
||||
status: Optional[str] = None,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
conditions = []
|
||||
params = {}
|
||||
if distribution_id:
|
||||
conditions.append("l.distribution_id = :did")
|
||||
params["did"] = distribution_id
|
||||
if status:
|
||||
conditions.append("l.status = :status")
|
||||
params["status"] = status
|
||||
|
||||
where = " AND ".join(conditions) if conditions else "1=1"
|
||||
|
||||
rows = _execute_raw(db, f"SELECT COUNT(*) FROM distribution_licenses l WHERE {where}", params)
|
||||
total = rows.fetchone()[0]
|
||||
|
||||
offset = (page - 1) * page_size
|
||||
rows = _execute_raw(db, f"""
|
||||
SELECT l.*, d.name as distribution_name
|
||||
FROM distribution_licenses l
|
||||
LEFT JOIN distributions d ON l.distribution_id = d.id
|
||||
WHERE {where}
|
||||
ORDER BY l.created_at DESC
|
||||
LIMIT :limit OFFSET :offset
|
||||
""", {**params, "limit": page_size, "offset": offset})
|
||||
|
||||
cols = list(rows.keys())
|
||||
items = []
|
||||
for row in rows.fetchall():
|
||||
item = _row_to_dict(row, cols)
|
||||
for key in ["features", "metadata_json"]:
|
||||
if item.get(key) and isinstance(item[key], str):
|
||||
try:
|
||||
item[key] = json.loads(item[key])
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
pass
|
||||
for key in ["created_at", "updated_at", "first_used_at", "last_used_at"]:
|
||||
item[key] = _serialize_dt(item[key])
|
||||
if item.get("expiry_date"):
|
||||
item["expiry_date"] = str(item["expiry_date"])
|
||||
items.append(item)
|
||||
|
||||
return {"code": 0, "data": {"total": total, "items": items, "page": page, "page_size": page_size}}
|
||||
|
||||
@router.post("/{distro_id}/licenses", summary="生成许可证")
|
||||
async def create_license(
|
||||
distro_id: str,
|
||||
body: LicenseCreate,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
rows = _execute_raw(db, "SELECT id, features FROM distributions WHERE id = :id", {"id": distro_id})
|
||||
row = rows.fetchone()
|
||||
if not row:
|
||||
raise HTTPException(status_code=404, detail="发行版不存在")
|
||||
|
||||
distro_features = row[1]
|
||||
if isinstance(distro_features, str):
|
||||
try:
|
||||
distro_features = json.loads(distro_features)
|
||||
except json.JSONDecodeError:
|
||||
distro_features = []
|
||||
|
||||
license_id = _generate_id("lic")
|
||||
# Features limited to what distribution supports
|
||||
requested_features = body.features or distro_features or ["chat"]
|
||||
license_key = _generate_license_key(distro_id, body.org_name, requested_features, body.expiry_date)
|
||||
|
||||
now = datetime.now()
|
||||
_execute_raw(db, """
|
||||
INSERT INTO distribution_licenses
|
||||
(id, distribution_id, org_name, licensee_email, license_key, features,
|
||||
max_users, max_executions, expiry_date, status, created_at, updated_at)
|
||||
VALUES (:id, :did, :org, :email, :key, :features,
|
||||
:max_users, :max_exec, :expiry, 'active', :now, :now)
|
||||
""", {
|
||||
"id": license_id, "did": distro_id, "org": body.org_name, "email": body.licensee_email,
|
||||
"key": license_key, "features": json.dumps(requested_features),
|
||||
"max_users": body.max_users, "max_exec": body.max_executions,
|
||||
"expiry": body.expiry_date, "now": now,
|
||||
})
|
||||
db.commit()
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {
|
||||
"id": license_id,
|
||||
"license_key": license_key,
|
||||
"org_name": body.org_name,
|
||||
"features": requested_features,
|
||||
"expiry_date": body.expiry_date,
|
||||
},
|
||||
"message": "许可证生成成功"
|
||||
}
|
||||
|
||||
@router.get("/licenses/{license_id}", summary="许可证详情")
|
||||
async def get_license(
|
||||
license_id: str,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
rows = _execute_raw(db, """
|
||||
SELECT l.*, d.name as distribution_name
|
||||
FROM distribution_licenses l
|
||||
LEFT JOIN distributions d ON l.distribution_id = d.id
|
||||
WHERE l.id = :id
|
||||
""", {"id": license_id})
|
||||
row = rows.fetchone()
|
||||
if not row:
|
||||
raise HTTPException(status_code=404, detail="许可证不存在")
|
||||
|
||||
cols = list(rows.keys())
|
||||
lic = _row_to_dict(row, cols)
|
||||
for key in ["features", "metadata_json"]:
|
||||
if lic.get(key) and isinstance(lic[key], str):
|
||||
try:
|
||||
lic[key] = json.loads(lic[key])
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
pass
|
||||
for key in ["created_at", "updated_at", "first_used_at", "last_used_at"]:
|
||||
lic[key] = _serialize_dt(lic[key])
|
||||
if lic.get("expiry_date"):
|
||||
lic["expiry_date"] = str(lic["expiry_date"])
|
||||
|
||||
return {"code": 0, "data": lic}
|
||||
|
||||
@router.post("/licenses/{license_id}/revoke", summary="吊销许可证")
|
||||
async def revoke_license(
|
||||
license_id: str,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
rows = _execute_raw(db, "SELECT id, status FROM distribution_licenses WHERE id = :id", {"id": license_id})
|
||||
row = rows.fetchone()
|
||||
if not row:
|
||||
raise HTTPException(status_code=404, detail="许可证不存在")
|
||||
|
||||
_execute_raw(db, "UPDATE distribution_licenses SET status='revoked', updated_at=NOW() WHERE id = :id", {"id": license_id})
|
||||
db.commit()
|
||||
|
||||
return {"code": 0, "message": "许可证已吊销"}
|
||||
|
||||
@router.post("/licenses/validate", summary="验证许可证")
|
||||
async def validate_license(
|
||||
body: LicenseValidate,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""公开 API:验证许可证有效性(不需要 admin 权限)"""
|
||||
rows = _execute_raw(db,
|
||||
"SELECT * FROM distribution_licenses WHERE license_key = :key AND status='active'",
|
||||
{"key": body.license_key}
|
||||
)
|
||||
row = rows.fetchone()
|
||||
if not row:
|
||||
return {"code": 1, "data": {"valid": False, "reason": "license_not_found"}, "message": "许可证不存在或已失效"}
|
||||
|
||||
cols = list(rows.keys())
|
||||
lic = _row_to_dict(row, cols)
|
||||
|
||||
# Check expiry
|
||||
if lic.get("expiry_date"):
|
||||
expiry = lic["expiry_date"]
|
||||
if isinstance(expiry, str):
|
||||
expiry = datetime.strptime(expiry, "%Y-%m-%d").date()
|
||||
if expiry < datetime.now().date():
|
||||
return {"code": 1, "data": {"valid": False, "reason": "expired"}, "message": "许可证已过期"}
|
||||
|
||||
# Check usage limit
|
||||
if lic.get("max_executions") and lic.get("usage_count", 0) >= lic["max_executions"]:
|
||||
return {"code": 1, "data": {"valid": False, "reason": "usage_exceeded"}, "message": "许可证用量已超限"}
|
||||
|
||||
features = lic.get("features")
|
||||
if isinstance(features, str):
|
||||
try:
|
||||
features = json.loads(features)
|
||||
except json.JSONDecodeError:
|
||||
features = []
|
||||
|
||||
# Update usage
|
||||
_execute_raw(db, """
|
||||
UPDATE distribution_licenses
|
||||
SET usage_count = usage_count + 1,
|
||||
last_used_at = NOW(),
|
||||
first_used_at = COALESCE(first_used_at, NOW())
|
||||
WHERE id = :id
|
||||
""", {"id": lic["id"]})
|
||||
db.commit()
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {
|
||||
"valid": True,
|
||||
"org_name": lic.get("org_name"),
|
||||
"features": features,
|
||||
"max_users": lic.get("max_users"),
|
||||
"max_executions": lic.get("max_executions"),
|
||||
"usage_count": lic.get("usage_count", 0) + 1,
|
||||
"expiry_date": str(lic["expiry_date"]) if lic.get("expiry_date") else None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@router.get("/{distro_id}", summary="发行版详情")
|
||||
async def get_distribution(
|
||||
distro_id: str,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
rows = _execute_raw(db, "SELECT * FROM distributions WHERE id = :id", {"id": distro_id})
|
||||
row = rows.fetchone()
|
||||
if not row:
|
||||
raise HTTPException(status_code=404, detail="发行版不存在")
|
||||
|
||||
cols = list(rows.keys())
|
||||
d = _row_to_dict(row, cols)
|
||||
for key in ["mcp_extensions", "workflow_ids", "knowledge_base_ids", "agent_ids", "features", "config_json"]:
|
||||
if d.get(key) and isinstance(d[key], str):
|
||||
try:
|
||||
d[key] = json.loads(d[key])
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
pass
|
||||
for key in ["created_at", "updated_at"]:
|
||||
d[key] = _serialize_dt(d[key])
|
||||
|
||||
# Get associated licenses count
|
||||
lic_rows = _execute_raw(db, "SELECT COUNT(*) as cnt FROM distribution_licenses WHERE distribution_id = :did", {"did": distro_id})
|
||||
d["license_count"] = lic_rows.fetchone()[0]
|
||||
|
||||
return {"code": 0, "data": d}
|
||||
|
||||
@router.put("/{distro_id}", summary="更新发行版")
|
||||
async def update_distribution(
|
||||
distro_id: str,
|
||||
body: DistroUpdate,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
rows = _execute_raw(db, "SELECT id FROM distributions WHERE id = :id", {"id": distro_id})
|
||||
if not rows.fetchone():
|
||||
raise HTTPException(status_code=404, detail="发行版不存在")
|
||||
|
||||
updates = []
|
||||
params = {"id": distro_id}
|
||||
for field, value in body.dict(exclude_unset=True).items():
|
||||
if field in ("mcp_extensions", "workflow_ids", "knowledge_base_ids", "agent_ids", "features"):
|
||||
updates.append(f"{field} = :{field}")
|
||||
params[field] = json.dumps(value) if value else None
|
||||
else:
|
||||
updates.append(f"{field} = :{field}")
|
||||
params[field] = value
|
||||
|
||||
if updates:
|
||||
updates.append("updated_at = NOW()")
|
||||
_execute_raw(db, f"UPDATE distributions SET {', '.join(updates)} WHERE id = :id", params)
|
||||
db.commit()
|
||||
|
||||
return {"code": 0, "message": "更新成功"}
|
||||
|
||||
@router.delete("/{distro_id}", summary="删除发行版")
|
||||
async def delete_distribution(
|
||||
distro_id: str,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
rows = _execute_raw(db, "SELECT id FROM distributions WHERE id = :id", {"id": distro_id})
|
||||
if not rows.fetchone():
|
||||
raise HTTPException(status_code=404, detail="发行版不存在")
|
||||
|
||||
# Delete associated licenses
|
||||
_execute_raw(db, "DELETE FROM distribution_licenses WHERE distribution_id = :id", {"id": distro_id})
|
||||
_execute_raw(db, "DELETE FROM distributions WHERE id = :id", {"id": distro_id})
|
||||
db.commit()
|
||||
|
||||
return {"code": 0, "message": "删除成功"}
|
||||
|
||||
# ==================== Build & Export ====================
|
||||
|
||||
@router.post("/{distro_id}/build", summary="构建配置包")
|
||||
async def build_distribution(
|
||||
distro_id: str,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""构建发行版配置包 ZIP (YAML config + recipes + extensions + license template)"""
|
||||
rows = _execute_raw(db, "SELECT * FROM distributions WHERE id = :id", {"id": distro_id})
|
||||
row = rows.fetchone()
|
||||
if not row:
|
||||
raise HTTPException(status_code=404, detail="发行版不存在")
|
||||
|
||||
cols = list(rows.keys())
|
||||
d = _row_to_dict(row, cols)
|
||||
|
||||
# Parse JSON fields
|
||||
for key in ["mcp_extensions", "workflow_ids", "knowledge_base_ids", "agent_ids", "features", "config_json"]:
|
||||
if d.get(key) and isinstance(d[key], str):
|
||||
try:
|
||||
d[key] = json.loads(d[key])
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
d[key] = [] if "ids" in key else ({} if key == "config_json" else [])
|
||||
|
||||
buf = io.BytesIO()
|
||||
with zipfile.ZipFile(buf, 'w', zipfile.ZIP_DEFLATED) as zf:
|
||||
|
||||
# 1. init-config.yaml
|
||||
init_config = {
|
||||
"GOOSE_PROVIDER": d.get("provider", "siliconflow"),
|
||||
"GOOSE_MODEL": d.get("model", "deepseek-ai/DeepSeek-V4-Flash"),
|
||||
"PLATFORM_API_URL": "https://your-platform.com/api",
|
||||
"LICENSE_KEY": "YOUR_LICENSE_KEY_HERE",
|
||||
}
|
||||
zf.writestr("init-config.yaml", yaml.dump(init_config, allow_unicode=True, default_flow_style=False))
|
||||
|
||||
# 2. config.yaml (MCP extensions)
|
||||
config = {
|
||||
"extensions": {},
|
||||
}
|
||||
for ext in (d.get("mcp_extensions") or []):
|
||||
ext_id = ext.get("id", str(uuid.uuid4().hex[:8]))
|
||||
config["extensions"][ext_id] = {
|
||||
"name": ext.get("name", "unnamed"),
|
||||
"cmd": ext.get("cmd", ""),
|
||||
"args": ext.get("args", []),
|
||||
"enabled": True,
|
||||
"type": ext.get("type", "stdio"),
|
||||
"timeout": ext.get("timeout", 30000),
|
||||
}
|
||||
zf.writestr("config.yaml", yaml.dump(config, allow_unicode=True, default_flow_style=False))
|
||||
|
||||
# 3. Recipes (workflow exports)
|
||||
wf_ids = d.get("workflow_ids") or []
|
||||
for wf_id in wf_ids:
|
||||
wf_rows = _execute_raw(db, "SELECT * FROM agent_workflows WHERE id = :id", {"id": wf_id})
|
||||
wf_row = wf_rows.fetchone()
|
||||
if wf_row:
|
||||
wf_cols = list(wf_rows.keys())
|
||||
wf = _row_to_dict(wf_row, wf_cols)
|
||||
recipe = {
|
||||
"name": wf.get("name", wf_id),
|
||||
"description": wf.get("description", ""),
|
||||
"steps": [],
|
||||
}
|
||||
nodes = wf.get("nodes")
|
||||
if isinstance(nodes, str):
|
||||
try:
|
||||
nodes = json.loads(nodes)
|
||||
except json.JSONDecodeError:
|
||||
nodes = []
|
||||
for node in (nodes or []):
|
||||
recipe["steps"].append({
|
||||
"type": node.get("type", ""),
|
||||
"label": node.get("data", {}).get("label", ""),
|
||||
"config": node.get("data", {}).get("config", {}),
|
||||
})
|
||||
zf.writestr(f"recipes/{wf_id}.yaml", yaml.dump(recipe, allow_unicode=True, default_flow_style=False))
|
||||
|
||||
# 4. Branding
|
||||
zf.writestr("branding/system-prompt.md", d.get("system_prompt", "# AI Assistant\n\nYou are a helpful AI assistant."))
|
||||
branding = {
|
||||
"app_name": d.get("brand_name", d.get("name", "AI Assistant")),
|
||||
"brand_color": d.get("brand_color", "#6366f1"),
|
||||
"version": d.get("version", "1.0.0"),
|
||||
}
|
||||
zf.writestr("branding/manifest.yaml", yaml.dump(branding, allow_unicode=True))
|
||||
|
||||
# 5. License template
|
||||
license_template = {
|
||||
"license_key": "TO_BE_GENERATED",
|
||||
"org_name": "YOUR_ORGANIZATION",
|
||||
"expiry": None,
|
||||
"features": d.get("features", ["chat"]),
|
||||
}
|
||||
zf.writestr("license.template.yaml", yaml.dump(license_template, allow_unicode=True))
|
||||
|
||||
# 6. install.sh
|
||||
install_script = """#!/bin/bash
|
||||
# Distribution Installation Script
|
||||
echo "Installing Distribution: {name}"
|
||||
echo "Version: {version}"
|
||||
echo ""
|
||||
echo "1. Copy init-config.yaml to ~/.config/goose/"
|
||||
cp init-config.yaml ~/.config/goose/init-config.yaml
|
||||
echo "2. Copy config.yaml"
|
||||
cp config.yaml ~/.config/goose/config.yaml
|
||||
echo "3. Copy recipes"
|
||||
mkdir -p ~/.config/goose/recipes/
|
||||
cp -r recipes/ ~/.config/goose/recipes/
|
||||
echo "4. Copy branding"
|
||||
mkdir -p ~/.config/goose/branding/
|
||||
cp -r branding/ ~/.config/goose/branding/
|
||||
echo "Installation complete!"
|
||||
""".format(name=d.get("name", "Unnamed"), version=d.get("version", "1.0.0"))
|
||||
zf.writestr("install.sh", install_script)
|
||||
|
||||
# 7. README
|
||||
readme = f"""# {d.get('brand_name', d.get('name', 'Distribution'))}
|
||||
|
||||
## Description
|
||||
{d.get('description', 'No description')}
|
||||
|
||||
## Included Features
|
||||
""" + "\n".join([f"- {f}" for f in (d.get("features") or ["chat"])]) + f"""
|
||||
|
||||
## Version
|
||||
{d.get('version', '1.0.0')}
|
||||
|
||||
## Installation
|
||||
Run `bash install.sh` to install.
|
||||
|
||||
## License
|
||||
A valid license key is required. Contact your administrator for details.
|
||||
"""
|
||||
zf.writestr("README.md", readme)
|
||||
|
||||
# Update download count and size
|
||||
size = buf.tell()
|
||||
_execute_raw(db, "UPDATE distributions SET download_count = download_count + 1, size_bytes = :size WHERE id = :id",
|
||||
{"size": size, "id": distro_id})
|
||||
db.commit()
|
||||
|
||||
buf.seek(0)
|
||||
|
||||
filename = f"{d.get('name', 'distro').replace(' ', '_')}-{d.get('version', '1.0.0')}.zip"
|
||||
return StreamingResponse(
|
||||
buf,
|
||||
media_type="application/zip",
|
||||
headers={"Content-Disposition": f'attachment; filename="{filename}"'}
|
||||
)
|
||||
|
||||
@router.get("/{distro_id}/download", summary="下载配置包")
|
||||
async def download_distribution(
|
||||
distro_id: str,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""直接下载已生成的配置包"""
|
||||
return await build_distribution(distro_id, admin_user, db)
|
||||
|
||||
# ==================== LLM Proxy API ====================
|
||||
|
||||
@router.post("/llm-proxy/{api_key:path}", summary="LLM API 代理")
|
||||
async def llm_proxy(
|
||||
api_key: str,
|
||||
admin_user=Depends(get_current_admin),
|
||||
):
|
||||
"""LLM 代理端点状态检查"""
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {
|
||||
"status": "ready",
|
||||
"providers": ["siliconflow", "deepseek"],
|
||||
"message": "LLM Proxy is operational. Use POST /admin/distributions/llm-proxy/chat to call models."
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user