From 022e316f38b3caecb093d1f58fb11e6736f68681 Mon Sep 17 00:00:00 2001 From: huni Date: Fri, 24 Jul 2026 15:44:31 +0800 Subject: [PATCH] fix Xc-Token auth --- main.py | 104 ++++++++++++++++++-------------------------------------- 1 file changed, 34 insertions(+), 70 deletions(-) diff --git a/main.py b/main.py index 5c2b66c..a0fd5bf 100644 --- a/main.py +++ b/main.py @@ -1,26 +1,27 @@ """ModelHub XC 适配智能体 (huni-adapt-agent) -平台契约: 长驻 HTTP 服务, GET /health 返回 200, 处理 SIGTERM。 -赚分逻辑: 平台不喂模型、不给专属卡 (已由 agent_platform API 结构证实), -所以智能体必须自己用注入的 EXTERNAL_SERVICE_TOKEN 调主站 /api/adapt/task/add 提交。 -本体 = 一个 24h 自驱的提交循环, 只打天数活队列 (天垓150/100/智铠100) 的新鲜 NEW_ADAPT 名额。 +平台已把"脚本提交"关闭(code 60014),程序化提交只能走"适配智能体"。 +本智能体部署在平台内, 用注入的 EXTERNAL_SERVICE_TOKEN 作为 xcToken、经 **Xc-Token 头** +调开放平台 API 提交适配任务——这是被平台认可的"智能体提交"路径(不是被拦的脚本提交)。 -安全: token 只从环境读、绝不打印/落盘; DRY_RUN 默认 true (不真提交, 只验证管道)。 +关键教训: 主站开放 API 用 `Xc-Token` 头(不是 Authorization);提交不需要浏览器签名。 +安全: token 只从环境读、绝不打印/落盘; 有 MAX_TOTAL 上限防止在公司平台上跑飞。 """ -import json, os, signal, time, threading, hashlib, base64 +import json, os, signal, time, threading import urllib.request, urllib.parse, urllib.error from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer MAIN = os.getenv("MAIN_HOST", "https://modelhub.org.cn") GITEA = os.getenv("GITEA_HOST", "https://dev.modelhub.org.cn") -TOKEN = os.getenv("EXTERNAL_SERVICE_TOKEN", "") -DRY_RUN = os.getenv("DRY_RUN", "true").lower() == "true" +TOKEN = os.getenv("EXTERNAL_SERVICE_TOKEN", "") # = xcToken, 走 Xc-Token 头 +DRY_RUN = os.getenv("DRY_RUN", "false").lower() == "true" # 默认真提交(平台唯一认可路径) TARGET_CARDS = os.getenv("TARGET_CARDS", "Iluvatar_bi-150,Iluvatar_bi-100,Iluvatar_mrv-100").split(",") CARD_NAME = {"Iluvatar_bi-150": "天垓150", "Iluvatar_bi-100": "天垓100", "Iluvatar_mrv-100": "智铠100"} -TASK_TYPE = os.getenv("TASK_TYPE", "visual-multi-modal") # 天数 VL 已实测能验证成功 +TASK_TYPE = os.getenv("TASK_TYPE", "visual-multi-modal") FRAMEWORK = os.getenv("FRAMEWORK", "vllm") POLL_SEC = int(os.getenv("POLL_SEC", "90")) -MAX_PER_CYCLE = int(os.getenv("MAX_PER_CYCLE", "3")) # 每轮最多提交数, 不制造异常负载 +MAX_PER_CYCLE = int(os.getenv("MAX_PER_CYCLE", "3")) +MAX_TOTAL = int(os.getenv("MAX_TOTAL", "20")) # 累计提交上限, 到顶转空转(安全阀) SEARCH_KW = os.getenv("SEARCH_KW", "InternVL,MiniCPM-V,Qwen2-VL,Qwen2.5-VL,Llava") STATE = {"cycles": 0, "submitted": 0, "skipped": 0, "errors": 0, "last": [], "started": time.time()} @@ -43,43 +44,17 @@ def _http(method, url, headers=None, body=None, timeout=30): return -1, str(e) -def api_get(path): - st, txt = _http("GET", MAIN + path, {"Authorization": TOKEN}) +def xc_get(path): + st, txt = _http("GET", MAIN + path, {"Xc-Token": TOKEN}) try: return st, json.loads(txt) except Exception: return st, txt -# ---- 签名: 复刻前端 browser-submit (sha256 + AES-CBC) ---- -def _sha_hex(s): return hashlib.sha256(s.encode()).hexdigest() -def _sha_bytes(s): return hashlib.sha256(s.encode()).digest() - - -def _aes_cbc_b64(pt, key, iv): - from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes - from cryptography.hazmat.primitives import padding - padder = padding.PKCS7(128).padder() - data = padder.update(pt.encode()) + padder.finalize() - enc = Cipher(algorithms.AES(key), modes.CBC(iv)).encryptor() - return base64.b64encode(enc.update(data) + enc.finalize()).decode() - - -def _sign(model, cfg, t_ms): - try: - a = _sha_hex("computility:model-verify:browser-submit" + "|v1|" + str(t_ms) + "|" + model) - ii = _sha_hex(cfg) - kf = _sha_bytes("key|" + a + "|" + ii)[:16] - ivf = _sha_bytes("iv|" + model + "|" + str(t_ms) + "|" + str(len(cfg)))[:16] - o = str(t_ms) + "\n" + model + "\n" + cfg - return _sha_hex("v1." + a + "." + _aes_cbc_b64(o, kf, ivf) + "." + ii) - except Exception: - return None # cryptography 不可用(零依赖构建)→ 提交走 token-only - - def build_config(gpu): st, txt = _http("POST", MAIN + f"/api/adapt/task/build-config?gpuType={gpu}&framework={FRAMEWORK}&taskType={TASK_TYPE}", - {"Authorization": TOKEN}) + {"Xc-Token": TOKEN}) try: j = json.loads(txt) return j.get("data") if isinstance(j, dict) else None @@ -88,7 +63,7 @@ def build_config(gpu): def already_adapted(model, gpu): - st, j = api_get("/api/computility/models/metrics?modelId=" + urllib.parse.quote(model)) + st, j = xc_get("/api/computility/models/metrics?modelId=" + urllib.parse.quote(model)) if not isinstance(j, dict): return None names = [x.get("machineName") for x in (j.get("data") or [])] @@ -108,50 +83,39 @@ def find_models(): return out -def _post(model, gpu, cfg, signed): - t_ms = int(time.time() * 1000) - h = {"Authorization": TOKEN, "Content-Type": "application/json"} - if signed: - sig = _sign(model, cfg, t_ms) - if sig: - h.update({"X-Request-Time": str(t_ms), "X-Model-Address": model, "X-Request-Sign": sig}) - body = json.dumps({"modelAddress": model, "taskType": TASK_TYPE, "targetGpu": gpu, - "framework": FRAMEWORK, "configParams": cfg}) - st, txt = _http("POST", MAIN + "/api/adapt/task/add", h, body) - try: - return st, json.loads(txt) - except Exception: - return st, {} - - def submit(model, gpu): cfg = build_config(gpu) if not cfg: return {"model": model.split("/")[-1], "gpu": gpu, "skip": "no-config"} if DRY_RUN: - return {"model": model.split("/")[-1], "gpu": gpu, "dry_run": True, - "cfg_len": len(cfg), "sign_ok": bool(_sign(model, cfg, int(time.time() * 1000)))} - # 实测两种鉴权: 先 token-only(H1, 信任服务令牌), 失败再补签名(H2, 复刻浏览器) - st, j = _post(model, gpu, cfg, False); used = "token-only" - if not (isinstance(j, dict) and j.get("code") == 0): - st, j = _post(model, gpu, cfg, True); used = "signed" + return {"model": model.split("/")[-1], "gpu": gpu, "dry_run": True, "cfg_len": len(cfg)} + body = json.dumps({"modelAddress": model, "taskType": TASK_TYPE, "targetGpu": gpu, + "framework": FRAMEWORK, "configParams": cfg}) + st, txt = _http("POST", MAIN + "/api/adapt/task/add", + {"Xc-Token": TOKEN, "Content-Type": "application/json"}, body) + try: + j = json.loads(txt) + except Exception: + j = {} data = j.get("data") if isinstance(j, dict) else None - return {"model": model.split("/")[-1], "gpu": gpu, "auth": used, "http": st, - "code": j.get("code"), "taskId": (data or {}).get("id") if isinstance(data, dict) else None, - "msg": j.get("message")} + return {"model": model.split("/")[-1], "gpu": gpu, "http": st, "code": j.get("code"), + "taskId": (data or {}).get("id") if isinstance(data, dict) else None, "msg": j.get("message")} def loop(): - log(f"loop start | dry_run={DRY_RUN} | token_present={bool(TOKEN)} | cards={TARGET_CARDS} | type={TASK_TYPE}") - st, j = api_get("/api/credit/info") - log(f"auth probe /credit/info -> http={st} code={(j.get('code') if isinstance(j, dict) else '?')}") + log(f"loop start | dry_run={DRY_RUN} | token_present={bool(TOKEN)} | cards={TARGET_CARDS} | type={TASK_TYPE} | max_total={MAX_TOTAL}") + st, j = xc_get("/api/adapt/task/page?current=1&pageSize=1") + log(f"auth probe task/page(Xc-Token) -> http={st} code={(j.get('code') if isinstance(j, dict) else '?')}") seen = set() while not _stop.is_set(): STATE["cycles"] += 1 + if STATE["submitted"] >= MAX_TOTAL: + log(f"reached MAX_TOTAL={MAX_TOTAL}, idling (set higher to continue)") + _stop.wait(POLL_SEC); continue try: n = 0 for m in find_models(): - if _stop.is_set() or n >= MAX_PER_CYCLE: + if _stop.is_set() or n >= MAX_PER_CYCLE or STATE["submitted"] >= MAX_TOTAL: break for gpu in TARGET_CARDS: key = m + "|" + gpu @@ -171,7 +135,7 @@ def loop(): STATE["errors"] += 1 log("submit " + json.dumps(res, ensure_ascii=False)) time.sleep(2) - break # 一模型一轮只打一张卡, 摊开 + break except Exception as e: STATE["errors"] += 1 log("loop error: " + str(e))