#!/usr/bin/env python3 """Qoder UMID 设备风控身份(runtime-info)管理器。 背景:Qoder 服务端只向携带 Cosy-MachineToken / Cosy-MachineCode / Cosy-MachineType 的请求下发「每天领 100 Credits」活动(社区实测,国际版尤其严格)。 这三个值由 Qoder 官方 UMID 程序 runtime-info 生成——桌面客户端自带, 官方 CLI 包 @qoder-ai/qodercli 也把各平台二进制以 base64 内嵌在 bundle/qodercli.js 中。 本模块在 NAS(Linux x86_64 / arm64)上下载官方 npm 包 → 提取对应架构的 ELF → 试跑一次确认可产出身份 → 缓存到应用数据目录(umid/)。 调用方式与客户端一致: runtime-info [--account-stdin],stdin 传入 {"account": ""} env: 国际版 4(SINGAPORE)/ 国内版 0(ONLINE) 第一行输出 JSON:{"machineToken","machineCode","machineType","vmInfo"...} 安全性:只从官方 npm registry 下载官方包,二进制仅本机使用,不上传任何信息。 """ import base64 import json import os import re import stat import subprocess import sys import time import urllib.request import ssl from common import DATA_DIR, log, USER_AGENT PKG = "@qoder-ai/qodercli" REGISTRIES = [ "https://registry.npmmirror.com", "https://registry.npmjs.org", ] BUNDLE_ENTRY = "package/bundle/qodercli.js" # ELF e_machine: x86_64=62, arm64=183 ELF_MACHINE = {"x86_64": 62, "aarch64": 183, "arm64": 183} RISK_ENV = {"qoder": 4, "qoder-cn": 0} # 国际版 / 国内版 _SSL_CTX = ssl.create_default_context() _SSL_CTX.check_hostname = False _SSL_CTX.verify_mode = ssl.CERT_NONE CACHE_DIR = os.path.join(DATA_DIR, "umid") BIN_PATH = os.path.join(CACHE_DIR, "runtime-info") MANIFEST = os.path.join(CACHE_DIR, "manifest.json") # --------------------------------------------------------------------------- # 缓存 # --------------------------------------------------------------------------- def installed(): """已安装的 runtime-info:{path, version, arch, sha256, installedAt} 或 None.""" try: m = json.load(open(MANIFEST, encoding="utf-8")) if not os.path.isfile(BIN_PATH) or m.get("arch") != _arch(): return None return dict(m, path=BIN_PATH) except (OSError, ValueError): return None def _arch(): machine = os.uname().machine return {"x86_64": "x86_64", "amd64": "x86_64", "aarch64": "aarch64", "arm64": "aarch64"}.get(machine, machine) # --------------------------------------------------------------------------- # 下载 / 提取 # --------------------------------------------------------------------------- def _fetch(url, timeout=180, binary=True): req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT}) with urllib.request.urlopen(req, timeout=timeout, context=_SSL_CTX) as r: return r.read() def _registry_meta(registry): meta = json.loads(_fetch(registry + "/" + PKG.replace("/", "%2F") + "/latest", 30).decode()) return meta def _extract_tar_entry(tar, wanted): """最小 tar 解析:按名字取一个普通文件内容。""" off = 0 long_name = None while off + 512 <= len(tar): h = tar[off:off + 512] if not any(h): break name = h[:100].split(b"\0")[0].decode("utf-8", "replace") size = int((h[124:136].split(b"\0")[0].strip() or b"0"), 8) typeflag = h[156:157] prefix = h[345:500].split(b"\0")[0].decode("utf-8", "replace") full = (prefix + "/" + name) if prefix else name if typeflag == b"L": # GNU long name long_name = tar[off + 512:off + 512 + size].split(b"\0")[0].decode("utf-8", "replace") elif typeflag in (b"0", b"\0"): real = long_name or full long_name = None if real == wanted: return tar[off + 512:off + 512 + size] off += 512 + ((size + 511) // 512) * 512 return None def _find_elf(bundle, arch): """在 bundle 里找指定架构的 ELF(base64 内嵌)。""" want = ELF_MACHINE.get(arch) if not want: raise RuntimeError("不支持的 CPU 架构:%s" % arch) for m in re.finditer(rb"[A-Za-z0-9+/]{100000,}", bundle): try: raw = base64.b64decode(m.group(0)) except Exception: continue if raw[:4] != b"\x7fELF" or len(raw) < 18: continue machine = int.from_bytes(raw[18:20], "little") if machine == want: return raw raise RuntimeError("官方包中未找到 %s 架构的 UMID 二进制" % arch) def install(force=False): """下载官方 qodercli 包并提取 runtime-info。返回 (ok, message).""" if not force and installed(): return True, "已安装" os.makedirs(CACHE_DIR, exist_ok=True) arch = _arch() if arch not in ELF_MACHINE: return False, "NAS CPU 架构 %s 暂不支持(仅 x86_64 / arm64)" % arch last_err = None for registry in REGISTRIES: try: log("umid: fetching %s from %s" % (PKG, registry), "INFO") meta = _registry_meta(registry) version = meta.get("version", "") tarball = (meta.get("dist") or {}).get("tarball") or "" if not tarball: raise RuntimeError("registry 未返回 tarball") log("umid: downloading %s %s" % (PKG, version), "INFO") tgz = _fetch(tarball, timeout=300) # gzip 解包 import gzip tar = gzip.decompress(tgz) bundle = _extract_tar_entry(tar, BUNDLE_ENTRY) if not bundle: raise RuntimeError("包内未找到 %s" % BUNDLE_ENTRY) elf = _find_elf(bundle, arch) tmp = BIN_PATH + ".tmp" with open(tmp, "wb") as f: f.write(elf) os.chmod(tmp, os.stat(tmp).st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) os.replace(tmp, BIN_PATH) import hashlib sha = hashlib.sha256(elf).hexdigest() json.dump({"version": version, "arch": arch, "sha256": sha, "installedAt": time.strftime("%Y-%m-%d %H:%M:%S"), "registry": registry}, open(MANIFEST, "w", encoding="utf-8"), ensure_ascii=False, indent=1) log("umid: installed %s (arch=%s sha256=%s)" % (version, arch, sha[:12]), "INFO") return True, "已安装 %s(%s)" % (version, arch) except Exception as e: # 尝试下一个 registry last_err = "%s: %s" % (registry, e) log("umid: registry %s failed: %s" % (registry, e), "WARN") return False, "下载失败(%s)" % last_err # --------------------------------------------------------------------------- # 调用 # --------------------------------------------------------------------------- def run_runtime_info(env_code, account=""): """以指定 env 直接运行 runtime-info,返回 (完整输出 JSON, error)。 env_code: 0=国内版 ONLINE,3=国际版(客户端默认),4=国际版(CLI 默认)。 返回的 JSON 除 machineToken/Code/Type 外通常还有 vmInfo 等字段。 """ info = installed() if not info: return None, "未安装 runtime-info(请先在设置里下载)" try: stdin_data = json.dumps({"account": str(account or "")}) + "\n" p = subprocess.run([BIN_PATH, str(env_code), "--account-stdin"], input=stdin_data, capture_output=True, text=True, timeout=30) if p.returncode != 0 or not p.stdout.strip(): return None, "runtime-info 执行失败(退出码 %s)%s" % (p.returncode, (p.stderr or "")[:120]) line = p.stdout.strip().split("\n")[0] data = json.loads(line) if not isinstance(data, dict): return None, "runtime-info 输出不是 JSON 对象" return data, None except subprocess.TimeoutExpired: return None, "runtime-info 执行超时" except Exception as e: return None, "runtime-info 调用异常:%s" % e def run_runtime_info_raw(args, stdin_data=None): """按给定 argv / stdin 调用 runtime-info,返回 (JSON, error)。 用途:对照不同的 env 取值与「是否带 --account-stdin」(客户端 Linux 分支 只用 `runtime-info `,完全不喂 stdin)。 """ info = installed() if not info: return None, "未安装 runtime-info(请先在设置里下载)" try: argv = [BIN_PATH] + [str(a) for a in args] if stdin_data is None: p = subprocess.run(argv, stdin=subprocess.DEVNULL, capture_output=True, text=True, timeout=30) else: p = subprocess.run(argv, input=stdin_data, capture_output=True, text=True, timeout=30) if p.returncode != 0 or not p.stdout.strip(): return None, "退出码 %s %s" % (p.returncode, (p.stderr or "")[:160]) line = p.stdout.strip().split("\n")[0] data = json.loads(line) if not isinstance(data, dict): return None, "输出不是 JSON 对象" return data, None except subprocess.TimeoutExpired: return None, "超时" except Exception as e: return None, "异常:%s" % e def risk_identity(provider="qoder", account="", force_install=False): """生成设备风控身份。返回 ({"machineToken","machineCode","machineType"}, error). provider: "qoder"(国际版 env=4)/ "qoder-cn"(国内版 env=0) """ info = installed() if not info or force_install: ok, msg = install(force=force_install) if not ok: return None, msg info = installed() data, err = run_runtime_info(RISK_ENV.get(provider, 0), account) if err: return None, err out = {} for k in ("machineToken", "machineCode", "machineType"): v = data.get(k) if not isinstance(v, str) or not v.strip(): return None, "runtime-info 输出缺少 %s" % k out[k] = v.strip() if data.get("vmInfo"): out["vmInfo"] = data["vmInfo"] return out, None def status(): """供 UI 展示的状态。""" info = installed() return { "installed": bool(info), "path": BIN_PATH, "version": (info or {}).get("version"), "arch": (info or {}).get("arch"), "installedAt": (info or {}).get("installedAt"), "supported": _arch() in ELF_MACHINE, "cpu_arch": _arch(), }