Phase 3.9: Developer Tools Enhancement - Webhooks, SDK Center, API Docs, Usage Dashboard

This commit is contained in:
qyyfw committed 2026-07-06 10:43:48 +08:00
1 parent 065d2f05f6
commit 49dcea0a11
6 files changed
+1460

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,
}
}
+4
View File
@@ -35,6 +35,8 @@ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from app.api import auth, chat, channels, knowledge, market, token_usage, payment, profile, notifications, assets, evolution from app.api import auth, chat, channels, knowledge, market, token_usage, payment, profile, notifications, assets, evolution
from app.api.admin import dashboard, users, models, settings, payment_admin, services, membership_admin, ads, market_admin, logs, skills_admin, agents_admin, knowledge_admin, workflow_admin, storage_admin, automation_admin, mcp_admin, developer_admin, apps_admin from app.api.admin import dashboard, users, models, settings, payment_admin, services, membership_admin, ads, market_admin, logs, skills_admin, agents_admin, knowledge_admin, workflow_admin, storage_admin, automation_admin, mcp_admin, developer_admin, apps_admin
from app.api.admin.developer_tools_admin import router as developer_tools_router
from app.api.admin import asset_admin
from app.config import setup_file_logging, SLOW_REQUEST_THRESHOLD from app.config import setup_file_logging, SLOW_REQUEST_THRESHOLD
from app.database import SessionLocal from app.database import SessionLocal
@@ -420,10 +422,12 @@ app.include_router(skills_admin.router, prefix="/app/api/admin/skills", tags=["
app.include_router(agents_admin.router, prefix="/app/api/admin/agents", tags=["后台-Agent管理"]) app.include_router(agents_admin.router, prefix="/app/api/admin/agents", tags=["后台-Agent管理"])
app.include_router(knowledge_admin.router, prefix="/app/api/admin/knowledge", tags=["后台-知识库管理"]) app.include_router(knowledge_admin.router, prefix="/app/api/admin/knowledge", tags=["后台-知识库管理"])
app.include_router(workflow_admin.router, prefix="/app/api/admin/workflows", tags=["后台-工作流编排"]) app.include_router(workflow_admin.router, prefix="/app/api/admin/workflows", tags=["后台-工作流编排"])
app.include_router(asset_admin.router, prefix="/app/api/admin/assets", tags=["后台-资产管理"])
app.include_router(storage_admin.router, prefix="/app/api/admin/storage", tags=["后台-存储配置"]) app.include_router(storage_admin.router, prefix="/app/api/admin/storage", tags=["后台-存储配置"])
app.include_router(automation_admin.router, prefix="/app/api/admin/automation", tags=["后台-自动化任务"]) app.include_router(automation_admin.router, prefix="/app/api/admin/automation", tags=["后台-自动化任务"])
app.include_router(mcp_admin.router, prefix="/app/api/admin/mcp", tags=["后台-MCP服务"]) app.include_router(mcp_admin.router, prefix="/app/api/admin/mcp", tags=["后台-MCP服务"])
app.include_router(developer_admin.router, prefix="/app/api/admin/developer", tags=["后台-开发者API"]) app.include_router(developer_admin.router, prefix="/app/api/admin/developer", tags=["后台-开发者API"])
app.include_router(developer_tools_router, prefix="/app/api/admin/developer-tools", tags=["dev-tools"])
app.include_router(apps_admin.router, prefix="/app/api/admin/apps", tags=["admin-ai-apps"]) app.include_router(apps_admin.router, prefix="/app/api/admin/apps", tags=["admin-ai-apps"])
+2
View File
@@ -40,6 +40,7 @@ const AdminKnowledge = lazy(() => import('@/pages/admin/Knowledge'));
const AdminApps = lazy(() => import('@/pages/admin/Apps')); const AdminApps = lazy(() => import('@/pages/admin/Apps'));
const AdminAutomation = lazy(() => import('@/pages/admin/Automation')); const AdminAutomation = lazy(() => import('@/pages/admin/Automation'));
const AdminDeveloper = lazy(() => import('@/pages/admin/Developer')); const AdminDeveloper = lazy(() => import('@/pages/admin/Developer'));
const AdminDeveloperTools = lazy(() => import("@/pages/admin/DeveloperTools"));
const AdminMcp = lazy(() => import('@/pages/admin/Mcp')); const AdminMcp = lazy(() => import('@/pages/admin/Mcp'));
const AdminStorage = lazy(() => import('@/pages/admin/Storage')); const AdminStorage = lazy(() => import('@/pages/admin/Storage'));
const AdminAssetCenter = lazy(() => import('@/pages/admin/AssetCenter')); const AdminAssetCenter = lazy(() => import('@/pages/admin/AssetCenter'));
@@ -132,6 +133,7 @@ const App: React.FC = () => {
<Route path="asset-center" element={<AdminAssetCenter />} /> <Route path="asset-center" element={<AdminAssetCenter />} />
<Route path="automation" element={<AdminAutomation />} /> <Route path="automation" element={<AdminAutomation />} />
<Route path="developer" element={<AdminDeveloper />} /> <Route path="developer" element={<AdminDeveloper />} />
<Route path="developer-tools" element={<AdminDeveloperTools />} />
</Route> </Route>
<Route path="*" element={<Navigate to="/" />} /> <Route path="*" element={<Navigate to="/" />} />
</Routes> </Routes>
@@ -59,6 +59,7 @@ const navGroups: NavGroup[] = [
{ path: '/admin/asset-center', icon: 'database', label: '资产中心' }, { path: '/admin/asset-center', icon: 'database', label: '资产中心' },
{ path: '/admin/automation', icon: 'automation', label: '自动化任务' }, { path: '/admin/automation', icon: 'automation', label: '自动化任务' },
{ path: '/admin/developer', icon: 'developer', label: '开发者工具' }, { path: '/admin/developer', icon: 'developer', label: '开发者工具' },
{ path: "/admin/developer-tools", label: "开发者工具", icon: "🔧" },
], ],
}, },
]; ];
+17
View File
@@ -136,3 +136,20 @@
.dev-stats-grid { grid-template-columns: repeat(2, 1fr); } .dev-stats-grid { grid-template-columns: repeat(2, 1fr); }
.dev-table { font-size: 12px; } .dev-table { font-size: 12px; }
} }
/* Phase 3.9: Developer Tools styles */
.dev-method { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; font-family: monospace; }
.method-get { background: rgba(82,196,26,0.2); color: #52c41a; }
.method-post { background: rgba(22,119,255,0.2); color: #1677ff; }
.method-put { background: rgba(250,173,20,0.2); color: #faad14; }
.method-delete { background: rgba(255,77,79,0.2); color: #ff4d4f; }
.dev-modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.6); display: flex; align-items: center; justify-content: center; z-index: 1000; }
.dev-modal { background: #1a1a2e; border: 1px solid rgba(255,255,255,0.1); border-radius: 12px; padding: 24px; width: 500px; max-width: 90vw; max-height: 80vh; overflow-y: auto; }
.dev-modal h3 { margin: 0 0 16px; color: rgba(255,255,255,0.85); }
.dev-form-group { margin-bottom: 14px; }
.dev-form-group label { display: block; font-size: 13px; color: rgba(255,255,255,0.55); margin-bottom: 4px; }
.dev-modal-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 20px; }
.dev-muted { color: rgba(255,255,255,0.35); font-size: 12px; }
.dev-test-result { margin: 12px 0; padding: 12px; border-radius: 8px; font-size: 14px; }
+537
View File
@@ -0,0 +1,537 @@
import { useState, useEffect, useCallback } from 'react';
import './Developer.css';
const API_BASE = '/app/api';
interface Webhook {
id: string; name: string; url: string; events: string[];
has_secret: boolean; headers: Record<string, string>; status: string;
last_triggered_at: string | null; last_status: string | null;
total_deliveries: number; success_count: number; fail_count: number;
success_rate: number; created_at: string; updated_at: string;
}
interface WebhookEvent {
value: string; label: string; desc: string;
}
interface WebhookDelivery {
id: string; event: string; status: string; response_code: number | null;
response_time_ms: number | null; attempts: number; error_message: string | null;
payload: any; response_body: string; created_at: string;
}
interface SDKPackage {
language: string; name: string; version: string; description: string;
install_cmd: string; download_url: string; github_url: string;
min_version: string; features: string[]; code_example: string;
}
interface ApiDocSection {
section: string; icon: string; description: string;
endpoints: Array<{
method: string; path: string; title: string; description: string;
params: Array<{ name: string; type: string; required: boolean; desc: string }>;
example_request: string; example_response: string;
}>;
}
interface PageData<T> { total: number; page: number; page_size: number; items: T[] }
async function api(path: string, options?: RequestInit) {
const token = localStorage.getItem('token');
const res = await fetch(`${API_BASE}${path}`, {
...options,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
...(options?.headers || {}),
},
});
if (res.status === 307) {
const location = res.headers.get('Location');
if (location) return api(location.replace(API_BASE, ''), options);
}
const data = await res.json();
if (!res.ok) throw new Error(data.detail || `HTTP ${res.status}`);
return data;
}
export default function AdminDeveloperTools() {
const [activeTab, setActiveTab] = useState<'webhooks' | 'sdk' | 'docs'>('webhooks');
const [loading, setLoading] = useState(false);
// Webhooks state
const [webhooks, setWebhooks] = useState<PageData<Webhook>>({ total: 0, page: 1, page_size: 20, items: [] });
const [webhookEvents, setWebhookEvents] = useState<WebhookEvent[]>([]);
const [showWebhookModal, setShowWebhookModal] = useState(false);
const [webhookForm, setWebhookForm] = useState({ name: '', url: '', events: [] as string[] });
const [selectedWebhook, setSelectedWebhook] = useState<Webhook | null>(null);
const [deliveries, setDeliveries] = useState<PageData<WebhookDelivery>>({ total: 0, page: 1, page_size: 20, items: [] });
const [testResult, setTestResult] = useState<any>(null);
// SDK state
const [sdkPackages, setSdkPackages] = useState<SDKPackage[]>([]);
const [selectedSdk, setSelectedSdk] = useState<string>('python');
// API Docs state
const [docSections, setDocSections] = useState<ApiDocSection[]>([]);
const [expandedSection, setExpandedSection] = useState<string | null>(null);
const [expandedEndpoint, setExpandedEndpoint] = useState<string | null>(null);
const loadWebhooks = useCallback(async (page = 1) => {
setLoading(true);
try {
const res = await api(`/admin/developer-tools/webhooks?page=${page}&page_size=20`);
if (res.code === 0) setWebhooks(res.data);
} catch (e) { console.error('Webhooks error:', e); }
setLoading(false);
}, []);
const loadWebhookEvents = useCallback(async () => {
try {
const res = await api('/admin/developer-tools/webhooks/events');
if (res.code === 0) setWebhookEvents(res.data.events);
} catch (e) { console.error('Events error:', e); }
}, []);
const loadDeliveries = useCallback(async (webhookId: string, page = 1) => {
try {
const res = await api(`/admin/developer-tools/webhooks/${webhookId}/deliveries?page=${page}&page_size=20`);
if (res.code === 0) setDeliveries(res.data);
} catch (e) { console.error('Deliveries error:', e); }
}, []);
const loadSdkPackages = useCallback(async () => {
try {
const res = await api('/admin/developer-tools/sdk/packages');
if (res.code === 0) setSdkPackages(res.data.packages);
} catch (e) { console.error('SDK error:', e); }
}, []);
const loadApiDocs = useCallback(async () => {
try {
const res = await api('/admin/developer-tools/api-docs');
if (res.code === 0) setDocSections(res.data.sections);
} catch (e) { console.error('Docs error:', e); }
}, []);
useEffect(() => { if (activeTab === 'webhooks') { loadWebhooks(); loadWebhookEvents(); } }, [activeTab, loadWebhooks, loadWebhookEvents]);
useEffect(() => { if (activeTab === 'sdk') loadSdkPackages(); }, [activeTab, loadSdkPackages]);
useEffect(() => { if (activeTab === 'docs') loadApiDocs(); }, [activeTab, loadApiDocs]);
const handleCreateWebhook = async () => {
try {
const body = { name: webhookForm.name, url: webhookForm.url, events: webhookForm.events, headers: {} };
const res = await api('/admin/developer-tools/webhooks/', { method: 'POST', body: JSON.stringify(body) });
setShowWebhookModal(false);
setWebhookForm({ name: '', url: '', events: [] });
loadWebhooks();
if (res.data?.secret) {
alert(`Webhook created! Secret: ${res.data.secret}\nSave this secret for HMAC verification.`);
}
} catch (e: any) { alert(e.message); }
};
const handleDeleteWebhook = async (id: string) => {
if (!confirm('Delete this webhook?')) return;
try {
await api(`/admin/developer-tools/webhooks/${id}`, { method: 'DELETE' });
loadWebhooks();
setSelectedWebhook(null);
} catch (e: any) { alert(e.message); }
};
const handleToggleWebhook = async (wh: Webhook) => {
const newStatus = wh.status === 'active' ? 'paused' : 'active';
try {
await api(`/admin/developer-tools/webhooks/${wh.id}`, { method: 'PUT', body: JSON.stringify({ status: newStatus }) });
loadWebhooks();
} catch (e: any) { alert(e.message); }
};
const handleTestWebhook = async (id: string) => {
setTestResult(null);
try {
const res = await api(`/admin/developer-tools/webhooks/${id}/test`, {
method: 'POST', body: JSON.stringify({ event: 'test', payload: { message: 'Test delivery from admin' } })
});
setTestResult(res.data);
loadWebhooks();
} catch (e: any) { alert(e.message); }
};
const handleViewDeliveries = (wh: Webhook) => {
setSelectedWebhook(wh);
setDeliveries({ total: 0, page: 1, page_size: 20, items: [] });
loadDeliveries(wh.id);
};
const fmtTime = (t: string | null) => {
if (!t) return '-';
try { return new Date(t).toLocaleString('zh-CN'); } catch { return t; }
};
const statusBadge = (status: string) => {
const cls = status === 'active' ? 'badge-active' : status === 'paused' ? 'badge-disabled' : status === 'success' ? 'badge-active' : 'badge-revoked';
return <span className={`dev-badge ${cls}`}>{status}</span>;
};
const methodBadge = (method: string) => {
const colors: Record<string, string> = { GET: 'method-get', POST: 'method-post', PUT: 'method-put', DELETE: 'method-delete' };
return <span className={`dev-method ${colors[method] || ''}`}>{method}</span>;
};
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text).then(() => {
alert('Copied to clipboard');
}).catch(() => alert('Copy failed'));
};
return (
<div className="admin-developer">
<div className="dev-header">
<h2>开发者工具</h2>
<p>Webhook 配置、SDK 下载中心、交互式 API 文档</p>
</div>
{/* Tab Navigation */}
<div className="dev-tabs">
<button className={`dev-tab ${activeTab === 'webhooks' ? 'active' : ''}`} onClick={() => setActiveTab('webhooks')}>Webhook 配置</button>
<button className={`dev-tab ${activeTab === 'sdk' ? 'active' : ''}`} onClick={() => setActiveTab('sdk')}>SDK 下载中心</button>
<button className={`dev-tab ${activeTab === 'docs' ? 'active' : ''}`} onClick={() => setActiveTab('docs')}>API 文档</button>
</div>
{/* Tab Content */}
<div className="dev-tab-content">
{loading && <div className="dev-loading">Loading...</div>}
{/* ===== Webhooks Tab ===== */}
{activeTab === 'webhooks' && !loading && (
<div>
{selectedWebhook ? (
/* Webhook Detail / Deliveries View */
<div>
<div className="dev-toolbar">
<button className="dev-btn" onClick={() => { setSelectedWebhook(null); setDeliveries({ total: 0, page: 1, page_size: 20, items: [] }); }}>
&larr; Back to list
</button>
<h3 style={{ margin: '0 16px' }}>{selectedWebhook.name} - Delivery History</h3>
<button className="dev-btn primary" onClick={() => handleTestWebhook(selectedWebhook.id)}>Send Test</button>
</div>
{testResult && (
<div className="dev-test-result" style={{ margin: '12px 0', padding: '12px', background: 'rgba(255,255,255,0.05)', borderRadius: '8px', border: `1px solid ${testResult.status === 'success' ? '#52c41a' : '#ff4d4f'}` }}>
<strong>Test Result: </strong>
<span style={{ color: testResult.status === 'success' ? '#52c41a' : '#ff4d4f' }}>{testResult.status}</span>
{testResult.response_code && <span> | HTTP {testResult.response_code}</span>}
{testResult.response_time_ms != null && <span> | {testResult.response_time_ms}ms</span>}
{testResult.error_message && <div style={{ color: '#ff4d4f', marginTop: '4px' }}>{testResult.error_message}</div>}
</div>
)}
<table className="dev-table">
<thead>
<tr><th>Event</th><th>Status</th><th>Response Code</th><th>Response Time</th><th>Attempts</th><th>Time</th><th>Error</th></tr>
</thead>
<tbody>
{deliveries.items.map(d => (
<tr key={d.id}>
<td className="mono-text">{d.event}</td>
<td>{statusBadge(d.status)}</td>
<td>{d.response_code || '-'}</td>
<td>{d.response_time_ms != null ? `${d.response_time_ms}ms` : '-'}</td>
<td>{d.attempts}</td>
<td>{fmtTime(d.created_at)}</td>
<td style={{ maxWidth: '200px', overflow: 'hidden', textOverflow: 'ellipsis' }}>{d.error_message || '-'}</td>
</tr>
))}
{deliveries.items.length === 0 && <tr><td colSpan={7} className="dev-empty">No delivery records</td></tr>}
</tbody>
</table>
{deliveries.total > 20 && (
<div className="dev-pagination">
<button disabled={deliveries.page <= 1} onClick={() => loadDeliveries(selectedWebhook.id, deliveries.page - 1)}>Prev</button>
<span>Page {deliveries.page} / {Math.ceil(deliveries.total / 20)}</span>
<button disabled={deliveries.page >= Math.ceil(deliveries.total / 20)} onClick={() => loadDeliveries(selectedWebhook.id, deliveries.page + 1)}>Next</button>
</div>
)}
</div>
) : (
/* Webhook List View */
<div>
<div className="dev-toolbar">
<button className="dev-btn primary" onClick={() => setShowWebhookModal(true)}>+ New Webhook</button>
</div>
<table className="dev-table">
<thead>
<tr><th>Name</th><th>URL</th><th>Events</th><th>Status</th><th>Deliveries</th><th>Success Rate</th><th>Last Triggered</th><th>Actions</th></tr>
</thead>
<tbody>
{webhooks.items.map(w => (
<tr key={w.id}>
<td>{w.name}</td>
<td className="mono-text" style={{ maxWidth: '200px', overflow: 'hidden', textOverflow: 'ellipsis' }}>{w.url}</td>
<td>
{w.events.length > 0 ? (
<span>{w.events.length} events</span>
) : <span className="dev-muted">All events</span>}
</td>
<td>{statusBadge(w.status)}</td>
<td>{w.total_deliveries}</td>
<td>
<span style={{ color: w.success_rate >= 80 ? '#52c41a' : w.success_rate >= 50 ? '#faad14' : '#ff4d4f' }}>
{w.success_rate}%
</span>
<span className="dev-muted"> ({w.success_count}/{w.total_deliveries})</span>
</td>
<td>{fmtTime(w.last_triggered_at)}</td>
<td className="dev-actions">
<button className="dev-btn-sm" onClick={() => handleViewDeliveries(w)}>Deliveries</button>
<button className="dev-btn-sm" onClick={() => handleTestWebhook(w.id)}>Test</button>
<button className="dev-btn-sm" onClick={() => handleToggleWebhook(w)}>{w.status === 'active' ? 'Pause' : 'Resume'}</button>
<button className="dev-btn-sm danger" onClick={() => handleDeleteWebhook(w.id)}>Delete</button>
</td>
</tr>
))}
{webhooks.items.length === 0 && <tr><td colSpan={8} className="dev-empty">No webhooks configured</td></tr>}
</tbody>
</table>
{webhooks.total > 20 && (
<div className="dev-pagination">
<button disabled={webhooks.page <= 1} onClick={() => loadWebhooks(webhooks.page - 1)}>Prev</button>
<span>Page {webhooks.page} / {Math.ceil(webhooks.total / 20)}</span>
<button disabled={webhooks.page >= Math.ceil(webhooks.total / 20)} onClick={() => loadWebhooks(webhooks.page + 1)}>Next</button>
</div>
)}
{/* Supported Events Info */}
{webhookEvents.length > 0 && (
<div style={{ marginTop: '20px', padding: '16px', background: 'rgba(255,255,255,0.03)', borderRadius: '8px' }}>
<h4 style={{ margin: '0 0 12px', color: 'rgba(255,255,255,0.85)' }}>Supported Events</h4>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))', gap: '8px' }}>
{webhookEvents.map(ev => (
<div key={ev.value} style={{ padding: '8px 12px', background: 'rgba(255,255,255,0.05)', borderRadius: '6px' }}>
<code style={{ color: '#69b1ff' }}>{ev.value}</code>
<div style={{ fontSize: '12px', color: 'rgba(255,255,255,0.45)', marginTop: '2px' }}>{ev.desc}</div>
</div>
))}
</div>
</div>
)}
</div>
)}
{/* Create Webhook Modal */}
{showWebhookModal && (
<div className="dev-modal-overlay" onClick={() => setShowWebhookModal(false)}>
<div className="dev-modal" onClick={e => e.stopPropagation()}>
<h3>New Webhook</h3>
<div className="dev-form-group">
<label>Name</label>
<input className="dev-input" value={webhookForm.name} onChange={e => setWebhookForm({ ...webhookForm, name: e.target.value })} placeholder="My Webhook" />
</div>
<div className="dev-form-group">
<label>URL</label>
<input className="dev-input" value={webhookForm.url} onChange={e => setWebhookForm({ ...webhookForm, url: e.target.value })} placeholder="https://example.com/webhook" />
</div>
<div className="dev-form-group">
<label>Events (leave empty for all)</label>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px', marginTop: '8px' }}>
{webhookEvents.map(ev => (
<label key={ev.value} style={{ display: 'flex', alignItems: 'center', gap: '4px', cursor: 'pointer', padding: '4px 10px', background: webhookForm.events.includes(ev.value) ? 'rgba(22,119,255,0.2)' : 'rgba(255,255,255,0.05)', borderRadius: '6px', fontSize: '13px' }}>
<input type="checkbox" checked={webhookForm.events.includes(ev.value)} onChange={e => {
if (e.target.checked) setWebhookForm({ ...webhookForm, events: [...webhookForm.events, ev.value] });
else setWebhookForm({ ...webhookForm, events: webhookForm.events.filter(x => x !== ev.value) });
}} />
{ev.label}
</label>
))}
</div>
</div>
<div className="dev-modal-actions">
<button className="dev-btn" onClick={() => setShowWebhookModal(false)}>Cancel</button>
<button className="dev-btn primary" onClick={handleCreateWebhook} disabled={!webhookForm.name || !webhookForm.url}>Create</button>
</div>
</div>
</div>
)}
</div>
)}
{/* ===== SDK Download Center Tab ===== */}
{activeTab === 'sdk' && !loading && (
<div>
{/* Language Selector */}
<div className="dev-toolbar">
{sdkPackages.map(pkg => (
<button key={pkg.language} className={`dev-tab ${selectedSdk === pkg.language ? 'active' : ''}`} onClick={() => setSelectedSdk(pkg.language)}>
{pkg.name}
</button>
))}
</div>
{sdkPackages.filter(p => p.language === selectedSdk).map(pkg => (
<div key={pkg.language} style={{ padding: '20px', background: 'rgba(255,255,255,0.03)', borderRadius: '12px' }}>
{/* Package Info */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '16px' }}>
<div>
<h3 style={{ margin: '0 0 4px' }}>{pkg.name} <span className="dev-badge badge-active">v{pkg.version}</span></h3>
<p style={{ margin: '0', color: 'rgba(255,255,255,0.55)' }}>{pkg.description}</p>
<p style={{ margin: '4px 0 0', fontSize: '13px', color: 'rgba(255,255,255,0.4)' }}>Requires: {pkg.min_version}</p>
</div>
<div style={{ display: 'flex', gap: '8px' }}>
<a href={pkg.download_url} target="_blank" rel="noopener noreferrer" className="dev-btn primary">Download</a>
{pkg.github_url && <a href={pkg.github_url} target="_blank" rel="noopener noreferrer" className="dev-btn">GitHub</a>}
</div>
</div>
{/* Install Command */}
<div style={{ marginBottom: '16px' }}>
<label style={{ fontSize: '13px', color: 'rgba(255,255,255,0.5)', marginBottom: '4px', display: 'block' }}>Installation</label>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<code style={{ flex: 1, padding: '10px 14px', background: 'rgba(0,0,0,0.3)', borderRadius: '8px', fontFamily: 'monospace', fontSize: '14px' }}>
{pkg.install_cmd}
</code>
<button className="dev-btn-sm" onClick={() => copyToClipboard(pkg.install_cmd)}>Copy</button>
</div>
</div>
{/* Features */}
<div style={{ marginBottom: '16px' }}>
<label style={{ fontSize: '13px', color: 'rgba(255,255,255,0.5)', marginBottom: '8px', display: 'block' }}>Features</label>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
{pkg.features.map(f => (
<span key={f} style={{ padding: '4px 12px', background: 'rgba(82,196,26,0.15)', borderRadius: '16px', fontSize: '13px', color: '#52c41a', border: '1px solid rgba(82,196,26,0.3)' }}>
{f}
</span>
))}
</div>
</div>
{/* Code Example */}
<div>
<label style={{ fontSize: '13px', color: 'rgba(255,255,255,0.5)', marginBottom: '4px', display: 'block' }}>Quick Start Example</label>
<div style={{ position: 'relative' }}>
<pre style={{ padding: '16px', background: 'rgba(0,0,0,0.4)', borderRadius: '8px', overflow: 'auto', fontSize: '13px', lineHeight: '1.6', maxHeight: '400px' }}>
<code>{pkg.code_example}</code>
</pre>
<button className="dev-btn-sm" style={{ position: 'absolute', top: '8px', right: '8px' }} onClick={() => copyToClipboard(pkg.code_example)}>Copy</button>
</div>
</div>
</div>
))}
{sdkPackages.length === 0 && <div className="dev-empty">Loading SDK packages...</div>}
</div>
)}
{/* ===== Interactive API Docs Tab ===== */}
{activeTab === 'docs' && !loading && (
<div>
<div style={{ marginBottom: '16px', padding: '12px 16px', background: 'rgba(22,119,255,0.1)', borderRadius: '8px', border: '1px solid rgba(22,119,255,0.3)' }}>
<strong style={{ color: '#69b1ff' }}>Base URL:</strong>
<code style={{ marginLeft: '8px', color: 'rgba(255,255,255,0.8)' }}>http://210.16.161.218:18080</code>
<span style={{ marginLeft: '16px', color: 'rgba(255,255,255,0.4)' }}>API Version: v1</span>
</div>
{docSections.map(section => {
const isExpanded = expandedSection === section.section;
return (
<div key={section.section} style={{ marginBottom: '8px', borderRadius: '8px', overflow: 'hidden', border: '1px solid rgba(255,255,255,0.1)' }}>
{/* Section Header */}
<div
onClick={() => setExpandedSection(isExpanded ? null : section.section)}
style={{ padding: '14px 16px', background: 'rgba(255,255,255,0.05)', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}
>
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
<span style={{ fontSize: '18px' }}>
{section.icon === 'lock' ? '🔒' : section.icon === 'chat' ? '💬' : section.icon === 'book' ? '📚' : section.icon === 'robot' ? '🤖' : section.icon === 'workflow' ? '🔄' : section.icon === 'storage' ? '💾' : section.icon === 'webhook' ? '🔗' : '📋'}
</span>
<strong style={{ color: 'rgba(255,255,255,0.85)' }}>{section.section}</strong>
<span style={{ color: 'rgba(255,255,255,0.4)', fontSize: '13px' }}>{section.endpoints.length} endpoints</span>
</div>
<span style={{ color: 'rgba(255,255,255,0.4)' }}>{isExpanded ? '▼' : '▶'}</span>
</div>
{/* Section Content */}
{isExpanded && (
<div style={{ background: 'rgba(0,0,0,0.2)' }}>
<p style={{ padding: '8px 16px', margin: '0', color: 'rgba(255,255,255,0.5)', fontSize: '13px' }}>{section.description}</p>
{section.endpoints.map(ep => {
const epKey = `${section.section}-${ep.path}`;
const isEpExpanded = expandedEndpoint === epKey;
return (
<div key={epKey} style={{ borderTop: '1px solid rgba(255,255,255,0.05)' }}>
<div
onClick={() => setExpandedEndpoint(isEpExpanded ? null : epKey)}
style={{ padding: '10px 16px', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '12px' }}
>
{methodBadge(ep.method)}
<code style={{ color: 'rgba(255,255,255,0.7)', fontSize: '13px' }}>{ep.path}</code>
<span style={{ color: 'rgba(255,255,255,0.5)', fontSize: '13px' }}>{ep.title}</span>
<span style={{ marginLeft: 'auto', color: 'rgba(255,255,255,0.3)' }}>{isEpExpanded ? '▼' : '▶'}</span>
</div>
{isEpExpanded && (
<div style={{ padding: '0 16px 16px 16px' }}>
<p style={{ color: 'rgba(255,255,255,0.55)', margin: '4px 0 12px', fontSize: '13px' }}>{ep.description}</p>
{/* Parameters */}
{ep.params.length > 0 && (
<div style={{ marginBottom: '12px' }}>
<label style={{ fontSize: '12px', color: 'rgba(255,255,255,0.4)', display: 'block', marginBottom: '6px' }}>Parameters</label>
<table className="dev-table" style={{ fontSize: '13px' }}>
<thead>
<tr><th>Name</th><th>Type</th><th>Required</th><th>Description</th></tr>
</thead>
<tbody>
{ep.params.map(p => (
<tr key={p.name}>
<td className="mono-text">{p.name}</td>
<td>{p.type}</td>
<td>{p.required ? <span style={{ color: '#ff4d4f' }}>Yes</span> : <span style={{ color: 'rgba(255,255,255,0.4)' }}>No</span>}</td>
<td>{p.desc}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
{/* Example Request */}
{ep.example_request && (
<div style={{ marginBottom: '12px' }}>
<label style={{ fontSize: '12px', color: 'rgba(255,255,255,0.4)', display: 'block', marginBottom: '4px' }}>Example Request</label>
<pre style={{ padding: '10px 14px', background: 'rgba(0,0,0,0.3)', borderRadius: '6px', fontSize: '12px', overflow: 'auto' }}>
<code>{ep.example_request}</code>
</pre>
</div>
)}
{/* Example Response */}
{ep.example_response && (
<div>
<label style={{ fontSize: '12px', color: 'rgba(255,255,255,0.4)', display: 'block', marginBottom: '4px' }}>Example Response</label>
<pre style={{ padding: '10px 14px', background: 'rgba(0,0,0,0.3)', borderRadius: '6px', fontSize: '12px', overflow: 'auto' }}>
<code>{ep.example_response}</code>
</pre>
</div>
)}
</div>
)}
</div>
);
})}
</div>
)}
</div>
);
})}
{docSections.length === 0 && <div className="dev-empty">Loading API documentation...</div>}
</div>
)}
</div>
</div>
);
}