#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""DeepSeek Harness 安装器 v2 —— 纯标准库，零依赖

用法：
  python installer.py                    # 先要卡密，再全自动装
  python installer.py --uninstall        # 全自动卸载清理
  python installer.py --fingerprint      # 只看机器指纹
  python installer.py --check            # 体检：快捷方式真实属性/图标/授权
  python installer.py --repair-shortcut  # 只重建桌面快捷方式（不重装）

流程：卡密 → 环境自检/补全 → 指纹 → 激活 → 下载解密 → 安装
      → 自检 → 桌面快捷方式 → 写独立卸载器
"""

import argparse
import base64
import hashlib
import hmac
import io
import json
import os
import platform
import shutil
import subprocess
import sys
import tempfile
import time
import urllib.error
import urllib.parse
import urllib.request
import zipfile

INSTALLER_VERSION = "2.5.7"
PRODUCT = "deepseek-harness"
PRODUCT_FULL = "DeepSeek Harness"
DEFAULT_SERVER = "https://dl.namezx.com"
MARKER = ".deepseek-harness-install.json"
SHORTCUT_NAME = "DeepSeek Harness"

try:  # Windows 控制台中文
    sys.stdout.reconfigure(encoding="utf-8", errors="replace")
    sys.stderr.reconfigure(encoding="utf-8", errors="replace")
except Exception:
    pass


# ---------------------------------------------------------------- 输出
def info(msg):
    print("  " + msg, flush=True)


def ok(msg):
    print("  [OK] " + msg, flush=True)


def warn(msg):
    print("  [!] " + msg, flush=True)


def die(msg, code=1):
    print("\n失败：" + msg, flush=True)
    sys.exit(code)


# ---------------------------------------------------------------- 卡密
def ask_card(cli_card):
    """卡密前置：没有卡密不进任何安装步骤。"""
    card = (cli_card or "").strip().upper()
    if card:
        return card
    print("")
    print("  请输入卡密（形如 XXXX-XXXX-XXXX-XXXX）")
    print("  没有卡密无法继续，输入 q 退出。")
    for _ in range(3):
        try:
            entered = input("  卡密：").strip().upper()
        except (EOFError, KeyboardInterrupt):
            print("")
            die("已取消")
        if entered in ("Q", "QUIT", "EXIT"):
            die("已取消")
        if len(entered) >= 8:
            return entered
        warn("卡密太短，请检查后重输")
    die("连续 3 次未输入有效卡密")


# ---------------------------------------------------------------- 环境
def detect_env():
    e = {
        "os": platform.system() or "unknown",
        "arch": platform.machine() or "unknown",
        "bits": 64 if sys.maxsize > 2 ** 32 else 32,
        "python": platform.python_version(),
        "python_exe": sys.executable,
    }
    node = shutil.which("node")
    e["node"] = "-"
    e["node_path"] = ""
    if node:
        try:
            e["node"] = subprocess.run([node, "-v"], capture_output=True,
                                       text=True, timeout=8).stdout.strip()
            e["node_path"] = node
        except Exception:
            pass
    return e


def detect_distro():
    if platform.system() != "Linux":
        return ""
    try:
        with open("/etc/os-release", encoding="utf-8", errors="ignore") as fh:
            for line in fh:
                if line.startswith("PRETTY_NAME="):
                    return line.split("=", 1)[1].strip().strip('"')
    except Exception:
        pass
    return ""


def disk_free(path):
    try:
        probe = path
        while probe and not os.path.exists(probe):
            nxt = os.path.dirname(probe)
            if nxt == probe:
                break
            probe = nxt
        return shutil.disk_usage(probe or ".").free
    except Exception:
        return None


def try_install_node():
    """尽力补全 Node，失败不阻断（演示载荷不依赖 Node）。"""
    system = platform.system()
    if system == "Windows":
        if not shutil.which("winget"):
            return False, "没有 winget，请手动安装 Node.js（https://nodejs.org）"
        cmd = ["winget", "install", "--id", "OpenJS.NodeJS.LTS", "-e",
               "--silent", "--accept-package-agreements",
               "--accept-source-agreements"]
    elif system == "Linux":
        if os.geteuid() != 0:
            return False, "非 root，跳过自动安装；需要时请手动装 Node.js"
        if shutil.which("apt-get"):
            cmd = ["apt-get", "install", "-y", "nodejs"]
        elif shutil.which("dnf"):
            cmd = ["dnf", "install", "-y", "nodejs"]
        else:
            return False, "未识别的包管理器"
    else:
        return False, "macOS 请用 brew install node 手动安装"
    try:
        out = subprocess.run(cmd, capture_output=True, text=True, timeout=420)
        if out.returncode == 0:
            return True, "已自动安装"
        tail = (out.stderr or out.stdout).strip().splitlines()
        return False, tail[-1] if tail else "安装命令失败"
    except Exception as exc:
        return False, str(exc)


def check_environment(auto_fix=True):
    """检查 + 补全。返回 {os, python, node, ...}"""
    env = detect_env()
    info("系统：%s %s（%d 位）" % (env["os"], env["arch"], env["bits"]))
    distro = detect_distro()
    if distro:
        info("发行版：%s" % distro)

    # Python
    major, minor = sys.version_info[:2]
    if (major, minor) < (3, 8):
        die("Python 版本过低（%d.%d），需要 3.8 以上" % (major, minor))
    ok("Python %s" % env["python"])

    # Node（可选依赖；官方 WebUI 载荷自带 Node 运行时，系统没有也没关系）
    if env["node"] == "-":
        info("未检测到系统 Node.js（载荷自带运行时，通常不需要）")
    else:
        ok("Node.js %s" % env["node"])

    # 磁盘
    free = disk_free(os.path.expanduser("~"))
    if free is not None:
        info("可用空间：%.1f GB" % (free / 1024 ** 3))
        if free < 200 * 1024 ** 2:
            die("磁盘空间不足 200MB，请先清理")
        ok("磁盘空间充足")
    return env


def check_server(server):
    try:
        with urllib.request.urlopen(server.rstrip("/") + "/health",
                                    timeout=10) as resp:
            data = json.loads(resp.read().decode())
        ok("授权服务可达（%s）" % data.get("version", "?"))
        return True
    except Exception as exc:
        die("连不上授权服务器 %s\n       原因：%s\n       请检查网络或服务器地址"
            % (server, exc))


# ---------------------------------------------------------------- 指纹
def _run(cmd, timeout=10):
    try:
        out = subprocess.run(cmd, capture_output=True, text=True,
                             timeout=timeout)
        return out.stdout.strip()
    except Exception:
        return ""


def machine_fingerprint():
    parts = {"os": platform.system(), "machine": platform.machine()}
    system = platform.system()

    if system == "Windows":
        # 指纹必须稳定：同一台机器两次运行不能算出两个值，否则会被误判成新设备。
        uuid = ""
        for _ in range(2):
            uuid = _run(["powershell", "-NoProfile", "-Command",
                         "(Get-CimInstance Win32_ComputerSystemProduct).UUID"])
            if uuid:
                break
        if not uuid:
            uuid = _run(["wmic", "csproduct", "get", "UUID"]).replace(
                "UUID", "").strip()
        parts["board_uuid"] = uuid or "na"

        cpu = ""
        for _ in range(2):
            cpu = _run(["powershell", "-NoProfile", "-Command",
                        "(Get-CimInstance Win32_Processor).ProcessorId"])
            if cpu:
                break
        parts["cpu"] = cpu or "na"

        # 卷序列号拿不到就不参与指纹，避免同一台机器算出两种结果
        vol = _run(["cmd", "/c", "vol", "C:"])
        for token in (vol or "").replace("\r", " ").split():
            if "-" in token and len(token) >= 8:
                parts["volume"] = token
                break
    elif system == "Linux":
        for name, path in (("machine_id", "/etc/machine-id"),
                           ("product_uuid", "/sys/class/dmi/id/product_uuid")):
            try:
                with open(path, encoding="utf-8", errors="ignore") as fh:
                    parts[name] = fh.read().strip()
            except Exception:
                parts[name] = "na"
        parts["distro"] = detect_distro()
        try:
            with open("/proc/cpuinfo", encoding="utf-8", errors="ignore") as fh:
                for line in fh:
                    if line.lower().startswith("model name"):
                        parts["cpu"] = line.split(":", 1)[1].strip()
                        break
        except Exception:
            pass
    elif system == "Darwin":
        parts["hw_uuid"] = _run(["ioreg", "-rd1", "-c",
                                 "IOPlatformExpertDevice"])

    # 只用稳定的硬件字段；卷序列号等可选字段波动会让同机算出不同指纹
    stable_keys = ("os", "machine", "board_uuid", "cpu", "product_uuid",
                   "machine_id", "hw_uuid")
    stable = {k: parts[k] for k in stable_keys if k in parts}
    material = json.dumps(stable, sort_keys=True, ensure_ascii=False)
    fp = hashlib.sha256(material.encode()).hexdigest()[:32]
    safe = {k: (("***" + str(v)[-6:]) if k in
                ("board_uuid", "product_uuid", "machine_id", "hw_uuid")
                else str(v)[:80]) for k, v in parts.items()}
    return fp, safe


# ---------------------------------------------------------------- 网络
def http_json(url, payload=None, timeout=25):
    headers = {"Content-Type": "application/json",
               "User-Agent": "deepseek-harness-installer/" + INSTALLER_VERSION}
    data = json.dumps(payload).encode() if payload is not None else None
    req = urllib.request.Request(url, data=data, headers=headers,
                                 method="POST" if data else "GET")
    try:
        with urllib.request.urlopen(req, timeout=timeout) as resp:
            return resp.status, json.loads(resp.read().decode())
    except urllib.error.HTTPError as exc:
        try:
            return exc.code, json.loads(exc.read().decode())
        except Exception:
            return exc.code, {"message": "服务端返回 %d" % exc.code}
    except urllib.error.URLError as exc:
        die("连不上服务器：%s" % exc.reason)
    except Exception as exc:
        die("请求出错：%s" % exc)


def http_bytes(url, timeout=120):
    req = urllib.request.Request(url, headers={
        "User-Agent": "deepseek-harness-installer/" + INSTALLER_VERSION})
    try:
        with urllib.request.urlopen(req, timeout=timeout) as resp:
            return resp.read()
    except urllib.error.HTTPError as exc:
        try:
            msg = json.loads(exc.read().decode()).get("message", "")
        except Exception:
            msg = ""
        die("下载失败（%d）%s" % (exc.code, msg))
    except Exception as exc:
        die("下载失败：%s" % exc)


# ---------------------------------------------------------------- 解密
def _keystream(key, nonce, length):
    out = bytearray()
    counter = 0
    while len(out) < length:
        out += hmac.new(key, nonce + counter.to_bytes(8, "big"),
                        hashlib.sha256).digest()
        counter += 1
    return bytes(out[:length])


def unseal_blob(blob, key, nonce):
    if not blob.startswith(b"HS1") or len(blob) < 20:
        die("安装包格式不对，可能下载不完整")
    tag = blob[3:19]
    ct = blob[19:]
    want = hmac.new(key, b"tag|" + nonce + ct, hashlib.sha256).digest()[:16]
    if not hmac.compare_digest(want, tag):
        die("安装包校验失败（内容被改动或密钥不匹配）")
    ks = _keystream(key, nonce, len(ct))
    return bytes(a ^ b for a, b in zip(ct, ks))


def safe_extract(zip_bytes, target):
    with zipfile.ZipFile(io.BytesIO(zip_bytes)) as zf:
        for item in zf.infolist():
            dest = os.path.abspath(os.path.join(target, item.filename))
            if not dest.startswith(os.path.abspath(target) + os.sep):
                die("安装包内有非法路径，已中止")
        zf.extractall(target)


# ---------------------------------------------------------------- 快捷方式
def desktop_dir():
    home = os.path.expanduser("~")
    if platform.system() == "Windows":
        # 先问注册表：OneDrive 接管桌面时，本地 Desktop 目录可能是个空壳
        try:
            import winreg
            key = winreg.OpenKey(
                winreg.HKEY_CURRENT_USER,
                r"Software\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders")
            value, _ = winreg.QueryValueEx(key, "Desktop")
            path = os.path.expandvars(value)
            if os.path.isdir(path):
                return path
        except Exception:
            pass
    for name in ("Desktop", "桌面", "OneDrive/Desktop"):
        path = os.path.join(home, name)
        if os.path.isdir(path):
            return path
    path = os.path.join(home, "Desktop")
    os.makedirs(path, exist_ok=True)
    return path


def _ps(text):
    return str(text).replace("'", "''")


def _ps_encoded(script, timeout=120):
    """用 EncodedCommand 传脚本：不落临时文件，绕开 BOM/编码/引号坑。"""
    payload = base64.b64encode(script.encode("utf-16-le")).decode("ascii")
    args = ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass",
            "-EncodedCommand", payload]
    full = os.path.join(os.environ.get("SystemRoot", r"C:\Windows"),
                        r"System32\WindowsPowerShell\v1.0\powershell.exe")
    last = None
    for exe in ("powershell", full):
        try:
            return subprocess.run([exe] + args, capture_output=True, text=True,
                                  timeout=timeout)
        except FileNotFoundError as exc:
            last = exc
        except Exception:
            raise
    raise last or FileNotFoundError("powershell not found")


def _ps_last_json(result):
    for line in reversed((result.stdout or "").strip().splitlines()):
        line = line.strip()
        if line.startswith("{") and line.endswith("}"):
            try:
                return json.loads(line)
            except Exception:
                continue
    return None


def _same_path(a, b):
    if not a or not b:
        return False
    return (os.path.normcase(os.path.normpath(str(a)))
            == os.path.normcase(os.path.normpath(str(b))))


def ico_entries(path):
    """读 ICO 头，返回 [(宽, 高, 位深)]；不是 ICO 则返回空表。"""
    import struct
    try:
        with open(path, "rb") as fh:
            data = fh.read()
        if len(data) < 6 or data[:4] != b"\x00\x00\x01\x00":
            return []
        count = struct.unpack("<H", data[4:6])[0]
        out = []
        for i in range(count):
            entry = data[6 + 16 * i:22 + 16 * i]
            if len(entry) < 16:
                break
            out.append((entry[0] or 256, entry[1] or 256,
                        struct.unpack("<H", entry[6:8])[0]))
        return out
    except Exception:
        return []


def _resolve_windows_python(exe):
    """应用商店的 python.exe 是转发别名，双击 .lnk 时可能被拦；换成真解释器。"""
    if "\\windowsapps\\" not in (exe or "").lower():
        return exe
    for cmd in (["py", "-3", "-c", "import sys;print(sys.executable)"],
                ["python", "-c", "import sys;print(sys.executable)"]):
        try:
            out = subprocess.run(cmd, capture_output=True, text=True, timeout=20)
            cand = (out.stdout or "").strip()
            if (out.returncode == 0 and cand
                    and "\\windowsapps\\" not in cand.lower()
                    and os.path.exists(cand)):
                return cand
        except Exception:
            continue
    return exe


def load_payload_config(target):
    """载荷自带 payload.json 时按其声明执行；没有就用默认值。"""
    cfg = {"entry": "run.py", "args": [], "icon": "", "window": "console",
           "shortcut": "installer", "setup": ""}
    path = os.path.join(target, "payload.json")
    if os.path.exists(path):
        try:
            with open(path, encoding="utf-8") as fh:
                data = json.load(fh)
            if isinstance(data, dict):
                for key in list(cfg):
                    if key in data and data[key] is not None:
                        cfg[key] = data[key]
        except Exception as exc:
            warn("payload.json 读取失败，改用默认：%s" % exc)
    return cfg


def python_launcher(window, target=""):
    """优先用安装目录里的 Python：解压目录可能被删，指向它快捷方式就会变成哑巴。"""
    cands = []
    if target:
        for name in ("pythonw.exe", "python.exe") if window == "gui" else ("python.exe",):
            cands.append(os.path.join(target, "python", name))
    exe = sys.executable
    if window == "gui":
        cands.append(exe.replace("python.exe", "pythonw.exe"))
    cands.append(exe)
    for c in cands:
        if c and os.path.exists(c):
            return c
    return exe


def install_portable_python(target, cfg):
    """把随包的嵌入式 Python 复制进安装目录，让快捷方式不依赖解压目录。"""
    bundle = os.path.dirname(os.path.abspath(__file__))
    src = os.path.join(bundle, "python")
    dst = os.path.join(target, "python")
    if not os.path.isdir(src) or os.path.isdir(dst):
        return ""
    try:
        shutil.copytree(src, dst)
        return dst
    except Exception as exc:
        warn("复制 Python 运行时失败：%s" % exc)
        return ""


def write_start_cmd(target, cfg, python_exe, entry):
    """安装目录里的兵底启动器：双击必定出窗口，出错时停住不闪退。"""
    path = os.path.join(target, "Start.cmd")
    extra = " ".join('"%s"' % a for a in (cfg.get("args") or []))
    call = '"%s" "%s"%s' % (python_exe, entry, (" " + extra) if extra else "")
    # 注意：下面这行不能参与 % 格式化，"%~dp0" 里的 %~ 会被当成格式符（v2.3.0 的崩溃点）
    lines = [
        "@echo off\r\n",
        "chcp 65001 >nul\r\n",
        'cd /d "%~dp0"\r\n',
        call + "\r\n",
        "if errorlevel 1 pause\r\n",
    ]
    with open(path, "w", encoding="utf-8", newline="") as fh:
        fh.write("".join(lines))
    return path


def _create_lnk(lnk, target_path, arguments, workdir, icon_loc):
    """创建 .lnk 并回读真实属性；参数经 Base64 JSON 传入，不受引号/编码影响。"""
    cfg = {"lnk": lnk, "target": target_path, "arguments": arguments,
           "workdir": workdir, "icon": icon_loc, "description": PRODUCT_FULL}
    b64 = base64.b64encode(
        json.dumps(cfg, ensure_ascii=False).encode("utf-8")).decode("ascii")
    script = (
        '$ErrorActionPreference = "Stop"\n'
        "$json = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('%s'))\n" % b64
        + "$cfg = $json | ConvertFrom-Json\n"
        "$ws = New-Object -ComObject WScript.Shell\n"
        "$sc = $ws.CreateShortcut($cfg.lnk)\n"
        "$sc.TargetPath = $cfg.target\n"
        "$sc.Arguments = $cfg.arguments\n"
        "$sc.WorkingDirectory = $cfg.workdir\n"
        "$sc.Description = $cfg.description\n"
        "$sc.IconLocation = $cfg.icon\n"
        "$sc.WindowStyle = 1\n"
        "$sc.Save()\n"
        "$chk = $ws.CreateShortcut($cfg.lnk)\n"
        "[pscustomobject]@{TargetPath=$chk.TargetPath;Arguments=$chk.Arguments;"
        "WorkingDirectory=$chk.WorkingDirectory;IconLocation=$chk.IconLocation}"
        " | ConvertTo-Json -Compress\n"
    )
    try:
        out = _ps_encoded(script)
    except Exception as exc:
        return None, "PowerShell 调用失败：%s" % exc
    if out.returncode != 0:
        lines = (out.stderr or out.stdout).strip().splitlines()
        return None, (lines[-1] if lines else "PowerShell 返回 %d" % out.returncode)
    props = _ps_last_json(out)
    if not props:
        return None, "无法回读快捷方式属性"
    return props, ""


def _verify_lnk(props, target_path, workdir, icon_path, args_needle):
    problems = []
    if not _same_path(props.get("TargetPath"), target_path):
        problems.append("TargetPath=%s" % (props.get("TargetPath") or "空"))
    if workdir and not _same_path(props.get("WorkingDirectory"), workdir):
        problems.append("WorkingDirectory=%s"
                        % (props.get("WorkingDirectory") or "空"))
    icon_got = (props.get("IconLocation") or "").split(",")[0].strip('"')
    if icon_path and not _same_path(icon_got, icon_path):
        problems.append("IconLocation=%s" % (props.get("IconLocation") or "空"))
    got_args = props.get("Arguments") or ""
    if args_needle and args_needle.lower() not in got_args.lower():
        problems.append("Arguments=%s" % (got_args or "空"))
    return problems


def make_shortcut(target, cfg=None):
    """按载荷声明创建快捷方式，直接指向官方入口，不套自己的启动器。"""
    cfg = cfg or {}
    system = platform.system()
    created = []
    desktop = desktop_dir()
    python_exe = python_launcher(cfg.get("window", "console"), target)
    entry = os.path.join(target, cfg.get("entry") or "run.py")
    extra = " ".join('"%s"' % a for a in (cfg.get("args") or []))
    icon_path = ""
    if cfg.get("icon"):
        cand = os.path.join(target, cfg["icon"])
        if os.path.exists(cand):
            icon_path = cand
    icon_loc = (icon_path + ",0") if icon_path else (python_exe + ",0")

    if system == "Windows":
        lnk = os.path.join(desktop, SHORTCUT_NAME + ".lnk")
        python_exe = _resolve_windows_python(
            python_launcher(cfg.get("window", "console"), target))
        arguments = '"%s"' % entry + ((" " + extra) if extra else "")

        if icon_path:
            sizes = [s[0] for s in ico_entries(icon_path)]
            if len(sizes) < 2 or max(sizes) < 256:
                warn("图标不是标准多尺寸 ICO（%s），可能显示系统默认图标"
                     % (",".join(str(s) for s in sizes) or "无法解析"))
        if "\\windowsapps\\" in python_exe.lower():
            warn("启动用的 Python 仍是应用商店别名，双击可能被系统拦截")

        # 兜底启动器：无论 .lnk 成不成，安装目录里都有一个能双击跑的入口
        try:
            start_cmd = write_start_cmd(target, cfg, python_exe, entry)
        except Exception as exc:
            warn("Start.cmd 写入失败（不影响安装）：%s" % exc)
            start_cmd = ""

        props, err = _create_lnk(lnk, python_exe, arguments, target, icon_loc)
        problems = (_verify_lnk(props, python_exe, target, icon_path, entry)
                    if props else [err or "创建失败"])
        if problems:
            warn("快捷方式回读校验未通过（%s），改用命令行启动器兜底"
                 % "；".join(problems))
            cmd_exe = os.environ.get("COMSPEC") or r"C:\Windows\System32\cmd.exe"
            if not start_cmd:
                problems = ["Start.cmd 未生成，无法兜底"]
                props = None
            else:
                props, err = _create_lnk(lnk, cmd_exe, '/c ""%s""' % start_cmd,
                                         target, icon_loc)
            problems = (_verify_lnk(props, cmd_exe, target, icon_path, start_cmd)
                        if props else [err or "兜底创建失败"])
            if problems:
                warn("兜底快捷方式也失败：%s" % "；".join(problems))
                if start_cmd:
                    try:
                        shutil.copy2(start_cmd,
                                     os.path.join(desktop, SHORTCUT_NAME + " Start.cmd"))
                        created.append(os.path.join(desktop, SHORTCUT_NAME + " Start.cmd"))
                    except Exception:
                        pass
            else:
                created.append(lnk + "（兜底：命令行启动）")
        else:
            created.append(lnk)

        # 图标缓存刷新（尽力而为，失败不影响安装）
        try:
            subprocess.run(["ie4uinit.exe", "-show"], capture_output=True,
                           timeout=15)
        except Exception:
            pass

    elif system == "Darwin":
        path = os.path.join(desktop, SHORTCUT_NAME + ".command")
        with open(path, "w", encoding="utf-8") as fh:
            fh.write("#!/bin/bash\ncd \"%s\"\n\"%s\" \"%s\" %s\n"
                     % (target, python_exe, entry, extra))
        os.chmod(path, 0o755)
        created.append(path)

    else:  # Linux
        terminal = "true" if cfg.get("window", "console") != "gui" else "false"
        icon_line = icon_path if icon_path else "utilities-terminal"
        content = (
            "[Desktop Entry]\n"
            "Version=1.0\n"
            "Type=Application\n"
            "Name=DeepSeek Harness\n"
            "Comment=DeepSeek Harness\n"
            'Exec="%s" "%s" %s\n'
            "Path=%s\n"
            "Terminal=%s\n"
            "Categories=Development;\n"
            "Icon=%s\n"
        ) % (python_exe, entry, extra, target, terminal, icon_line)
        for path in (os.path.join(desktop, SHORTCUT_NAME + ".desktop"),
                     os.path.join(os.path.expanduser("~"),
                                  ".local/share/applications",
                                  SHORTCUT_NAME + ".desktop")):
            try:
                os.makedirs(os.path.dirname(path), exist_ok=True)
                with open(path, "w", encoding="utf-8") as fh:
                    fh.write(content)
                os.chmod(path, 0o755)
                created.append(path)
            except Exception as exc:
                warn("写入 %s 失败：%s" % (path, exc))
    return created


# ---------------------------------------------------------------- 卸载
def shortcut_paths(target):
    paths = []
    desktop = desktop_dir()
    paths += [os.path.join(desktop, SHORTCUT_NAME + ".lnk"),
              os.path.join(desktop, SHORTCUT_NAME + ".cmd"),
              os.path.join(desktop, SHORTCUT_NAME + ".desktop"),
              os.path.join(desktop, SHORTCUT_NAME + ".command"),
              os.path.join(os.path.expanduser("~"),
                           ".local/share/applications",
                           SHORTCUT_NAME + ".desktop"),
              os.path.join(os.path.expanduser("~"), ".deepseek-harness-uninstall.py"),
              os.path.join(desktop, SHORTCUT_NAME + " Start.cmd"),
              os.path.join(desktop, "dsh.lnk"),
              os.path.join(desktop, "dsh.cmd"),
              os.path.join(desktop, "dsh.desktop"),
              os.path.join(desktop, "dsh.command"),
              os.path.join(os.path.expanduser("~"), ".local/share/applications/dsh.desktop"),
              os.path.join(os.path.expanduser("~"), ".dsh-uninstall.py")]
    if target:
        paths.append(os.path.join(target, "deepseek-harness.cmd"))
    return paths


def write_uninstaller(target):
    """生两份卸载器：家目录一份（带提示），安装目录一份（给系统卸载入口用）。"""
    body = '''#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""DeepSeek Harness 独立卸载器（安装时自动生成）"""
