Files
QihuAI Design a2504bce34 fix(chat): resolve Auto-route failover event-loop freeze (#812)
Root cause: ensure_structured_column() executed a synchronous blocking
engine.connect().execute(SELECT information_schema.columns ...) inside the
async request path (_build_chat_history / _maybe_compress_history), freezing
the uvicorn event loop and hanging all endpoints (~200s, recovered only by
docker restart).

Fix:
- chat_context.py: remove per-request ensure_structured_column() calls; run
  once idempotently at app startup (main.py lifespan). Use CURRENT_DATABASE()
  for PostgreSQL compatibility.
- main.py: add startup-only ensure_structured_column() hook (try/except).
- llm_client.py: trust_env=False + bounded httpx.Timeout/wait_for so broken
  endpoints fail fast instead of hanging the event loop.
- chat_service.py: run media generation in a background daemon thread with its
  own event loop (_launch_media_task_bg) so SSE end does not cancel image/video
  tasks (#811); remove TEMP faulthandler diagnostic.

Safe-align: persist accumulated live container changes (admin/*, knowledge,
market, models, config, observability, mcp_client, platform_knowledge,
im_bot_platform, chat_media, capability_registry, requirements) and add
workflow_templates.

Verified: live failover test (model-006 -> conn-refused) returns in ~1.9s with
concurrent login 0.83s/200; event-loop responsiveness restored (sub-second).
2026-07-26 00:35:35 +08:00

753 lines
27 KiB
Python

"""
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."
}
}