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:
qyyfw committed 2026-07-11 11:40:39 +08:00
1 parent d95b847fee
commit e8e5ffa1a9
65 files changed
+35624 -339

No files matched your search

@@ -0,0 +1,899 @@
"""
Developer Tools Enhancement - Webhooks, SDK Center, API Docs, Usage Dashboard.
Phase 3.9: Extends the developer module with webhook management,
SDK download center, interactive API documentation, and enhanced analytics.
"""
import json
import secrets
import hashlib
import hmac
import urllib.request
import urllib.error
from datetime import datetime, timedelta, timezone
from fastapi import APIRouter, Depends, HTTPException, Query
from pydantic import BaseModel
from sqlalchemy import text
from sqlalchemy.orm import Session
from ...database import get_db, SessionLocal
from ...auth import get_current_admin
router = APIRouter()
CST_TZ = timezone(timedelta(hours=8))
def cst_now():
return datetime.now(CST_TZ)
def fmt_time(dt):
if dt is None:
return None
if dt.tzinfo is None:
return dt.replace(tzinfo=CST_TZ).isoformat()
return dt.isoformat()
def _execute_raw(db, sql, params=None):
"""Execute raw SQL and return list of dicts."""
result = db.execute(text(sql), params or {})
rows = []
for row in result:
rows.append(dict(row._mapping))
return rows
# ============================================================
# Pydantic Models
# ============================================================
class WebhookCreate(BaseModel):
name: str
url: str
events: list = []
secret: str = ""
headers: dict = {}
class WebhookUpdate(BaseModel):
name: str = None
url: str = None
events: list = None
secret: str = None
headers: dict = None
status: str = None
class WebhookTest(BaseModel):
event: str = "test"
payload: dict = {"message": "Test webhook delivery"}
# ============================================================
# Webhook Management
# ============================================================
SUPPORTED_EVENTS = [
{"value": "app.created", "label": "App Created", "desc": "Triggered when a new AI app is created"},
{"value": "app.published", "label": "App Published", "desc": "Triggered when an app is published to market"},
{"value": "agent.created", "label": "Agent Created", "desc": "Triggered when a new agent is created"},
{"value": "knowledge.updated", "label": "Knowledge Updated", "desc": "Triggered when knowledge base is updated"},
{"value": "api_key.created", "label": "API Key Created", "desc": "Triggered when a new API key is generated"},
{"value": "api_key.revoked", "label": "API Key Revoked", "desc": "Triggered when an API key is revoked"},
{"value": "user.registered", "label": "User Registered", "desc": "Triggered when a new user registers"},
{"value": "model.connected", "label": "Model Connected", "desc": "Triggered when a model connection test passes"},
{"value": "workflow.completed", "label": "Workflow Completed", "desc": "Triggered when a workflow execution finishes"},
{"value": "storage.threshold", "label": "Storage Threshold", "desc": "Triggered when storage usage exceeds threshold"},
]
@router.get("/webhooks", summary="List webhooks")
async def list_webhooks(
status: str = Query(None),
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100),
admin_user=Depends(get_current_admin),
db: Session = Depends(get_db),
):
"""List all webhook configurations."""
where_clause = "WHERE 1=1"
params = {}
if status:
where_clause += " AND status = :status"
params["status"] = status
count_sql = f"SELECT COUNT(*) as cnt FROM webhook_configs {where_clause}"
count_rows = _execute_raw(db, count_sql, params)
total = count_rows[0]["cnt"] if count_rows else 0
offset = (page - 1) * page_size
list_sql = f"""SELECT id, name, url, events, secret, headers, status,
last_triggered_at, last_status, total_deliveries, success_count, fail_count,
created_at, updated_at
FROM webhook_configs {where_clause}
ORDER BY created_at DESC LIMIT :limit OFFSET :offset"""
list_params = dict(params)
list_params["limit"] = page_size
list_params["offset"] = offset
rows = _execute_raw(db, list_sql, list_params)
items = []
for r in rows:
events_raw = r.get("events")
if isinstance(events_raw, str):
try:
events_val = json.loads(events_raw)
except Exception:
events_val = []
else:
events_val = events_raw or []
headers_raw = r.get("headers")
if isinstance(headers_raw, str):
try:
headers_val = json.loads(headers_raw)
except Exception:
headers_val = {}
else:
headers_val = headers_raw or {}
success_rate = 0
if r.get("total_deliveries", 0) > 0:
success_rate = round(r["success_count"] / r["total_deliveries"] * 100, 1)
items.append({
"id": r["id"],
"name": r["name"],
"url": r["url"],
"events": events_val,
"has_secret": bool(r.get("secret")),
"headers": headers_val,
"status": r["status"],
"last_triggered_at": fmt_time(r.get("last_triggered_at")),
"last_status": r.get("last_status"),
"total_deliveries": r.get("total_deliveries", 0),
"success_count": r.get("success_count", 0),
"fail_count": r.get("fail_count", 0),
"success_rate": success_rate,
"created_at": fmt_time(r.get("created_at")),
"updated_at": fmt_time(r.get("updated_at")),
})
return {"code": 0, "data": {"total": total, "page": page, "page_size": page_size, "items": items}}
@router.get("/webhooks/events", summary="List supported webhook events")
async def list_webhook_events(
admin_user=Depends(get_current_admin),
):
"""List all supported webhook event types."""
return {"code": 0, "data": {"events": SUPPORTED_EVENTS}}
@router.post("/webhooks/", summary="Create webhook")
async def create_webhook(
body: WebhookCreate,
admin_user=Depends(get_current_admin),
db: Session = Depends(get_db),
):
"""Create a new webhook configuration."""
webhook_id = "wh_" + secrets.token_hex(12)
now = cst_now()
secret_val = body.secret if body.secret else "whsec_" + secrets.token_hex(16)
insert_sql = """INSERT INTO webhook_configs
(id, name, url, events, secret, headers, status, last_triggered_at,
last_status, total_deliveries, success_count, fail_count, created_at, updated_at)
VALUES (:id, :name, :url, :events, :secret, :headers, 'active',
NULL, NULL, 0, 0, 0, :now, :now)"""
params = {
"id": webhook_id,
"name": body.name,
"url": body.url,
"events": json.dumps(body.events),
"secret": secret_val,
"headers": json.dumps(body.headers),
"now": now,
}
db.execute(text(insert_sql), params)
db.commit()
return {"code": 0, "data": {"id": webhook_id, "name": body.name, "secret": secret_val, "message": "Webhook created"}}
@router.put("/webhooks/{webhook_id}", summary="Update webhook")
async def update_webhook(
webhook_id: str,
body: WebhookUpdate,
admin_user=Depends(get_current_admin),
db: Session = Depends(get_db),
):
"""Update an existing webhook configuration."""
check_sql = "SELECT id FROM webhook_configs WHERE id = :id"
rows = _execute_raw(db, check_sql, {"id": webhook_id})
if not rows:
raise HTTPException(status_code=404, detail="Webhook not found")
updates = []
params = {"id": webhook_id, "now": cst_now()}
if body.name is not None:
updates.append("name = :name")
params["name"] = body.name
if body.url is not None:
updates.append("url = :url")
params["url"] = body.url
if body.events is not None:
updates.append("events = :events")
params["events"] = json.dumps(body.events)
if body.secret is not None:
updates.append("secret = :secret")
params["secret"] = body.secret
if body.headers is not None:
updates.append("headers = :headers")
params["headers"] = json.dumps(body.headers)
if body.status is not None:
updates.append("status = :status")
params["status"] = body.status
if not updates:
return {"code": 0, "message": "No changes"}
updates.append("updated_at = :now")
update_sql = f"UPDATE webhook_configs SET {', '.join(updates)} WHERE id = :id"
db.execute(text(update_sql), params)
db.commit()
return {"code": 0, "message": "Webhook updated"}
@router.delete("/webhooks/{webhook_id}", summary="Delete webhook")
async def delete_webhook(
webhook_id: str,
admin_user=Depends(get_current_admin),
db: Session = Depends(get_db),
):
"""Delete a webhook configuration."""
check_sql = "SELECT id FROM webhook_configs WHERE id = :id"
rows = _execute_raw(db, check_sql, {"id": webhook_id})
if not rows:
raise HTTPException(status_code=404, detail="Webhook not found")
db.execute(text("DELETE FROM webhook_configs WHERE id = :id"), {"id": webhook_id})
db.commit()
return {"code": 0, "message": "Webhook deleted"}
@router.post("/webhooks/{webhook_id}/test", summary="Test webhook delivery")
async def test_webhook(
webhook_id: str,
body: WebhookTest = None,
admin_user=Depends(get_current_admin),
db: Session = Depends(get_db),
):
"""Send a test payload to the webhook endpoint."""
select_sql = "SELECT * FROM webhook_configs WHERE id = :id"
rows = _execute_raw(db, select_sql, {"id": webhook_id})
if not rows:
raise HTTPException(status_code=404, detail="Webhook not found")
wh = rows[0]
url = wh["url"]
secret = wh.get("secret", "")
test_event = body.event if body else "test"
test_payload = body.payload if body else {"message": "Test webhook delivery"}
full_payload = {
"event": test_event,
"timestamp": cst_now().isoformat(),
"webhook_id": webhook_id,
"data": test_payload,
}
payload_bytes = json.dumps(full_payload).encode("utf-8")
# Build headers
headers = {"Content-Type": "application/json"}
headers_raw = wh.get("headers")
if isinstance(headers_raw, str):
try:
headers.update(json.loads(headers_raw))
except Exception:
pass
elif isinstance(headers_raw, dict):
headers.update(headers_raw)
# HMAC signature
if secret:
signature = hmac.new(secret.encode("utf-8"), payload_bytes, hashlib.sha256).hexdigest()
headers["X-Webhook-Signature"] = signature
# Send request
delivery_id = "dlv_" + secrets.token_hex(12)
now = cst_now()
response_code = None
response_body = None
response_time_ms = None
status = "failed"
error_message = None
try:
req = urllib.request.Request(url, data=payload_bytes, headers=headers, method="POST")
start_time = datetime.now()
resp = urllib.request.urlopen(req, timeout=10)
end_time = datetime.now()
response_code = resp.getcode()
response_body = resp.read().decode("utf-8", errors="replace")[:2000]
response_time_ms = int((end_time - start_time).total_seconds() * 1000)
if 200 <= response_code < 300:
status = "success"
except urllib.error.HTTPError as e:
response_code = e.code
try:
response_body = e.read().decode("utf-8", errors="replace")[:2000]
except Exception:
response_body = str(e)
error_message = f"HTTP {e.code}: {str(e)[:200]}"
except Exception as e:
error_message = str(e)[:500]
# Record delivery
insert_sql = """INSERT INTO webhook_deliveries
(id, webhook_id, event, payload, status, response_code, response_body,
response_time_ms, attempts, error_message, created_at)
VALUES (:id, :wid, :event, :payload, :status, :rcode, :rbody,
:rtime, 1, :err, :now)"""
db.execute(text(insert_sql), {
"id": delivery_id, "wid": webhook_id, "event": test_event,
"payload": json.dumps(full_payload), "status": status,
"rcode": response_code, "rbody": response_body,
"rtime": response_time_ms, "err": error_message, "now": now,
})
# Update webhook stats
total_deliveries = (wh.get("total_deliveries") or 0) + 1
success_count = (wh.get("success_count") or 0) + (1 if status == "success" else 0)
fail_count = (wh.get("fail_count") or 0) + (1 if status != "success" else 0)
db.execute(text("""UPDATE webhook_configs SET
total_deliveries = :td, success_count = :sc, fail_count = :fc,
last_triggered_at = :now, last_status = :ls, updated_at = :now
WHERE id = :id"""), {
"td": total_deliveries, "sc": success_count, "fc": fail_count,
"now": now, "ls": status, "id": webhook_id,
})
db.commit()
return {
"code": 0,
"data": {
"delivery_id": delivery_id,
"status": status,
"response_code": response_code,
"response_time_ms": response_time_ms,
"response_body": response_body[:500] if response_body else None,
"error_message": error_message,
}
}
@router.get("/webhooks/{webhook_id}/deliveries", summary="Webhook delivery history")
async def webhook_deliveries(
webhook_id: str,
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100),
admin_user=Depends(get_current_admin),
db: Session = Depends(get_db),
):
"""Get delivery history for a specific webhook."""
count_sql = "SELECT COUNT(*) as cnt FROM webhook_deliveries WHERE webhook_id = :wid"
count_rows = _execute_raw(db, count_sql, {"wid": webhook_id})
total = count_rows[0]["cnt"] if count_rows else 0
offset = (page - 1) * page_size
list_sql = """SELECT id, webhook_id, event, payload, status, response_code,
response_body, response_time_ms, attempts, error_message, created_at
FROM webhook_deliveries WHERE webhook_id = :wid
ORDER BY created_at DESC LIMIT :limit OFFSET :offset"""
rows = _execute_raw(db, list_sql, {"wid": webhook_id, "limit": page_size, "offset": offset})
items = []
for r in rows:
payload_raw = r.get("payload")
if isinstance(payload_raw, str):
try:
payload_val = json.loads(payload_raw)
except Exception:
payload_val = payload_raw
else:
payload_val = payload_raw
items.append({
"id": r["id"],
"event": r["event"],
"status": r["status"],
"response_code": r.get("response_code"),
"response_time_ms": r.get("response_time_ms"),
"attempts": r.get("attempts", 0),
"error_message": r.get("error_message"),
"payload": payload_val,
"response_body": (r.get("response_body") or "")[:500],
"created_at": fmt_time(r.get("created_at")),
})
return {"code": 0, "data": {"total": total, "page": page, "page_size": page_size, "items": items}}
# ============================================================
# SDK Download Center
# ============================================================
SDK_PACKAGES = [
{
"language": "python",
"name": "Python SDK",
"version": "1.2.0",
"description": "Official Python SDK for Qihu-AIFlow platform. Supports chat completions, knowledge base, agent execution, and file upload.",
"install_cmd": "pip install qihu-aiflow",
"download_url": "https://pypi.org/project/qihu-aiflow/",
"github_url": "https://github.com/qihuyun/qihu-aiflow-sdk-python",
"min_version": "Python 3.8+",
"features": ["Chat completions (streaming)", "Knowledge base queries", "Agent execution", "File upload", "Webhook verification", "Async support"],
"code_example": '''from qihu_aiflow import QihuClient
client = QihuClient(api_key="sk-your-api-key", base_url="http://210.16.161.218:18080")
# Chat completion
response = client.chat.create(
model="deepseek-ai/DeepSeek-V4-Flash",
messages=[{"role": "user", "content": "Hello!"}]
)
print(response.choices[0].message.content)
# Streaming
for chunk in client.chat.create(
model="deepseek-ai/DeepSeek-V4-Flash",
messages=[{"role": "user", "content": "Tell me a story"}],
stream=True
):
print(chunk.choices[0].delta.content, end="")''',
},
{
"language": "javascript",
"name": "JavaScript SDK",
"version": "1.1.0",
"description": "Official JavaScript/TypeScript SDK for browser and Node.js. Full TypeScript support with type definitions.",
"install_cmd": "npm install @qihu/aiflow",
"download_url": "https://www.npmjs.com/package/@qihu/aiflow",
"github_url": "https://github.com/qihuyun/qihu-aiflow-sdk-js",
"min_version": "Node.js 16+ / Browser",
"features": ["Chat completions (streaming)", "Knowledge base queries", "Agent execution", "File upload", "WebSocket support", "TypeScript types"],
"code_example": '''import { QihuClient } from "@qihu/aiflow";
const client = new QihuClient({
apiKey: "sk-your-api-key",
baseUrl: "http://210.16.161.218:18080",
});
// Chat completion
const response = await client.chat.create({
model: "deepseek-ai/DeepSeek-V4-Flash",
messages: [{ role: "user", content: "Hello!" }],
});
console.log(response.choices[0].message.content);
// Streaming
const stream = await client.chat.create({
model: "deepseek-ai/DeepSeek-V4-Flash",
messages: [{ role: "user", content: "Tell me a story" }],
stream: true,
});
for await (const chunk of stream) {
process.stdout.write(chunk.choices[0].delta.content || "");
}''',
},
{
"language": "go",
"name": "Go SDK",
"version": "1.0.0",
"description": "Official Go SDK for Qihu-AIFlow. Lightweight and fast, suitable for microservices and CLI tools.",
"install_cmd": "go get github.com/qihuyun/qihu-aiflow-sdk-go",
"download_url": "https://github.com/qihuyun/qihu-aiflow-sdk-go/releases",
"github_url": "https://github.com/qihuyun/qihu-aiflow-sdk-go",
"min_version": "Go 1.19+",
"features": ["Chat completions", "Knowledge base queries", "Agent execution", "Context cancellation", "Retry logic", "Connection pooling"],
"code_example": '''package main
import (
"context"
"fmt"
"github.com/qihuyun/qihu-aiflow-sdk-go/qihu"
)
func main() {
client := qihu.NewClient("sk-your-api-key", "http://210.16.161.218:18080")
resp, err := client.Chat.Create(context.Background(), &qihu.ChatRequest{
Model: "deepseek-ai/DeepSeek-V4-Flash",
Messages: []qihu.Message{
{Role: "user", Content: "Hello!"},
},
})
if err != nil {
panic(err)
}
fmt.Println(resp.Choices[0].Message.Content)
}''',
},
{
"language": "curl",
"name": "REST API (cURL)",
"version": "1.0.0",
"description": "Direct REST API access without SDK. Use any HTTP client to interact with the platform.",
"install_cmd": "# No installation needed - use curl directly",
"download_url": "https://docs.qihuyun.com/api-reference",
"github_url": "",
"min_version": "Any HTTP client",
"features": ["Chat completions", "Knowledge base queries", "Agent execution", "File upload", "Webhook management", "No dependencies"],
"code_example": '''# Chat completion
curl -X POST http://210.16.161.218:18080/app/api/chat/completions \\
-H "Authorization: Bearer sk-your-api-key" \\
-H "Content-Type: application/json" \\
-d '{
"model": "deepseek-ai/DeepSeek-V4-Flash",
"messages": [{"role": "user", "content": "Hello!"}]
}'
# List knowledge bases
curl http://210.16.161.218:18080/app/api/knowledge-bases \\
-H "Authorization: Bearer sk-your-api-key"''',
},
]
@router.get("/sdk/packages", summary="List SDK packages")
async def list_sdk_packages(
admin_user=Depends(get_current_admin),
):
"""List all available SDK packages with download info and code examples."""
return {"code": 0, "data": {"packages": SDK_PACKAGES}}
# ============================================================
# Interactive API Documentation
# ============================================================
API_DOC_SECTIONS = [
{
"section": "Authentication",
"icon": "lock",
"description": "All API requests require authentication via Bearer token.",
"endpoints": [
{
"method": "POST",
"path": "/app/api/auth/login",
"title": "User Login",
"description": "Authenticate and obtain an access token.",
"params": [
{"name": "username", "type": "string", "required": True, "desc": "Username"},
{"name": "password", "type": "string", "required": True, "desc": "Password"},
],
"example_request": '{"username": "admin", "password": "admin123"}',
"example_response": '{"code": 0, "data": {"access_token": "eyJ...", "token_type": "bearer", "user": {"id": "admin-001", "username": "admin"}}}',
},
],
},
{
"section": "Chat & Completions",
"icon": "chat",
"description": "AI chat completion endpoints for conversational AI.",
"endpoints": [
{
"method": "POST",
"path": "/app/api/chat/completions",
"title": "Chat Completion",
"description": "Create a chat completion with the specified model.",
"params": [
{"name": "model", "type": "string", "required": True, "desc": "Model ID (e.g. deepseek-ai/DeepSeek-V4-Flash)"},
{"name": "messages", "type": "array", "required": True, "desc": "Array of message objects"},
{"name": "stream", "type": "boolean", "required": False, "desc": "Enable streaming response"},
{"name": "temperature", "type": "number", "required": False, "desc": "Sampling temperature (0-2)"},
{"name": "max_tokens", "type": "integer", "required": False, "desc": "Maximum tokens to generate"},
],
"example_request": '{"model": "deepseek-ai/DeepSeek-V4-Flash", "messages": [{"role": "user", "content": "Hello"}]}',
"example_response": '{"code": 0, "data": {"id": "chat-xxx", "choices": [{"message": {"role": "assistant", "content": "Hi there!"}}]}}',
},
],
},
{
"section": "Knowledge Base",
"icon": "book",
"description": "Manage knowledge bases and documents for RAG.",
"endpoints": [
{
"method": "GET",
"path": "/app/api/knowledge-bases",
"title": "List Knowledge Bases",
"description": "Get all knowledge bases for the current user.",
"params": [],
"example_request": "",
"example_response": '{"code": 0, "data": [{"id": "kb-001", "name": "Product Docs", "document_count": 15}]}',
},
{
"method": "POST",
"path": "/app/api/knowledge-bases/{kb_id}/documents",
"title": "Upload Document",
"description": "Upload a document to a knowledge base for processing.",
"params": [
{"name": "file", "type": "file", "required": True, "desc": "Document file (PDF, TXT, DOCX, MD)"},
],
"example_request": "multipart/form-data: file=@document.pdf",
"example_response": '{"code": 0, "data": {"document_id": "doc-001", "status": "processing"}}',
},
],
},
{
"section": "Agents",
"icon": "robot",
"description": "Agent management and execution endpoints.",
"endpoints": [
{
"method": "GET",
"path": "/app/api/agents",
"title": "List Agents",
"description": "Get all available agents.",
"params": [],
"example_request": "",
"example_response": '{"code": 0, "data": [{"id": "agent-001", "name": "Code Assistant", "type": "agent"}]}',
},
{
"method": "POST",
"path": "/app/api/agents/{agent_id}/execute",
"title": "Execute Agent",
"description": "Execute an agent with a given input.",
"params": [
{"name": "input", "type": "string", "required": True, "desc": "Input text for the agent"},
{"name": "context", "type": "object", "required": False, "desc": "Additional context"},
],
"example_request": '{"input": "Help me write a Python function"}',
"example_response": '{"code": 0, "data": {"output": "Here is a Python function...", "tool_calls": []}}',
},
],
},
{
"section": "Workflows",
"icon": "workflow",
"description": "DAG workflow orchestration endpoints.",
"endpoints": [
{
"method": "GET",
"path": "/app/api/admin/workflows/templates",
"title": "List Workflow Templates",
"description": "Get predefined workflow templates.",
"params": [],
"example_request": "",
"example_response": '{"code": 0, "data": [{"id": "tpl-001", "name": "Q&A Pipeline", "nodes": [...]}]}',
},
{
"method": "POST",
"path": "/app/api/admin/workflows/{id}/execute",
"title": "Execute Workflow",
"description": "Execute a workflow with input data.",
"params": [
{"name": "input", "type": "string", "required": True, "desc": "Input for the workflow"},
],
"example_request": '{"input": "Summarize this article"}',
"example_response": '{"code": 0, "data": {"execution_id": "exec-001", "status": "completed", "result": "..."}}',
},
],
},
{
"section": "File Storage",
"icon": "storage",
"description": "File upload and storage management.",
"endpoints": [
{
"method": "POST",
"path": "/app/api/files/upload",
"title": "Upload File",
"description": "Upload a file to storage (MinIO/OSS).",
"params": [
{"name": "file", "type": "file", "required": True, "desc": "File to upload"},
{"name": "category", "type": "string", "required": False, "desc": "File category"},
],
"example_request": "multipart/form-data: file=@image.png",
"example_response": '{"code": 0, "data": {"url": "https://...", "size": 102400, "file_type": "image"}}',
},
],
},
{
"section": "Webhooks",
"icon": "webhook",
"description": "Webhook configuration and management.",
"endpoints": [
{
"method": "GET",
"path": "/app/api/admin/developer-tools/webhooks",
"title": "List Webhooks",
"description": "List all configured webhooks.",
"params": [],
"example_request": "",
"example_response": '{"code": 0, "data": {"total": 2, "items": [{"id": "wh_xxx", "name": "My Webhook", "url": "https://...", "status": "active"}]}}',
},
{
"method": "POST",
"path": "/app/api/admin/developer-tools/webhooks/",
"title": "Create Webhook",
"description": "Create a new webhook endpoint.",
"params": [
{"name": "name", "type": "string", "required": True, "desc": "Webhook name"},
{"name": "url", "type": "string", "required": True, "desc": "Webhook URL"},
{"name": "events", "type": "array", "required": True, "desc": "Events to subscribe"},
],
"example_request": '{"name": "My Webhook", "url": "https://example.com/webhook", "events": ["app.created"]}',
"example_response": '{"code": 0, "data": {"id": "wh_xxx", "secret": "whsec_..."}}',
},
],
},
]
@router.get("/api-docs", summary="Interactive API documentation")
async def get_api_docs(
admin_user=Depends(get_current_admin),
):
"""Get structured API documentation for the interactive docs viewer."""
total_endpoints = sum(len(s["endpoints"]) for s in API_DOC_SECTIONS)
return {
"code": 0,
"data": {
"sections": API_DOC_SECTIONS,
"total_sections": len(API_DOC_SECTIONS),
"total_endpoints": total_endpoints,
"base_url": "http://210.16.161.218:18080",
"api_version": "v1",
}
}
# ============================================================
# Enhanced Usage Dashboard
# ============================================================
@router.get("/usage/dashboard", summary="Enhanced usage dashboard")
async def usage_dashboard(
days: int = Query(7, ge=1, le=90),
admin_user=Depends(get_current_admin),
db: Session = Depends(get_db),
):
"""Enhanced usage statistics dashboard with trend data."""
now = cst_now()
start_date = now - timedelta(days=days)
# API call trends (daily)
trend_sql = """SELECT DATE(created_at) as date, COUNT(*) as calls,
SUM(CASE WHEN error_count > 0 THEN 1 ELSE 0 END) as errors,
SUM(CASE WHEN call_count > 0 THEN total_response_time / call_count ELSE 0 END) as avg_time
FROM api_call_stats
WHERE created_at >= :start
GROUP BY DATE(created_at)
ORDER BY date"""
trend_rows = _execute_raw(db, trend_sql, {"start": start_date})
trend = []
for r in trend_rows:
trend.append({
"date": r["date"].strftime("%Y-%m-%d") if r.get("date") else "",
"calls": r.get("calls", 0),
"errors": r.get("errors", 0),
"avg_time": round(r.get("avg_time", 0), 2) if r.get("avg_time") else 0,
})
# Top endpoints by call count
top_sql = """SELECT endpoint, method, call_count, error_count,
last_called_at, total_response_time
FROM api_call_stats
ORDER BY call_count DESC
LIMIT 10"""
top_rows = _execute_raw(db, top_sql)
top_endpoints = []
for r in top_rows:
top_endpoints.append({
"endpoint": r.get("endpoint", ""),
"method": r.get("method", ""),
"call_count": r.get("call_count", 0),
"error_count": r.get("error_count", 0),
"error_rate": round(r.get("error_count", 0) / max(r.get("call_count", 1), 1) * 100, 2) if r.get("call_count", 0) > 0 else 0,
"avg_response_time": round(r.get("total_response_time", 0) / max(r.get("call_count", 1), 1), 2) if r.get("call_count", 0) > 0 else 0,
"last_called_at": fmt_time(r.get("last_called_at")),
})
# Error distribution by status code
error_sql = """SELECT
CASE
WHEN error_count > 0 THEN 'has_errors'
ELSE 'success'
END as status_group,
COUNT(*) as count,
SUM(call_count) as total_calls
FROM api_call_stats
GROUP BY status_group"""
error_rows = _execute_raw(db, error_sql)
error_distribution = {"success": 0, "has_errors": 0, "success_calls": 0, "error_calls": 0}
for r in error_rows:
if r["status_group"] == "success":
error_distribution["success"] = r["count"]
error_distribution["success_calls"] = r.get("total_calls", 0)
else:
error_distribution["has_errors"] = r["count"]
error_distribution["error_calls"] = r.get("total_calls", 0)
# Response time distribution
latency_sql = """SELECT
CASE
WHEN (CASE WHEN call_count > 0 THEN total_response_time / call_count ELSE 0 END) < 100 THEN 'fast'
WHEN (CASE WHEN call_count > 0 THEN total_response_time / call_count ELSE 0 END) < 500 THEN 'normal'
WHEN (CASE WHEN call_count > 0 THEN total_response_time / call_count ELSE 0 END) < 1000 THEN 'slow'
ELSE 'very_slow'
END as latency_bucket,
COUNT(*) as count,
SUM(CASE WHEN call_count > 0 THEN total_response_time / call_count ELSE 0 END) as avg_time
FROM api_call_stats
WHERE total_response_time IS NOT NULL AND call_count > 0
GROUP BY latency_bucket"""
latency_rows = _execute_raw(db, latency_sql)
latency_distribution = {"fast": 0, "normal": 0, "slow": 0, "very_slow": 0}
latency_avg = {"fast": 0, "normal": 0, "slow": 0, "very_slow": 0}
for r in latency_rows:
bucket = r["latency_bucket"]
latency_distribution[bucket] = r["count"]
latency_avg[bucket] = round(r.get("avg_time", 0), 2) if r.get("avg_time") else 0
# API key usage distribution
key_sql = """SELECT k.name, k.status, COUNT(s.id) as endpoint_count,
COALESCE(SUM(s.call_count), 0) as total_calls,
COALESCE(SUM(s.error_count), 0) as total_errors
FROM api_keys k
LEFT JOIN api_call_stats s ON s.endpoint LIKE CONCAT('%api-key%', k.id, '%')
GROUP BY k.id, k.name, k.status
ORDER BY total_calls DESC
LIMIT 10"""
key_rows = _execute_raw(db, key_sql)
key_usage = []
for r in key_rows:
key_usage.append({
"name": r.get("name", "Unknown"),
"status": r.get("status", "unknown"),
"total_calls": r.get("total_calls", 0),
"total_errors": r.get("total_errors", 0),
})
# Summary
total_calls = sum(t["calls"] for t in trend)
total_errors = sum(t["errors"] for t in trend)
avg_response = sum(t["avg_time"] for t in trend) / len(trend) if trend else 0
error_rate = round(total_errors / total_calls * 100, 2) if total_calls > 0 else 0
return {
"code": 0,
"data": {
"summary": {
"total_calls": total_calls,
"total_errors": total_errors,
"error_rate": error_rate,
"avg_response_time": round(avg_response, 2),
"active_api_keys": error_distribution["success"] + error_distribution["has_errors"],
},
"trend": trend,
"top_endpoints": top_endpoints,
"error_distribution": error_distribution,
"latency_distribution": latency_distribution,
"latency_avg": latency_avg,
"key_usage": key_usage,
}
}