@@ -303,8 +324,9 @@
function credCell(a){
const isJwt=a.mode==='jwt';
+ const realm=realmLabel(a.realm||'intl');
return `
${esc(a.name)}${tierBadgeHtml(a)}
-
${esc(a.token_masked)}·${esc(a.provider)} / ${isJwt?'JWT':'API Key'}
`;
+
${esc(a.token_masked)}·${esc(a.provider)} / ${isJwt?'JWT':'API Key'}·${esc(realm||'—')}
`;
}
function statusCell(a){
@@ -439,19 +461,34 @@
}
/* ── Actions ── */
+function realmLabel(r){return r==='cn'?'国内版':(r==='intl'?'国际版':'');}
function openAdd(){
document.getElementById('add-tokens').value='';
switchAddTab('paste');
resetLoginPane();
+ onProviderChange();
openModal('modal-add');
}
+/* 粘贴凭证:bigmodel 提供商只收国内域 API Key,区域选择仅对 zai 有意义 */
+function onProviderChange(){
+ const isZai=document.getElementById('add-provider').value==='zai';
+ document.getElementById('add-realm-field').style.display=isZai?'':'none';
+}
+/* 授权登录:切换区域 = 更换授权出口,未完成的在途链接一并作废重置 */
+function onLoginRealmChange(){
+ const realm=document.getElementById('login-realm').value;
+ document.getElementById('login-help-intl').style.display=realm==='intl'?'':'none';
+ document.getElementById('login-help-cn').style.display=realm==='cn'?'':'none';
+ resetLoginPane();
+}
async function doAdd(){
const tokens=document.getElementById('add-tokens').value;
const provider=document.getElementById('add-provider').value;
+ const realm=provider==='zai'?document.getElementById('add-realm').value:'';
const list=tokens.split('\n').map(s=>s.trim()).filter(Boolean);
if(!list.length)return showToast('请输入至少一个 Token','error');
try{
- const d=await api('POST','/accounts',{provider,tokens:list});
+ const d=await api('POST','/accounts',{provider,tokens:list,...(realm?{realm}:{})});
closeModal('modal-add');
showToast(`新增 ${d.count} 个账号`,'success');
load();
@@ -480,10 +517,14 @@
btn.disabled=true;
try{
const label=document.getElementById('login-label').value.trim();
- const d=await api('POST','/login/start',label?{label}:{});
+ const realm=document.getElementById('login-realm').value;
+ const d=await api('POST','/login/start',{realm,...(label?{label}:{})});
_loginUrl=d.authorize_url;
_loginDeadline=Date.now()+(d.expires_in||300)*1000;
document.getElementById('login-url').value=_loginUrl;
+ // 激活态提示块随区域切换(intl 的邮箱/Google 预登录技巧不适用于 cn)
+ document.getElementById('login-active-tip-intl').style.display=realm==='intl'?'':'none';
+ document.getElementById('login-active-tip-cn').style.display=realm==='cn'?'':'none';
document.getElementById('login-idle').style.display='none';
document.getElementById('login-active').style.display='';
setLoginStatus('等待授权中…',false);
diff --git a/tests/conftest.py b/tests/conftest.py
index aacfe68..67a3c92 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -123,6 +123,8 @@ async def gateway_client(fresh_app, mock_server, monkeypatch, stub_captcha):
# OAuth(cli init/poll + api-key 兑换链)全部收敛到 Mock —— 测试永不打真网
monkeypatch.setattr(settings, "OAUTH_API_BASE", f"{base}/api/v1")
monkeypatch.setattr(settings, "ZAI_EXCHANGE_ORIGIN", base)
+ # 国内版(bigmodel.cn)兑换链业务域同样指向 Mock
+ monkeypatch.setattr(settings, "BIGMODEL_EXCHANGE_ORIGIN", base)
from app.routes import gateway as gateway_module
monkeypatch.setattr(gateway_module, "captcha_manager", stub_captcha)
diff --git a/tests/integration/test_oauth_login.py b/tests/integration/test_oauth_login.py
index 3070714..e1c5208 100644
--- a/tests/integration/test_oauth_login.py
+++ b/tests/integration/test_oauth_login.py
@@ -7,6 +7,7 @@
from __future__ import annotations
import asyncio
+import json
import pytest
@@ -273,3 +274,131 @@ async def test_exchange_api_key_chain_on_ready(self, gateway_client):
assert any(p.endswith("/api_keys/copy/mock-api-key-id") for p in paths)
# ready 后立即触发一次额度刷新(billing 三端点)
assert any(p.endswith("/billing/current") for p in paths)
+
+
+@pytest.mark.integration
+class TestOAuthCnRealm:
+ """国内版(bigmodel.cn)OAuth 双区域回归(对齐 codeapi providers/zcode 实证):
+
+ - init 请求体 provider=bigmodel(国内版授权出口)
+ - poll 业务 token 在 data.bigmodel 块,且本身就是业务 token —— 不打 z/login
+ - 兑换链(getCustomerInfo → api_keys → copy)照走,Key 挂回 JWT 作回退
+ - 账号以 realm=cn 入池;非法 realm 400
+ """
+
+ @pytest.fixture(autouse=True)
+ async def _reset_mock(self, gateway_client):
+ _, mock = gateway_client
+ mock.state.oauth_state = "pending"
+ mock.state.oauth_poll_status = 200
+ mock.state.oauth_poll_realm = "cn"
+ yield
+ mock.state.oauth_poll_realm = None
+ await _drain_login_followup()
+
+ async def test_invalid_realm_rejected(self, gateway_client):
+ client, _ = gateway_client
+ res = await client.post("/admin/api/login/start", json={"realm": "xx"},
+ headers={"Authorization": "Bearer zcode"})
+ assert res.status_code == 400
+
+ async def test_cn_start_uses_bigmodel_provider(self, gateway_client):
+ client, mock = gateway_client
+ # mock 上游 session 级共享,calls 跨用例累积 —— 记录基线后只断言增量
+ base = len(mock.state.calls)
+ start = (await client.post("/admin/api/login/start", json={"realm": "cn"},
+ headers={"Authorization": "Bearer zcode"})).json()
+ assert start["realm"] == "cn"
+ init = next(c for c in mock.state.calls[base:] if c[1].endswith("/oauth/cli/init"))
+ assert json.loads(init[3])["provider"] == "bigmodel"
+ # 对照:缺省(intl)发起时 provider=zai
+ base2 = len(mock.state.calls)
+ await client.post("/admin/api/login/start",
+ headers={"Authorization": "Bearer zcode"})
+ init_intl = next(c for c in mock.state.calls[base2:]
+ if c[1].endswith("/oauth/cli/init"))
+ assert json.loads(init_intl[3])["provider"] == "zai"
+
+ async def test_cn_full_flow_skips_z_login_and_pools_cn_account(self, gateway_client):
+ client, mock = gateway_client
+ base = len(mock.state.calls)
+ start = (await client.post("/admin/api/login/start",
+ json={"label": "cn-1", "realm": "cn"},
+ headers={"Authorization": "Bearer zcode"})).json()
+ mock.state.oauth_state = "ready"
+ poll = (await client.get(f"/admin/api/login/poll/{start['flow_id']}",
+ headers={"Authorization": "Bearer zcode"})).json()
+ assert poll["status"] == "ready"
+ acc = poll["account"]
+ assert acc["provider"] == "zai"
+ assert acc["realm"] == "cn"
+ assert acc["mode"] == "jwt"
+
+ await _drain_login_followup()
+ paths = [c[1] for c in mock.state.calls[base:]]
+ # cn 的业务 token 即 access_token:绝不打国际版的 z/login 换取
+ assert "/api/auth/z/login" not in paths
+ # 兑换链照走,回退 Key 挂回同一 JWT 账号
+ assert "/api/biz/customer/getCustomerInfo" in paths
+ assert any(p.endswith("/api_keys/copy/mock-api-key-id") for p in paths)
+ accounts = (await client.get("/admin/api/accounts",
+ headers={"Authorization": "Bearer zcode"})).json()
+ stored = next(a for a in accounts["accounts"] if a["id"] == acc["id"])
+ assert stored["realm"] == "cn"
+ assert (stored["token_masked"] or "") != ""
+
+
+@pytest.mark.integration
+class TestPasteRealm:
+ """粘贴入池的区域语义:
+
+ - zai + realm=cn 的两段点 JWT → JWT 模式、realm=cn 入池
+ - bigmodel 提供商粘入两段点 JWT(zcodejwttoken)→ 自动归位 zai/cn
+ (bigmodel 通道只接受 open.bigmodel.cn 的 x-api-key,JWT 在该通道永不可用)
+ - 非法 realm 400
+ """
+
+ async def _post(self, client, payload):
+ return await client.post("/admin/api/accounts", json=payload,
+ headers={"Authorization": "Bearer zcode"})
+
+ async def test_zai_cn_jwt_pools_with_cn_realm(self, gateway_client):
+ client, _ = gateway_client
+ res = await self._post(client, {"provider": "zai", "realm": "cn",
+ "tokens": ["h.cnjwt.sig"]})
+ assert res.status_code == 200
+ accounts = (await client.get("/admin/api/accounts",
+ headers={"Authorization": "Bearer zcode"})).json()
+ acc = accounts["accounts"][0]
+ assert acc["realm"] == "cn"
+ assert acc["mode"] == "jwt"
+
+ async def test_bigmodel_jwt_auto_migrates_to_zai_cn(self, gateway_client):
+ client, _ = gateway_client
+ res = await self._post(client, {"provider": "bigmodel",
+ "tokens": ["h.migrated.sig"]})
+ assert res.status_code == 200
+ accounts = (await client.get("/admin/api/accounts",
+ headers={"Authorization": "Bearer zcode"})).json()
+ acc = accounts["accounts"][0]
+ assert acc["provider"] == "zai"
+ assert acc["realm"] == "cn"
+ assert acc["mode"] == "jwt"
+
+ async def test_bigmodel_apikey_defaults_cn(self, gateway_client):
+ client, _ = gateway_client
+ res = await self._post(client, {"provider": "bigmodel",
+ "tokens": ["mock-api-key-id.secret"]})
+ assert res.status_code == 200
+ accounts = (await client.get("/admin/api/accounts",
+ headers={"Authorization": "Bearer zcode"})).json()
+ acc = accounts["accounts"][0]
+ assert acc["provider"] == "bigmodel"
+ assert acc["realm"] == "cn"
+ assert acc["mode"] == "apiKey"
+
+ async def test_invalid_realm_rejected(self, gateway_client):
+ client, _ = gateway_client
+ res = await self._post(client, {"provider": "zai", "realm": "xx",
+ "tokens": ["abc"]})
+ assert res.status_code == 400
diff --git a/tests/mock_upstream/server.py b/tests/mock_upstream/server.py
index 050b144..a709f9b 100644
--- a/tests/mock_upstream/server.py
+++ b/tests/mock_upstream/server.py
@@ -412,11 +412,15 @@ async def oauth_poll(flow_id: str, request: Request) -> Response:
if state == "failed":
data["message"] = getattr(app.state, "oauth_fail_message", "user denied")
if state == "ready":
- data.update({
- "status": "ready",
- "token": "mock-gateway-jwt-header.eyJzdWIiOiJtb2NrIn0.sig",
- "zai": {"access_token": "mock-access-token"},
- })
+ data["status"] = "ready"
+ data["token"] = "mock-gateway-jwt-header.eyJzdWIiOiJtb2NrIn0.sig"
+ # 业务 token 块随区域(真实上游只下发与 init provider 匹配的块):
+ # oauth_poll_realm="cn" → data.bigmodel(业务 token 即 access_token);
+ # 缺省 intl → data.zai(OAuth 会话凭证,还需 z/login 换业务 token)
+ if str(getattr(app.state, "oauth_poll_realm", "") or "") == "cn":
+ data["bigmodel"] = {"access_token": "mock-cn-access-token"}
+ else:
+ data["zai"] = {"access_token": "mock-access-token"}
return Response(json.dumps({"data": data}), media_type="application/json")
@app.post("/api/auth/z/login")
diff --git a/tests/unit/test_constants.py b/tests/unit/test_constants.py
index 78bb687..756f9bb 100644
--- a/tests/unit/test_constants.py
+++ b/tests/unit/test_constants.py
@@ -21,6 +21,19 @@ def test_billing_base():
assert constants.BILLING_BASE == "https://zcode.z.ai/api/v1/zcode-plan"
+def test_oauth_realms():
+ """双区域表(对齐 codeapi providers/zcode/realms.py,官方客户端 3.14.x 实证):
+ CLI init/poll 端点 intl/cn 共用,provider 参数、业务 token 块名与业务域随区域。"""
+ assert constants.BIGMODEL_BUSINESS_ORIGIN == "https://bigmodel.cn"
+ assert constants.OAUTH_REALMS["intl"] == {
+ "oauth_provider": "zai", "business_origin": "https://api.z.ai",
+ }
+ assert constants.OAUTH_REALMS["cn"] == {
+ "oauth_provider": "bigmodel", "business_origin": "https://bigmodel.cn",
+ }
+ assert constants.OAUTH_PROVIDER_BLOCKS == {"intl": "zai", "cn": "bigmodel"}
+
+
def test_client_configs():
assert constants.CLIENT_CONFIGS_URL == "https://zcode.z.ai/api/v1/client/configs"
# 实测带 platform 参数上游 3001,只允许 app_version
@@ -88,6 +101,9 @@ def test_settings_upstream_defaults_from_constants():
assert settings.UPSTREAM["zai_fallback"] == constants.MESSAGES_URLS["zai_fallback"]
assert settings.UPSTREAM["bigmodel"] == constants.MESSAGES_URLS["bigmodel"]
assert settings.ZCODE_BILLING_BASE == constants.BILLING_BASE
+ # 兑换链业务域随区域:intl=api.z.ai,cn=bigmodel.cn
+ assert settings.ZAI_EXCHANGE_ORIGIN == constants.ZAI_API_ORIGIN
+ assert settings.BIGMODEL_EXCHANGE_ORIGIN == constants.BIGMODEL_BUSINESS_ORIGIN
# 网关常量(底座原值,回归锁定)
diff --git a/tests/unit/test_models_realm.py b/tests/unit/test_models_realm.py
new file mode 100644
index 0000000..b069ddf
--- /dev/null
+++ b/tests/unit/test_models_realm.py
@@ -0,0 +1,65 @@
+"""账号区域(realm)单元测试:
+
+- Account.create 的区域默认与纠偏(bigmodel 提供商 → cn;非法值回退 intl)
+- realm 随 to_dict/from_dict 持久化往返
+- public_view 暴露 realm
+- agent.build_request 回退 Key 通道按区域分流(intl → api.z.ai,cn → open.bigmodel.cn)
+"""
+
+from __future__ import annotations
+
+from app import settings
+from app.agent import build_request
+from app.models import Account
+
+
+def test_create_zai_defaults_intl():
+ acc = Account.create("zai", "a1", "h.payload.sig")
+ assert acc.realm == "intl"
+ assert acc.mode == "jwt" # 两段点 + zai 提供商 → JWT
+
+
+def test_create_bigmodel_defaults_cn():
+ acc = Account.create("bigmodel", "a2", "keyid.secret")
+ assert acc.realm == "cn"
+ assert acc.mode == "apiKey"
+
+
+def test_create_invalid_realm_falls_back():
+ assert Account.create("zai", "a3", "k", realm="xx").realm == "intl"
+ assert Account.create("bigmodel", "a4", "k", realm="xx").realm == "cn"
+
+
+def test_create_explicit_cn_keeps_jwt_mode():
+ acc = Account.create("zai", "a5", "h.cn.sig", realm="cn")
+ assert acc.realm == "cn"
+ assert acc.mode == "jwt"
+
+
+def test_realm_roundtrip_persistence():
+ acc = Account.create("zai", "a6", "h.cn.sig", realm="cn")
+ restored = Account.from_dict(acc.to_dict())
+ assert restored.realm == "cn"
+ # 旧数据(无 realm 字段)加载后回退默认 intl
+ legacy = {k: v for k, v in acc.to_dict().items() if k != "realm"}
+ assert Account.from_dict(legacy).realm == "intl"
+
+
+def test_public_view_exposes_realm():
+ view = Account.create("zai", "a7", "h.cn.sig", realm="cn").public_view()
+ assert view["realm"] == "cn"
+
+
+def _fallback_url(realm: str) -> str:
+ acc = Account.create("zai", "n", "keyid.secret", realm=realm)
+ url, headers, _ = build_request(acc, {"model": "GLM-5.3-Flash"}, None)
+ assert headers.get("x-api-key") == "keyid.secret"
+ return url
+
+
+def test_fallback_channel_intl_routes_to_zai():
+ assert _fallback_url("intl") == settings.UPSTREAM["zai_fallback"]
+
+
+def test_fallback_channel_cn_routes_to_bigmodel():
+ assert _fallback_url("cn") == settings.UPSTREAM["bigmodel"]
From b4bcab3f4f21348fce22eef2f6818e1b7a0bb167 Mon Sep 17 00:00:00 2001
From: jk <2081217232@qq.com>
Date: Sun, 4 Oct 2026 09:41:50 +0800
Subject: [PATCH 02/14] =?UTF-8?q?feat(openai-compat):=20thinking/reasoning?=
=?UTF-8?q?=5Fcontent=20=E5=8F=8C=E5=90=91=E9=80=8F=E4=BC=A0=E4=B8=8E=20re?=
=?UTF-8?q?asoning=5Feffort?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 请求侧:历史轮 reasoning_content → Anthropic thinking 块(signature 空串可过);
reasoning_effort low/high/max 透传上游,其余值不带(上游不支持关思考)
- 响应侧:thinking 块聚合为 message.reasoning_content(DeepSeek/pi-ai 风格)
- 流式:thinking_delta → reasoning_content 增量 chunk
---
app/openai_compat.py | 24 +++++-
tests/unit/test_openai_compat.py | 125 +++++++++++++++++++++++++++++++
2 files changed, 148 insertions(+), 1 deletion(-)
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/tests/unit/test_openai_compat.py b/tests/unit/test_openai_compat.py
index 26ba0cb..547142e 100644
--- a/tests/unit/test_openai_compat.py
+++ b/tests/unit/test_openai_compat.py
@@ -223,3 +223,128 @@ def test_unknown_events_ignored(self):
conv.start()
assert conv.feed({"type": "ping"}) == []
assert conv.feed({"type": "content_block_stop", "index": 0}) == []
+
+class TestReasoningMapping:
+ """reasoning 双向映射:reasoning_effort 入站透传 + thinking 块 ↔ reasoning_content 出站。"""
+
+ def test_reasoning_effort_passthrough(self):
+ body, err = openai_to_anthropic({
+ "model": "glm-5.3",
+ "messages": [{"role": "user", "content": "hi"}],
+ "reasoning_effort": "high",
+ })
+ assert err is None
+ assert body["reasoning_effort"] == "high"
+
+ def test_reasoning_effort_normalizes_case(self):
+ body, _ = openai_to_anthropic({
+ "model": "glm-5.3",
+ "messages": [{"role": "user", "content": "hi"}],
+ "reasoning_effort": "MAX",
+ })
+ assert body["reasoning_effort"] == "max"
+
+ def test_reasoning_effort_invalid_omitted(self):
+ # 上游不支持关闭思考,off/none/未知值一律不带该字段
+ body, _ = openai_to_anthropic({
+ "model": "glm-5.3",
+ "messages": [{"role": "user", "content": "hi"}],
+ "reasoning_effort": "off",
+ })
+ assert "reasoning_effort" not in body
+
+ def test_reasoning_effort_absent(self):
+ body, _ = openai_to_anthropic({
+ "model": "glm-5.3",
+ "messages": [{"role": "user", "content": "hi"}],
+ })
+ assert "reasoning_effort" not in body
+
+ def test_assistant_reasoning_content_replays_as_thinking(self):
+ body, err = openai_to_anthropic({
+ "model": "glm-5.3",
+ "messages": [
+ {"role": "user", "content": "1+1"},
+ {"role": "assistant", "content": "2", "reasoning_content": "用户问 1+1,答案是 2"},
+ {"role": "user", "content": "再加 1"},
+ ],
+ })
+ assert err is None
+ assistant = body["messages"][1]
+ assert [b["type"] for b in assistant["content"]] == ["thinking", "text"]
+ assert assistant["content"][0]["thinking"] == "用户问 1+1,答案是 2"
+
+ def test_assistant_reasoning_alias_fields(self):
+ for alias in ("reasoning", "reasoning_text"):
+ body, _ = openai_to_anthropic({
+ "model": "glm-5.3",
+ "messages": [
+ {"role": "assistant", "content": "ok", alias: "思考"},
+ ],
+ })
+ assert body["messages"][0]["content"][0]["type"] == "thinking"
+
+ def test_assistant_blank_reasoning_skipped(self):
+ body, _ = openai_to_anthropic({
+ "model": "glm-5.3",
+ "messages": [{"role": "assistant", "content": "ok", "reasoning_content": " "}],
+ })
+ # 空白思考不产生 thinking 块,也不丢 assistant 消息
+ assert body["messages"][0]["content"] == [{"type": "text", "text": "ok"}]
+
+ def test_nonstream_thinking_to_reasoning_content(self):
+ data = {
+ "id": "msg_1", "type": "message", "role": "assistant", "model": "glm-5.3",
+ "content": [
+ {"type": "thinking", "thinking": "先想一下", "signature": "sig"},
+ {"type": "text", "text": "答案"},
+ ],
+ "stop_reason": "end_turn",
+ "usage": {"input_tokens": 5, "output_tokens": 9},
+ }
+ out = anthropic_to_openai(data, "glm-5.3")
+ msg = out["choices"][0]["message"]
+ assert msg["content"] == "答案"
+ assert msg["reasoning_content"] == "先想一下"
+
+ def test_nonstream_no_thinking_no_field(self):
+ data = {
+ "id": "msg_1", "type": "message", "role": "assistant", "model": "glm-5.3",
+ "content": [{"type": "text", "text": "答案"}],
+ "stop_reason": "end_turn", "usage": {"input_tokens": 5, "output_tokens": 1},
+ }
+ out = anthropic_to_openai(data, "glm-5.3")
+ assert "reasoning_content" not in out["choices"][0]["message"]
+
+ def test_stream_thinking_to_reasoning_content(self):
+ conv = StreamConverter("GLM-5.3")
+ outs = [conv.start()]
+ events = [
+ {"type": "message_start", "message": {"id": "msg_r", "usage": {"input_tokens": 3}}},
+ {"type": "content_block_start", "index": 0, "content_block": {"type": "thinking"}},
+ {"type": "content_block_delta", "index": 0, "delta": {"type": "thinking_delta", "thinking": "想"}},
+ {"type": "content_block_delta", "index": 0, "delta": {"type": "thinking_delta", "thinking": "一下"}},
+ {"type": "content_block_stop", "index": 0},
+ {"type": "content_block_start", "index": 1, "content_block": {"type": "text"}},
+ {"type": "content_block_delta", "index": 1, "delta": {"type": "text_delta", "text": "答案"}},
+ {"type": "message_delta", "delta": {"stop_reason": "end_turn"}, "usage": {"output_tokens": 4}},
+ ]
+ for evt in events:
+ outs.extend(conv.feed(evt))
+ outs.append(conv.done())
+
+ chunks = _parse_sse("".join(outs))
+ reasoning = "".join(c["choices"][0]["delta"].get("reasoning_content") or "" for c in chunks)
+ text = "".join(c["choices"][0]["delta"].get("content") or "" for c in chunks)
+ assert reasoning == "想一下"
+ assert text == "答案"
+
+ def test_stream_no_thinking_keeps_shape(self):
+ # 原有纯文本流不应引入 reasoning_content 字段(与既有客户端行为兼容)
+ conv = StreamConverter("GLM-5.3")
+ conv.start()
+ outs = conv.feed({"type": "content_block_delta", "index": 0,
+ "delta": {"type": "text_delta", "text": "你好"}})
+ chunks = _parse_sse("".join(outs))
+ assert all("reasoning_content" not in c["choices"][0]["delta"] for c in chunks)
+
From 5e945856e2a8ae2e3ce4ee07e66431054b977442 Mon Sep 17 00:00:00 2001
From: jk <2081217232@qq.com>
Date: Sun, 4 Oct 2026 09:41:50 +0800
Subject: [PATCH 03/14] =?UTF-8?q?docs:=20README=20=E5=A2=9E=E5=8A=A0=20Doc?=
=?UTF-8?q?ker=20=E9=83=A8=E7=BD=B2=E8=AF=B4=E6=98=8E=EF=BC=88compose=20?=
=?UTF-8?q?=E4=B8=80=E9=94=AE=E8=B5=B7=EF=BC=8C=E6=95=B0=E6=8D=AE=E6=8C=81?=
=?UTF-8?q?=E4=B9=85=E5=8C=96=20./data=EF=BC=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
README.md | 15 +++++++++++++++
1 file changed, 15 insertions(+)
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
From cd18534e26361b80621daa0b0dccb25199bc241d Mon Sep 17 00:00:00 2001
From: jk <2081217232@qq.com>
Date: Sun, 4 Oct 2026 09:41:50 +0800
Subject: [PATCH 04/14] =?UTF-8?q?chore:=20=E5=89=8D=E7=AB=AF=E7=89=88?=
=?UTF-8?q?=E6=9C=AC=E5=8F=B7=E5=AF=B9=E9=BD=90=20+=20captcha=5Fnode=20loc?=
=?UTF-8?q?kfile=20=E5=90=8C=E6=AD=A5=EF=BC=88puppeteer-core=20=E7=9C=9F?=
=?UTF-8?q?=E6=B5=8F=E8=A7=88=E5=99=A8=E6=B1=82=E8=A7=A3=EF=BC=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
captcha_node/package-lock.json | 953 ++++++++++++++++++++++++++++++++-
frontend/version | 2 +-
2 files changed, 951 insertions(+), 4 deletions(-)
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/frontend/version b/frontend/version
index bd4053b..2714f53 100644
--- a/frontend/version
+++ b/frontend/version
@@ -1 +1 @@
-2.6.3
\ No newline at end of file
+2.6.4
From 89ff25a8568caee43f3a4915c6998fed58df20e6 Mon Sep 17 00:00:00 2001
From: jk <2081217232@qq.com>
Date: Sun, 4 Oct 2026 10:33:01 +0800
Subject: [PATCH 05/14] =?UTF-8?q?chore:=20Linux/Windows=20=E5=90=AF?=
=?UTF-8?q?=E5=8A=A8=E8=84=9A=E6=9C=AC=EF=BC=88=E5=8A=9F=E8=83=BD=E5=AF=B9?=
=?UTF-8?q?=E9=BD=90=EF=BC=89+=20Docker=20=E9=83=A8=E7=BD=B2=E6=96=87?=
=?UTF-8?q?=E4=BB=B6=20+=20=E6=9C=AC=E5=9C=B0=E6=9D=82=E7=89=A9=E5=BF=BD?=
=?UTF-8?q?=E7=95=A5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- start.sh / start.bat:启动/停止/重启语义对齐——停旧实例(serve.pid, 优雅→强杀)、
端口占用检测、依赖自动安装、后台运行+日志、/meta 健康检查
- .gitattributes:*.bat/*.cmd 强制 CRLF(cmd 对 LF 的 goto/label 有解析怪癖)
- Dockerfile + compose + .dockerignore:多阶段镜像(python3.11-slim + node22 +
Debian chromium + tini + CJK 字体),数据卷 ./data,.env 不入镜像
- .gitignore:排除 serve.log/pid、.trae_*、.sg_* 等本地运行产物
---
.dockerignore | 22 +++++++
.gitattributes | 3 +
.gitignore | 8 +++
Dockerfile | 70 +++++++++++++++++++++
docker-compose.yml | 23 +++++++
start.bat | 147 +++++++++++++++++++++++++++++++++++++++++++++
start.sh | 93 ++++++++++++++++++++++++++++
7 files changed, 366 insertions(+)
create mode 100644 .dockerignore
create mode 100644 Dockerfile
create mode 100644 docker-compose.yml
create mode 100644 start.bat
create mode 100755 start.sh
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/.gitignore b/.gitignore
index 0d237a6..c735577 100644
--- a/.gitignore
+++ b/.gitignore
@@ -24,3 +24,11 @@ node_modules/
# 秘钥 / 凭证导出
*accounts*.json
zcode-accounts*.json
+
+# 本地运行 / 工具杂物
+serve.log
+serve.err.log
+serve.pid
+.trae_*
+.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/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/start.bat b/start.bat
new file mode 100644
index 0000000..ee8bc63
--- /dev/null
+++ b/start.bat
@@ -0,0 +1,147 @@
+@echo off
+setlocal EnableExtensions
+chcp 65001 >nul
+cd /d "%~dp0"
+
+rem =====================================================================
+rem zcode-hub Windows launcher -- feature parity with start.sh (Linux)
+rem
+rem Usage:
+rem start.bat Start (auto-stops a running instance first)
+rem start.bat stop Stop
+rem
+rem Notes:
+rem - Runs the server hidden in background; logs: serve.log + serve.err.log
+rem - Health check via GET /meta after start
+rem - Console messages are ASCII on purpose (avoids codepage mojibake)
+rem =====================================================================
+
+rem -- Edit as needed ----------------------------------------------------
+rem Upstream proxy (Clash mixed port); leave empty "" for direct connection
+set "PROXY=http://127.0.0.1:7890"
+rem ----------------------------------------------------------------------
+
+set "PORT=3000"
+if exist .env for /f "tokens=1,* delims==" %%a in ('findstr /b /c:"ZCODE_PORT=" .env') do set "PORT=%%b"
+if defined PORT set "PORT=%PORT: =%"
+if not defined PORT set "PORT=3000"
+
+rem Pause on exit only when launched by double-click (cmd /c), never in scripts
+set "DBLCLK=0"
+echo %cmdcmdline% | find /i "/c" >nul 2>&1 && set "DBLCLK=1"
+
+if /i "%~1"=="stop" (
+ call :stop_instance
+ echo [OK] Stopped.
+ if "%DBLCLK%"=="1" pause
+ exit /b 0
+)
+
+rem -- 1. venv + Python dependencies -------------------------------------
+if not exist ".venv\Scripts\python.exe" (
+ where python >nul 2>&1
+ if errorlevel 1 (
+ echo [!] python not found on PATH. Install Python 3.11+ first.
+ goto :fail
+ )
+ echo [*] Creating venv (.venv) ...
+ python -m venv .venv || goto :fail
+)
+".venv\Scripts\python.exe" -c "import fastapi, uvicorn, httpx" >nul 2>&1
+if errorlevel 1 (
+ echo [*] Installing Python dependencies ...
+ if defined PROXY (
+ ".venv\Scripts\python.exe" -m pip install -q --proxy "%PROXY%" -r requirements.txt || goto :fail
+ ) else (
+ ".venv\Scripts\python.exe" -m pip install -q -r requirements.txt || goto :fail
+ )
+)
+
+rem -- 2. captcha_node (npm) dependencies --------------------------------
+if not exist "captcha_node\node_modules" (
+ echo [*] Installing captcha_node dependencies (npm install) ...
+ pushd captcha_node
+ call npm install --no-audit --no-fund
+ if errorlevel 1 (
+ popd
+ goto :fail
+ )
+ popd
+)
+
+rem -- 3. Stop old instance (restart semantics, clears admin lock too) ---
+call :stop_instance
+
+rem -- 4. Port must be free ----------------------------------------------
+set "OCCUPY="
+for /f "tokens=5" %%p in ('netstat -ano ^| findstr /r /c:":%PORT% .*LISTENING"') do set "OCCUPY=%%p"
+if defined OCCUPY (
+ echo [!] Port %PORT% still occupied by another process ^(PID %OCCUPY%^):
+ netstat -ano | findstr /r /c:":%PORT% .*LISTENING"
+ echo Stop it manually first, e.g.: taskkill /F /PID %OCCUPY%
+ goto :fail
+)
+
+rem -- 5. Start hidden in background, capture real python PID ------------
+echo [*] Starting zcode-hub (port=%PORT%) ...
+if defined PROXY (
+ set "HTTP_PROXY=%PROXY%/"
+ set "HTTPS_PROXY=%PROXY%/"
+)
+powershell -NoProfile -Command "$d=(Get-Location).Path; $p = Start-Process -FilePath (Join-Path $d '.venv\Scripts\python.exe') -ArgumentList 'cli.py','serve' -WorkingDirectory $d -WindowStyle Hidden -RedirectStandardOutput (Join-Path $d 'serve.log') -RedirectStandardError (Join-Path $d 'serve.err.log') -PassThru; Write-Output $p.Id" > "%TEMP%\zcode_hub_pid.txt" 2>nul
+set "NEWPID="
+set /p NEWPID=<"%TEMP%\zcode_hub_pid.txt"
+del "%TEMP%\zcode_hub_pid.txt" >nul 2>&1
+if not defined NEWPID (
+ echo [!] Failed to launch python process.
+ goto :fail
+)
+(echo %NEWPID%)>"serve.pid"
+
+rem -- 6. Health check (up to ~15s) --------------------------------------
+set /a TRIES=0
+:health_wait
+ping -n 2 127.0.0.1 >nul
+curl -sf -m 2 "http://127.0.0.1:%PORT%/meta" >nul 2>&1 && goto :started
+set /a TRIES+=1
+if %TRIES% lss 15 goto :health_wait
+
+echo [!] Start FAILED. Last log lines:
+echo --- serve.log ---
+powershell -NoProfile -Command "if (Test-Path serve.log) { Get-Content serve.log -Tail 15 }"
+echo --- serve.err.log ---
+powershell -NoProfile -Command "if (Test-Path serve.err.log) { Get-Content serve.err.log -Tail 15 }"
+goto :fail
+
+:started
+echo [OK] Started: http://127.0.0.1:%PORT%
+echo Admin UI : http://127.0.0.1:%PORT%/admin/login
+echo Logs : serve.log + serve.err.log
+echo Stop : start.bat stop Restart: just run start.bat again
+if "%DBLCLK%"=="1" pause
+exit /b 0
+
+:fail
+echo [!] start.bat aborted.
+if "%DBLCLK%"=="1" pause
+exit /b 1
+
+:stop_instance
+set "OLDPID="
+if exist serve.pid for /f %%p in (serve.pid) do set "OLDPID=%%p"
+if not defined OLDPID goto :stop_cleanup
+tasklist /FI "PID eq %OLDPID%" /NH 2>nul | find /i "python.exe" >nul 2>&1 || goto :stop_cleanup
+echo [*] Stopping old instance (pid=%OLDPID%) ...
+taskkill /PID %OLDPID% >nul 2>&1
+set /a N=0
+:stop_wait
+tasklist /FI "PID eq %OLDPID%" /NH 2>nul | find /i "python.exe" >nul 2>&1 || goto :stop_cleanup
+set /a N+=1
+if %N% geq 10 goto :stop_force
+ping -n 2 127.0.0.1 >nul
+goto :stop_wait
+:stop_force
+taskkill /F /PID %OLDPID% >nul 2>&1
+:stop_cleanup
+if exist serve.pid del serve.pid >nul 2>&1
+goto :eof
diff --git a/start.sh b/start.sh
new file mode 100755
index 0000000..9e1beaa
--- /dev/null
+++ b/start.sh
@@ -0,0 +1,93 @@
+#!/usr/bin/env bash
+# zcode-hub Linux 启动脚本(对应 Windows 的 start.bat)
+#
+# 用法:
+# ./start.sh 启动(若已在运行则先停旧实例再启动,同时清除后台登录失败锁定)
+# ./start.sh stop 停止
+set -euo pipefail
+
+cd "$(dirname "$(readlink -f "$0")")"
+
+# ── 可按需修改 ──────────────────────────────────────────────
+# 上游代理(Clash 混合端口);直连环境把它留空即可
+PROXY="http://127.0.0.1:7890"
+# ────────────────────────────────────────────────────────────
+
+PORT=$(grep -E '^ZCODE_PORT=' .env 2>/dev/null | cut -d= -f2 | tr -d '[:space:]')
+PORT=${PORT:-3000}
+
+stop_instance() {
+ local old_pid
+ old_pid=$(cat serve.pid 2>/dev/null || true)
+ if [ -n "$old_pid" ] && kill -0 "$old_pid" 2>/dev/null; then
+ echo "[*] 停止旧实例 (pid=$old_pid)..."
+ kill "$old_pid" 2>/dev/null || true
+ for _ in $(seq 1 10); do
+ kill -0 "$old_pid" 2>/dev/null || break
+ sleep 1
+ done
+ kill -9 "$old_pid" 2>/dev/null || true
+ fi
+ rm -f serve.pid
+}
+
+if [ "${1:-}" = "stop" ]; then
+ stop_instance
+ echo "[✔] 已停止"
+ exit 0
+fi
+
+# 1. Python 虚拟环境:缺失则创建,依赖缺失则安装
+if [ ! -x .venv/bin/python ]; then
+ echo "[*] 创建虚拟环境 (.venv)..."
+ python3 -m venv .venv
+fi
+if ! .venv/bin/python -c "import fastapi, uvicorn, httpx" 2>/dev/null; then
+ echo "[*] 安装 Python 依赖..."
+ if [ -n "$PROXY" ]; then
+ .venv/bin/pip install -q --proxy "$PROXY" -r requirements.txt
+ else
+ .venv/bin/pip install -q -r requirements.txt
+ fi
+fi
+
+# 2. 验证码求解 Node 依赖(JWT 账号需要)
+if [ ! -d captcha_node/node_modules ]; then
+ echo "[*] 安装 captcha_node 依赖 (npm install)..."
+ (cd captcha_node && npm install --no-audit --no-fund)
+fi
+
+# 3. 已在运行则先停(重启同时清掉内存里的后台登录失败锁定)
+stop_instance
+
+# 4. 端口仍被其他进程占用则报错
+if ss -tln 2>/dev/null | grep -q ":${PORT} "; then
+ echo "[!] 端口 ${PORT} 仍被其他进程占用,请手动处理:" >&2
+ ss -tlnp 2>/dev/null | grep ":${PORT} " >&2 || true
+ exit 1
+fi
+
+# 5. 启动(不设 ALL_PROXY/socks —— httpx 缺 socks 支持会报 Unknown scheme)
+echo "[*] 启动 zcode-hub (port=${PORT})..."
+if [ -n "$PROXY" ]; then
+ env "HTTP_PROXY=${PROXY}/" "HTTPS_PROXY=${PROXY}/" \
+ nohup .venv/bin/python cli.py serve > serve.log 2>&1 &
+else
+ nohup .venv/bin/python cli.py serve > serve.log 2>&1 &
+fi
+echo $! > serve.pid
+
+# 6. 健康检查
+for _ in $(seq 1 15); do
+ sleep 1
+ if curl -sf -m 2 "http://127.0.0.1:${PORT}/meta" >/dev/null 2>&1; then
+ echo "[✔] 已启动: http://127.0.0.1:${PORT}"
+ echo " 后台: http://127.0.0.1:${PORT}/admin/login"
+ echo " 日志: serve.log 停止: ./start.sh stop"
+ exit 0
+ fi
+done
+
+echo "[!] 启动失败,最近日志:" >&2
+tail -20 serve.log >&2
+exit 1
From e3222efe67acffe86a887bb631c048ed5c406e15 Mon Sep 17 00:00:00 2001
From: jk <2081217232@qq.com>
Date: Sun, 4 Oct 2026 11:07:22 +0800
Subject: [PATCH 06/14] =?UTF-8?q?fix(scripts):=20start.sh=20=E5=90=AF?=
=?UTF-8?q?=E5=8A=A8=E5=89=8D=E5=89=94=E9=99=A4=20ALL=5FPROXY=20=E2=80=94?=
=?UTF-8?q?=E2=80=94=20Clash=20=E7=B3=BB=E7=BB=9F=E4=BB=A3=E7=90=86?=
=?UTF-8?q?=E6=B3=A8=E5=85=A5=E7=9A=84=20socks://=20=E4=BC=9A=E8=AE=A9=20h?=
=?UTF-8?q?ttpx=20=E5=85=A8=E9=87=8F=20500?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
start.sh | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/start.sh b/start.sh
index 9e1beaa..15df39e 100755
--- a/start.sh
+++ b/start.sh
@@ -67,7 +67,9 @@ if ss -tln 2>/dev/null | grep -q ":${PORT} "; then
exit 1
fi
-# 5. 启动(不设 ALL_PROXY/socks —— httpx 缺 socks 支持会报 Unknown scheme)
+# 5. 启动。剔除环境里已有的 ALL_PROXY/socks(Clash 系统代理常注入 socks://,
+# httpx 缺 socks 支持会报 Unknown scheme → 全部请求 500);仅留 HTTP(S)_PROXY
+unset ALL_PROXY all_proxy
echo "[*] 启动 zcode-hub (port=${PORT})..."
if [ -n "$PROXY" ]; then
env "HTTP_PROXY=${PROXY}/" "HTTPS_PROXY=${PROXY}/" \
From a9f633cfc92826c8d78fcadf9e356402e7b1064b Mon Sep 17 00:00:00 2001
From: JK <2081217232@qq.com>
Date: Sun, 4 Oct 2026 11:17:00 +0800
Subject: [PATCH 07/14] =?UTF-8?q?chore:=20=E5=89=8D=E7=AB=AF=E7=89=88?=
=?UTF-8?q?=E6=9C=AC=E5=8F=B7=E5=AF=B9=E9=BD=90=202.6.8=EF=BC=88=E5=90=88?=
=?UTF-8?q?=E5=B9=B6=20main=20=E6=8C=87=E7=BA=B9=E7=BA=BF=E5=90=8E?=
=?UTF-8?q?=E4=B8=8E=20APP=5FVERSION=20=E4=BF=9D=E6=8C=81=E4=B8=80?=
=?UTF-8?q?=E8=87=B4=EF=BC=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
frontend/version | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frontend/version b/frontend/version
index 2714f53..4484c2b 100644
--- a/frontend/version
+++ b/frontend/version
@@ -1 +1 @@
-2.6.4
+2.6.8
\ No newline at end of file
From 1f7cddc5575fa70996d73d4bd1599ab09960a380 Mon Sep 17 00:00:00 2001
From: JK <2081217232@qq.com>
Date: Sun, 4 Oct 2026 11:23:31 +0800
Subject: [PATCH 08/14] =?UTF-8?q?test:=20fresh=5Fapp=20=E9=92=89=E4=BD=8F?=
=?UTF-8?q?=E9=BB=98=E8=AE=A4=E7=AE=A1=E7=90=86=E5=AF=86=E9=92=A5=E2=80=94?=
=?UTF-8?q?=E2=80=94=E6=9C=AC=E6=9C=BA=20.env=20=E8=87=AA=E5=AE=9A?=
=?UTF-8?q?=E4=B9=89=20ZCODE=5FADMIN=5FKEY=20=E4=B8=8D=E5=86=8D=E6=BC=8F?=
=?UTF-8?q?=E5=85=A5=E5=A4=B9=E5=85=B7?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
tests/conftest.py | 2 ++
1 file changed, 2 insertions(+)
diff --git a/tests/conftest.py b/tests/conftest.py
index 67a3c92..ed5ba96 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -71,6 +71,8 @@ def fresh_app(tmp_path, monkeypatch):
monkeypatch.setattr(settings, "DATA_DIR", data_dir)
monkeypatch.setattr(settings, "DB_PATH", data_dir / "accounts.db")
monkeypatch.setattr(settings, "COOLING_SECONDS", 300)
+ # 本机 .env 的自定义 ZCODE_ADMIN_KEY 不得漏进夹具:admin 接口测试依赖默认密钥 "zcode"
+ monkeypatch.setattr(settings, "DEFAULT_ADMIN_KEY", "zcode")
from app import store as store_module
from app.store import Store
From d5a4e7a74096843095824294ff86cd495a00febb Mon Sep 17 00:00:00 2001
From: JK <2081217232@qq.com>
Date: Sun, 4 Oct 2026 12:01:39 +0800
Subject: [PATCH 09/14] =?UTF-8?q?ci:=20=E5=8F=8C=E5=B9=B3=E5=8F=B0?=
=?UTF-8?q?=E7=9F=A9=E9=98=B5(ubuntu+windows)=E2=80=94=E2=80=94push=20?=
=?UTF-8?q?=E8=A7=A6=E5=8F=91=E6=89=A9=E5=B1=95=E5=88=B0=20develop/feat/**?=
=?UTF-8?q?=EF=BC=8Cfeature=20=E5=88=86=E6=94=AF=E6=8E=A8=E9=80=81?=
=?UTF-8?q?=E5=8D=B3=E8=B7=91=E5=8F=8C=E5=B9=B3=E5=8F=B0=E6=A3=80=E6=9F=A5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.github/workflows/ci.yml | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
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
From ed40f7dd4895563a189399b00e2aa3c7f6075077 Mon Sep 17 00:00:00 2001
From: JK <2081217232@qq.com>
Date: Sun, 4 Oct 2026 12:30:24 +0800
Subject: [PATCH 10/14] =?UTF-8?q?chore:=20.gitignore=20=E8=A1=A5=E5=9B=9E?=
=?UTF-8?q?=20.sg=5F*=20=E9=80=9A=E9=85=8D=EF=BC=88=E5=87=AD=E8=AF=81?=
=?UTF-8?q?=E6=96=87=E4=BB=B6=E5=85=9C=E5=BA=95=EF=BC=8C=E6=B0=B8=E4=B8=8D?=
=?UTF-8?q?=E5=85=A5=E5=BA=93=EF=BC=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.gitignore | 1 +
1 file changed, 1 insertion(+)
diff --git a/.gitignore b/.gitignore
index c735577..9279c51 100644
--- a/.gitignore
+++ b/.gitignore
@@ -30,5 +30,6 @@ serve.log
serve.err.log
serve.pid
.trae_*
+.sg_*
.sg-js/
.sg_login.html
From 1d599ed6f802cf77e78dd4a135d31cffc4df2ce7 Mon Sep 17 00:00:00 2001
From: JK <2081217232@qq.com>
Date: Sun, 4 Oct 2026 14:09:52 +0800
Subject: [PATCH 11/14] =?UTF-8?q?refactor:=20=E7=AE=80=E5=8C=96=E5=90=AF?=
=?UTF-8?q?=E5=8A=A8=E8=84=9A=E6=9C=AC=E4=B8=BA=E6=9E=81=E7=AE=80=E5=89=8D?=
=?UTF-8?q?=E5=8F=B0=E8=BF=90=E8=A1=8C=E7=89=88=E6=9C=AC=EF=BC=8C=E5=B9=B6?=
=?UTF-8?q?=E6=B7=BB=E5=8A=A0=E8=B0=83=E8=AF=95=E8=BE=93=E5=87=BA=E6=96=87?=
=?UTF-8?q?=E4=BB=B6?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
_debug_out.txt | 95 +++++++++++++++++++++++++++++
_debug_start.bat | 153 +++++++++++++++++++++++++++++++++++++++++++++++
start.bat | 147 +--------------------------------------------
start.sh | 96 +----------------------------
4 files changed, 254 insertions(+), 237 deletions(-)
create mode 100644 _debug_out.txt
create mode 100644 _debug_start.bat
diff --git a/_debug_out.txt b/_debug_out.txt
new file mode 100644
index 0000000..768b92b
--- /dev/null
+++ b/_debug_out.txt
@@ -0,0 +1,95 @@
+
+D:\zcode2api>setlocal EnableExtensions
+
+D:\zcode2api>chcp 65001 1>nul
+
+D:\zcode2api>cd /d "D:\zcode2api\"
+
+D:\zcode2api>rem =====================================================================
+
+D:\zcode2api>rem zcode-hub Windows launcher -- feature parity with start.sh (Linux)
+
+D:\zcode2api>rem
+
+D:\zcode2api>rem Usage:
+
+D:\zcode2api>rem start.bat Start (auto-stops a running instance first)
+
+D:\zcode2api>rem start.bat stop Stop
+
+D:\zcode2api>rem
+
+D:\zcode2api>rem Notes:
+
+D:\zcode2api>rem - Runs the server hidden in background; logs: serve.log + serve.err.log
+
+D:\zcode2api>rem - Health check via GET /meta after start
+
+D:\zcode2api>rem - Console messages are ASCII on purpose (avoids codepage mojibake)
+
+D:\zcode2api>rem =====================================================================
+
+D:\zcode2api>rem -- Edit as needed ----------------------------------------------------
+
+D:\zcode2api>rem Upstream proxy (Clash mixed port); leave empty "" for direct connection
+
+D:\zcode2api>set "PROXY=http://127.0.0.1:7890"
+
+D:\zcode2api>rem ----------------------------------------------------------------------
+
+D:\zcode2api>rem Strip ALL_PROXY (on Windows var names are case-insensitive, so this
+
+D:\zcode2api>rem also covers all_proxy): Clash may inject socks:// there, which httpx
+
+D:\zcode2api>rem and pip cannot use without PySocks -> every request fails with 500.
+
+D:\zcode2api>rem Parity with start.sh (commit e3222ef).
+
+D:\zcode2api>set "ALL_PROXY="
+
+D:\zcode2api>set "PORT=3000"
+
+D:\zcode2api>if exist .env for /F "tokens=1,* delims==" %a in ('findstr /b /c:"ZCODE_PORT=" .env') do set "PORT=%b"
+
+D:\zcode2api>set "PORT=3000"
+
+D:\zcode2api>if defined PORT set "PORT=3000"
+
+D:\zcode2api>if not defined PORT set "PORT=3000"
+
+D:\zcode2api>rem Pause on exit only when launched by double-click (cmd /c), never in scripts
+
+D:\zcode2api>set "DBLCLK=0"
+
+D:\zcode2api>echo C:\Windows\system32\cmd.exe /c D:\zcode2api\_debug_start.bat | find /i "/c" 1>nul 2>&1 && set "DBLCLK=1"
+
+D:\zcode2api>if /I "" == "stop" (
+call :stop_instance
+ echo [OK] Stopped.
+ if "0" == "1" pause
+ exit /b 0
+)
+
+D:\zcode2api>rem -- 1. venv + Python dependencies -------------------------------------
+
+D:\zcode2api>if not exist ".venv\Scripts\python.exe" (
+where python 1>nul 2>&1
+ if errorlevel 1 (
+echo [!] python not found on PATH. Install Python 3.11+ first.
+ goto :fail
+)
+ echo [*] Creating venv directory .venv ...
+ python -m venv .venv || goto :fail
+)
+
+D:\zcode2api>".venv\Scripts\python.exe" -c "import fastapi, uvicorn, httpx" 1>nul 2>&1
+
+D:\zcode2api>if errorlevel 1 (
+echo [*] Installing Python dependencies ...
+ if defined PROXY (".venv\Scripts\python.exe" -m pip install -q --proxy "http://127.0.0.1:7890" -r requirements.txt || goto :fail ) else (".venv\Scripts\python.exe" -m pip install -q -r requirements.txt || goto :fail )
+)
+
+D:\zcode2api>rem -- 2. captcha_node (npm) dependencies --------------------------------
+... was unexpected at this time.
+
+D:\zcode2api> echo [*] Installing captcha_node dependencies (npm install) ...
diff --git a/_debug_start.bat b/_debug_start.bat
new file mode 100644
index 0000000..9357eac
--- /dev/null
+++ b/_debug_start.bat
@@ -0,0 +1,153 @@
+@echo on
+setlocal EnableExtensions
+chcp 65001 >nul
+cd /d "%~dp0"
+
+rem =====================================================================
+rem zcode-hub Windows launcher -- feature parity with start.sh (Linux)
+rem
+rem Usage:
+rem start.bat Start (auto-stops a running instance first)
+rem start.bat stop Stop
+rem
+rem Notes:
+rem - Runs the server hidden in background; logs: serve.log + serve.err.log
+rem - Health check via GET /meta after start
+rem - Console messages are ASCII on purpose (avoids codepage mojibake)
+rem =====================================================================
+
+rem -- Edit as needed ----------------------------------------------------
+rem Upstream proxy (Clash mixed port); leave empty "" for direct connection
+set "PROXY=http://127.0.0.1:7890"
+rem ----------------------------------------------------------------------
+
+rem Strip ALL_PROXY (on Windows var names are case-insensitive, so this
+rem also covers all_proxy): Clash may inject socks:// there, which httpx
+rem and pip cannot use without PySocks -> every request fails with 500.
+rem Parity with start.sh (commit e3222ef).
+set "ALL_PROXY="
+
+set "PORT=3000"
+if exist .env for /f "tokens=1,* delims==" %%a in ('findstr /b /c:"ZCODE_PORT=" .env') do set "PORT=%%b"
+if defined PORT set "PORT=%PORT: =%"
+if not defined PORT set "PORT=3000"
+
+rem Pause on exit only when launched by double-click (cmd /c), never in scripts
+set "DBLCLK=0"
+echo %cmdcmdline% | find /i "/c" >nul 2>&1 && set "DBLCLK=1"
+
+if /i "%~1"=="stop" (
+ call :stop_instance
+ echo [OK] Stopped.
+ if "%DBLCLK%"=="1" pause
+ exit /b 0
+)
+
+rem -- 1. venv + Python dependencies -------------------------------------
+if not exist ".venv\Scripts\python.exe" (
+ where python >nul 2>&1
+ if errorlevel 1 (
+ echo [!] python not found on PATH. Install Python 3.11+ first.
+ goto :fail
+ )
+ echo [*] Creating venv directory .venv ...
+ python -m venv .venv || goto :fail
+)
+".venv\Scripts\python.exe" -c "import fastapi, uvicorn, httpx" >nul 2>&1
+if errorlevel 1 (
+ echo [*] Installing Python dependencies ...
+ if defined PROXY (
+ ".venv\Scripts\python.exe" -m pip install -q --proxy "%PROXY%" -r requirements.txt || goto :fail
+ ) else (
+ ".venv\Scripts\python.exe" -m pip install -q -r requirements.txt || goto :fail
+ )
+)
+
+rem -- 2. captcha_node (npm) dependencies --------------------------------
+if not exist "captcha_node\node_modules" (
+ echo [*] Installing captcha_node dependencies (npm install) ...
+ pushd captcha_node
+ call npm install --no-audit --no-fund
+ if errorlevel 1 (
+ popd
+ goto :fail
+ )
+ popd
+)
+
+rem -- 3. Stop old instance (restart semantics, clears admin lock too) ---
+call :stop_instance
+
+rem -- 4. Port must be free ----------------------------------------------
+set "OCCUPY="
+for /f "tokens=5" %%p in ('netstat -ano ^| findstr /r /c:":%PORT% .*LISTENING"') do set "OCCUPY=%%p"
+if defined OCCUPY (
+ echo [!] Port %PORT% still occupied by another process ^(PID %OCCUPY%^):
+ netstat -ano | findstr /r /c:":%PORT% .*LISTENING"
+ echo Stop it manually first, e.g.: taskkill /F /PID %OCCUPY%
+ goto :fail
+)
+
+rem -- 5. Start hidden in background, capture real python PID ------------
+echo [*] Starting zcode-hub (port=%PORT%) ...
+if defined PROXY (
+ set "HTTP_PROXY=%PROXY%/"
+ set "HTTPS_PROXY=%PROXY%/"
+)
+powershell -NoProfile -Command "$d=(Get-Location).Path; $p = Start-Process -FilePath (Join-Path $d '.venv\Scripts\python.exe') -ArgumentList 'cli.py','serve' -WorkingDirectory $d -WindowStyle Hidden -RedirectStandardOutput (Join-Path $d 'serve.log') -RedirectStandardError (Join-Path $d 'serve.err.log') -PassThru; Write-Output $p.Id" > "%TEMP%\zcode_hub_pid.txt" 2>nul
+set "NEWPID="
+set /p NEWPID=<"%TEMP%\zcode_hub_pid.txt"
+del "%TEMP%\zcode_hub_pid.txt" >nul 2>&1
+if not defined NEWPID (
+ echo [!] Failed to launch python process.
+ goto :fail
+)
+(echo %NEWPID%)>"serve.pid"
+
+rem -- 6. Health check (up to ~15s) --------------------------------------
+set /a TRIES=0
+:health_wait
+ping -n 2 127.0.0.1 >nul
+curl -sf -m 2 "http://127.0.0.1:%PORT%/meta" >nul 2>&1 && goto :started
+set /a TRIES+=1
+if %TRIES% lss 15 goto :health_wait
+
+echo [!] Start FAILED. Last log lines:
+echo --- serve.log ---
+powershell -NoProfile -Command "if (Test-Path serve.log) { Get-Content serve.log -Tail 15 }"
+echo --- serve.err.log ---
+powershell -NoProfile -Command "if (Test-Path serve.err.log) { Get-Content serve.err.log -Tail 15 }"
+goto :fail
+
+:started
+echo [OK] Started: http://127.0.0.1:%PORT%
+echo Admin UI : http://127.0.0.1:%PORT%/admin/login
+echo Logs : serve.log + serve.err.log
+echo Stop : start.bat stop Restart: just run start.bat again
+if "%DBLCLK%"=="1" pause
+exit /b 0
+
+:fail
+echo [!] start.bat aborted.
+if "%DBLCLK%"=="1" pause
+exit /b 1
+
+:stop_instance
+set "OLDPID="
+if exist serve.pid for /f %%p in (serve.pid) do set "OLDPID=%%p"
+if not defined OLDPID goto :stop_cleanup
+tasklist /FI "PID eq %OLDPID%" /NH 2>nul | find /i "python.exe" >nul 2>&1 || goto :stop_cleanup
+echo [*] Stopping old instance (pid=%OLDPID%) ...
+taskkill /PID %OLDPID% >nul 2>&1
+set /a N=0
+:stop_wait
+tasklist /FI "PID eq %OLDPID%" /NH 2>nul | find /i "python.exe" >nul 2>&1 || goto :stop_cleanup
+set /a N+=1
+if %N% geq 10 goto :stop_force
+ping -n 2 127.0.0.1 >nul
+goto :stop_wait
+:stop_force
+taskkill /F /PID %OLDPID% >nul 2>&1
+:stop_cleanup
+if exist serve.pid del serve.pid >nul 2>&1
+goto :eof
diff --git a/start.bat b/start.bat
index ee8bc63..3e6b0a9 100644
--- a/start.bat
+++ b/start.bat
@@ -1,147 +1,6 @@
@echo off
-setlocal EnableExtensions
chcp 65001 >nul
cd /d "%~dp0"
-
-rem =====================================================================
-rem zcode-hub Windows launcher -- feature parity with start.sh (Linux)
-rem
-rem Usage:
-rem start.bat Start (auto-stops a running instance first)
-rem start.bat stop Stop
-rem
-rem Notes:
-rem - Runs the server hidden in background; logs: serve.log + serve.err.log
-rem - Health check via GET /meta after start
-rem - Console messages are ASCII on purpose (avoids codepage mojibake)
-rem =====================================================================
-
-rem -- Edit as needed ----------------------------------------------------
-rem Upstream proxy (Clash mixed port); leave empty "" for direct connection
-set "PROXY=http://127.0.0.1:7890"
-rem ----------------------------------------------------------------------
-
-set "PORT=3000"
-if exist .env for /f "tokens=1,* delims==" %%a in ('findstr /b /c:"ZCODE_PORT=" .env') do set "PORT=%%b"
-if defined PORT set "PORT=%PORT: =%"
-if not defined PORT set "PORT=3000"
-
-rem Pause on exit only when launched by double-click (cmd /c), never in scripts
-set "DBLCLK=0"
-echo %cmdcmdline% | find /i "/c" >nul 2>&1 && set "DBLCLK=1"
-
-if /i "%~1"=="stop" (
- call :stop_instance
- echo [OK] Stopped.
- if "%DBLCLK%"=="1" pause
- exit /b 0
-)
-
-rem -- 1. venv + Python dependencies -------------------------------------
-if not exist ".venv\Scripts\python.exe" (
- where python >nul 2>&1
- if errorlevel 1 (
- echo [!] python not found on PATH. Install Python 3.11+ first.
- goto :fail
- )
- echo [*] Creating venv (.venv) ...
- python -m venv .venv || goto :fail
-)
-".venv\Scripts\python.exe" -c "import fastapi, uvicorn, httpx" >nul 2>&1
-if errorlevel 1 (
- echo [*] Installing Python dependencies ...
- if defined PROXY (
- ".venv\Scripts\python.exe" -m pip install -q --proxy "%PROXY%" -r requirements.txt || goto :fail
- ) else (
- ".venv\Scripts\python.exe" -m pip install -q -r requirements.txt || goto :fail
- )
-)
-
-rem -- 2. captcha_node (npm) dependencies --------------------------------
-if not exist "captcha_node\node_modules" (
- echo [*] Installing captcha_node dependencies (npm install) ...
- pushd captcha_node
- call npm install --no-audit --no-fund
- if errorlevel 1 (
- popd
- goto :fail
- )
- popd
-)
-
-rem -- 3. Stop old instance (restart semantics, clears admin lock too) ---
-call :stop_instance
-
-rem -- 4. Port must be free ----------------------------------------------
-set "OCCUPY="
-for /f "tokens=5" %%p in ('netstat -ano ^| findstr /r /c:":%PORT% .*LISTENING"') do set "OCCUPY=%%p"
-if defined OCCUPY (
- echo [!] Port %PORT% still occupied by another process ^(PID %OCCUPY%^):
- netstat -ano | findstr /r /c:":%PORT% .*LISTENING"
- echo Stop it manually first, e.g.: taskkill /F /PID %OCCUPY%
- goto :fail
-)
-
-rem -- 5. Start hidden in background, capture real python PID ------------
-echo [*] Starting zcode-hub (port=%PORT%) ...
-if defined PROXY (
- set "HTTP_PROXY=%PROXY%/"
- set "HTTPS_PROXY=%PROXY%/"
-)
-powershell -NoProfile -Command "$d=(Get-Location).Path; $p = Start-Process -FilePath (Join-Path $d '.venv\Scripts\python.exe') -ArgumentList 'cli.py','serve' -WorkingDirectory $d -WindowStyle Hidden -RedirectStandardOutput (Join-Path $d 'serve.log') -RedirectStandardError (Join-Path $d 'serve.err.log') -PassThru; Write-Output $p.Id" > "%TEMP%\zcode_hub_pid.txt" 2>nul
-set "NEWPID="
-set /p NEWPID=<"%TEMP%\zcode_hub_pid.txt"
-del "%TEMP%\zcode_hub_pid.txt" >nul 2>&1
-if not defined NEWPID (
- echo [!] Failed to launch python process.
- goto :fail
-)
-(echo %NEWPID%)>"serve.pid"
-
-rem -- 6. Health check (up to ~15s) --------------------------------------
-set /a TRIES=0
-:health_wait
-ping -n 2 127.0.0.1 >nul
-curl -sf -m 2 "http://127.0.0.1:%PORT%/meta" >nul 2>&1 && goto :started
-set /a TRIES+=1
-if %TRIES% lss 15 goto :health_wait
-
-echo [!] Start FAILED. Last log lines:
-echo --- serve.log ---
-powershell -NoProfile -Command "if (Test-Path serve.log) { Get-Content serve.log -Tail 15 }"
-echo --- serve.err.log ---
-powershell -NoProfile -Command "if (Test-Path serve.err.log) { Get-Content serve.err.log -Tail 15 }"
-goto :fail
-
-:started
-echo [OK] Started: http://127.0.0.1:%PORT%
-echo Admin UI : http://127.0.0.1:%PORT%/admin/login
-echo Logs : serve.log + serve.err.log
-echo Stop : start.bat stop Restart: just run start.bat again
-if "%DBLCLK%"=="1" pause
-exit /b 0
-
-:fail
-echo [!] start.bat aborted.
-if "%DBLCLK%"=="1" pause
-exit /b 1
-
-:stop_instance
-set "OLDPID="
-if exist serve.pid for /f %%p in (serve.pid) do set "OLDPID=%%p"
-if not defined OLDPID goto :stop_cleanup
-tasklist /FI "PID eq %OLDPID%" /NH 2>nul | find /i "python.exe" >nul 2>&1 || goto :stop_cleanup
-echo [*] Stopping old instance (pid=%OLDPID%) ...
-taskkill /PID %OLDPID% >nul 2>&1
-set /a N=0
-:stop_wait
-tasklist /FI "PID eq %OLDPID%" /NH 2>nul | find /i "python.exe" >nul 2>&1 || goto :stop_cleanup
-set /a N+=1
-if %N% geq 10 goto :stop_force
-ping -n 2 127.0.0.1 >nul
-goto :stop_wait
-:stop_force
-taskkill /F /PID %OLDPID% >nul 2>&1
-:stop_cleanup
-if exist serve.pid del serve.pid >nul 2>&1
-goto :eof
+echo Starting zcode-hub on http://127.0.0.1:3000 ...
+".venv\Scripts\python.exe" cli.py serve
+pause
diff --git a/start.sh b/start.sh
index 15df39e..bb1eff7 100755
--- a/start.sh
+++ b/start.sh
@@ -1,95 +1,5 @@
#!/usr/bin/env bash
-# zcode-hub Linux 启动脚本(对应 Windows 的 start.bat)
-#
-# 用法:
-# ./start.sh 启动(若已在运行则先停旧实例再启动,同时清除后台登录失败锁定)
-# ./start.sh stop 停止
-set -euo pipefail
-
+# zcode-hub Linux 启动脚本(极简版,与 Windows 6 行 start.bat 逐行对应:前台运行、关终端即停)
cd "$(dirname "$(readlink -f "$0")")"
-
-# ── 可按需修改 ──────────────────────────────────────────────
-# 上游代理(Clash 混合端口);直连环境把它留空即可
-PROXY="http://127.0.0.1:7890"
-# ────────────────────────────────────────────────────────────
-
-PORT=$(grep -E '^ZCODE_PORT=' .env 2>/dev/null | cut -d= -f2 | tr -d '[:space:]')
-PORT=${PORT:-3000}
-
-stop_instance() {
- local old_pid
- old_pid=$(cat serve.pid 2>/dev/null || true)
- if [ -n "$old_pid" ] && kill -0 "$old_pid" 2>/dev/null; then
- echo "[*] 停止旧实例 (pid=$old_pid)..."
- kill "$old_pid" 2>/dev/null || true
- for _ in $(seq 1 10); do
- kill -0 "$old_pid" 2>/dev/null || break
- sleep 1
- done
- kill -9 "$old_pid" 2>/dev/null || true
- fi
- rm -f serve.pid
-}
-
-if [ "${1:-}" = "stop" ]; then
- stop_instance
- echo "[✔] 已停止"
- exit 0
-fi
-
-# 1. Python 虚拟环境:缺失则创建,依赖缺失则安装
-if [ ! -x .venv/bin/python ]; then
- echo "[*] 创建虚拟环境 (.venv)..."
- python3 -m venv .venv
-fi
-if ! .venv/bin/python -c "import fastapi, uvicorn, httpx" 2>/dev/null; then
- echo "[*] 安装 Python 依赖..."
- if [ -n "$PROXY" ]; then
- .venv/bin/pip install -q --proxy "$PROXY" -r requirements.txt
- else
- .venv/bin/pip install -q -r requirements.txt
- fi
-fi
-
-# 2. 验证码求解 Node 依赖(JWT 账号需要)
-if [ ! -d captcha_node/node_modules ]; then
- echo "[*] 安装 captcha_node 依赖 (npm install)..."
- (cd captcha_node && npm install --no-audit --no-fund)
-fi
-
-# 3. 已在运行则先停(重启同时清掉内存里的后台登录失败锁定)
-stop_instance
-
-# 4. 端口仍被其他进程占用则报错
-if ss -tln 2>/dev/null | grep -q ":${PORT} "; then
- echo "[!] 端口 ${PORT} 仍被其他进程占用,请手动处理:" >&2
- ss -tlnp 2>/dev/null | grep ":${PORT} " >&2 || true
- exit 1
-fi
-
-# 5. 启动。剔除环境里已有的 ALL_PROXY/socks(Clash 系统代理常注入 socks://,
-# httpx 缺 socks 支持会报 Unknown scheme → 全部请求 500);仅留 HTTP(S)_PROXY
-unset ALL_PROXY all_proxy
-echo "[*] 启动 zcode-hub (port=${PORT})..."
-if [ -n "$PROXY" ]; then
- env "HTTP_PROXY=${PROXY}/" "HTTPS_PROXY=${PROXY}/" \
- nohup .venv/bin/python cli.py serve > serve.log 2>&1 &
-else
- nohup .venv/bin/python cli.py serve > serve.log 2>&1 &
-fi
-echo $! > serve.pid
-
-# 6. 健康检查
-for _ in $(seq 1 15); do
- sleep 1
- if curl -sf -m 2 "http://127.0.0.1:${PORT}/meta" >/dev/null 2>&1; then
- echo "[✔] 已启动: http://127.0.0.1:${PORT}"
- echo " 后台: http://127.0.0.1:${PORT}/admin/login"
- echo " 日志: serve.log 停止: ./start.sh stop"
- exit 0
- fi
-done
-
-echo "[!] 启动失败,最近日志:" >&2
-tail -20 serve.log >&2
-exit 1
+echo "Starting zcode-hub on http://127.0.0.1:3000 ..."
+.venv/bin/python cli.py serve
From 69c19badbf842cef5c84f3fb2bd133cd436a9548 Mon Sep 17 00:00:00 2001
From: jk <2081217232@qq.com>
Date: Sun, 4 Oct 2026 14:32:35 +0800
Subject: [PATCH 12/14] =?UTF-8?q?fix(scripts):=20start.sh=20=E4=BB=A3?=
=?UTF-8?q?=E7=90=86=E5=85=9C=E5=BA=95=E6=9D=A1=E4=BB=B6=E5=8C=96=E2=80=94?=
=?UTF-8?q?=E2=80=94ALL=5FPROXY=20=E4=B8=BA=E9=9D=9E=20http(s)=20scheme?=
=?UTF-8?q?=EF=BC=88=E5=A6=82=20Clash=20=E6=B3=A8=E5=85=A5=E7=9A=84=20sock?=
=?UTF-8?q?s://=EF=BC=89=E6=97=B6=E6=89=8D=E5=89=94=E9=99=A4=EF=BC=8C?=
=?UTF-8?q?=E6=AD=A3=E5=B8=B8=E7=8E=AF=E5=A2=83=E5=8E=9F=E6=A0=B7=E5=90=AF?=
=?UTF-8?q?=E5=8A=A8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
start.sh | 14 +++++++++++++-
1 file changed, 13 insertions(+), 1 deletion(-)
diff --git a/start.sh b/start.sh
index bb1eff7..cf56997 100755
--- a/start.sh
+++ b/start.sh
@@ -1,5 +1,17 @@
#!/usr/bin/env bash
-# zcode-hub Linux 启动脚本(极简版,与 Windows 6 行 start.bat 逐行对应:前台运行、关终端即停)
+# zcode-hub Linux 启动脚本(极简版 + 代理兜底:前台运行、关终端即停)
cd "$(dirname "$(readlink -f "$0")")"
+
+# 代理兜底(对齐 e3222ef):Clash 系统代理可能注入 socks:// 形式的 ALL_PROXY,
+# httpx 不支持该 scheme,会导致全部上游请求 500。仅在检测到非 http(s) 值时剔除,
+# 正常环境原样启动。
+for v in ALL_PROXY all_proxy; do
+ val="${!v:-}"
+ case "$val" in
+ ""|http://*|https://*) ;;
+ *) unset "$v"; echo "[i] 已剔除 $v=$val(httpx 不支持该代理 scheme)" ;;
+ esac
+done
+
echo "Starting zcode-hub on http://127.0.0.1:3000 ..."
.venv/bin/python cli.py serve
From b7df3ce09c2dcee3feafb1f9a22b7db03413aefb Mon Sep 17 00:00:00 2001
From: JK <2081217232@qq.com>
Date: Sun, 4 Oct 2026 14:55:46 +0800
Subject: [PATCH 13/14] =?UTF-8?q?fix(scripts):=20start.bat=20=E4=BB=A3?=
=?UTF-8?q?=E7=90=86=E5=85=9C=E5=BA=95=E6=9D=A1=E4=BB=B6=E5=8C=96=E2=80=94?=
=?UTF-8?q?=E2=80=94=E4=B8=8E=20start.sh=20=E5=AF=B9=E9=BD=90=EF=BC=9AALL?=
=?UTF-8?q?=5FPROXY=20=E4=B8=BA=E9=9D=9E=20http(s)=20scheme=EF=BC=88?=
=?UTF-8?q?=E5=A6=82=20Clash=20=E6=B3=A8=E5=85=A5=E7=9A=84=20socks://?=
=?UTF-8?q?=EF=BC=89=E6=97=B6=E6=89=8D=E5=89=94=E9=99=A4=EF=BC=8C=E6=AD=A3?=
=?UTF-8?q?=E5=B8=B8=E7=8E=AF=E5=A2=83=E5=8E=9F=E6=A0=B7=E5=90=AF=E5=8A=A8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 延迟展开(EnableDelayedExpansion):未定义变量的 %VAR:~0,7% 会在解析期坍缩破坏 if,!VAR! 执行期展开安全
- 扁平顺序判断:管道+|| 嵌套在 if 块内会解析崩坏;输出保持 ASCII(cmd 代码页下中文回显必乱码)
- Windows 变量名不区分大小写,ALL_PROXY 即覆盖 all_proxy
---
start.bat | 20 ++++++++++++++++++++
1 file changed, 20 insertions(+)
diff --git a/start.bat b/start.bat
index 3e6b0a9..8dec23e 100644
--- a/start.bat
+++ b/start.bat
@@ -1,6 +1,26 @@
@echo off
+setlocal EnableDelayedExpansion
chcp 65001 >nul
cd /d "%~dp0"
+
+rem Proxy fallback (parity with start.sh / e3222ef): Clash system proxy may
+rem inject a socks:// ALL_PROXY, which httpx cannot use -> every upstream
+rem request 500s. Strip it only when the scheme is not http(s). On Windows
+rem var names are case-insensitive, so ALL_PROXY also covers all_proxy.
+rem Delayed expansion is required: %VAR:~0,7% of an undefined VAR collapses
+rem at parse time and breaks the if statement; !VAR! expands safely at run.
+rem Keep console output ASCII-only: Chinese in bat echo mojibakes badly.
+set "STRIP=0"
+if defined ALL_PROXY (
+ set "STRIP=1"
+ if /i "!ALL_PROXY:~0,7!"=="http://" set "STRIP=0"
+ if /i "!ALL_PROXY:~0,8!"=="https://" set "STRIP=0"
+)
+if "!STRIP!"=="1" (
+ echo [i] Stripped ALL_PROXY=!ALL_PROXY! - httpx does not support this proxy scheme
+ set "ALL_PROXY="
+)
+
echo Starting zcode-hub on http://127.0.0.1:3000 ...
".venv\Scripts\python.exe" cli.py serve
pause
From 4df382c24482371313e4e87f4bdc1457ee780e30 Mon Sep 17 00:00:00 2001
From: JK <2081217232@qq.com>
Date: Sun, 4 Oct 2026 14:55:46 +0800
Subject: [PATCH 14/14] =?UTF-8?q?chore:=20=E6=B8=85=E7=90=86=E8=AF=AF?=
=?UTF-8?q?=E6=8F=90=E4=BA=A4=E7=9A=84=E8=B0=83=E8=AF=95=E6=AE=8B=E7=95=99?=
=?UTF-8?q?=EF=BC=88=5Fdebug=5Fstart.bat/=5Fdebug=5Fout.txt=EF=BC=8C?=
=?UTF-8?q?=E9=9A=8F=201d599ed=20=E8=AF=AF=E5=85=A5=E5=BA=93=EF=BC=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
_debug_out.txt | 95 -----------------------------
_debug_start.bat | 153 -----------------------------------------------
2 files changed, 248 deletions(-)
delete mode 100644 _debug_out.txt
delete mode 100644 _debug_start.bat
diff --git a/_debug_out.txt b/_debug_out.txt
deleted file mode 100644
index 768b92b..0000000
--- a/_debug_out.txt
+++ /dev/null
@@ -1,95 +0,0 @@
-
-D:\zcode2api>setlocal EnableExtensions
-
-D:\zcode2api>chcp 65001 1>nul
-
-D:\zcode2api>cd /d "D:\zcode2api\"
-
-D:\zcode2api>rem =====================================================================
-
-D:\zcode2api>rem zcode-hub Windows launcher -- feature parity with start.sh (Linux)
-
-D:\zcode2api>rem
-
-D:\zcode2api>rem Usage:
-
-D:\zcode2api>rem start.bat Start (auto-stops a running instance first)
-
-D:\zcode2api>rem start.bat stop Stop
-
-D:\zcode2api>rem
-
-D:\zcode2api>rem Notes:
-
-D:\zcode2api>rem - Runs the server hidden in background; logs: serve.log + serve.err.log
-
-D:\zcode2api>rem - Health check via GET /meta after start
-
-D:\zcode2api>rem - Console messages are ASCII on purpose (avoids codepage mojibake)
-
-D:\zcode2api>rem =====================================================================
-
-D:\zcode2api>rem -- Edit as needed ----------------------------------------------------
-
-D:\zcode2api>rem Upstream proxy (Clash mixed port); leave empty "" for direct connection
-
-D:\zcode2api>set "PROXY=http://127.0.0.1:7890"
-
-D:\zcode2api>rem ----------------------------------------------------------------------
-
-D:\zcode2api>rem Strip ALL_PROXY (on Windows var names are case-insensitive, so this
-
-D:\zcode2api>rem also covers all_proxy): Clash may inject socks:// there, which httpx
-
-D:\zcode2api>rem and pip cannot use without PySocks -> every request fails with 500.
-
-D:\zcode2api>rem Parity with start.sh (commit e3222ef).
-
-D:\zcode2api>set "ALL_PROXY="
-
-D:\zcode2api>set "PORT=3000"
-
-D:\zcode2api>if exist .env for /F "tokens=1,* delims==" %a in ('findstr /b /c:"ZCODE_PORT=" .env') do set "PORT=%b"
-
-D:\zcode2api>set "PORT=3000"
-
-D:\zcode2api>if defined PORT set "PORT=3000"
-
-D:\zcode2api>if not defined PORT set "PORT=3000"
-
-D:\zcode2api>rem Pause on exit only when launched by double-click (cmd /c), never in scripts
-
-D:\zcode2api>set "DBLCLK=0"
-
-D:\zcode2api>echo C:\Windows\system32\cmd.exe /c D:\zcode2api\_debug_start.bat | find /i "/c" 1>nul 2>&1 && set "DBLCLK=1"
-
-D:\zcode2api>if /I "" == "stop" (
-call :stop_instance
- echo [OK] Stopped.
- if "0" == "1" pause
- exit /b 0
-)
-
-D:\zcode2api>rem -- 1. venv + Python dependencies -------------------------------------
-
-D:\zcode2api>if not exist ".venv\Scripts\python.exe" (
-where python 1>nul 2>&1
- if errorlevel 1 (
-echo [!] python not found on PATH. Install Python 3.11+ first.
- goto :fail
-)
- echo [*] Creating venv directory .venv ...
- python -m venv .venv || goto :fail
-)
-
-D:\zcode2api>".venv\Scripts\python.exe" -c "import fastapi, uvicorn, httpx" 1>nul 2>&1
-
-D:\zcode2api>if errorlevel 1 (
-echo [*] Installing Python dependencies ...
- if defined PROXY (".venv\Scripts\python.exe" -m pip install -q --proxy "http://127.0.0.1:7890" -r requirements.txt || goto :fail ) else (".venv\Scripts\python.exe" -m pip install -q -r requirements.txt || goto :fail )
-)
-
-D:\zcode2api>rem -- 2. captcha_node (npm) dependencies --------------------------------
-... was unexpected at this time.
-
-D:\zcode2api> echo [*] Installing captcha_node dependencies (npm install) ...
diff --git a/_debug_start.bat b/_debug_start.bat
deleted file mode 100644
index 9357eac..0000000
--- a/_debug_start.bat
+++ /dev/null
@@ -1,153 +0,0 @@
-@echo on
-setlocal EnableExtensions
-chcp 65001 >nul
-cd /d "%~dp0"
-
-rem =====================================================================
-rem zcode-hub Windows launcher -- feature parity with start.sh (Linux)
-rem
-rem Usage:
-rem start.bat Start (auto-stops a running instance first)
-rem start.bat stop Stop
-rem
-rem Notes:
-rem - Runs the server hidden in background; logs: serve.log + serve.err.log
-rem - Health check via GET /meta after start
-rem - Console messages are ASCII on purpose (avoids codepage mojibake)
-rem =====================================================================
-
-rem -- Edit as needed ----------------------------------------------------
-rem Upstream proxy (Clash mixed port); leave empty "" for direct connection
-set "PROXY=http://127.0.0.1:7890"
-rem ----------------------------------------------------------------------
-
-rem Strip ALL_PROXY (on Windows var names are case-insensitive, so this
-rem also covers all_proxy): Clash may inject socks:// there, which httpx
-rem and pip cannot use without PySocks -> every request fails with 500.
-rem Parity with start.sh (commit e3222ef).
-set "ALL_PROXY="
-
-set "PORT=3000"
-if exist .env for /f "tokens=1,* delims==" %%a in ('findstr /b /c:"ZCODE_PORT=" .env') do set "PORT=%%b"
-if defined PORT set "PORT=%PORT: =%"
-if not defined PORT set "PORT=3000"
-
-rem Pause on exit only when launched by double-click (cmd /c), never in scripts
-set "DBLCLK=0"
-echo %cmdcmdline% | find /i "/c" >nul 2>&1 && set "DBLCLK=1"
-
-if /i "%~1"=="stop" (
- call :stop_instance
- echo [OK] Stopped.
- if "%DBLCLK%"=="1" pause
- exit /b 0
-)
-
-rem -- 1. venv + Python dependencies -------------------------------------
-if not exist ".venv\Scripts\python.exe" (
- where python >nul 2>&1
- if errorlevel 1 (
- echo [!] python not found on PATH. Install Python 3.11+ first.
- goto :fail
- )
- echo [*] Creating venv directory .venv ...
- python -m venv .venv || goto :fail
-)
-".venv\Scripts\python.exe" -c "import fastapi, uvicorn, httpx" >nul 2>&1
-if errorlevel 1 (
- echo [*] Installing Python dependencies ...
- if defined PROXY (
- ".venv\Scripts\python.exe" -m pip install -q --proxy "%PROXY%" -r requirements.txt || goto :fail
- ) else (
- ".venv\Scripts\python.exe" -m pip install -q -r requirements.txt || goto :fail
- )
-)
-
-rem -- 2. captcha_node (npm) dependencies --------------------------------
-if not exist "captcha_node\node_modules" (
- echo [*] Installing captcha_node dependencies (npm install) ...
- pushd captcha_node
- call npm install --no-audit --no-fund
- if errorlevel 1 (
- popd
- goto :fail
- )
- popd
-)
-
-rem -- 3. Stop old instance (restart semantics, clears admin lock too) ---
-call :stop_instance
-
-rem -- 4. Port must be free ----------------------------------------------
-set "OCCUPY="
-for /f "tokens=5" %%p in ('netstat -ano ^| findstr /r /c:":%PORT% .*LISTENING"') do set "OCCUPY=%%p"
-if defined OCCUPY (
- echo [!] Port %PORT% still occupied by another process ^(PID %OCCUPY%^):
- netstat -ano | findstr /r /c:":%PORT% .*LISTENING"
- echo Stop it manually first, e.g.: taskkill /F /PID %OCCUPY%
- goto :fail
-)
-
-rem -- 5. Start hidden in background, capture real python PID ------------
-echo [*] Starting zcode-hub (port=%PORT%) ...
-if defined PROXY (
- set "HTTP_PROXY=%PROXY%/"
- set "HTTPS_PROXY=%PROXY%/"
-)
-powershell -NoProfile -Command "$d=(Get-Location).Path; $p = Start-Process -FilePath (Join-Path $d '.venv\Scripts\python.exe') -ArgumentList 'cli.py','serve' -WorkingDirectory $d -WindowStyle Hidden -RedirectStandardOutput (Join-Path $d 'serve.log') -RedirectStandardError (Join-Path $d 'serve.err.log') -PassThru; Write-Output $p.Id" > "%TEMP%\zcode_hub_pid.txt" 2>nul
-set "NEWPID="
-set /p NEWPID=<"%TEMP%\zcode_hub_pid.txt"
-del "%TEMP%\zcode_hub_pid.txt" >nul 2>&1
-if not defined NEWPID (
- echo [!] Failed to launch python process.
- goto :fail
-)
-(echo %NEWPID%)>"serve.pid"
-
-rem -- 6. Health check (up to ~15s) --------------------------------------
-set /a TRIES=0
-:health_wait
-ping -n 2 127.0.0.1 >nul
-curl -sf -m 2 "http://127.0.0.1:%PORT%/meta" >nul 2>&1 && goto :started
-set /a TRIES+=1
-if %TRIES% lss 15 goto :health_wait
-
-echo [!] Start FAILED. Last log lines:
-echo --- serve.log ---
-powershell -NoProfile -Command "if (Test-Path serve.log) { Get-Content serve.log -Tail 15 }"
-echo --- serve.err.log ---
-powershell -NoProfile -Command "if (Test-Path serve.err.log) { Get-Content serve.err.log -Tail 15 }"
-goto :fail
-
-:started
-echo [OK] Started: http://127.0.0.1:%PORT%
-echo Admin UI : http://127.0.0.1:%PORT%/admin/login
-echo Logs : serve.log + serve.err.log
-echo Stop : start.bat stop Restart: just run start.bat again
-if "%DBLCLK%"=="1" pause
-exit /b 0
-
-:fail
-echo [!] start.bat aborted.
-if "%DBLCLK%"=="1" pause
-exit /b 1
-
-:stop_instance
-set "OLDPID="
-if exist serve.pid for /f %%p in (serve.pid) do set "OLDPID=%%p"
-if not defined OLDPID goto :stop_cleanup
-tasklist /FI "PID eq %OLDPID%" /NH 2>nul | find /i "python.exe" >nul 2>&1 || goto :stop_cleanup
-echo [*] Stopping old instance (pid=%OLDPID%) ...
-taskkill /PID %OLDPID% >nul 2>&1
-set /a N=0
-:stop_wait
-tasklist /FI "PID eq %OLDPID%" /NH 2>nul | find /i "python.exe" >nul 2>&1 || goto :stop_cleanup
-set /a N+=1
-if %N% geq 10 goto :stop_force
-ping -n 2 127.0.0.1 >nul
-goto :stop_wait
-:stop_force
-taskkill /F /PID %OLDPID% >nul 2>&1
-:stop_cleanup
-if exist serve.pid del serve.pid >nul 2>&1
-goto :eof