108 lines
3.4 KiB
Python
Executable File
108 lines
3.4 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""gateway_importer.py — eternal lane: keys.db (KeyHunter) -> dashscope rotator pool.
|
|
|
|
Every cycle:
|
|
1. Pull WORKING/FREE/LIMITED DASHSCOPE keys from keys.db
|
|
2. Smoke-test new candidates (1-token chat) before import
|
|
3. Dedup, atomic-write into dashscope_keys.json
|
|
4. Rotator hot-reloads by mtime (no restart needed)
|
|
|
|
Run: python gateway_importer.py >> gateway_importer.log 2>&1
|
|
"""
|
|
import json
|
|
import os
|
|
import sqlite3
|
|
import sys
|
|
import time
|
|
|
|
import requests
|
|
|
|
KEYS_DB = r"C:/Users/User/Desktop/keyhunter/data/keys.db"
|
|
POOL_PATH = r"C:/Users/User/.omp/agent/dashscope_keys.json"
|
|
INTERVAL = 120 # seconds between cycles
|
|
BASE = "https://dashscope.aliyuncs.com/compatible-mode/v1"
|
|
GOOD_STATUS = ("WORKING", "FREE", "LIMITED")
|
|
STALE_DAYS = 30 # skip keys not seen recently
|
|
|
|
|
|
def log(msg: str) -> None:
|
|
print(f"{time.strftime('%H:%M:%S')} {msg}", flush=True)
|
|
|
|
|
|
def load_pool() -> dict:
|
|
try:
|
|
with open(POOL_PATH, encoding="utf-8-sig") as f:
|
|
return json.load(f)
|
|
except Exception:
|
|
return {"keys": []}
|
|
|
|
|
|
def save_pool(cfg: dict) -> None:
|
|
tmp = POOL_PATH + ".importer.tmp"
|
|
with open(tmp, "w", encoding="utf-8") as f:
|
|
json.dump(cfg, f, indent=2, ensure_ascii=False)
|
|
f.write("\n")
|
|
os.replace(tmp, POOL_PATH)
|
|
|
|
|
|
def fetch_candidates() -> list[tuple]:
|
|
db = sqlite3.connect(f"file:{KEYS_DB}?mode=ro", uri=True)
|
|
try:
|
|
rows = db.execute(
|
|
"""SELECT val, status, found, repo FROM keys
|
|
WHERE prov = 'DASHSCOPE'
|
|
AND status IN ('WORKING','FREE','LIMITED')
|
|
AND val LIKE 'sk-%'
|
|
AND found >= datetime('now', ?)""",
|
|
(f"-{STALE_DAYS} days",),
|
|
).fetchall()
|
|
finally:
|
|
db.close()
|
|
return rows
|
|
|
|
|
|
def smoke(key: str) -> bool:
|
|
try:
|
|
r = requests.post(
|
|
BASE + "/chat/completions",
|
|
headers={"Authorization": f"Bearer {key}"},
|
|
json={"model": "qwen3.8-flash", "messages": [{"role": "user", "content": "hi"}], "max_tokens": 1},
|
|
timeout=25,
|
|
)
|
|
return r.status_code == 200
|
|
except Exception:
|
|
return False
|
|
|
|
|
|
def main() -> None:
|
|
log(f"[importer] start: db={KEYS_DB} pool={POOL_PATH}")
|
|
while True:
|
|
try:
|
|
cfg = load_pool()
|
|
pool_keys = set(cfg.get("keys") or [])
|
|
rows = fetch_candidates()
|
|
fresh = [(v, s, rep) for v, s, f, rep in rows if v not in pool_keys]
|
|
log(f"[importer] candidates={len(rows)} new={len(fresh)} pool={len(pool_keys)}")
|
|
added = 0
|
|
for val, status, repo in fresh:
|
|
if smoke(val):
|
|
cfg.setdefault("keys", []).append(val)
|
|
pool_keys.add(val)
|
|
added += 1
|
|
log(f"[importer] + {val[:14]}... ({status}) src={str(repo)[:40]}")
|
|
save_pool(cfg) # save immediately: rotator picks up per key
|
|
time.sleep(2)
|
|
else:
|
|
log(f"[importer] skip (smoke fail) {val[:14]}...")
|
|
if added:
|
|
log(f"[importer] pushed {added} keys -> pool={len(pool_keys)} (rotator hot-reload by mtime)")
|
|
except Exception as e:
|
|
log(f"[importer] ERROR {e!r}")
|
|
time.sleep(INTERVAL)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
main()
|
|
except KeyboardInterrupt:
|
|
sys.exit(0) |