900 lines
34 KiB
Python
900 lines
34 KiB
Python
"""
|
|
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,
|
|
}
|
|
}
|