Spaces:
Paused
Paused
Deploy emergent2api
Browse files- Dockerfile +1 -5
- emergent2api/__init__.py +0 -1
- emergent2api/app.py +2 -9
- emergent2api/backends/__init__.py +0 -1
- emergent2api/routes/__init__.py +0 -1
- emergent2api/static/admin/index.html +404 -0
Dockerfile
CHANGED
|
@@ -2,11 +2,7 @@ FROM python:3.12-slim
|
|
| 2 |
|
| 3 |
WORKDIR /app
|
| 4 |
|
| 5 |
-
|
| 6 |
-
gcc libpq-dev && \
|
| 7 |
-
rm -rf /var/lib/apt/lists/*
|
| 8 |
-
|
| 9 |
-
COPY requirements.txt requirements.txt
|
| 10 |
RUN pip install --no-cache-dir -r requirements.txt
|
| 11 |
|
| 12 |
COPY . .
|
|
|
|
| 2 |
|
| 3 |
WORKDIR /app
|
| 4 |
|
| 5 |
+
COPY requirements.txt .
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
RUN pip install --no-cache-dir -r requirements.txt
|
| 7 |
|
| 8 |
COPY . .
|
emergent2api/__init__.py
CHANGED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
|
|
|
|
|
|
emergent2api/app.py
CHANGED
|
@@ -128,16 +128,9 @@ async def root():
|
|
| 128 |
|
| 129 |
@app.get("/admin")
|
| 130 |
@app.get("/admin/")
|
| 131 |
-
async def admin_index():
|
| 132 |
-
return FileResponse(_STATIC_DIR / "login.html")
|
| 133 |
-
|
| 134 |
-
|
| 135 |
@app.get("/admin/{page}")
|
| 136 |
-
async def admin_page(page: str):
|
| 137 |
-
|
| 138 |
-
if html.is_file():
|
| 139 |
-
return FileResponse(html)
|
| 140 |
-
return FileResponse(_STATIC_DIR / "login.html")
|
| 141 |
|
| 142 |
|
| 143 |
@app.get("/health")
|
|
|
|
| 128 |
|
| 129 |
@app.get("/admin")
|
| 130 |
@app.get("/admin/")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
@app.get("/admin/{page}")
|
| 132 |
+
async def admin_page(page: str = ""):
|
| 133 |
+
return FileResponse(_STATIC_DIR / "index.html")
|
|
|
|
|
|
|
|
|
|
| 134 |
|
| 135 |
|
| 136 |
@app.get("/health")
|
emergent2api/backends/__init__.py
CHANGED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
|
|
|
|
|
|
emergent2api/routes/__init__.py
CHANGED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
|
|
|
|
|
|
emergent2api/static/admin/index.html
ADDED
|
@@ -0,0 +1,404 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="zh-CN">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
| 5 |
+
<title>Emergent2API</title>
|
| 6 |
+
<style>
|
| 7 |
+
:root{--bg:#f8f9fa;--card:#fff;--border:#e8e8e8;--text:#1a1a1a;--text2:#666;--text3:#999;--primary:#1a1a1a;--green:#22c55e;--red:#ef4444;--orange:#f59e0b;--blue:#3b82f6;--radius:8px}
|
| 8 |
+
*{margin:0;padding:0;box-sizing:border-box}
|
| 9 |
+
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;background:var(--bg);color:var(--text);font-size:14px;line-height:1.5}
|
| 10 |
+
a{color:var(--primary);text-decoration:none}
|
| 11 |
+
|
| 12 |
+
/* NAV */
|
| 13 |
+
.nav{background:var(--card);border-bottom:1px solid var(--border);height:48px;display:flex;align-items:center;padding:0 24px;position:sticky;top:0;z-index:100}
|
| 14 |
+
.nav-brand{font-weight:700;font-size:16px;margin-right:8px}
|
| 15 |
+
.nav-user{color:var(--text3);font-size:13px;margin-right:28px}
|
| 16 |
+
.nav-links{display:flex;gap:0}
|
| 17 |
+
.nav-link{padding:12px 16px;font-size:14px;color:var(--text2);cursor:pointer;border-bottom:2px solid transparent;transition:.15s}
|
| 18 |
+
.nav-link:hover{color:var(--text)}
|
| 19 |
+
.nav-link.active{color:var(--text);font-weight:600;border-bottom-color:var(--text)}
|
| 20 |
+
.nav-right{margin-left:auto;display:flex;gap:12px;align-items:center}
|
| 21 |
+
.nav-right span{font-size:13px;color:var(--text3);cursor:pointer}
|
| 22 |
+
.nav-right span:hover{color:var(--red)}
|
| 23 |
+
|
| 24 |
+
/* PAGE */
|
| 25 |
+
.page{display:none;max-width:1100px;margin:0 auto;padding:24px}
|
| 26 |
+
.page.active{display:block}
|
| 27 |
+
.page-title{font-size:22px;font-weight:700;margin-bottom:4px}
|
| 28 |
+
.page-desc{color:var(--text3);font-size:13px;margin-bottom:20px}
|
| 29 |
+
|
| 30 |
+
/* STATS */
|
| 31 |
+
.stats{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:20px}
|
| 32 |
+
.stat{background:var(--card);border:1px solid var(--border);border-radius:var(--radius);padding:16px 20px}
|
| 33 |
+
.stat-val{font-size:26px;font-weight:700}
|
| 34 |
+
.stat-val.green{color:var(--green)}.stat-val.red{color:var(--red)}.stat-val.orange{color:var(--orange)}
|
| 35 |
+
.stat-label{font-size:12px;color:var(--text3);margin-top:2px}
|
| 36 |
+
|
| 37 |
+
/* FILTERS */
|
| 38 |
+
.filters{display:flex;align-items:center;gap:8px;margin-bottom:16px;flex-wrap:wrap}
|
| 39 |
+
.filter-btn{padding:6px 14px;border:1px solid var(--border);border-radius:16px;font-size:13px;cursor:pointer;background:var(--card);color:var(--text2);transition:.15s}
|
| 40 |
+
.filter-btn:hover{border-color:var(--text3)}
|
| 41 |
+
.filter-btn.active{background:var(--primary);color:#fff;border-color:var(--primary)}
|
| 42 |
+
.filter-count{font-weight:600;margin-left:2px}
|
| 43 |
+
.filter-spacer{flex:1}
|
| 44 |
+
.action-btn{padding:6px 16px;border:none;border-radius:6px;font-size:13px;cursor:pointer;font-weight:500;transition:.15s}
|
| 45 |
+
.btn-dark{background:var(--primary);color:#fff}.btn-dark:hover{opacity:.85}
|
| 46 |
+
.btn-outline{background:var(--card);color:var(--text);border:1px solid var(--border)}.btn-outline:hover{background:var(--bg)}
|
| 47 |
+
.btn-green{background:var(--green);color:#fff}.btn-green:hover{opacity:.85}
|
| 48 |
+
.btn-red{background:var(--red);color:#fff}.btn-red:hover{opacity:.85}
|
| 49 |
+
|
| 50 |
+
/* TABLE */
|
| 51 |
+
.table-wrap{background:var(--card);border:1px solid var(--border);border-radius:var(--radius);overflow-x:auto}
|
| 52 |
+
table{width:100%;border-collapse:collapse;min-width:700px}
|
| 53 |
+
th{text-align:left;padding:10px 14px;font-size:12px;color:var(--text3);font-weight:500;border-bottom:1px solid var(--border);white-space:nowrap}
|
| 54 |
+
td{padding:10px 14px;font-size:13px;border-bottom:1px solid #f5f5f5;white-space:nowrap}
|
| 55 |
+
tr:last-child td{border-bottom:none}
|
| 56 |
+
tr:hover td{background:#fafafa}
|
| 57 |
+
.email-cell{font-family:monospace;font-size:12px;max-width:220px;overflow:hidden;text-overflow:ellipsis}
|
| 58 |
+
.badge{display:inline-block;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:600}
|
| 59 |
+
.badge-active{background:#dcfce7;color:#16a34a}.badge-inactive{background:#fee2e2;color:#dc2626}
|
| 60 |
+
.ops{display:flex;gap:6px}
|
| 61 |
+
.op-btn{width:28px;height:28px;border:1px solid var(--border);border-radius:4px;background:var(--card);cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:14px;transition:.15s}
|
| 62 |
+
.op-btn:hover{background:var(--bg)}
|
| 63 |
+
.pagination{display:flex;align-items:center;gap:8px;justify-content:center;padding:12px;font-size:13px;color:var(--text3)}
|
| 64 |
+
.pagination button{padding:4px 10px;border:1px solid var(--border);border-radius:4px;background:var(--card);cursor:pointer;font-size:12px}
|
| 65 |
+
.pagination button:hover{background:var(--bg)}
|
| 66 |
+
.pagination button:disabled{opacity:.4;cursor:default}
|
| 67 |
+
.chk{width:15px;height:15px;cursor:pointer}
|
| 68 |
+
|
| 69 |
+
/* MODAL */
|
| 70 |
+
.modal-bg{display:none;position:fixed;inset:0;background:rgba(0,0,0,.35);z-index:200;align-items:center;justify-content:center}
|
| 71 |
+
.modal-bg.open{display:flex}
|
| 72 |
+
.modal{background:var(--card);border-radius:12px;padding:24px;width:480px;max-height:80vh;overflow-y:auto;box-shadow:0 8px 32px rgba(0,0,0,.12)}
|
| 73 |
+
.modal h3{font-size:16px;margin-bottom:12px}
|
| 74 |
+
.modal textarea{width:100%;height:180px;padding:10px;border:1px solid var(--border);border-radius:6px;font-family:monospace;font-size:12px;resize:vertical}
|
| 75 |
+
.modal-footer{display:flex;gap:8px;justify-content:flex-end;margin-top:14px}
|
| 76 |
+
|
| 77 |
+
/* CONFIG */
|
| 78 |
+
.config-card{background:var(--card);border:1px solid var(--border);border-radius:var(--radius);padding:24px;margin-bottom:16px}
|
| 79 |
+
.config-card h3{font-size:15px;font-weight:600;margin-bottom:14px;padding-bottom:10px;border-bottom:1px solid #f0f0f0}
|
| 80 |
+
.field{margin-bottom:14px}
|
| 81 |
+
.field label{display:block;font-size:13px;font-weight:500;color:var(--text2);margin-bottom:4px}
|
| 82 |
+
.field input,.field select{width:100%;padding:8px 12px;border:1px solid var(--border);border-radius:6px;font-size:13px;outline:none}
|
| 83 |
+
.field input:focus,.field select:focus{border-color:var(--text3)}
|
| 84 |
+
.field-row{display:flex;gap:10px;align-items:end}
|
| 85 |
+
.field-row .field{flex:1}
|
| 86 |
+
.hint{font-size:11px;color:var(--text3);margin-top:2px}
|
| 87 |
+
|
| 88 |
+
/* DOCS */
|
| 89 |
+
.doc-section{background:var(--card);border:1px solid var(--border);border-radius:var(--radius);padding:24px;margin-bottom:16px}
|
| 90 |
+
.doc-section h3{font-size:15px;font-weight:600;margin-bottom:10px}
|
| 91 |
+
.doc-section p{font-size:13px;color:var(--text2);margin-bottom:8px}
|
| 92 |
+
pre{background:#1e1e2e;color:#cdd6f4;padding:14px;border-radius:6px;overflow-x:auto;font-size:12px;line-height:1.5;margin:8px 0 12px}
|
| 93 |
+
.doc-table{width:100%;border-collapse:collapse;font-size:13px;margin:8px 0}
|
| 94 |
+
.doc-table th{text-align:left;padding:6px 10px;background:#f9fafb;font-weight:500;border-bottom:1px solid var(--border)}
|
| 95 |
+
.doc-table td{padding:6px 10px;border-bottom:1px solid #f5f5f5}
|
| 96 |
+
.doc-table code{background:#f1f5f9;padding:1px 5px;border-radius:3px;font-size:12px}
|
| 97 |
+
|
| 98 |
+
/* LOGIN */
|
| 99 |
+
.login-wrap{display:flex;align-items:center;justify-content:center;min-height:100vh;background:var(--bg)}
|
| 100 |
+
.login-card{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:40px;width:360px;text-align:center}
|
| 101 |
+
.login-card h1{font-size:20px;margin-bottom:4px}
|
| 102 |
+
.login-card p{color:var(--text3);font-size:13px;margin-bottom:24px}
|
| 103 |
+
.login-card input{width:100%;padding:10px 14px;border:1px solid var(--border);border-radius:6px;font-size:14px;outline:none;margin-bottom:14px}
|
| 104 |
+
.login-card input:focus{border-color:var(--text3)}
|
| 105 |
+
.login-card button{width:100%;padding:10px;background:var(--primary);color:#fff;border:none;border-radius:6px;font-size:14px;cursor:pointer}
|
| 106 |
+
.login-card button:hover{opacity:.85}
|
| 107 |
+
.login-err{color:var(--red);font-size:12px;display:none;margin-top:8px}
|
| 108 |
+
|
| 109 |
+
/* TOAST */
|
| 110 |
+
.toast{position:fixed;bottom:20px;right:20px;background:var(--primary);color:#fff;padding:10px 18px;border-radius:8px;font-size:13px;z-index:300;opacity:0;transition:opacity .25s;pointer-events:none}
|
| 111 |
+
.toast.show{opacity:1}
|
| 112 |
+
|
| 113 |
+
@media(max-width:768px){
|
| 114 |
+
.stats{grid-template-columns:repeat(2,1fr)}
|
| 115 |
+
.nav-links{font-size:13px}
|
| 116 |
+
.page{padding:16px}
|
| 117 |
+
}
|
| 118 |
+
</style>
|
| 119 |
+
</head>
|
| 120 |
+
<body>
|
| 121 |
+
|
| 122 |
+
<!-- LOGIN -->
|
| 123 |
+
<div id="view-login" class="login-wrap">
|
| 124 |
+
<div class="login-card">
|
| 125 |
+
<h1>Emergent2API</h1>
|
| 126 |
+
<p>管理面板</p>
|
| 127 |
+
<input type="password" id="pwd" placeholder="管理员密码" autofocus>
|
| 128 |
+
<button onclick="doLogin()">登录</button>
|
| 129 |
+
<div class="login-err" id="login-err">密码错误</div>
|
| 130 |
+
</div>
|
| 131 |
+
</div>
|
| 132 |
+
|
| 133 |
+
<!-- MAIN -->
|
| 134 |
+
<div id="view-main" style="display:none">
|
| 135 |
+
<nav class="nav">
|
| 136 |
+
<span class="nav-brand">Emergent2API</span>
|
| 137 |
+
<div class="nav-links">
|
| 138 |
+
<span class="nav-link active" data-page="tokens" onclick="showPage('tokens')">Token管理</span>
|
| 139 |
+
<span class="nav-link" data-page="config" onclick="showPage('config')">配置管理</span>
|
| 140 |
+
<span class="nav-link" data-page="docs" onclick="showPage('docs')">使用文档</span>
|
| 141 |
+
</div>
|
| 142 |
+
<div class="nav-right">
|
| 143 |
+
<span onclick="doLogout()">退出</span>
|
| 144 |
+
</div>
|
| 145 |
+
</nav>
|
| 146 |
+
|
| 147 |
+
<!-- TOKEN PAGE -->
|
| 148 |
+
<div id="page-tokens" class="page active">
|
| 149 |
+
<div class="page-title">Token 列表</div>
|
| 150 |
+
<div class="page-desc">管理 Emergent2API 的 Token 服务号池。</div>
|
| 151 |
+
<div class="stats">
|
| 152 |
+
<div class="stat"><div class="stat-val" id="s-total">0</div><div class="stat-label">Token 总数</div></div>
|
| 153 |
+
<div class="stat"><div class="stat-val green" id="s-active">0</div><div class="stat-label">Token 正常</div></div>
|
| 154 |
+
<div class="stat"><div class="stat-val red" id="s-inactive">0</div><div class="stat-label">Token 失效</div></div>
|
| 155 |
+
<div class="stat"><div class="stat-val" id="s-usage">0</div><div class="stat-label">总调用次数</div></div>
|
| 156 |
+
</div>
|
| 157 |
+
<div class="filters">
|
| 158 |
+
<span class="filter-btn active" data-f="all" onclick="setFilter('all')">全部 <b class="filter-count" id="fc-all">0</b></span>
|
| 159 |
+
<span class="filter-btn" data-f="active" onclick="setFilter('active')">正常 <b class="filter-count" id="fc-active">0</b></span>
|
| 160 |
+
<span class="filter-btn" data-f="inactive" onclick="setFilter('inactive')">失效 <b class="filter-count" id="fc-inactive">0</b></span>
|
| 161 |
+
<span class="filter-spacer"></span>
|
| 162 |
+
<button class="action-btn btn-outline" onclick="openImport()">导入</button>
|
| 163 |
+
<button class="action-btn btn-outline" onclick="doExport()">导出</button>
|
| 164 |
+
<button class="action-btn btn-green" onclick="batchOp('test')">测试</button>
|
| 165 |
+
<button class="action-btn btn-red" onclick="batchOp('delete')">删除选中</button>
|
| 166 |
+
</div>
|
| 167 |
+
<div class="table-wrap">
|
| 168 |
+
<table>
|
| 169 |
+
<thead><tr>
|
| 170 |
+
<th><input type="checkbox" class="chk" id="chk-all" onchange="toggleAll(this.checked)"></th>
|
| 171 |
+
<th>Token</th><th>状态</th><th>余额</th><th>最后使用</th><th>操作</th>
|
| 172 |
+
</tr></thead>
|
| 173 |
+
<tbody id="tbody"></tbody>
|
| 174 |
+
</table>
|
| 175 |
+
<div class="pagination" id="pager"></div>
|
| 176 |
+
</div>
|
| 177 |
+
</div>
|
| 178 |
+
|
| 179 |
+
<!-- CONFIG PAGE -->
|
| 180 |
+
<div id="page-config" class="page">
|
| 181 |
+
<div class="page-title">配置管理</div>
|
| 182 |
+
<div class="page-desc">管理 API 密钥、后端设置和管理员密码。</div>
|
| 183 |
+
<div class="config-card">
|
| 184 |
+
<h3>API 设置</h3>
|
| 185 |
+
<div class="field-row">
|
| 186 |
+
<div class="field"><label>API Key</label><input id="cfg-api_key" placeholder="sk-..."></div>
|
| 187 |
+
<div style="padding-bottom:14px"><button class="action-btn btn-dark" onclick="genKey()">生成</button></div>
|
| 188 |
+
</div>
|
| 189 |
+
<div class="field">
|
| 190 |
+
<label>后端</label>
|
| 191 |
+
<select id="cfg-backend"><option value="jobs">Jobs API(无 IP 限制)</option><option value="integrations">Integrations API(更快,需要代理)</option></select>
|
| 192 |
+
</div>
|
| 193 |
+
<div class="field"><label>代理</label><input id="cfg-proxy" placeholder="http://user:pass@host:port"><div class="hint">Integrations 后端必填;Jobs 可选</div></div>
|
| 194 |
+
</div>
|
| 195 |
+
<div class="config-card">
|
| 196 |
+
<h3>管理员设置</h3>
|
| 197 |
+
<div class="field"><label>管理员密码</label><input id="cfg-admin_password" type="password"></div>
|
| 198 |
+
</div>
|
| 199 |
+
<div style="display:flex;gap:8px;justify-content:flex-end">
|
| 200 |
+
<button class="action-btn btn-outline" onclick="loadConfig()">重置</button>
|
| 201 |
+
<button class="action-btn btn-dark" onclick="saveConfig()">保存</button>
|
| 202 |
+
</div>
|
| 203 |
+
</div>
|
| 204 |
+
|
| 205 |
+
<!-- DOCS PAGE -->
|
| 206 |
+
<div id="page-docs" class="page">
|
| 207 |
+
<div class="page-title">使用文档</div>
|
| 208 |
+
<div class="page-desc">API 端点文档和使用示例。</div>
|
| 209 |
+
<div class="doc-section">
|
| 210 |
+
<h3>可用模型</h3>
|
| 211 |
+
<table class="doc-table"><tr><th>Model ID</th><th>说明</th></tr>
|
| 212 |
+
<tr><td><code>claude-opus-4-6</code></td><td>Claude Opus 4.6(满血版)</td></tr>
|
| 213 |
+
<tr><td><code>claude-sonnet-4-5</code></td><td>Claude Sonnet 4.5</td></tr>
|
| 214 |
+
<tr><td><code>claude-sonnet-4-5-thinking</code></td><td>Claude Sonnet 4.5(深度思考)</td></tr>
|
| 215 |
+
</table>
|
| 216 |
+
</div>
|
| 217 |
+
<div class="doc-section">
|
| 218 |
+
<h3>API 端点</h3>
|
| 219 |
+
<table class="doc-table"><tr><th>方法</th><th>端点</th><th>格式</th></tr>
|
| 220 |
+
<tr><td>POST</td><td><code>/v1/chat/completions</code></td><td>OpenAI Chat</td></tr>
|
| 221 |
+
<tr><td>POST</td><td><code>/v1/messages</code></td><td>Anthropic Messages</td></tr>
|
| 222 |
+
<tr><td>POST</td><td><code>/v1/responses</code></td><td>OpenAI Response API</td></tr>
|
| 223 |
+
<tr><td>GET</td><td><code>/v1/models</code></td><td>模型列表</td></tr>
|
| 224 |
+
</table>
|
| 225 |
+
</div>
|
| 226 |
+
<div class="doc-section">
|
| 227 |
+
<h3>curl 示例</h3>
|
| 228 |
+
<pre>curl -X POST https://your-host/v1/chat/completions \
|
| 229 |
+
-H "Authorization: Bearer YOUR_API_KEY" \
|
| 230 |
+
-H "Content-Type: application/json" \
|
| 231 |
+
-d '{
|
| 232 |
+
"model": "claude-opus-4-6",
|
| 233 |
+
"messages": [{"role": "user", "content": "Hello!"}],
|
| 234 |
+
"stream": true
|
| 235 |
+
}'</pre>
|
| 236 |
+
</div>
|
| 237 |
+
<div class="doc-section">
|
| 238 |
+
<h3>Python 示例(OpenAI SDK)</h3>
|
| 239 |
+
<pre>from openai import OpenAI
|
| 240 |
+
|
| 241 |
+
client = OpenAI(api_key="YOUR_API_KEY", base_url="https://your-host/v1")
|
| 242 |
+
resp = client.chat.completions.create(
|
| 243 |
+
model="claude-opus-4-6",
|
| 244 |
+
messages=[{"role": "user", "content": "Hello!"}],
|
| 245 |
+
stream=True
|
| 246 |
+
)
|
| 247 |
+
for chunk in resp:
|
| 248 |
+
print(chunk.choices[0].delta.content or "", end="")</pre>
|
| 249 |
+
</div>
|
| 250 |
+
</div>
|
| 251 |
+
|
| 252 |
+
</div><!-- /view-main -->
|
| 253 |
+
|
| 254 |
+
<!-- IMPORT MODAL -->
|
| 255 |
+
<div class="modal-bg" id="modal-import">
|
| 256 |
+
<div class="modal">
|
| 257 |
+
<h3>导入 Token</h3>
|
| 258 |
+
<p style="font-size:13px;color:var(--text3);margin-bottom:8px">粘贴 JSONL 数据(每行一个账号:email, password, jwt 必填)</p>
|
| 259 |
+
<textarea id="import-text" placeholder='{"email":"...","password":"...","jwt":"..."}'></textarea>
|
| 260 |
+
<div class="modal-footer">
|
| 261 |
+
<button class="action-btn btn-outline" onclick="closeImport()">取消</button>
|
| 262 |
+
<button class="action-btn btn-dark" onclick="doImport()">导入</button>
|
| 263 |
+
</div>
|
| 264 |
+
</div>
|
| 265 |
+
</div>
|
| 266 |
+
|
| 267 |
+
<div class="toast" id="toast"></div>
|
| 268 |
+
|
| 269 |
+
<script>
|
| 270 |
+
const API='/v1/admin';
|
| 271 |
+
let tokens=[],filter='all',selected=new Set(),currentPage=1,pageSize=50;
|
| 272 |
+
|
| 273 |
+
async function api(p,o={}){const r=await fetch(API+p,o);if(r.status===401){showLogin();return null}return r}
|
| 274 |
+
function toast(m,d=2500){const t=document.getElementById('toast');t.textContent=m;t.classList.add('show');setTimeout(()=>t.classList.remove('show'),d)}
|
| 275 |
+
function showLogin(){document.getElementById('view-login').style.display='';document.getElementById('view-main').style.display='none'}
|
| 276 |
+
function showMain(){document.getElementById('view-login').style.display='none';document.getElementById('view-main').style.display=''}
|
| 277 |
+
|
| 278 |
+
document.getElementById('pwd').addEventListener('keydown',e=>{if(e.key==='Enter')doLogin()});
|
| 279 |
+
|
| 280 |
+
async function doLogin(){
|
| 281 |
+
const p=document.getElementById('pwd').value;
|
| 282 |
+
const r=await fetch(API+'/login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({password:p})});
|
| 283 |
+
if(r.ok){showMain();load();loadConfig()}
|
| 284 |
+
else document.getElementById('login-err').style.display='block';
|
| 285 |
+
}
|
| 286 |
+
async function doLogout(){await api('/logout',{method:'POST'});showLogin()}
|
| 287 |
+
|
| 288 |
+
async function checkAuth(){
|
| 289 |
+
const r=await fetch(API+'/verify');
|
| 290 |
+
if(r.ok){showMain();load();loadConfig()}
|
| 291 |
+
}
|
| 292 |
+
checkAuth();
|
| 293 |
+
|
| 294 |
+
function showPage(p){
|
| 295 |
+
document.querySelectorAll('.page').forEach(el=>el.classList.remove('active'));
|
| 296 |
+
document.getElementById('page-'+p).classList.add('active');
|
| 297 |
+
document.querySelectorAll('.nav-link').forEach(el=>el.classList.toggle('active',el.dataset.page===p));
|
| 298 |
+
}
|
| 299 |
+
|
| 300 |
+
async function load(){
|
| 301 |
+
const r=await api('/tokens');if(!r)return;
|
| 302 |
+
const d=await r.json();
|
| 303 |
+
tokens=d.tokens||[];
|
| 304 |
+
const act=d.active,tot=d.total,inact=tot-act;
|
| 305 |
+
document.getElementById('s-total').textContent=tot;
|
| 306 |
+
document.getElementById('s-active').textContent=act;
|
| 307 |
+
document.getElementById('s-inactive').textContent=inact;
|
| 308 |
+
document.getElementById('s-usage').textContent=tokens.reduce((s,t)=>s+(t.use_count||0),0);
|
| 309 |
+
document.getElementById('fc-all').textContent=tot;
|
| 310 |
+
document.getElementById('fc-active').textContent=act;
|
| 311 |
+
document.getElementById('fc-inactive').textContent=inact;
|
| 312 |
+
currentPage=1;
|
| 313 |
+
render();
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
+
function getFiltered(){
|
| 317 |
+
if(filter==='active')return tokens.filter(t=>t.is_active);
|
| 318 |
+
if(filter==='inactive')return tokens.filter(t=>!t.is_active);
|
| 319 |
+
return tokens;
|
| 320 |
+
}
|
| 321 |
+
|
| 322 |
+
function render(){
|
| 323 |
+
const list=getFiltered();
|
| 324 |
+
const totalPages=Math.max(1,Math.ceil(list.length/pageSize));
|
| 325 |
+
if(currentPage>totalPages)currentPage=totalPages;
|
| 326 |
+
const start=(currentPage-1)*pageSize,end=start+pageSize;
|
| 327 |
+
const page=list.slice(start,end);
|
| 328 |
+
const tbody=document.getElementById('tbody');
|
| 329 |
+
tbody.innerHTML=page.map(t=>{
|
| 330 |
+
const em=t.email||'';
|
| 331 |
+
const short=em.length>28?em.slice(0,12)+'...'+em.slice(-12):em;
|
| 332 |
+
return `<tr>
|
| 333 |
+
<td><input type="checkbox" class="chk" data-id="${t.id}" ${selected.has(t.id)?'checked':''} onchange="toggleSel(${t.id},this.checked)"></td>
|
| 334 |
+
<td class="email-cell" title="${em}">${short}</td>
|
| 335 |
+
<td><span class="badge ${t.is_active?'badge-active':'badge-inactive'}">${t.is_active?'active':'inactive'}</span></td>
|
| 336 |
+
<td>$${(t.balance||0).toFixed(2)}</td>
|
| 337 |
+
<td style="color:var(--text3);font-size:12px">${t.last_used?new Date(t.last_used).toLocaleString('zh-CN'):'-'}</td>
|
| 338 |
+
<td class="ops">
|
| 339 |
+
<span class="op-btn" title="刷新" onclick="refreshOne(${t.id})">↻</span>
|
| 340 |
+
<span class="op-btn" title="${t.is_active?'停用':'启用'}" onclick="toggleOne(${t.id})">×</span>
|
| 341 |
+
<span class="op-btn" title="删除" onclick="deleteOne(${t.id})">🗑</span>
|
| 342 |
+
</td>
|
| 343 |
+
</tr>`}).join('');
|
| 344 |
+
document.getElementById('pager').innerHTML=list.length>pageSize?`
|
| 345 |
+
<button ${currentPage<=1?'disabled':''} onclick="goPage(${currentPage-1})">‹</button>
|
| 346 |
+
<span>第 ${currentPage}/${totalPages} 页 · 共 ${list.length} 条</span>
|
| 347 |
+
<button ${currentPage>=totalPages?'disabled':''} onclick="goPage(${currentPage+1})">›</button>`:'';
|
| 348 |
+
}
|
| 349 |
+
|
| 350 |
+
function goPage(p){currentPage=p;render()}
|
| 351 |
+
function setFilter(f){filter=f;currentPage=1;document.querySelectorAll('.filter-btn').forEach(el=>el.classList.toggle('active',el.dataset.f===f));render()}
|
| 352 |
+
function toggleSel(id,c){if(c)selected.add(id);else selected.delete(id)}
|
| 353 |
+
function toggleAll(c){const list=getFiltered();const start=(currentPage-1)*pageSize;list.slice(start,start+pageSize).forEach(t=>{if(c)selected.add(t.id);else selected.delete(t.id)});render()}
|
| 354 |
+
|
| 355 |
+
async function toggleOne(id){
|
| 356 |
+
const t=tokens.find(x=>x.id===id);if(!t)return;
|
| 357 |
+
await api('/tokens/toggle',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({ids:[id],active:!t.is_active})});
|
| 358 |
+
load();
|
| 359 |
+
}
|
| 360 |
+
async function deleteOne(id){if(!confirm('确定删除?'))return;await api('/tokens/delete',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({ids:[id]})});load()}
|
| 361 |
+
async function refreshOne(id){toast('刷新中...');const r=await api('/tokens/refresh',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({ids:[id]})});if(r){const d=await r.json();toast(d.refreshed?'刷新成功':'刷新失败')}load()}
|
| 362 |
+
|
| 363 |
+
async function batchOp(action){
|
| 364 |
+
const ids=[...selected];if(!ids.length){toast('请先选择 Token');return}
|
| 365 |
+
if(action==='delete'&&!confirm(`确定删除 ${ids.length} 个 Token?`))return;
|
| 366 |
+
toast(`处理中 (${ids.length})...`);
|
| 367 |
+
const r=await api(`/tokens/${action}`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({ids})});
|
| 368 |
+
if(r){const d=await r.json();toast(JSON.stringify(d))}
|
| 369 |
+
selected.clear();load();
|
| 370 |
+
}
|
| 371 |
+
|
| 372 |
+
function openImport(){document.getElementById('modal-import').classList.add('open')}
|
| 373 |
+
function closeImport(){document.getElementById('modal-import').classList.remove('open')}
|
| 374 |
+
async function doImport(){
|
| 375 |
+
const t=document.getElementById('import-text').value.trim();
|
| 376 |
+
if(!t){toast('请粘贴 JSONL 数据');return}
|
| 377 |
+
const r=await api('/tokens/import',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({jsonl:t})});
|
| 378 |
+
if(r){const d=await r.json();toast(`导入 ${d.imported} 个`+(d.errors.length?`,${d.errors.length} 个错误`:''));closeImport();load()}
|
| 379 |
+
}
|
| 380 |
+
async function doExport(){
|
| 381 |
+
const r=await api('/tokens/export');if(!r)return;
|
| 382 |
+
const b=await r.blob();const a=document.createElement('a');a.href=URL.createObjectURL(b);a.download='emergent_tokens.zip';a.click();
|
| 383 |
+
}
|
| 384 |
+
|
| 385 |
+
async function loadConfig(){
|
| 386 |
+
const r=await api('/config');if(!r)return;
|
| 387 |
+
const d=await r.json();
|
| 388 |
+
document.getElementById('cfg-api_key').value=d.api_key||'';
|
| 389 |
+
document.getElementById('cfg-backend').value=d.backend||'jobs';
|
| 390 |
+
document.getElementById('cfg-proxy').value=d.proxy||'';
|
| 391 |
+
document.getElementById('cfg-admin_password').value=d.admin_password||'';
|
| 392 |
+
}
|
| 393 |
+
async function saveConfig(){
|
| 394 |
+
const b={api_key:document.getElementById('cfg-api_key').value,backend:document.getElementById('cfg-backend').value,proxy:document.getElementById('cfg-proxy').value,admin_password:document.getElementById('cfg-admin_password').value};
|
| 395 |
+
const r=await api('/config',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(b)});
|
| 396 |
+
if(r){const d=await r.json();toast(`已保存: ${d.saved.join(', ')}`)}
|
| 397 |
+
}
|
| 398 |
+
async function genKey(){
|
| 399 |
+
const r=await api('/config/generate-key',{method:'POST'});
|
| 400 |
+
if(r){const d=await r.json();document.getElementById('cfg-api_key').value=d.api_key;toast('已生成新密钥')}
|
| 401 |
+
}
|
| 402 |
+
</script>
|
| 403 |
+
</body>
|
| 404 |
+
</html>
|