ohmyapi commited on
Commit
a3866a9
·
1 Parent(s): f98d11a

Deploy emergent2api

Browse files
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
- if req.system:
49
- messages.append({"role": "system", "content": req.system})
 
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
- function openImport(){$('dlg-import').classList.add('open')}
435
- function closeImport(){$('dlg-import').classList.remove('open')}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
436
  async function doImport(){
437
- const t=$('import-text').value.trim();if(!t){toast('Paste JSONL data');return}
438
- const r=await api('/tokens/import',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({jsonl:t})});
439
- if(r){const d=await r.json();toast(`导入 ${d.imported} 个`+(d.errors.length?`,${d.errors.length} 个错误`:''));closeImport();load()}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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