diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..3f56a82 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,22 @@ +# 构建上下文瘦身 + 秘密不入镜像(roadmap 约定:.env / data/ 永不入镜像) +.git +.github +.venv +.env +data +tests +docs +scripts +__pycache__ +**/__pycache__ +.pytest_cache +.ruff_cache +.mypy_cache +captcha_node/node_modules +*.md +Dockerfile +docker-compose.yml +.dockerignore +start.bat +start.sh +uv.lock diff --git a/.gitattributes b/.gitattributes index 6313b56..a2237bc 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,4 @@ * text=auto eol=lf + +*.bat text eol=crlf +*.cmd text eol=crlf diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a791631..9c133fc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,12 +2,16 @@ name: ci on: push: - branches: [main] + branches: [main, develop, "feat/**"] pull_request: jobs: test: - runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest] + runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 diff --git a/.gitignore b/.gitignore index 0d237a6..9279c51 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,12 @@ node_modules/ # 秘钥 / 凭证导出 *accounts*.json zcode-accounts*.json + +# 本地运行 / 工具杂物 +serve.log +serve.err.log +serve.pid +.trae_* +.sg_* +.sg-js/ +.sg_login.html diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..63d47cf --- /dev/null +++ b/Dockerfile @@ -0,0 +1,70 @@ +# zcode-hub 容器镜像:Python 3.11 网关 + Node 22 验证码求解子进程 + Debian Chromium +# +# 运行时结构: +# /app 代码(app/ cli.py frontend/ captcha_node/) +# /app/data 持久化(accounts.db + device_mid),挂载卷 +# /usr/bin/node 从 node:22 官方镜像复制(bookworm 与运行时基础镜像同系,glibc 兼容) +# /usr/bin/chromium Debian chromium,solver_pw.js findChromium() 默认探测路径之一 +# +# captcha 求解依赖真浏览器(solver 已带 --no-sandbox/--disable-dev-shm-usage), +# 且 oom 看门狗需要写 /proc/*/oom_score_adj —— 容器以 root 运行是前提,勿改 USER。 + +# ── 阶段 1:求解器 Node 依赖(纯 JS,无原生编译) ───────────────────────────── +FROM node:22-bookworm-slim AS captcha-deps +WORKDIR /build +COPY captcha_node/package.json captcha_node/package-lock.json ./ +RUN npm ci --omit=dev + +# ── 阶段 2:运行时 ──────────────────────────────────────────────────────────── +FROM python:3.11-slim-bookworm + +ENV PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + PIP_NO_CACHE_DIR=1 \ + ZCODE_HOST=0.0.0.0 \ + ZCODE_PORT=3000 \ + ZCODE_DATA_DIR=/app/data \ + ZCODE_NODE_PATH=/usr/local/bin/node \ + ZCODE_CHROMIUM_PATH=/usr/bin/chromium + +# tini 作 PID 1:回收 uvicorn→node→chrome 进程树遗留的僵尸 +# fonts-noto-cjk:验证码挑战页含中文文案,缺字形可能影响求解 +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + chromium \ + fonts-noto-cjk \ + fonts-liberation \ + ca-certificates \ + tini \ + && rm -rf /var/lib/apt/lists/* + +# Node 运行时(构建期不需要 npm,只带 node 本体) +COPY --from=captcha-deps /usr/local/bin/node /usr/local/bin/node + +WORKDIR /app + +# 运行时依赖(与 requirements.txt 上半段保持一致;dev/test 工具不进镜像) +RUN pip install --no-cache-dir \ + "fastapi>=0.115.0" \ + "uvicorn[standard]>=0.30.0" \ + "httpx>=0.27.0" \ + "python-dotenv>=1.0.1" \ + "cryptography>=42.0.0" \ + "pyyaml>=6.0.0" + +COPY app/ app/ +COPY captcha_node/ captcha_node/ +COPY --from=captcha-deps /build/node_modules/ captcha_node/node_modules/ +COPY frontend/ frontend/ +COPY cli.py ./ + +# data/ 不打进镜像(.dockerignore 双保险),挂卷持久化 +RUN mkdir -p /app/data + +EXPOSE 3000 + +ENTRYPOINT ["/usr/bin/tini", "--"] +CMD ["python", "cli.py", "serve"] + +HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \ + CMD ["python", "-c", "import urllib.request;urllib.request.urlopen('http://127.0.0.1:3000/meta', timeout=4)"] diff --git a/README.md b/README.md index 0d9effa..70f74d0 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,21 @@ cp .env.example .env # 按需修改密钥、端口等 > 使用 Z.AI **JWT 模式**需要 Node.js 求解验证码,首次先执行:`cd captcha_node && npm install`。 +### Docker 部署(推荐) + +镜像自带 Python 3.11 + Node 22 + Chromium,无需宿主机装任何运行时: + +```bash +cp .env.example .env # 按需修改 ZCODE_ADMIN_KEY、端口等 +docker compose up -d --build +``` + +- 访问 `http://127.0.0.1:3000/admin/login`;宿主机端口取 `.env` 的 `ZCODE_PORT` +- 账号库与设备指纹持久化在宿主机 `./data/`(首次启动自动生成,已有数据直接沿用) +- `.env` 里的 `ZCODE_CHROMIUM_PATH` 等宿主机路径会被 compose 覆盖为容器内路径,无需改动 +- 容器内 CLI:`docker compose exec zcode-hub python cli.py accounts`(其余子命令同理) +- 停止:`docker compose down`(数据保留在 `./data/`) + ## 快速上手一个账号 ```bash diff --git a/app/agent.py b/app/agent.py index f189963..d36e0a5 100644 --- a/app/agent.py +++ b/app/agent.py @@ -75,10 +75,16 @@ def build_request( if provider == "zai": if account.uses_plan_channel() and not force_fallback: + # Plan 通道 intl/cn 同端点(zcode.z.ai,JWT 决定归属) target_url = settings.UPSTREAM["zai"] auth = {"Authorization": f"Bearer {account.jwt_token}"} elif account.api_key: - target_url = settings.UPSTREAM["zai_fallback"] + # 回退 Key 的域随账号区域:intl → api.z.ai;cn(bigmodel OAuth + # 兑换的 Key)→ open.bigmodel.cn + if account.realm == "cn": + target_url = settings.UPSTREAM["bigmodel"] + else: + target_url = settings.UPSTREAM["zai_fallback"] auth = {"x-api-key": account.api_key} else: raise RuntimeError("账号缺少有效凭证") diff --git a/app/constants.py b/app/constants.py index 53104f5..cf8e8ea 100644 --- a/app/constants.py +++ b/app/constants.py @@ -41,6 +41,23 @@ OAUTH_CLI_INIT_PATH = "/api/v1/oauth/cli/init" OAUTH_CLI_POLL_PATH = "/api/v1/oauth/cli/poll" # + /{flow_id} +# OAuth 双区域(对齐 codeapi providers/zcode/realms.py,官方客户端 3.14.x 实证): +# CLI init/poll 端点 intl/cn 共用,provider 参数与业务域随区域—— +# intl → provider=zai,授权页 chat.z.ai;poll 的 data.zai.access_token 是 +# OAuth 会话凭证,需经 api.z.ai/api/auth/z/login 换业务 token; +# cn → provider=bigmodel,授权页 bigmodel.cn/login(服务端回调); +# poll 的 data.bigmodel.access_token 本身就是业务 token(官方客户端 +# BigModel adapter 不做二次换取)。 +# 两个区域的 data.token 都是 zcodejwttoken(Plan 通道 Bearer,JWT 决定归属, +# messages / billing / claim 端点 intl/cn 完全相同,无需分域)。 +BIGMODEL_BUSINESS_ORIGIN = "https://bigmodel.cn" +OAUTH_REALMS = { + "intl": {"oauth_provider": "zai", "business_origin": ZAI_API_ORIGIN}, + "cn": {"oauth_provider": "bigmodel", "business_origin": BIGMODEL_BUSINESS_ORIGIN}, +} +# poll 响应里业务 token 块的块名随区域 +OAUTH_PROVIDER_BLOCKS = {"intl": "zai", "cn": "bigmodel"} + # ── 客户端版本(单一真相源:官方客户端 3.14.4,2026-10-02 跟进)──────────────── # 客户端 claim 头实测缺版本/平台头 → 上游 3007;client/configs 带 platform 参数 → 3001 # 出处:本机官方 ZCode.exe(注册表 DisplayVersion=3.14.4,2026-10-02 实测; diff --git a/app/models.py b/app/models.py index 36b10c0..5f32793 100644 --- a/app/models.py +++ b/app/models.py @@ -7,6 +7,9 @@ from dataclasses import asdict, dataclass, field PROVIDERS = ("zai", "bigmodel") +# 账号区域:intl=国际版(z.ai 授权,业务域 api.z.ai);cn=国内版(bigmodel.cn 授权, +# 业务域 bigmodel.cn)。Plan 通道(messages/billing/claim)两区域同端点,JWT 决定归属 +REALMS = ("intl", "cn") class Status: @@ -33,6 +36,7 @@ class Account: name: str provider: str mode: str # "jwt" | "apiKey" + realm: str = "intl" # 账号区域(models.REALMS):intl=国际版 / cn=国内版 jwt_token: str | None = None api_key: str | None = None enabled: bool = True @@ -61,14 +65,18 @@ class Account: installed_at: float | None = None # 按账号安装序完成时间;None = 未安装 @staticmethod - def create(provider: str, name: str, secret: str) -> Account: + def create(provider: str, name: str, secret: str, realm: str = "") -> Account: secret = (secret or "").strip() is_jwt = secret.count(".") == 2 and provider == "zai" + # 区域缺省/非法值时自动归区:bigmodel 提供商即国内域 Key,其余国际版 + if realm not in REALMS: + realm = "cn" if provider == "bigmodel" else "intl" return Account( id=_account_id(name), name=name or f"{provider}-account", provider=provider, mode="jwt" if is_jwt else "apiKey", + realm=realm, jwt_token=secret if is_jwt else None, api_key=None if is_jwt else secret, ) @@ -192,6 +200,7 @@ def public_view(self) -> dict: "id": self.id, "name": self.name, "provider": self.provider, + "realm": self.realm, "mode": self.mode, "token_masked": masked, "enabled": self.enabled, diff --git a/app/oauth.py b/app/oauth.py index eab588b..b09223a 100644 --- a/app/oauth.py +++ b/app/oauth.py @@ -1,6 +1,16 @@ -"""Z.AI OAuth 登录流程。 - -主要供 CLI `login zai` 使用:发起 OAuth → 轮询 → 兑换 API Key。 +"""Z.AI / BigModel(国内版)OAuth 登录流程。 + +主要供 CLI `login zai` 与后台「授权登录」使用:发起 OAuth → 轮询 → 兑换 API Key。 + +双区域(constants.OAUTH_REALMS,对齐官方客户端 3.14.x 实证): +- intl(zai):init `{"provider":"zai"}`,授权页 chat.z.ai;poll 的 + `data.zai.access_token` 是 OAuth 会话凭证,需经 `api.z.ai/api/auth/z/login` + 换业务 token 后再走机构/项目/Key 兑换链(业务域 api.z.ai); +- cn(bigmodel):init `{"provider":"bigmodel"}`,授权页 bigmodel.cn/login + (服务端回调);poll 的 `data.bigmodel.access_token` 本身就是业务 token + (官方客户端 BigModel adapter 不做二次换取),业务链在 bigmodel.cn 域。 +两个区域的 `data.token` 都是 zcodejwttoken(Plan 通道 Bearer),messages / +billing / claim 端点 intl/cn 完全相同,无需分域。 """ from __future__ import annotations @@ -9,11 +19,16 @@ import httpx -from . import settings +from . import constants, settings + +REALM_INTL = "intl" +REALM_CN = "cn" class ZaiAuthFlow: - """api_base / exchange_origin 可注入(测试指向 Mock 上游); + """一次 OAuth CLI 登录会话;realm 决定 provider 参数、业务 token 块与业务域。 + + api_base / exchange_origin 可注入(测试指向 Mock 上游); 默认值来自 settings(其缺省又来自 constants 收口)。 官方 CLI 规范(对齐 zcode.cjs createZaiCliOAuthClient): @@ -26,9 +41,19 @@ class ZaiAuthFlow: (保留旧协议兼容)。 """ - def __init__(self, api_base: str | None = None, exchange_origin: str | None = None) -> None: + def __init__(self, api_base: str | None = None, exchange_origin: str | None = None, + realm: str = REALM_INTL) -> None: + realm = str(realm or REALM_INTL).strip().lower() + self.realm = realm if realm in constants.OAUTH_REALMS else REALM_INTL + self.oauth_provider = constants.OAUTH_REALMS[self.realm]["oauth_provider"] self.api_base = api_base or settings.OAUTH_API_BASE - self.exchange_origin = exchange_origin or settings.ZAI_EXCHANGE_ORIGIN + if exchange_origin: + self.exchange_origin = exchange_origin + else: + self.exchange_origin = ( + settings.BIGMODEL_EXCHANGE_ORIGIN if self.realm == REALM_CN + else settings.ZAI_EXCHANGE_ORIGIN + ) self.poll_token = secrets.token_hex(32) async def init(self) -> tuple[str, str]: @@ -39,7 +64,7 @@ async def init(self) -> tuple[str, str]: "Authorization": f"Bearer {self.poll_token}", "Content-Type": "application/json", }, - json={"provider": "zai"}, + json={"provider": self.oauth_provider}, ) res.raise_for_status() body = res.json() @@ -64,19 +89,39 @@ async def poll(self, flow_id: str) -> dict: res.raise_for_status() return res.json().get("data") or {} + def business_access_token(self, data: dict) -> str: + """从 poll 结果提取业务 access_token;块名随区域(intl=data.zai,cn=data.bigmodel)。""" + block = data.get(constants.OAUTH_PROVIDER_BLOCKS[self.realm]) + if isinstance(block, dict): + return str(block.get("access_token") or "").strip() + return "" + + async def _resolve_business_token(self, client: httpx.AsyncClient, access_token: str) -> str: + """OAuth access_token → 业务 token。 + + cn(bigmodel):poll 下发的 access_token 本身就是业务 token,直接使用; + intl(zai):需经 exchange_origin 的 /api/auth/z/login 换业务 token。 + """ + if self.realm == REALM_CN: + return access_token + login = await client.post( + f"{self.exchange_origin}/api/auth/z/login", + headers={"Content-Type": "application/json"}, + json={"token": access_token}, + ) + login.raise_for_status() + biz = (login.json().get("data") or {}) + biz_token = biz.get("access_token") or biz.get("accessToken") + if not biz_token: + raise RuntimeError("返回数据中不含业务凭证") + return biz_token + async def exchange_api_key(self, access_token: str) -> str: - """OAuth access_token → 业务 token → 机构/项目 → API Key。""" + """OAuth access_token → 业务 token → 机构/项目 → API Key(业务域随区域)。""" + if not access_token: + raise RuntimeError("缺少 access_token,无法兑换 API Key") async with httpx.AsyncClient(timeout=30) as client: - login = await client.post( - f"{self.exchange_origin}/api/auth/z/login", - headers={"Content-Type": "application/json"}, - json={"token": access_token}, - ) - login.raise_for_status() - biz = (login.json().get("data") or {}) - biz_token = biz.get("access_token") or biz.get("accessToken") - if not biz_token: - raise RuntimeError("返回数据中不含业务凭证") + biz_token = await self._resolve_business_token(client, access_token) info = await client.get( f"{self.exchange_origin}/api/biz/customer/getCustomerInfo", diff --git a/app/openai_compat.py b/app/openai_compat.py index 87aa012..5789ce2 100644 --- a/app/openai_compat.py +++ b/app/openai_compat.py @@ -106,6 +106,11 @@ def openai_to_anthropic(payload: dict) -> tuple[dict | None, str | None]: }) elif role == "assistant": blocks: list[dict] = [] + # 多轮重放:reasoning_content(历史轮思考文本)→ Anthropic thinking 块。 + # 上游对 thinking 块 signature 不校验(实测伪造值可通过),空串亦可。 + rc = msg.get("reasoning_content") or msg.get("reasoning") or msg.get("reasoning_text") + if isinstance(rc, str) and rc.strip(): + blocks.append({"type": "thinking", "thinking": rc, "signature": ""}) text = _text_from_content(content) if text: blocks.append({"type": "text", "text": text}) @@ -148,6 +153,12 @@ def openai_to_anthropic(payload: dict) -> tuple[dict | None, str | None]: body["top_p"] = float(payload["top_p"]) except (TypeError, ValueError): pass + # 推理强度:上游 GLM-5.3 系原生接受 reasoning_effort(low/high/max)。 + # 其余值(含 off/none)不带该字段——上游不支持关闭思考,交给默认档。 + effort = payload.get("reasoning_effort") + if isinstance(effort, str) and effort.strip().lower() in ("low", "high", "max"): + body["reasoning_effort"] = effort.strip().lower() + stop = payload.get("stop") if isinstance(stop, str) and stop: body["stop_sequences"] = [stop] @@ -186,11 +197,14 @@ def openai_to_anthropic(payload: dict) -> tuple[dict | None, str | None]: def anthropic_to_openai(data: dict, model: str) -> dict: """Anthropic message 响应 → OpenAI chat.completion。""" text_parts: list[str] = [] + thinking_parts: list[str] = [] tool_calls: list[dict] = [] for block in data.get("content") or []: if not isinstance(block, dict): continue - if block.get("type") == "text" and isinstance(block.get("text"), str): + if block.get("type") == "thinking" and isinstance(block.get("thinking"), str): + thinking_parts.append(block["thinking"]) + elif block.get("type") == "text" and isinstance(block.get("text"), str): text_parts.append(block["text"]) elif block.get("type") == "tool_use": tool_calls.append({ @@ -205,6 +219,10 @@ def anthropic_to_openai(data: dict, model: str) -> dict: in_tok = _as_int(usage.get("input_tokens")) or 0 out_tok = _as_int(usage.get("output_tokens")) or 0 message: dict = {"role": "assistant", "content": "".join(text_parts) or None} + # OpenAI 兼容侧通用约定:思考文本放 reasoning_content(pi-ai/DeepSeek 风格均可识别) + reasoning = "\n".join(p for p in thinking_parts if p) + if reasoning: + message["reasoning_content"] = reasoning if tool_calls: message["tool_calls"] = tool_calls return { @@ -261,6 +279,8 @@ def feed(self, evt: dict) -> list[str]: return [] if etype == "content_block_start": block = evt.get("content_block") or {} + if block.get("type") == "thinking": + return [self._chunk({"reasoning_content": ""})] if block.get("type") == "tool_use": idx = self._tool_seq self._tool_seq += 1 @@ -273,6 +293,8 @@ def feed(self, evt: dict) -> list[str]: return [] if etype == "content_block_delta": delta = evt.get("delta") or {} + if delta.get("type") == "thinking_delta" and isinstance(delta.get("thinking"), str): + return [self._chunk({"reasoning_content": delta["thinking"]})] if delta.get("type") == "text_delta" and isinstance(delta.get("text"), str): return [self._chunk({"content": delta["text"]})] if delta.get("type") == "input_json_delta" and isinstance(delta.get("partial_json"), str): diff --git a/app/routes/admin_api.py b/app/routes/admin_api.py index 0299316..316222c 100644 --- a/app/routes/admin_api.py +++ b/app/routes/admin_api.py @@ -21,8 +21,8 @@ report_activation_events, ) from ..claim import claim as do_claim -from ..models import PROVIDERS, Status -from ..oauth import ZaiAuthFlow +from ..models import PROVIDERS, REALMS, Status +from ..oauth import REALM_INTL, ZaiAuthFlow from ..quota import fetch_quota, refresh_accounts from ..store import store @@ -68,8 +68,11 @@ async def status_info(): @router.post("/accounts") async def add_accounts(payload: dict = Body(...)): provider = payload.get("provider", "zai") + realm = str(payload.get("realm") or "").strip().lower() if provider not in PROVIDERS: raise HTTPException(400, "不支持的 provider") + if realm and realm not in REALMS: + raise HTTPException(400, "realm 必须是 intl 或 cn") tokens = payload.get("tokens") or [] if isinstance(tokens, str): tokens = [t.strip() for t in tokens.splitlines() if t.strip()] @@ -77,11 +80,18 @@ async def add_accounts(payload: dict = Body(...)): if not tokens: raise HTTPException(400, "请输入至少一个 Token / API Key") + # 纠偏:bigmodel 提供商通道只接受 open.bigmodel.cn 的 API Key(x-api-key); + # 两段点的 zcodejwttoken 在该通道永远不可用,自动归位为 zai + 国内版 + migrated = [t for t in tokens if provider == "bigmodel" and t.count(".") == 2] + if migrated: + provider, realm = "zai", "cn" + logs.info("accounts", f"检测到 {len(migrated)} 个 JWT 凭证,已从 bigmodel 归位为 zai/cn") + added = [] existing = {a.id for a in store.list_accounts(provider)} # 识别真新增(重复 token 跳过) for tok in dict.fromkeys(tokens): # 去重保序 name = payload.get("name") or f"{provider}-{len(store.list_accounts(provider)) + 1}" - acc = store.add_account(provider, name, tok) + acc = store.add_account(provider, name, tok, realm=realm) added.append(acc.id) # 立即刷新一次额度(仅 zai jwt) fresh = [a for a in store.list_accounts(provider) if a.id in added and a.mode == "jwt"] @@ -225,24 +235,30 @@ def _login_gc() -> None: @router.post("/login/start") async def login_start(payload: dict = Body(default=None)): - """发起 Z.AI OAuth,返回授权链接供前端展示。 + """发起 OAuth 授权,返回授权链接供前端展示。 - payload 可选 {"label": "acct-1"} —— 作为账号名前缀入池,便于多号识别。 + payload 可选 {"label": "acct-1", "realm": "intl"|"cn"} —— label 作为账号名 + 前缀入池,便于多号识别;realm 决定授权页与业务域(intl=z.ai 国际版, + cn=bigmodel.cn 国内版),缺省 intl。 """ payload = payload or {} label = (payload.get("label") or "").strip()[:32] + realm = str(payload.get("realm") or REALM_INTL).strip().lower() + if realm not in REALMS: + raise HTTPException(400, "realm 必须是 intl 或 cn") _login_gc() - flow = ZaiAuthFlow() + flow = ZaiAuthFlow(realm=realm) try: flow_id, authorize_url = await flow.init() except Exception as err: # noqa: BLE001 - logs.warn("oauth", f"登录初始化失败: {type(err).__name__}: {err}") + logs.warn("oauth", f"登录初始化失败 realm={realm}: {type(err).__name__}: {err}") raise HTTPException(502, f"登录初始化失败: {err}") from err _login_flows[flow_id] = {"flow": flow, "created": time.time(), "label": label} - logs.info("oauth", f"发起登录 flow_id={flow_id} label={label or '-'}") + logs.info("oauth", f"发起登录 realm={realm} flow_id={flow_id} label={label or '-'}") return { "flow_id": flow_id, "authorize_url": authorize_url, + "realm": realm, "expires_in": int(LOGIN_FLOW_TTL), } @@ -297,17 +313,19 @@ async def login_poll(flow_id: str): # JWT 先入池并立刻 ready;兑换 API Key / 额度刷新改后台,避免卡住前端下一轮 poll。 zcode_jwt = data.get("token") - access_token = (data.get("zai") or {}).get("access_token") + # 业务 token 块随区域:intl=data.zai,cn=data.bigmodel(flow 已带 realm) + access_token = flow.business_access_token(data) + realm = getattr(flow, "realm", REALM_INTL) label = entry.get("label") or "oauth-login" account = None if zcode_jwt: - account = store.add_account("zai", label, zcode_jwt) + account = store.add_account("zai", label, zcode_jwt, realm=realm) elif access_token: try: api_key = await asyncio.wait_for( flow.exchange_api_key(access_token), timeout=LOGIN_EXCHANGE_TIMEOUT ) - account = store.add_account("zai", label, api_key) + account = store.add_account("zai", label, api_key, realm=realm) except Exception as err: # noqa: BLE001 logs.warn("oauth", f"无 JWT 时兑换 API Key 失败: {err}") diff --git a/app/settings.py b/app/settings.py index 67f0ab5..c5bcd10 100644 --- a/app/settings.py +++ b/app/settings.py @@ -138,6 +138,9 @@ def _bool(env_name: str, default: bool) -> bool: # OAuth 与兑换链 origin(测试时指向 Mock 上游) OAUTH_API_BASE = os.getenv("ZCODE_OAUTH_API_BASE", constants.ZCODE_ORIGIN + "/api/v1") ZAI_EXCHANGE_ORIGIN = os.getenv("ZCODE_EXCHANGE_ORIGIN", constants.ZAI_API_ORIGIN) +BIGMODEL_EXCHANGE_ORIGIN = os.getenv( + "ZCODE_BIGMODEL_EXCHANGE_ORIGIN", constants.BIGMODEL_BUSINESS_ORIGIN +) USER_AGENT = os.getenv("UPSTREAM_USER_AGENT", constants.USER_AGENT) APP_VERSION = "2.6.8" diff --git a/app/store.py b/app/store.py index d9d6f74..8a379a7 100644 --- a/app/store.py +++ b/app/store.py @@ -205,10 +205,10 @@ def _find_locked(self, provider: str, id_or_name: str) -> Account | None: return None # ── 账号增删改 ─────────────────────────────────────────────────────────── - def add_account(self, provider: str, name: str, secret: str) -> Account: + def add_account(self, provider: str, name: str, secret: str, realm: str = "") -> Account: if provider not in PROVIDERS: raise ValueError(f"不支持的 provider: {provider}") - account = Account.create(provider, name, secret) + account = Account.create(provider, name, secret, realm=realm) with self._lock: for a in self._accounts[provider]: if a.secret and a.secret == account.secret: @@ -294,7 +294,8 @@ def export(self) -> dict: "exported_at": time.time(), "providers": { p: [ - {"name": a.name, "mode": a.mode, "secret": a.secret} + {"name": a.name, "mode": a.mode, "secret": a.secret, + "realm": a.realm} for a in self._accounts[p] ] for p in PROVIDERS @@ -311,7 +312,8 @@ def import_accounts(self, payload: dict) -> int: secret = it.get("secret") or it.get("token") or it.get("jwtToken") or it.get("apiKey") if not secret: continue - self.add_account(provider, it.get("name", provider), secret) + self.add_account(provider, it.get("name", provider), secret, + realm=str(it.get("realm") or "")) count += 1 return count diff --git a/captcha_node/package-lock.json b/captcha_node/package-lock.json index 5c71a08..4516427 100644 --- a/captcha_node/package-lock.json +++ b/captcha_node/package-lock.json @@ -1,16 +1,45 @@ { "name": "zcode2api-captcha-solver", - "version": "2.0.0", + "version": "2.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "zcode2api-captcha-solver", - "version": "2.0.0", + "version": "2.1.0", "dependencies": { - "happy-dom": "^20.14.0" + "happy-dom": "^20.14.0", + "puppeteer-core": "^23.0.0" } }, + "node_modules/@puppeteer/browsers": { + "version": "2.6.1", + "resolved": "https://registry.npmmirror.com/@puppeteer/browsers/-/browsers-2.6.1.tgz", + "integrity": "sha512-aBSREisdsGH890S2rQqK82qmQYU3uFpSH8wcZWHgHzl3LfzsxAKbLNiAG9mO8v1Y0UICBeClICxPJvyr0rcuxg==", + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.4.0", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.5.0", + "semver": "^7.6.3", + "tar-fs": "^3.0.6", + "unbzip2-stream": "^1.4.3", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmmirror.com/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "license": "MIT" + }, "node_modules/@types/node": { "version": "26.4.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-26.4.1.tgz", @@ -35,6 +64,217 @@ "@types/node": "*" } }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmmirror.com/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmmirror.com/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/b4a": { + "version": "1.9.0", + "resolved": "https://registry.npmmirror.com/b4a/-/b4a-1.9.0.tgz", + "integrity": "sha512-dpfcF9fDNR6++cthXR67iyhgqWy9CBouAvIWhIntzBG6cvK/cnIPiZQjBwi/ZqjjBEDGfoNDtmB0kTjroOJ3pQ==", + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "node_modules/bare-events": { + "version": "2.9.2", + "resolved": "https://registry.npmmirror.com/bare-events/-/bare-events-2.9.2.tgz", + "integrity": "sha512-AIPKioV7/Y/8KfZ3AAhjPJxLLbY49S64Ym5DakZlUg75qQiTgUq9hEJoEwa4eUezPUlXRy/i5NpsKvo9jgKmoA==", + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/bare-fs": { + "version": "4.8.2", + "resolved": "https://registry.npmmirror.com/bare-fs/-/bare-fs-4.8.2.tgz", + "integrity": "sha512-+ZI68KHMUvosXfKbg/UOHK0tbCdRnegbvPEdEcZ3Nd6TetieQsJPRXBRXPdLyy8+3VSEbPXtsumTpEtt78xv9w==", + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + }, + "engines": { + "bare": ">=1.28.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-path": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/bare-path/-/bare-path-3.1.2.tgz", + "integrity": "sha512-ZyKbsuuqK6Ag0K8pX6V5Txq6XeJRvY+wXucnFGRjiyVYP9YWDpIQugk/b+enRYrEYBJaqLzghRQpXPMR7341Nw==", + "license": "Apache-2.0" + }, + "node_modules/bare-stream": { + "version": "2.13.4", + "resolved": "https://registry.npmmirror.com/bare-stream/-/bare-stream-2.13.4.tgz", + "integrity": "sha512-PcrQ8lVLbiJscNm1Kez+Yp4Gy4AHGcN1lzwjvf5NybWen7VvEgUfyfnXYJ2zNqWnzOfCb1Abq6lH8ti0syQszA==", + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.8.1", + "streamx": "^2.25.0", + "teex": "^1.0.1" + }, + "peerDependencies": { + "bare-abort-controller": "*", + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + }, + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/bare-url": { + "version": "2.5.4", + "resolved": "https://registry.npmmirror.com/bare-url/-/bare-url-2.5.4.tgz", + "integrity": "sha512-Gxa7UVWBr0/edU1b+TJhn/AZvMQUj9OGspvYsaTYQrAbZA4BOTZGL3LiZxvD+CeMlDH4juwD84+eTAp/bLYW5g==", + "license": "Apache-2.0", + "dependencies": { + "bare-path": "^3.0.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmmirror.com/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/basic-ftp": { + "version": "5.3.1", + "resolved": "https://registry.npmmirror.com/basic-ftp/-/basic-ftp-5.3.1.tgz", + "integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmmirror.com/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmmirror.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/buffer-image-size": { "version": "0.6.4", "resolved": "https://registry.npmjs.org/buffer-image-size/-/buffer-image-size-0.6.4.tgz", @@ -47,6 +287,255 @@ "node": ">=4.0" } }, + "node_modules/chromium-bidi": { + "version": "0.11.0", + "resolved": "https://registry.npmmirror.com/chromium-bidi/-/chromium-bidi-0.11.0.tgz", + "integrity": "sha512-6CJWHkNRoyZyjV9Rwv2lYONZf1Xm0IuDyNq97nwSsxxP3wf5Bwy15K5rOvVKMtJ127jJBmxFUanSAOjgFRxgrA==", + "license": "Apache-2.0", + "dependencies": { + "mitt": "3.0.1", + "zod": "3.23.8" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmmirror.com/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "license": "MIT", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/devtools-protocol": { + "version": "0.0.1367902", + "resolved": "https://registry.npmmirror.com/devtools-protocol/-/devtools-protocol-0.0.1367902.tgz", + "integrity": "sha512-XxtPuC3PGakY6PD7dG66/o8KwJ/LkH2/EKe19Dcw58w53dv4/vSQEkn/SzuyhHE2q4zPgCkxQBxus3VV4ql+Pg==", + "license": "BSD-3-Clause" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmmirror.com/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmmirror.com/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmmirror.com/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "license": "MIT" + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmmirror.com/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-uri": { + "version": "6.0.5", + "resolved": "https://registry.npmmirror.com/get-uri/-/get-uri-6.0.5.tgz", + "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", + "license": "MIT", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/happy-dom": { "version": "20.14.0", "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-20.14.0.tgz", @@ -86,12 +575,415 @@ "node": ">=12" } }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmmirror.com/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ip-address": { + "version": "10.7.2", + "resolved": "https://registry.npmmirror.com/ip-address/-/ip-address-10.7.2.tgz", + "integrity": "sha512-7H/2gFSIitxc0hG3nOI1glS8QLo/EHBFFLk8vEUjXY/xu0AdL8jZ9U1IzO2PUm0d2D/ofQcAifb0g6OBkt8U7w==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/netmask": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/netmask/-/netmask-2.1.1.tgz", + "integrity": "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/pac-proxy-agent": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", + "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", + "license": "MIT", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmmirror.com/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "license": "MIT", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "license": "MIT" + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/proxy-agent": { + "version": "6.5.0", + "resolved": "https://registry.npmmirror.com/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.1.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/puppeteer-core": { + "version": "23.11.1", + "resolved": "https://registry.npmmirror.com/puppeteer-core/-/puppeteer-core-23.11.1.tgz", + "integrity": "sha512-3HZ2/7hdDKZvZQ7dhhITOUg4/wOrDRjyK2ZBllRB0ZCOi9u0cwq1ACHDjBB+nX+7+kltHjQvBRdeY7+W0T+7Gg==", + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.6.1", + "chromium-bidi": "0.11.0", + "debug": "^4.4.0", + "devtools-protocol": "0.0.1367902", + "typed-query-selector": "^2.12.0", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.10", + "resolved": "https://registry.npmmirror.com/socks/-/socks-2.8.10.tgz", + "integrity": "sha512-e0VyvkVTwVYViNovRkZ9aodhxVlyoMn7eJhVUPxZ+eK9P/7CBkxvvsBOHqFPEH416726W8tLXXXjKwqgTErrCQ==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.1.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmmirror.com/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/streamx": { + "version": "2.28.1", + "resolved": "https://registry.npmmirror.com/streamx/-/streamx-2.28.1.tgz", + "integrity": "sha512-zEzXb0s5Cds7tqMH6rhZ05lcJydCWiQPEwiNngVqzsxCc962vLY4Uw+mW7od8kDH258k2Uz/JrOkdIAAhSh9VA==", + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar-fs": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/tar-fs/-/tar-fs-3.1.3.tgz", + "integrity": "sha512-/hU4AXnIdZu+Gvl1pk0oI5f5HxWsCJRtY2aFaJdk9VvyL48DWU6iU5WAIPG+wIi1YvWA6eTJvIviP/tMAZZNwQ==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0" + } + }, + "node_modules/tar-stream": { + "version": "3.2.1", + "resolved": "https://registry.npmmirror.com/tar-stream/-/tar-stream-3.2.1.tgz", + "integrity": "sha512-nqsEO8zLZJvrOMdEwkA0QdCLFbetHMn95Zqu4fKwX+hkaTWJPZZOrxx/PwtxoK0MMGQmBQNRW3CPs8IFYQz4cQ==", + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "bare-fs": "^4.5.5", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" + } + }, + "node_modules/text-decoder": { + "version": "1.2.7", + "resolved": "https://registry.npmmirror.com/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmmirror.com/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typed-query-selector": { + "version": "2.12.3", + "resolved": "https://registry.npmmirror.com/typed-query-selector/-/typed-query-selector-2.12.3.tgz", + "integrity": "sha512-/dtuMZyutHLlrxWhhC1uib00ELUr2dZ8C9B8YzZYsJmeE3V56OxRblrSM2UaF142pcMtak7ehZ6+poWGeehNIg==", + "license": "MIT" + }, + "node_modules/unbzip2-stream": { + "version": "1.4.3", + "resolved": "https://registry.npmmirror.com/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", + "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", + "license": "MIT", + "dependencies": { + "buffer": "^5.2.1", + "through": "^2.3.8" + } + }, "node_modules/undici-types": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", "license": "MIT" }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, "node_modules/ws": { "version": "8.21.0", "resolved": "https://registry.npmmirror.com/ws/-/ws-8.21.0.tgz", @@ -112,6 +1004,61 @@ "optional": true } } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmmirror.com/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmmirror.com/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmmirror.com/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/zod": { + "version": "3.23.8", + "resolved": "https://registry.npmmirror.com/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..fb06b38 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,23 @@ +services: + zcode-hub: + build: . + image: zcode-hub:latest + container_name: zcode-hub + restart: unless-stopped + init: true + ports: + # 宿主机端口取 .env 的 ZCODE_PORT(默认 3000);容器内固定 3000 + - "${ZCODE_PORT:-3000}:3000" + env_file: + - path: .env + required: false + environment: + # 容器侧固定值,覆盖 .env 中的宿主机配置(如 Windows Chrome 路径) + ZCODE_HOST: 0.0.0.0 + ZCODE_PORT: "3000" + ZCODE_DATA_DIR: /app/data + ZCODE_NODE_PATH: /usr/local/bin/node + ZCODE_CHROMIUM_PATH: /usr/bin/chromium + volumes: + # 账号库(accounts.db)与设备指纹(device_mid)持久化;沿用宿主机已有 data/ + - ./data:/app/data diff --git a/frontend/admin/accounts.html b/frontend/admin/accounts.html index 3fd356b..70e3ffa 100644 --- a/frontend/admin/accounts.html +++ b/frontend/admin/accounts.html @@ -77,7 +77,11 @@