Spaces:
Paused
Paused
Deploy emergent2api
Browse files- emergent2api/app.py +11 -2
- emergent2api/models.py +16 -1
- emergent2api/routes/anthropic.py +3 -2
- emergent2api/static/admin/index.html +67 -6
emergent2api/app.py
CHANGED
|
@@ -124,10 +124,18 @@ async def health():
|
|
| 124 |
return {"status": "ok", "accounts": count}
|
| 125 |
|
| 126 |
|
| 127 |
-
# Legacy import endpoint (backward compat for CI push)
|
| 128 |
@app.post("/admin/accounts/import-jsonl")
|
| 129 |
async def admin_import_jsonl(request: Request):
|
| 130 |
-
"""Import accounts from JSONL text. Body: {jsonl: "..."} or raw JSONL body.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
content_type = request.headers.get("content-type", "")
|
| 132 |
if "application/json" in content_type:
|
| 133 |
body = await request.json()
|
|
@@ -153,6 +161,7 @@ async def admin_import_jsonl(request: Request):
|
|
| 153 |
errors.append({"line": i + 1, "error": "invalid JSON"})
|
| 154 |
except Exception as e:
|
| 155 |
errors.append({"line": i + 1, "error": str(e)})
|
|
|
|
| 156 |
return {"imported": imported, "total_lines": len(lines), "errors": errors}
|
| 157 |
|
| 158 |
|
|
|
|
| 124 |
return {"status": "ok", "accounts": count}
|
| 125 |
|
| 126 |
|
| 127 |
+
# Legacy import endpoint (backward compat for CI push, requires API key)
|
| 128 |
@app.post("/admin/accounts/import-jsonl")
|
| 129 |
async def admin_import_jsonl(request: Request):
|
| 130 |
+
"""Import accounts from JSONL text. Body: {jsonl: "..."} or raw JSONL body.
|
| 131 |
+
Requires API key via Authorization header or x-api-key header.
|
| 132 |
+
"""
|
| 133 |
+
auth_header = request.headers.get("authorization", "")
|
| 134 |
+
api_key = request.headers.get("x-api-key", "")
|
| 135 |
+
token = auth_header[7:] if auth_header.startswith("Bearer ") else api_key
|
| 136 |
+
if token != settings.api_key:
|
| 137 |
+
return JSONResponse(status_code=401, content={"error": "Invalid API key"})
|
| 138 |
+
|
| 139 |
content_type = request.headers.get("content-type", "")
|
| 140 |
if "application/json" in content_type:
|
| 141 |
body = await request.json()
|
|
|
|
| 161 |
errors.append({"line": i + 1, "error": "invalid JSON"})
|
| 162 |
except Exception as e:
|
| 163 |
errors.append({"line": i + 1, "error": str(e)})
|
| 164 |
+
logger.info(f"Legacy import: {imported} accounts imported, {len(errors)} errors")
|
| 165 |
return {"imported": imported, "total_lines": len(lines), "errors": errors}
|
| 166 |
|
| 167 |
|
emergent2api/models.py
CHANGED
|
@@ -129,7 +129,22 @@ class AnthropicRequest(BaseModel):
|
|
| 129 |
stream: bool = False
|
| 130 |
temperature: Optional[float] = None
|
| 131 |
top_p: Optional[float] = None
|
| 132 |
-
system: Optional[str] = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 133 |
|
| 134 |
|
| 135 |
class AnthropicContentBlock(BaseModel):
|
|
|
|
| 129 |
stream: bool = False
|
| 130 |
temperature: Optional[float] = None
|
| 131 |
top_p: Optional[float] = None
|
| 132 |
+
system: Optional[Union[str, list[Any]]] = None
|
| 133 |
+
|
| 134 |
+
@property
|
| 135 |
+
def system_text(self) -> str:
|
| 136 |
+
"""Normalize system to plain string (handles list-of-blocks format)."""
|
| 137 |
+
if self.system is None:
|
| 138 |
+
return ""
|
| 139 |
+
if isinstance(self.system, str):
|
| 140 |
+
return self.system
|
| 141 |
+
parts = []
|
| 142 |
+
for block in self.system:
|
| 143 |
+
if isinstance(block, dict):
|
| 144 |
+
parts.append(block.get("text", ""))
|
| 145 |
+
elif isinstance(block, str):
|
| 146 |
+
parts.append(block)
|
| 147 |
+
return "\n".join(parts)
|
| 148 |
|
| 149 |
|
| 150 |
class AnthropicContentBlock(BaseModel):
|
emergent2api/routes/anthropic.py
CHANGED
|
@@ -45,8 +45,9 @@ async def messages(request: Request):
|
|
| 45 |
|
| 46 |
# Build messages list (prepend system if present)
|
| 47 |
messages = []
|
| 48 |
-
|
| 49 |
-
|
|
|
|
| 50 |
for m in req.messages:
|
| 51 |
messages.append({"role": m.role, "content": m.content})
|
| 52 |
|
|
|
|
| 45 |
|
| 46 |
# Build messages list (prepend system if present)
|
| 47 |
messages = []
|
| 48 |
+
system_text = req.system_text
|
| 49 |
+
if system_text:
|
| 50 |
+
messages.append({"role": "system", "content": system_text})
|
| 51 |
for m in req.messages:
|
| 52 |
messages.append({"role": m.role, "content": m.content})
|
| 53 |
|
emergent2api/static/admin/index.html
CHANGED
|
@@ -320,7 +320,12 @@ for chunk in client.chat.completions.create(
|
|
| 320 |
<div class="overlay" id="dlg-import">
|
| 321 |
<div class="dialog">
|
| 322 |
<h3>导入 Token</h3>
|
| 323 |
-
<div class="sub">粘贴 JSONL — 每行一个账号(email, password, jwt 必填)</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 324 |
<textarea id="import-text" placeholder='{"email":"...","password":"...","jwt":"..."}'></textarea>
|
| 325 |
<div class="dialog-foot">
|
| 326 |
<button class="tbtn" onclick="closeImport()">取消</button>
|
|
@@ -431,12 +436,68 @@ async function batchDeactivate(){
|
|
| 431 |
if(r){const d=await r.json();toast(`已停用 ${d.updated} 个`)}sel.clear();load();
|
| 432 |
}
|
| 433 |
|
| 434 |
-
|
| 435 |
-
function
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 436 |
async function doImport(){
|
| 437 |
-
const
|
| 438 |
-
const
|
| 439 |
-
if(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 440 |
}
|
| 441 |
async function doExport(){const r=await api('/tokens/export');if(!r)return;const b=await r.blob();const a=document.createElement('a');a.href=URL.createObjectURL(b);a.download='emergent_tokens.zip';a.click()}
|
| 442 |
|
|
|
|
| 320 |
<div class="overlay" id="dlg-import">
|
| 321 |
<div class="dialog">
|
| 322 |
<h3>导入 Token</h3>
|
| 323 |
+
<div class="sub">粘贴 JSONL / 上传 .jsonl 文件 / 拖放文件 — 每行一个账号(email, password, jwt 必填)</div>
|
| 324 |
+
<div id="import-dropzone" style="border:2px dashed var(--border2);border-radius:8px;padding:20px;text-align:center;margin-bottom:12px;cursor:pointer;transition:border-color .2s,background .2s;color:var(--text3);font-size:13px">
|
| 325 |
+
<div style="margin-bottom:8px"><svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg></div>
|
| 326 |
+
<div>拖放 .jsonl / .zip 文件到此处,或 <label style="color:var(--accent);cursor:pointer;text-decoration:underline">选择文件<input type="file" id="import-file" accept=".jsonl,.json,.txt,.zip" style="display:none" multiple></label></div>
|
| 327 |
+
<div id="import-file-info" style="margin-top:8px;color:var(--accent);font-weight:500;display:none"></div>
|
| 328 |
+
</div>
|
| 329 |
<textarea id="import-text" placeholder='{"email":"...","password":"...","jwt":"..."}'></textarea>
|
| 330 |
<div class="dialog-foot">
|
| 331 |
<button class="tbtn" onclick="closeImport()">取消</button>
|
|
|
|
| 436 |
if(r){const d=await r.json();toast(`已停用 ${d.updated} 个`)}sel.clear();load();
|
| 437 |
}
|
| 438 |
|
| 439 |
+
let _importFiles=[];
|
| 440 |
+
function openImport(){_importFiles=[];$('import-text').value='';$('import-file').value='';$('import-file-info').style.display='none';$('dlg-import').classList.add('open')}
|
| 441 |
+
function closeImport(){$('dlg-import').classList.remove('open');_importFiles=[]}
|
| 442 |
+
|
| 443 |
+
// File drop zone
|
| 444 |
+
(function(){
|
| 445 |
+
const dz=$('import-dropzone'),fi=$('import-file');
|
| 446 |
+
dz.addEventListener('dragover',e=>{e.preventDefault();dz.style.borderColor='var(--accent)';dz.style.background='rgba(201,122,71,.06)'});
|
| 447 |
+
dz.addEventListener('dragleave',()=>{dz.style.borderColor='';dz.style.background=''});
|
| 448 |
+
dz.addEventListener('drop',e=>{e.preventDefault();dz.style.borderColor='';dz.style.background='';handleImportFiles(e.dataTransfer.files)});
|
| 449 |
+
fi.addEventListener('change',()=>handleImportFiles(fi.files));
|
| 450 |
+
// Paste file support on the entire dialog
|
| 451 |
+
$('dlg-import').addEventListener('paste',e=>{
|
| 452 |
+
const items=e.clipboardData?.items;if(!items)return;
|
| 453 |
+
const files=[];
|
| 454 |
+
for(let i=0;i<items.length;i++){if(items[i].kind==='file'){files.push(items[i].getAsFile())}}
|
| 455 |
+
if(files.length)handleImportFiles(files);
|
| 456 |
+
});
|
| 457 |
+
})();
|
| 458 |
+
|
| 459 |
+
function handleImportFiles(fileList){
|
| 460 |
+
_importFiles=Array.from(fileList);
|
| 461 |
+
const info=$('import-file-info');
|
| 462 |
+
if(_importFiles.length){
|
| 463 |
+
info.textContent=_importFiles.map(f=>`${f.name} (${(f.size/1024).toFixed(1)} KB)`).join(', ');
|
| 464 |
+
info.style.display='block';
|
| 465 |
+
// Auto-read text files into textarea for preview
|
| 466 |
+
const textFiles=_importFiles.filter(f=>!f.name.endsWith('.zip'));
|
| 467 |
+
if(textFiles.length){
|
| 468 |
+
Promise.all(textFiles.map(f=>f.text())).then(texts=>{
|
| 469 |
+
const existing=$('import-text').value.trim();
|
| 470 |
+
const combined=[existing,...texts].filter(Boolean).join('\n');
|
| 471 |
+
$('import-text').value=combined;
|
| 472 |
+
});
|
| 473 |
+
}
|
| 474 |
+
}else{info.style.display='none'}
|
| 475 |
+
}
|
| 476 |
+
|
| 477 |
async function doImport(){
|
| 478 |
+
const zipFiles=_importFiles.filter(f=>f.name.endsWith('.zip'));
|
| 479 |
+
const textContent=$('import-text').value.trim();
|
| 480 |
+
if(!textContent&&!zipFiles.length){toast('请粘贴 JSONL 数据或上传文件');return}
|
| 481 |
+
|
| 482 |
+
let totalImported=0,totalErrors=0;
|
| 483 |
+
|
| 484 |
+
// Import zip files via /tokens/import-zip
|
| 485 |
+
for(const zf of zipFiles){
|
| 486 |
+
try{
|
| 487 |
+
const buf=await zf.arrayBuffer();
|
| 488 |
+
const r=await api('/tokens/import-zip',{method:'POST',headers:{'Content-Type':'application/octet-stream'},body:buf});
|
| 489 |
+
if(r){const d=await r.json();totalImported+=d.imported||0;totalErrors+=(d.errors||[]).length}
|
| 490 |
+
}catch(e){toast('Zip 导入失败: '+e.message)}
|
| 491 |
+
}
|
| 492 |
+
|
| 493 |
+
// Import JSONL text
|
| 494 |
+
if(textContent){
|
| 495 |
+
const r=await api('/tokens/import',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({jsonl:textContent})});
|
| 496 |
+
if(r){const d=await r.json();totalImported+=d.imported||0;totalErrors+=(d.errors||[]).length}
|
| 497 |
+
}
|
| 498 |
+
|
| 499 |
+
toast(`导入 ${totalImported} 个`+(totalErrors?`,${totalErrors} 个错误`:''));
|
| 500 |
+
closeImport();load();
|
| 501 |
}
|
| 502 |
async function doExport(){const r=await api('/tokens/export');if(!r)return;const b=await r.blob();const a=document.createElement('a');a.href=URL.createObjectURL(b);a.download='emergent_tokens.zip';a.click()}
|
| 503 |
|