import os, shutil, sys

TARGET = __TARGET__
SHORTCUTS = __SHORTCUTS__
QUIET = "--yes" in sys.argv
_LOCAL = os.environ.get("LOCALAPPDATA") or os.path.join(
    os.path.expanduser("~"), "AppData", "Local")
USER_RUNTIME = os.path.join(_LOCAL, "DeepSeekHarness")


def drop_registry():
    if os.name != "nt":
        return
    try:
        import winreg
        winreg.DeleteKey(winreg.HKEY_CURRENT_USER,
                         r"Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\DeepSeekHarness")
    except Exception:
        pass


def main():
    removed = []
    for p in SHORTCUTS:
        try:
            if os.path.isfile(p):
                os.remove(p)
                removed.append(p)
            elif os.path.isdir(p) and not os.path.islink(p):
                shutil.rmtree(p, ignore_errors=True)
                removed.append(p)
        except Exception as exc:
            print("  跳过 %s：%s" % (p, exc))
    if os.path.isdir(TARGET):
        if not os.path.exists(os.path.join(TARGET, ".deepseek-harness-install.json")) \\
                and not os.path.exists(os.path.join(TARGET, "license.json")):
            print("目标目录没有 DeepSeek Harness 安装标记，已跳过：%s" % TARGET)
        else:
            shutil.rmtree(TARGET, ignore_errors=True)
            removed.append(TARGET)
    if os.path.isdir(USER_RUNTIME) and os.path.abspath(USER_RUNTIME) != os.path.abspath(TARGET):
        shutil.rmtree(USER_RUNTIME, ignore_errors=True)
        removed.append(USER_RUNTIME)
    drop_registry()
    print("")
    print("已清理 %d 项：" % len(removed))
    for p in removed:
        print("  - %s" % p)
    print("DeepSeek Harness 已卸载。")
    if not QUIET and sys.stdin is not None and sys.stdin.isatty():
        try:
            input("按回车键关闭。")
        except (EOFError, KeyboardInterrupt):
            pass


if __name__ == "__main__":
    main()
'''
    body = (body.replace("__TARGET__", repr(target))
                .replace("__SHORTCUTS__", repr(shortcut_paths(target))))
    home_path = os.path.join(os.path.expanduser("~"),
                             ".deepseek-harness-uninstall.py")
    inner_path = os.path.join(target, "uninstall.py")
    for path in (home_path, inner_path):
        with open(path, "w", encoding="utf-8") as fh:
            fh.write(body)
        try:
            os.chmod(path, 0o755)
        except Exception:
            pass
    return home_path


def write_stop_script(target):
    """在安装目录生成「停止服务.cmd」，避免文案承诺一个不存在的文件。"""
    if platform.system() != "Windows":
        return ""
    path = os.path.join(target, "停止服务.cmd")
    body = ("@echo off\r\n"
            "chcp 65001 >nul\r\n"
            "\"%s\" \"%s\" --stop\r\n"
            "pause\r\n" % (sys.executable, os.path.join(target, "run.py")))
    try:
        with open(path, "w", encoding="utf-8", newline="") as fh:
            fh.write(body)
        return path
    except Exception as exc:
        warn("生成停止脚本失败：%s" % exc)
        return ""


def is_dsh_install(target):
    """新版本有标记文件；v1 老安装有 license.json + run.py，同样认。"""
    if os.path.exists(os.path.join(target, MARKER)):
        return True
    return (os.path.exists(os.path.join(target, "license.json"))
            and os.path.exists(os.path.join(target, "run.py")))


def remove_tree_with_progress(path):
    """删目录并打印进度，避免大目录看起来像卡死。"""
    total = 0
    for root, _dirs, files in os.walk(path):
        total += len(files)
    if total == 0:
        shutil.rmtree(path, ignore_errors=True)
        return
    done = 0
    step = max(1, total // 20)
    for root, dirs, files in os.walk(path, topdown=False):
        for name in files:
            try:
                os.remove(os.path.join(root, name))
            except Exception:
                pass
            done += 1
            if done % step == 0 or done == total:
                pct = done * 100 // total
                width = 24
                filled = int(width * pct / 100)
                sys.stdout.write("\r       [%s%s] %3d%%  %d/%d 个文件"
                                 % ("#" * filled, "-" * (width - filled),
                                    pct, done, total))
                sys.stdout.flush()
        for name in dirs:
            try:
                os.rmdir(os.path.join(root, name))
            except Exception:
                pass
    shutil.rmtree(path, ignore_errors=True)
    sys.stdout.write("\n")
    sys.stdout.flush()


def do_uninstall(target, assume_yes=False, force=False):
    print("")
    print("DeepSeek Harness 卸载")
    print("=" * 42)
    target = resolve_installed_target(target)
    if not target:
        die("没找到已安装的 DeepSeek Harness；如果装在别处，用 --target 指定目录。")
    target = os.path.abspath(os.path.expanduser(target))
    if not os.path.isdir(target):
        die("安装目录不存在：%s" % target)
    elif not is_dsh_install(target) and not force:
        die("目标目录不像 DeepSeek Harness 安装目录，为安全起见不删除：%s\n"
            "       确认要删就加 --force" % target)
    else:
        info("将删除：%s" % target)
        size = 0
        for root, _dirs, files in os.walk(target):
            for f in files:
                try:
                    size += os.path.getsize(os.path.join(root, f))
                except Exception:
                    pass
        info("占用空间：%.1f MB" % (size / 1024 ** 2))

    if not assume_yes:
        try:
            ans = input("  确认卸载？(y/N)：").strip().lower()
        except (EOFError, KeyboardInterrupt):
            ans = ""
        if ans not in ("y", "yes", "是"):
            die("已取消")

    print("[1/4] 停止后台服务")
    try:
        entry = os.path.join(target, "run.py")
        if os.path.exists(entry):
            subprocess.call([sys.executable, entry, "--stop"],
                            stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
                            timeout=60)
    except Exception:
        pass

    print("[2/4] 删除快捷方式与注册表登记")
    removed = []
    for p in shortcut_paths(target):
        try:
            if os.path.isfile(p):
                os.remove(p)
                removed.append(p)
            elif os.path.isdir(p) and not os.path.islink(p):
                shutil.rmtree(p, ignore_errors=True)
                removed.append(p)
        except Exception as exc:
            warn("跳过 %s：%s" % (p, exc))

    if os.path.isdir(target):
        print("[3/4] 删除安装目录（%s）" % target)
        remove_tree_with_progress(target)
        removed.append(target)
    else:
        print("[3/4] 安装目录已不存在，跳过")

    print("[4/4] 清理用户运行时与注册表项")
    local_base = os.environ.get("LOCALAPPDATA") or os.path.join(
        os.path.expanduser("~"), "AppData", "Local")
    user_runtime = os.path.join(local_base, "DeepSeekHarness")
    if os.path.isdir(user_runtime) and \
            os.path.abspath(user_runtime) != os.path.abspath(target):
        print("       清理用户运行时：%s" % user_runtime)
        remove_tree_with_progress(user_runtime)
        removed.append(user_runtime)
    drop_uninstall_entry()

    print("")
    ok("已清理 %d 项" % len(removed))
    for p in removed:
        info("- " + p)
    print("")
    print("卸载完成。授权卡密仍在服务器上，如需停用请找发卡方。")


# ---------------------------------------------------------------- 主流程
def read_lnk(path):
    """回读一个 .lnk 的真实属性（仅 Windows）。"""
    if platform.system() != "Windows":
        return {"note": "非 Windows，无法解析 .lnk"}
    b64 = base64.b64encode(
        json.dumps({"lnk": path}, ensure_ascii=False).encode("utf-8")).decode("ascii")
    script = (
        '$ErrorActionPreference = "Stop"\n'
        "$json = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('%s'))\n" % b64
        + "$cfg = $json | ConvertFrom-Json\n"
        "$ws = New-Object -ComObject WScript.Shell\n"
        "$chk = $ws.CreateShortcut($cfg.lnk)\n"
        "[pscustomobject]@{TargetPath=$chk.TargetPath;Arguments=$chk.Arguments;"
        "WorkingDirectory=$chk.WorkingDirectory;IconLocation=$chk.IconLocation}"
        " | ConvertTo-Json -Compress\n"
    )
    try:
        out = _ps_encoded(script, timeout=60)
    except Exception as exc:
        return {"error": str(exc)}
    if out.returncode != 0:
        lines = (out.stderr or out.stdout).strip().splitlines()
        return {"error": lines[-1] if lines else "PowerShell 返回 %d" % out.returncode}
    return _ps_last_json(out) or {"error": "无法回读属性"}


def cmd_check(target):
    """体检：把快捷方式的真实属性、图标尺寸、授权状态打成 JSON，方便远程排错。"""
    report = {
        "installer_version": INSTALLER_VERSION,
        "product": PRODUCT_FULL,
        "platform": platform.system(),
        "python": sys.executable,
        "target": target,
        "default_install_dir": default_install_dir(),
        "registry_install": registry_install_location(),
        "installed": is_dsh_install(target),
    }
    if not report["installed"]:
        report["hint"] = "目标目录没有安装标记；如需体检请加 --target <安装目录>"
        print(json.dumps(report, ensure_ascii=False, indent=2))
        return

    cfg = load_payload_config(target)
    report["payload"] = cfg
    entry = os.path.join(target, cfg.get("entry") or "run.py")
    report["entry_exists"] = os.path.exists(entry)
    start_cmd = os.path.join(target, "Start.cmd")
    report["start_cmd"] = {"path": start_cmd, "exists": os.path.exists(start_cmd)}

    icon_path = os.path.join(target, cfg["icon"]) if cfg.get("icon") else ""
    sizes = ico_entries(icon_path) if icon_path and os.path.exists(icon_path) else []
    report["icon"] = {"path": icon_path,
                      "exists": bool(icon_path and os.path.exists(icon_path)),
                      "sizes": [s[0] for s in sizes]}

    license_path = os.path.join(target, "license.json")
    if os.path.exists(license_path):
        try:
            with open(license_path, encoding="utf-8") as fh:
                lic = json.load(fh)
            exp = lic.get("expires")
            report["license"] = {
                "card": lic.get("card"),
                "expires": exp,
                "days_left": round((exp - time.time()) / 86400.0, 1) if exp else None,
                "fingerprint": lic.get("fingerprint"),
                "server": lic.get("server"),
            }
        except Exception as exc:
            report["license"] = {"error": str(exc)}
    else:
        report["license"] = {"error": "license.json 不存在"}

    desktop = desktop_dir()
    report["desktop"] = desktop
    shortcuts = []
    for name in (SHORTCUT_NAME + ".lnk", SHORTCUT_NAME + ".cmd",
                 SHORTCUT_NAME + ".desktop", SHORTCUT_NAME + " Start.cmd"):
        path = os.path.join(desktop, name)
        item = {"path": path, "exists": os.path.exists(path)}
        if path.lower().endswith(".lnk") and item["exists"]:
            item["properties"] = read_lnk(path)
        shortcuts.append(item)
    report["shortcuts"] = shortcuts
    print(json.dumps(report, ensure_ascii=False, indent=2))


def cmd_repair_shortcut(target):
    """只重建快捷方式，不重新下载、不重新激活。"""
    if not is_dsh_install(target):
        die("目标目录不是 DeepSeek Harness 安装：%s" % target)
    cfg = load_payload_config(target)
    desktop = desktop_dir()
    for name in (SHORTCUT_NAME + ".lnk", SHORTCUT_NAME + ".cmd",
                 SHORTCUT_NAME + " Start.cmd"):
        path = os.path.join(desktop, name)
        try:
            if os.path.exists(path):
                os.remove(path)
        except Exception:
            pass
    try:
        made = make_shortcut(target, cfg)
    except Exception as exc:
        made = []
        warn("快捷方式创建出错（安装已完成，不影响使用）：%s" % exc)
    if made:
        for path in made:
            ok("已创建：%s" % path)
    else:
        warn("未能创建快捷方式，请直接双击 %s" % os.path.join(target, "Start.cmd"))
    print(json.dumps({"ok": bool(made), "created": made,
                      "check": "python installer.py --check"},
                     ensure_ascii=False, indent=2))
    return 0 if made else 1


def is_admin():
    if platform.system() != "Windows":
        return False
    try:
        import ctypes
        return bool(ctypes.windll.shell32.IsUserAnAdmin())
    except Exception:
        return False


def default_install_dir():
    """跟普通安装程序一致：默认装 C 盘。管理员装 Program Files，否则装当前用户目录。"""
    if platform.system() == "Windows":
        if is_admin():
            base = os.environ.get("ProgramFiles") or r"C:\Program Files"
            return os.path.join(base, PRODUCT_FULL)
        base = os.environ.get("LOCALAPPDATA") or os.path.join(
            os.path.expanduser("~"), "AppData", "Local")
        return os.path.join(base, "Programs", PRODUCT_FULL)
    return os.path.join(os.path.expanduser("~"), ".deepseek-harness")


def choose_target(cli_target, assume_yes):
    if cli_target:
        return os.path.abspath(os.path.expanduser(cli_target))
    default = default_install_dir()
    if assume_yes or not (sys.stdin and sys.stdin.isatty()):
        return default
    print("")
    print("  安装位置（直接回车用默认）：")
    try:
        entered = input("  安装目录 [%s]：" % default).strip().strip('"')
    except (EOFError, KeyboardInterrupt):
        print("")
        die("已取消")
    return os.path.abspath(os.path.expanduser(entered)) if entered else default


UNINSTALL_KEY = ("Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\"
                 "DeepSeekHarness")


def registry_install_location():
    if platform.system() != "Windows":
        return ""
    try:
        import winreg
        key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, UNINSTALL_KEY)
        value, _ = winreg.QueryValueEx(key, "InstallLocation")
        return value if value and os.path.isdir(value) else ""
    except Exception:
        return ""


def register_uninstall_entry(target, cfg):
    """让「设置 → 应用」里出现 DeepSeek Harness，卸载会调到安装目录里的卸载器。"""
    if platform.system() != "Windows":
        return False
    try:
        import winreg
        icon = ""
        if cfg.get("icon"):
            cand = os.path.join(target, cfg["icon"])
            if os.path.exists(cand):
                icon = cand
        uninst = os.path.join(target, "uninstall.py")
        python_exe = sys.executable
        size = 0
        for root, _dirs, files in os.walk(target):
            for name in files:
                try:
                    size += os.path.getsize(os.path.join(root, name))
                except Exception:
                    pass
        with winreg.CreateKey(winreg.HKEY_CURRENT_USER, UNINSTALL_KEY) as key:
            def s(name, value):
                winreg.SetValueEx(key, name, 0, winreg.REG_SZ, str(value))

            def d(name, value):
                winreg.SetValueEx(key, name, 0, winreg.REG_DWORD, int(value))

            s("DisplayName", PRODUCT_FULL)
            s("DisplayVersion", INSTALLER_VERSION)
            s("Publisher", PRODUCT_FULL)
            s("InstallLocation", target)
            s("DisplayIcon", icon or python_exe)
            s("UninstallString", '"%s" "%s" --yes' % (python_exe, uninst))
            s("QuietUninstallString", '"%s" "%s" --yes' % (python_exe, uninst))
            s("InstallDate", time.strftime("%Y%m%d"))
            d("NoModify", 1)
            d("NoRepair", 1)
            d("EstimatedSize", max(1, size // 1024))
        return True
    except Exception as exc:
        warn("写入系统卸载入口失败（不影响使用）：%s" % exc)
        return False


def drop_uninstall_entry():
    if platform.system() != "Windows":
        return
    try:
        import winreg
        winreg.DeleteKey(winreg.HKEY_CURRENT_USER, UNINSTALL_KEY)
    except Exception:
        pass


def resolve_installed_target(target=""):
    if target:
        return os.path.abspath(os.path.expanduser(target))
    reg = registry_install_location()
    if reg:
        return reg
    legacy = os.path.join(os.path.expanduser("~"), ".deepseek-harness")
    if is_dsh_install(legacy):
        return legacy
    return ""


def cmd_stop(target):
    target = resolve_installed_target(target) or default_install_dir()
    entry = os.path.join(target, "run.py")
    if not os.path.exists(entry):
        die("没找到已安装的 DeepSeek Harness：%s" % target)
    return subprocess.call([sys.executable, entry, "--stop"])


def set_console_icon():
    """给控制台窗口换上产品图标（Windows 专用，失败无妨）。"""
    if platform.system() != "Windows":
        return
    path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "icon.ico")
    if not os.path.exists(path):
        return
    try:
        import ctypes
        IMAGE_ICON, LR_LOADFROMFILE, LR_DEFAULTSIZE = 1, 0x10, 0x40
        h = ctypes.windll.user32.LoadImageW(
            None, path, IMAGE_ICON, 0, 0, LR_LOADFROMFILE | LR_DEFAULTSIZE)
        if h:
            ctypes.windll.kernel32.SetConsoleIcon(h)
    except Exception:
        pass


def main():
    set_console_icon()
    ap = argparse.ArgumentParser(description="DeepSeek Harness 安装器")
    ap.add_argument("--card", default="", help="卡密（不给则启动后询问）")
    ap.add_argument("--server",
                    default=os.environ.get("HARNESS_SERVER", DEFAULT_SERVER))
    ap.add_argument("--target",
                    default="",
                    help="安装目录（默认 C 盘；不给则询问）")
    ap.add_argument("--uninstall", action="store_true", help="卸载并清理")
    ap.add_argument("--fingerprint", action="store_true", help="只显示指纹")
    ap.add_argument("--yes", action="store_true", help="跳过确认")
    ap.add_argument("--force", action="store_true",
                    help="卸载时跳过安装目录校验（危险）")
    ap.add_argument("--no-auto-install", action="store_true",
                    help="不自动补全缺失组件")
    ap.add_argument("--local-payload", default="",
                    help="使用随包载荷（离线安装，不占用服务器下载）")
    ap.add_argument("--no-launch", action="store_true",
                    help="安装完成后不自动启动服务")
    ap.add_argument("--no-shortcut", action="store_true",
                    help="不创建桌面快捷方式")
    ap.add_argument("--stop", action="store_true",
                    help="停止后台运行的 Web 服务")
    ap.add_argument("--check", action="store_true",
                    help="体检：输出快捷方式真实属性、图标尺寸、授权状态")
    ap.add_argument("--repair-shortcut", action="store_true",
                    help="只重建桌面快捷方式（不重新安装）")
    args = ap.parse_args()

    target = os.path.abspath(os.path.expanduser(args.target)) if args.target else ""

    # 体检输出必须是纯 JSON（方便对端脚本直接解析），不打印横幅
    if args.check:
        return cmd_check(target or default_install_dir())

    print("")
    print("%s 安装器 v%s" % (PRODUCT_FULL, INSTALLER_VERSION))
    print("=" * 46)

    if args.repair_shortcut:
        return cmd_repair_shortcut(resolve_installed_target(target)
                                   or default_install_dir())

    if args.stop:
        return cmd_stop(target)

    if args.uninstall:
        return do_uninstall(target, args.yes, args.force)

    target = choose_target(target, args.yes)

    if args.fingerprint:
        fp, parts = machine_fingerprint()
        print("指纹：%s" % fp)
        print(json.dumps(parts, ensure_ascii=False, indent=2))
        return

    # ① 卡密前置
    card = ask_card(args.card)

    # ② 环境自检与补全
    print("[1/9] 检查并补全基础环境")
    check_environment(auto_fix=not args.no_auto_install)
    check_server(args.server)

    print("[2/9] 计算机器指纹")
    fp, parts = machine_fingerprint()
    info("指纹：%s" % fp)

    print("[3/9] 联网激活")
    status, resp = http_json(args.server.rstrip("/") + "/activate",
                             {"card": card, "fingerprint": fp, "parts": parts})
    if status != 200:
        die(resp.get("message") or "激活被拒绝（%d）" % status)
    ok("激活成功，有效期至 %s"
       % time.strftime("%Y-%m-%d %H:%M（北京）",
                       time.localtime(resp["expires"])))

    print("[4/9] 准备安装包")
    local_payload = args.local_payload
    if local_payload and not os.path.isabs(local_payload):
        local_payload = os.path.join(os.path.dirname(os.path.abspath(__file__)),
                                     local_payload)
    if os.path.exists(local_payload):
        with open(local_payload, "rb") as fh:
            plain = fh.read()
        ok("使用随包载荷 %.1f KB（不占用服务器带宽）" % (len(plain) / 1024.0))
    else:
        dl = resp["download"]
        url = "%s/download?card=%s&nonce=%s" % (
            args.server.rstrip("/"), urllib.parse.quote(dl["card"]),
            urllib.parse.quote(dl["nonce"]))
        blob = http_bytes(url)
        ok("已下载 %.1f KB" % (len(blob) / 1024.0))
        print("[5/9] 校验并安装 %s" % PRODUCT)
        key = base64.urlsafe_b64decode(
            resp["package_key"] + "=" * (-len(resp["package_key"]) % 4))
        plain = unseal_blob(blob, key, dl["nonce"].encode())
        ok("内容校验通过")

    if local_payload and os.path.exists(local_payload):
        with zipfile.ZipFile(io.BytesIO(plain)) as zf:
            if "run.py" not in zf.namelist():
                die("随包载荷不完整（缺少 run.py），请重新解压完整压缩包")
        ok("载荷完整性检查通过")

    staging = tempfile.mkdtemp(prefix="deepseek-harness-install-")
    try:
        safe_extract(plain, staging)
        if os.path.isdir(target):
            if is_dsh_install(target):
                shutil.rmtree(target, ignore_errors=True)
                info("已移除旧版本（重新安装会重新签发授权）")
            else:
                backup = target + ".bak-%d" % int(time.time())
                shutil.move(target, backup)
                info("目标目录不是 DeepSeek Harness，已备份到 %s" % backup)
        os.makedirs(os.path.dirname(target) or ".", exist_ok=True)
        shutil.move(staging, target)
        staging = None
        ok("已安装到 %s" % target)

        cfg = load_payload_config(target)
        print("[6/9] 自检")
        passed, detail = self_check(target, cfg)
        if not passed:
            shutil.rmtree(target, ignore_errors=True)
            die("%s（已回滚）" % detail)
        ok("自检通过：%s" % (detail if isinstance(detail, str) else detail[0]))

        with open(os.path.join(target, "license.json"), "w",
                  encoding="utf-8") as fh:
            json.dump({"product": PRODUCT_FULL, "card": card,
                       "token": resp["token"], "expires": resp["expires"],
                       "fingerprint": fp, "server": args.server,
                       "activated_at": int(time.time())},
                      fh, ensure_ascii=False, indent=2)
        try:
            with open(os.path.join(target, "WATERMARK.txt"), "w",
                      encoding="utf-8") as fh:
                fh.write("card=%s\nwatermark=%s\ninstalled_at=%s\n"
                         % (card, resp.get("watermark") or "",
                            time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())))
        except Exception:
            pass
        with open(os.path.join(target, MARKER), "w", encoding="utf-8") as fh:
            json.dump({"product": PRODUCT, "installer": INSTALLER_VERSION,
                       "installed_at": int(time.time()), "card": card,
                       "entry": cfg.get("entry", "run.py"),
                       "shortcut_name": SHORTCUT_NAME},
                      fh, ensure_ascii=False, indent=2)
        ok("授权信息与安装标记已写入")

        print("[7/9] 预装运行时组件（约 1 GB，2-4 分钟，只需一次）")
        install_portable_python(target, cfg)
        prepared = False
        entry = os.path.join(target, cfg.get("entry") or "run.py")
        if platform.system() != "Windows":
            info("非 Windows 平台，跳过组件预装")
        elif os.path.exists(entry):
            try:
                code = subprocess.call([sys.executable, entry, "--prepare"],
                                       cwd=target)
                prepared = (code == 0)
                if prepared:
                    ok("组件已预装，之后双击快捷方式即可秒开")
                else:
                    warn("预装未完成（返回 %d），首次启动会再试一次" % code)
            except Exception as exc:
                warn("预装无法执行：%s" % exc)
        else:
            warn("载荷缺少入口 run.py，跳过预装")

        print("[8/9] 创建快捷方式")
        if cfg.get("setup"):
            hook = os.path.join(target, str(cfg["setup"]))
            if os.path.exists(hook):
                info("运行载荷自带的安装步骤：%s" % cfg["setup"])
                try:
                    out = subprocess.run([sys.executable, hook], cwd=target,
                                         capture_output=True, text=True,
                                         timeout=600)
                    if out.returncode == 0:
                        ok("载荷安装步骤完成")
                    else:
                        lines = (out.stderr or out.stdout).strip().splitlines()
                        warn("载荷安装步骤返回 %d：%s"
                             % (out.returncode, lines[-1] if lines else ""))
                except Exception as exc:
                    warn("载荷安装步骤失败：%s" % exc)

        made = []
        policy = str(cfg.get("shortcut", "installer"))
        if args.no_shortcut:
            info("已按参数跳过快捷方式")
        elif policy == "none":
            info("载荷声明不创建快捷方式，跳过")
        elif policy == "self":
            info("载荷自行创建快捷方式，安装器不介入")
        else:
            try:
                made = make_shortcut(target, cfg)
            except Exception as exc:
                made = []
                warn("快捷方式创建出错（安装已完成，不影响使用）：%s" % exc)
            if made:
                for pth in made:
                    ok("已创建：%s" % pth)
            else:
                warn("未创建快捷方式")
        uninst = write_uninstaller(target)
        ok("独立卸载器：%s" % uninst)
        if register_uninstall_entry(target, cfg):
            ok("已登记到「设置 → 应用」，可从系统卸载")
        stop_script = write_stop_script(target)
        if stop_script:
            info("停止服务：%s" % stop_script)
        info("卸载命令：python \"%s\"  （或 python installer.py --uninstall）"
             % uninst)

    finally:
        if staging and os.path.isdir(staging):
            shutil.rmtree(staging, ignore_errors=True)

    print("")
    print("[9/9] 首次启动自检（等同于双击快捷方式）")
    launched = False
    if platform.system() == "Windows" and not args.no_launch:
        entry = os.path.join(target, cfg.get("entry") or "run.py")
        runner = python_launcher(cfg.get("window", "gui"), target)
        if os.path.exists(entry):
            try:
                subprocess.Popen([runner, entry], cwd=target,
                                 stdin=subprocess.DEVNULL,
                                 stdout=subprocess.DEVNULL,
                                 stderr=subprocess.DEVNULL)
                launched = True
            except Exception as exc:
                warn("启动失败：%s" % exc)
    if launched:
        ok_port = False
        for _ in range(90):
            try:
                with urllib.request.urlopen("http://127.0.0.1:3080/", timeout=1):
                    ok_port = True
                    break
            except urllib.error.HTTPError:
                ok_port = True
                break
            except Exception:
                time.sleep(1)
        if ok_port:
            ok("服务已启动，浏览器应已打开 http://127.0.0.1:3080")
        else:
            warn("服务 90 秒内未就绪，请把 %s 发回"
                 % os.path.join(os.environ.get("LOCALAPPDATA", ""),
                                "DeepSeekHarness", "launch.log"))
    elif platform.system() == "Windows":
        info("已按参数跳过启动自检")

    print("")
    print("完成。安装目录：%s" % target)
    if made:
        print("启动方式：桌面上的 %s 快捷方式" % PRODUCT_FULL)
    else:
        print("启动方式：python \"%s\"" % os.path.join(target, "run.py"))


def self_check(target, cfg=None):
    cfg = cfg or {}
    entry = os.path.join(target, cfg.get("entry") or "run.py")
    if not os.path.exists(entry):
        return False, "安装包缺少入口 run.py"
    try:
        out = subprocess.run([sys.executable, entry, "--version"],
                             capture_output=True, text=True, timeout=60,
                             cwd=target)
    except Exception as exc:
        return False, "自检无法执行：%s" % exc
    if out.returncode != 0:
        detail = (out.stderr or out.stdout).strip().splitlines()
        return False, "自检未通过：%s" % (detail[-1] if detail else "未知错误")
    lines = (out.stdout or "").strip().splitlines()
    return True, lines[0] if lines else "通过"


if __name__ == "__main__":
    main()
