@lightharu/krouter 1.8.0 → 1.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,128 @@
1
+ # Kiro 账户管理器 v1.3.8 更新说明
2
+
3
+ 发布日期:2025-01-18
4
+
5
+ ## 🏢 IAM Identity Center SSO 登录
6
+
7
+ ### 新增组织身份登录
8
+ - **登录入口**:添加账户对话框新增"组织身份"登录按钮
9
+ - **SSO Start URL**:支持输入组织的 SSO Start URL 进行认证
10
+ - **设备授权流程**:采用 AWS IAM Identity Center 设备授权流程
11
+
12
+ ### AWS Region 支持
13
+ 支持 20+ 个 AWS 区域,按地区分组:
14
+
15
+ | 地区 | 支持的 Region |
16
+ |------|--------------|
17
+ | 美国 | us-east-1, us-east-2, us-west-1, us-west-2 |
18
+ | 欧洲 | eu-west-1, eu-west-2, eu-west-3, eu-central-1, eu-north-1, eu-south-1 |
19
+ | 亚太 | ap-northeast-1, ap-northeast-2, ap-northeast-3, ap-southeast-1, ap-southeast-2, ap-south-1, ap-east-1 |
20
+ | 其他 | ca-central-1, sa-east-1, me-south-1, af-south-1 |
21
+
22
+ ## 🏷️ Enterprise Provider 支持
23
+
24
+ ### OIDC 凭证导入
25
+ - **单个导入**:登录类型新增三个选项:Builder ID | 组织身份 | Social
26
+ - **批量导入**:支持 `provider: "Enterprise"` 字段
27
+
28
+ ### 支持的 Provider 类型
29
+
30
+ | Provider | 说明 | authMethod |
31
+ |----------|------|------------|
32
+ | `BuilderId` | AWS Builder ID | IdC |
33
+ | `Enterprise` | 组织身份 (IAM SSO) | IdC |
34
+ | `Google` | Google 登录 | social |
35
+ | `Github` | GitHub 登录 | social |
36
+
37
+ ### 批量导入 JSON 示例
38
+ ```json
39
+ [
40
+ {
41
+ "refreshToken": "xxx",
42
+ "clientId": "xxx",
43
+ "clientSecret": "xxx",
44
+ "provider": "BuilderId"
45
+ },
46
+ {
47
+ "refreshToken": "yyy",
48
+ "clientId": "yyy",
49
+ "clientSecret": "yyy",
50
+ "provider": "Enterprise"
51
+ },
52
+ {
53
+ "refreshToken": "zzz",
54
+ "provider": "Github"
55
+ },
56
+ {
57
+ "refreshToken": "aaa",
58
+ "provider": "Google"
59
+ }
60
+ ]
61
+ ```
62
+
63
+ ## 🔄 一键切号兼容性
64
+
65
+ ### 账户切换支持
66
+ - 完全兼容 Enterprise 和 IAM_SSO 身份类型
67
+ - 切换账户时正确传递 provider 信息
68
+
69
+ ## 📊 统计功能增强
70
+
71
+ ### 账户统计
72
+ - `byIdp` 统计新增 `Enterprise` 和 `IAM_SSO` 类型
73
+ - 账户卡片正确显示身份提供商标签
74
+
75
+ ## 📌 系统托盘增强
76
+
77
+ ### 托盘图标优化
78
+ - **外部图标支持**:托盘菜单图标改用外部 PNG 文件,支持自定义替换
79
+ - **图标缓存**:图标加载后缓存,提升菜单响应速度
80
+ - **新增图标**:用量、请求数等信息现有专属图标
81
+
82
+ ### 托盘状态同步
83
+ - **界面操作同步**:在软件界面启动/停止代理服务时,托盘状态实时同步更新
84
+
85
+ ### 关闭确认对话框
86
+ - **自定义对话框**:关闭窗口时显示自定义确认对话框
87
+ - **记住选择**:支持记住用户的关闭操作选择
88
+
89
+ ## 📝 代码变更
90
+
91
+ ### 类型定义 (account.ts)
92
+ - `IdpType` 新增 `Enterprise` 和 `IAM_SSO`
93
+ - `AccountCredentials.provider` 支持新类型
94
+
95
+ ### 主进程 (index.ts)
96
+ - 新增 `start-iam-sso-login` IPC handler
97
+ - 新增 `poll-iam-sso-auth` IPC handler
98
+ - 新增 `cancel-iam-sso-login` IPC handler
99
+ - `currentLoginState.type` 支持 `'iamsso'`
100
+
101
+ ### Preload API (index.ts, index.d.ts)
102
+ - 新增 `startIamSsoLogin(startUrl, region)` 方法
103
+ - 新增 `pollIamSsoAuth(region)` 方法
104
+ - 新增 `cancelIamSsoLogin()` 方法
105
+
106
+ ### 添加账户对话框 (AddAccountDialog.tsx)
107
+ - `LoginType` 新增 `'iamsso'`
108
+ - 新增 SSO Start URL 输入框
109
+ - 新增 AWS Region 下拉选择
110
+ - 登录类型按钮:Builder ID | 组织身份 | Social
111
+ - 批量导入 JSON 示例更新
112
+
113
+ ### 账户存储 (accounts.ts)
114
+ - `byIdp` 统计支持 Enterprise 和 IAM_SSO
115
+
116
+ ---
117
+
118
+ **完整更新列表**:
119
+ - 🏢 IAM Identity Center SSO 登录
120
+ - 🔗 SSO Start URL 输入
121
+ - 🌍 20+ AWS Region 选择
122
+ - 🏷️ Enterprise Provider 支持
123
+ - 📦 批量导入增强
124
+ - 🔄 一键切号兼容
125
+ - 📊 统计功能增强
126
+ - 📌 托盘图标优化
127
+ - 🔄 托盘状态同步
128
+ - 📝 关闭确认对话框
@@ -0,0 +1,42 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="1120" height="620" viewBox="0 0 1120 620" role="img" aria-label="API key and client setup">
2
+ <rect width="1120" height="620" rx="28" fill="#f8fafc"/>
3
+ <text x="56" y="72" fill="#0f172a" font-family="Inter,Segoe UI,Arial" font-size="34" font-weight="800">Create Key And Connect Clients</text>
4
+ <text x="56" y="106" fill="#64748b" font-family="Inter,Segoe UI,Arial" font-size="17">Use one Krouter key for OpenClaw and OpenAI-compatible tools.</text>
5
+ <g font-family="Inter,Segoe UI,Arial">
6
+ <rect x="64" y="164" width="300" height="340" rx="22" fill="#ffffff" stroke="#cbd5e1"/>
7
+ <circle cx="108" cy="218" r="22" fill="#dbeafe"/>
8
+ <text x="96" y="226" fill="#2563eb" font-size="24" font-weight="800">1</text>
9
+ <text x="140" y="222" fill="#0f172a" font-size="21" font-weight="800">Create key</text>
10
+ <rect x="96" y="270" width="236" height="44" rx="12" fill="#0f172a"/>
11
+ <text x="116" y="298" fill="#dbeafe" font-size="15">sk-krouter-********</text>
12
+ <text x="96" y="356" fill="#475569" font-size="15">Dashboard -> API Proxy</text>
13
+ <text x="96" y="384" fill="#475569" font-size="15">Configure Clients</text>
14
+ <text x="96" y="412" fill="#475569" font-size="15">Create API Key</text>
15
+ </g>
16
+ <g font-family="Inter,Segoe UI,Arial">
17
+ <rect x="410" y="164" width="300" height="340" rx="22" fill="#ffffff" stroke="#cbd5e1"/>
18
+ <circle cx="454" cy="218" r="22" fill="#dcfce7"/>
19
+ <text x="442" y="226" fill="#16a34a" font-size="24" font-weight="800">2</text>
20
+ <text x="486" y="222" fill="#0f172a" font-size="21" font-weight="800">Set base URL</text>
21
+ <rect x="442" y="270" width="236" height="44" rx="12" fill="#ecfeff" stroke="#99f6e4"/>
22
+ <text x="462" y="298" fill="#0f766e" font-size="15">http://127.0.0.1:5580/v1</text>
23
+ <text x="442" y="356" fill="#475569" font-size="15">Provider: OpenAI compatible</text>
24
+ <text x="442" y="384" fill="#475569" font-size="15">Model: claude-sonnet-4.5</text>
25
+ <text x="442" y="412" fill="#475569" font-size="15">Header: Authorization Bearer</text>
26
+ </g>
27
+ <g font-family="Inter,Segoe UI,Arial">
28
+ <rect x="756" y="164" width="300" height="340" rx="22" fill="#ffffff" stroke="#cbd5e1"/>
29
+ <circle cx="800" cy="218" r="22" fill="#fef3c7"/>
30
+ <text x="788" y="226" fill="#d97706" font-size="24" font-weight="800">3</text>
31
+ <text x="832" y="222" fill="#0f172a" font-size="21" font-weight="800">Verify</text>
32
+ <rect x="788" y="270" width="224" height="44" rx="12" fill="#f0fdf4" stroke="#bbf7d0"/>
33
+ <text x="812" y="298" fill="#166534" font-size="15">GET /v1/models</text>
34
+ <text x="788" y="356" fill="#475569" font-size="15">Run /models in OpenClaw</text>
35
+ <text x="788" y="384" fill="#475569" font-size="15">Send a small chat test</text>
36
+ <text x="788" y="412" fill="#475569" font-size="15">Watch request logs</text>
37
+ </g>
38
+ <path d="M374 334 H398" stroke="#94a3b8" stroke-width="5" stroke-linecap="round"/>
39
+ <path d="M398 334 L388 324 M398 334 L388 344" stroke="#94a3b8" stroke-width="5" stroke-linecap="round"/>
40
+ <path d="M720 334 H744" stroke="#94a3b8" stroke-width="5" stroke-linecap="round"/>
41
+ <path d="M744 334 L734 324 M744 334 L734 344" stroke="#94a3b8" stroke-width="5" stroke-linecap="round"/>
42
+ </svg>
@@ -0,0 +1,37 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="1120" height="620" viewBox="0 0 1120 620" role="img" aria-label="API Proxy dashboard settings">
2
+ <rect width="1120" height="620" rx="28" fill="#eaf1ff"/>
3
+ <rect x="56" y="48" width="1008" height="98" rx="24" fill="#ffffff" stroke="#cbd5e1"/>
4
+ <rect x="82" y="76" width="54" height="54" rx="16" fill="#3b82f6"/>
5
+ <text x="158" y="98" fill="#2563eb" font-family="Inter,Segoe UI,Arial" font-size="28" font-weight="800">API Proxy Service</text>
6
+ <text x="158" y="126" fill="#64748b" font-family="Inter,Segoe UI,Arial" font-size="16">OpenAI, Claude and Gemini compatible endpoint with multi-account rotation</text>
7
+ <rect x="56" y="176" width="1008" height="380" rx="24" fill="#ffffff" stroke="#cbd5e1"/>
8
+ <text x="88" y="220" fill="#0f172a" font-family="Inter,Segoe UI,Arial" font-size="21" font-weight="800">Recommended setup</text>
9
+ <g font-family="Inter,Segoe UI,Arial" font-size="16">
10
+ <text x="96" y="274" fill="#334155">Port</text>
11
+ <rect x="96" y="290" width="210" height="44" rx="12" fill="#f8fafc" stroke="#cbd5e1"/>
12
+ <text x="116" y="318" fill="#0f172a">5580</text>
13
+ <text x="342" y="274" fill="#334155">Host</text>
14
+ <rect x="342" y="290" width="260" height="44" rx="12" fill="#f8fafc" stroke="#cbd5e1"/>
15
+ <text x="362" y="318" fill="#0f172a">127.0.0.1</text>
16
+ <text x="638" y="274" fill="#334155">API Key</text>
17
+ <rect x="638" y="290" width="330" height="44" rx="12" fill="#f8fafc" stroke="#cbd5e1"/>
18
+ <text x="658" y="318" fill="#0f172a">sk-...</text>
19
+ </g>
20
+ <g font-family="Inter,Segoe UI,Arial" font-size="15" font-weight="700">
21
+ <rect x="96" y="370" width="128" height="38" rx="19" fill="#dbeafe"/>
22
+ <circle cx="118" cy="389" r="11" fill="#2563eb"/>
23
+ <text x="140" y="394" fill="#1e40af">Auto Start</text>
24
+ <rect x="252" y="370" width="168" height="38" rx="19" fill="#dcfce7"/>
25
+ <circle cx="274" cy="389" r="11" fill="#16a34a"/>
26
+ <text x="296" y="394" fill="#166534">Multi-Account</text>
27
+ <rect x="448" y="370" width="116" height="38" rx="19" fill="#fef3c7"/>
28
+ <text x="482" y="394" fill="#92400e">Smart</text>
29
+ <rect x="592" y="370" width="146" height="38" rx="19" fill="#ede9fe"/>
30
+ <circle cx="614" cy="389" r="11" fill="#7c3aed"/>
31
+ <text x="636" y="394" fill="#5b21b6">Log Requests</text>
32
+ </g>
33
+ <rect x="96" y="450" width="872" height="72" rx="18" fill="#f8fafc" stroke="#cbd5e1"/>
34
+ <text x="126" y="482" fill="#0f172a" font-family="Inter,Segoe UI,Arial" font-size="17" font-weight="800">Smart rotation</text>
35
+ <text x="126" y="510" fill="#475569" font-family="Inter,Segoe UI,Arial" font-size="15">Scores quota, errors, latency, request count and token freshness before selecting an account.</text>
36
+ </svg>
37
+
@@ -0,0 +1,51 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="1120" height="520" viewBox="0 0 1120 520" role="img" aria-label="Krouter API Proxy overview">
2
+ <defs>
3
+ <linearGradient id="bg" x1="0" x2="1" y1="0" y2="1">
4
+ <stop offset="0" stop-color="#0f172a"/>
5
+ <stop offset="1" stop-color="#172554"/>
6
+ </linearGradient>
7
+ <linearGradient id="card" x1="0" x2="1">
8
+ <stop offset="0" stop-color="#1e293b"/>
9
+ <stop offset="1" stop-color="#111827"/>
10
+ </linearGradient>
11
+ <filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
12
+ <feDropShadow dx="0" dy="16" stdDeviation="18" flood-color="#020617" flood-opacity=".35"/>
13
+ </filter>
14
+ </defs>
15
+ <rect width="1120" height="520" rx="28" fill="url(#bg)"/>
16
+ <text x="56" y="72" fill="#f8fafc" font-family="Inter,Segoe UI,Arial" font-size="36" font-weight="800">Krouter API Proxy</text>
17
+ <text x="56" y="108" fill="#bfdbfe" font-family="Inter,Segoe UI,Arial" font-size="17">One endpoint for OpenAI, Claude, Gemini compatible AI tools</text>
18
+ <g filter="url(#shadow)">
19
+ <rect x="56" y="168" width="250" height="170" rx="18" fill="url(#card)" stroke="#334155"/>
20
+ <text x="84" y="214" fill="#e0f2fe" font-family="Inter,Segoe UI,Arial" font-size="22" font-weight="700">AI Clients</text>
21
+ <text x="84" y="252" fill="#cbd5e1" font-family="Inter,Segoe UI,Arial" font-size="16">OpenClaw / Aira</text>
22
+ <text x="84" y="282" fill="#cbd5e1" font-family="Inter,Segoe UI,Arial" font-size="16">Codex-compatible tools</text>
23
+ <text x="84" y="312" fill="#cbd5e1" font-family="Inter,Segoe UI,Arial" font-size="16">Cursor / Cline / custom apps</text>
24
+ </g>
25
+ <g filter="url(#shadow)">
26
+ <rect x="410" y="138" width="300" height="230" rx="22" fill="#0f766e" stroke="#5eead4"/>
27
+ <text x="448" y="194" fill="#ecfeff" font-family="Inter,Segoe UI,Arial" font-size="25" font-weight="800">Krouter Proxy</text>
28
+ <text x="448" y="236" fill="#ccfbf1" font-family="Inter,Segoe UI,Arial" font-size="16">/v1/chat/completions</text>
29
+ <text x="448" y="266" fill="#ccfbf1" font-family="Inter,Segoe UI,Arial" font-size="16">/v1/messages</text>
30
+ <text x="448" y="296" fill="#ccfbf1" font-family="Inter,Segoe UI,Arial" font-size="16">/v1beta/models/*</text>
31
+ <rect x="448" y="322" width="214" height="34" rx="17" fill="#134e4a"/>
32
+ <text x="472" y="344" fill="#99f6e4" font-family="Inter,Segoe UI,Arial" font-size="14" font-weight="700">Smart account rotation</text>
33
+ </g>
34
+ <g filter="url(#shadow)">
35
+ <rect x="816" y="140" width="248" height="228" rx="18" fill="url(#card)" stroke="#334155"/>
36
+ <text x="846" y="194" fill="#fef3c7" font-family="Inter,Segoe UI,Arial" font-size="22" font-weight="800">Kiro Accounts</text>
37
+ <text x="846" y="236" fill="#cbd5e1" font-family="Inter,Segoe UI,Arial" font-size="16">Quota and plan checks</text>
38
+ <text x="846" y="266" fill="#cbd5e1" font-family="Inter,Segoe UI,Arial" font-size="16">Token auto refresh</text>
39
+ <text x="846" y="296" fill="#cbd5e1" font-family="Inter,Segoe UI,Arial" font-size="16">Profile ARN routing</text>
40
+ <text x="846" y="326" fill="#cbd5e1" font-family="Inter,Segoe UI,Arial" font-size="16">Model capability cache</text>
41
+ </g>
42
+ <path d="M318 253 H396" stroke="#93c5fd" stroke-width="5" stroke-linecap="round"/>
43
+ <path d="M386 239 L410 253 L386 267" fill="none" stroke="#93c5fd" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
44
+ <path d="M724 253 H800" stroke="#5eead4" stroke-width="5" stroke-linecap="round"/>
45
+ <path d="M790 239 L814 253 L790 267" fill="none" stroke="#5eead4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
46
+ <text x="342" y="224" fill="#dbeafe" font-family="Inter,Segoe UI,Arial" font-size="14">Bearer sk-...</text>
47
+ <text x="742" y="224" fill="#ccfbf1" font-family="Inter,Segoe UI,Arial" font-size="14">Best account</text>
48
+ <rect x="56" y="422" width="1008" height="52" rx="16" fill="#020617" opacity=".45"/>
49
+ <text x="84" y="454" fill="#c7d2fe" font-family="Inter,Segoe UI,Arial" font-size="16">Recommended: local host 127.0.0.1, API key enabled, Multi-Account on, Strategy Smart, dashboard tunnel for VPS access.</text>
50
+ </svg>
51
+
@@ -0,0 +1,85 @@
1
+ # Web Port Notes
2
+
3
+ This branch starts the Electron-to-web port for the `v1.7.2` codebase.
4
+
5
+ ## Current Architecture
6
+
7
+ - `vite.web.config.ts` builds the existing React renderer as a browser app.
8
+ - `src/renderer/src/api/browserApi.ts` installs a browser `window.api` shim compatible with the Electron preload surface.
9
+ - `src/server/index.ts` provides the CLI backend entry point:
10
+ - admin login/session cookies
11
+ - encrypted account data persistence
12
+ - generic `/api/ipc` bridge for renderer calls
13
+ - `/api/events` server-sent events bridge for realtime IPC-style events
14
+ - `src/server/services/proxyRuntime.ts` runs the upstream reverse proxy on the VPS backend and wires proxy IPC handlers to the real `ProxyServer`.
15
+ - `src/server/services/kproxyRuntime.ts` runs the upstream K-Proxy service on the VPS backend, including CA generation/export and device ID mapping.
16
+ - `src/server/services/machineIdRuntime.ts` maps the Electron machine ID feature to the VPS host or to a configured machine-id file override.
17
+ - `src/server/services/localKiroCredentials.ts` maps local Kiro IDE/CLI credential import, switch, and logout to VPS filesystem targets.
18
+ - `src/server/services/protonBrowserRuntime.ts` maps the Electron Proton `BrowserWindow` flow to a server-side Chromium profile controlled through Chrome DevTools Protocol.
19
+
20
+ ## Deploy
21
+
22
+ 1. Copy `.env.web.example` to `.env.web`.
23
+ 2. Set strong values for `APP_ENCRYPTION_KEY` and `SESSION_SECRET`.
24
+ Leave `KROUTER_ADMIN_PASSWORD` unset to use the first-run setup screen, or set `KROUTER_ADMIN_EMAIL` and `KROUTER_ADMIN_PASSWORD` for unattended installs.
25
+ 3. Review the VPS filesystem targets:
26
+ - `KIRO_CONFIG_HOME` controls the Kiro settings/MCP/Steering directory.
27
+ - `KIRO_SSO_CACHE_DIR` controls the Kiro IDE SSO cache target.
28
+ - `KIRO_CLI_DB_PATH` controls the Kiro CLI SQLite database target.
29
+ - `KIRO_MACHINE_ID_FILE` is optional. Leave it unset to read/write the host OS machine ID; set it for Docker/test deployments where machine ID writes should stay isolated.
30
+ - `PUBLIC_BASE_URL` is used by legacy/manual OAuth callback helpers. Web IAM SSO uses the AWS device authorization flow because public OIDC clients only allow loopback redirect URIs.
31
+ - `PROTON_BROWSER_PATH` must point to Chrome/Chromium for the Proton OTP source. Dockerfile.web installs `/usr/bin/chromium`.
32
+ - `PROTON_BROWSER_HEADLESS=true` runs the server browser in headless mode and exposes `/proton-login` for remote login/captcha/2FA interaction.
33
+ - `PROTON_BROWSER_NO_SANDBOX=true` can be set when Chrome cannot start under a container or constrained desktop session. Docker/root deployments enable this automatically.
34
+ 4. Build and run the backend CLI:
35
+
36
+ ```bash
37
+ npm run build:fullstack
38
+ npm run start:backend
39
+ ```
40
+
41
+ `start:backend` runs `node out-server/server/index.js --api-only`. In this mode the backend serves `/api/*`, `/api/events`, `/healthz`, and `/proton-login`, but it does not serve frontend assets. Put nginx or another static web server in front of it:
42
+
43
+ ```nginx
44
+ root /path/to/Krouter/dist-web;
45
+
46
+ location /api/ { proxy_pass http://127.0.0.1:4010; }
47
+ location = /healthz { proxy_pass http://127.0.0.1:4010; }
48
+ location = /proton-login { proxy_pass http://127.0.0.1:4010; }
49
+ location / { try_files $uri /index.html; }
50
+ ```
51
+
52
+ The old fullstack mode is still available with `node out-server/server/index.js --serve-static` or `SERVE_STATIC=true`.
53
+
54
+ 5. Docker fullstack deployment is still supported:
55
+
56
+ ```bash
57
+ docker compose -f docker-compose.web.yml up -d --build
58
+ ```
59
+
60
+ When deploying in Docker, filesystem-targeted features operate inside the container unless the matching host paths are bind-mounted. For example, host Kiro IDE/CLI switching requires mounting the host SSO cache or CLI database path into the container and pointing the env vars at the mounted paths.
61
+
62
+ ## Porting Queue
63
+
64
+ The web foundation intentionally preserves the renderer API names, so Electron handlers can be ported one by one from `src/main/index.ts` and the split modules under `src/main`.
65
+
66
+ Ported backend runtime areas:
67
+
68
+ - Admin login/session and encrypted account store.
69
+ - Account token refresh/check/verify via the VPS backend.
70
+ - Kiro settings/MCP/Steering file management against `KIRO_CONFIG_HOME` or VPS `~/.kiro`.
71
+ - Reverse proxy runtime, API keys, account pool sync, logs, models, metrics/status, TLS cert info.
72
+ - K-Proxy runtime, CA generation/export, device ID mappings, status/start/stop.
73
+ - Machine ID read/write, backup/restore, random generation, admin/root permission checks, and optional file override.
74
+ - VPS-local Kiro IDE credential import/switch/logout against `KIRO_SSO_CACHE_DIR`.
75
+ - VPS-local Kiro CLI credential switch against `KIRO_CLI_DB_PATH`, using `sqlite3` when installed or Node `node:sqlite` as fallback.
76
+ - Account model list, subscription list, subscription URL, and overage preference handlers.
77
+ - Builder ID, IAM SSO, social login, and SSO-token import handlers with web callback routes under `/api/auth/*/callback`.
78
+ - Registration auto/manual handlers are wired to the upstream `Registrar` and emit logs/steps/completion through SSE.
79
+ - Tingamefi temp mail can be used as an auto-registration source. It creates addresses through the Cloudflare Email Worker admin API (`/admin/new_address`) and reads verification mail through `/admin/mails`.
80
+ - Proton mailbox login/OTP is wired to a VPS-side Chromium profile. Use the registration page's "Login Proton" action to open `/proton-login`, complete Proton login/captcha/2FA there, then run Proton or mixed-source registration. The remote login page supports screenshot refresh, click, scroll, text input, and basic keys against the server browser.
81
+ - Background batch refresh/check and diagnostics/proxy-pool/account-liveness handlers.
82
+
83
+ High-priority handlers still requiring full backend parity:
84
+
85
+ - Production PostgreSQL migration from the current encrypted file store.
@@ -64,7 +64,7 @@ class AccountPool {
64
64
  config;
65
65
  // 默认 round-robin: 选中账号时立即前进,避免并发请求集中到同一账号
66
66
  // sticky: 一个账号成功就粘住 (保留 prompt cache 命中)
67
- strategy = 'round-robin';
67
+ strategy = 'smart';
68
68
  constructor(config = {}) {
69
69
  this.config = { ...DEFAULT_CONFIG, ...config };
70
70
  }
@@ -133,6 +133,9 @@ class AccountPool {
133
133
  return account;
134
134
  }
135
135
  const now = Date.now();
136
+ if (this.strategy === 'smart') {
137
+ return this.getSmartBalancedAccount(accountList, now, excludeIds);
138
+ }
136
139
  if (this.strategy === 'least-used') {
137
140
  return this.getLeastUsedAccount(accountList, now, excludeIds);
138
141
  }
@@ -173,6 +176,9 @@ class AccountPool {
173
176
  if (accountList.length === 0)
174
177
  return null;
175
178
  const now = Date.now();
179
+ if (this.strategy === 'smart') {
180
+ return this.getSmartBalancedAccount(accountList, now, excludeSet);
181
+ }
176
182
  if (this.strategy === 'least-used') {
177
183
  return this.getLeastUsedAccount(accountList, now, excludeSet);
178
184
  }
@@ -322,6 +328,74 @@ class AccountPool {
322
328
  }
323
329
  return best;
324
330
  }
331
+ getSmartBalancedAccount(accountList, now, excludeIds) {
332
+ let best = null;
333
+ for (const account of accountList) {
334
+ if (excludeIds?.has(account.id))
335
+ continue;
336
+ if (!this.isAccountAvailable(account, now))
337
+ continue;
338
+ const score = this.scoreAccountForSmartBalance(account, now);
339
+ if (!best ||
340
+ score > best.score ||
341
+ (score === best.score && (account.lastUsed || 0) < (best.account.lastUsed || 0))) {
342
+ best = { account, score };
343
+ }
344
+ }
345
+ if (best) {
346
+ this.accounts.set(best.account.id, { ...best.account, lastUsed: now });
347
+ }
348
+ return best?.account || null;
349
+ }
350
+ scoreAccountForSmartBalance(account, now) {
351
+ const stats = this.accountStats.get(account.id);
352
+ let score = 1000;
353
+ const quotaLimit = account.quotaLimit || 0;
354
+ if (quotaLimit > 0) {
355
+ const used = Math.max(0, account.quotaUsed || 0);
356
+ const remainingRatio = Math.max(0, Math.min(1, (quotaLimit - used) / quotaLimit));
357
+ score += remainingRatio * 500;
358
+ if (remainingRatio < 0.1)
359
+ score -= 300;
360
+ else if (remainingRatio < 0.2)
361
+ score -= 150;
362
+ }
363
+ else {
364
+ score += 100;
365
+ }
366
+ score -= Math.min(500, (account.errorCount || 0) * 140);
367
+ score -= Math.min(200, (stats?.errors || 0) * 25);
368
+ score -= Math.min(180, (account.requestCount || 0) * 3);
369
+ score -= Math.min(120, (stats?.requests || 0) * 2);
370
+ if (stats?.avgResponseTime) {
371
+ score -= Math.min(120, stats.avgResponseTime / 100);
372
+ }
373
+ const lastUsed = account.lastUsed || stats?.lastUsed || 0;
374
+ if (lastUsed > 0) {
375
+ const idleMs = now - lastUsed;
376
+ score += Math.min(120, Math.max(0, idleMs) / 1000 / 2);
377
+ }
378
+ else {
379
+ score += 120;
380
+ }
381
+ if (account.expiresAt) {
382
+ const minutesLeft = (account.expiresAt - now) / 60000;
383
+ if (minutesLeft < 5)
384
+ score -= 250;
385
+ else if (minutesLeft < 15)
386
+ score -= 80;
387
+ }
388
+ // Tiny deterministic jitter prevents permanent ties without defeating balance.
389
+ score += this.stableAccountJitter(account.id);
390
+ return score;
391
+ }
392
+ stableAccountJitter(accountId) {
393
+ let hash = 0;
394
+ for (let i = 0; i < accountId.length; i++) {
395
+ hash = ((hash << 5) - hash + accountId.charCodeAt(i)) | 0;
396
+ }
397
+ return Math.abs(hash % 17) / 10;
398
+ }
325
399
  // 记录请求成功(重置断路器 + 粘滞到当前账号)
326
400
  recordSuccess(accountId, tokens = 0) {
327
401
  const account = this.accounts.get(accountId);
@@ -270,13 +270,13 @@ class ProxyServer {
270
270
  tokenRefreshBeforeExpiry: 300, // 5分钟提前刷新
271
271
  autoStart: false, // 是否自动启动
272
272
  clientDrivenToolExecution: true,
273
- accountSelectionStrategy: 'round-robin',
273
+ accountSelectionStrategy: 'smart',
274
274
  sessionAffinityEnabled: false,
275
275
  ...config
276
276
  };
277
277
  this.normalizeAccountBalancingConfig();
278
278
  this.accountPool = new accountPool_1.AccountPool();
279
- this.accountPool.setStrategy(this.config.accountSelectionStrategy || 'round-robin');
279
+ this.accountPool.setStrategy(this.config.accountSelectionStrategy || 'smart');
280
280
  this.stats = {
281
281
  totalRequests: 0,
282
282
  successRequests: 0,
@@ -560,10 +560,10 @@ class ProxyServer {
560
560
  this.config = { ...this.config, ...config };
561
561
  this.normalizeAccountBalancingConfig();
562
562
  // 同步账号选择策略到 accountPool
563
- this.accountPool.setStrategy(this.config.accountSelectionStrategy || 'round-robin');
563
+ this.accountPool.setStrategy(this.config.accountSelectionStrategy || 'smart');
564
564
  }
565
565
  normalizeAccountBalancingConfig() {
566
- const strategy = this.config.accountSelectionStrategy || 'round-robin';
566
+ const strategy = this.config.accountSelectionStrategy || 'smart';
567
567
  this.config.accountSelectionStrategy = strategy;
568
568
  if (this.config.enableMultiAccount && strategy !== 'sticky') {
569
569
  this.config.sessionAffinityEnabled = false;
@@ -571,7 +571,7 @@ class ProxyServer {
571
571
  }
572
572
  isSessionAffinityActive() {
573
573
  return Boolean(this.config.sessionAffinityEnabled &&
574
- (this.config.accountSelectionStrategy || 'round-robin') === 'sticky');
574
+ (this.config.accountSelectionStrategy || 'smart') === 'sticky');
575
575
  }
576
576
  /** UI 可用此判断是否需提示用户重启 */
577
577
  needsRestart() {
@@ -347,6 +347,18 @@ function accountNeedsBackendRefresh(account, now) {
347
347
  const expiresAt = Number(credentials.expiresAt || 0);
348
348
  return !credentials.accessToken || !expiresAt || expiresAt - now <= TOKEN_REFRESH_BEFORE_EXPIRY_MS;
349
349
  }
350
+ function normalizeStoredUsagePercent(usage) {
351
+ const current = Number(usage.current);
352
+ const limit = Number(usage.limit);
353
+ if (Number.isFinite(current) && Number.isFinite(limit) && limit > 0) {
354
+ return { ...usage, percentUsed: current / limit };
355
+ }
356
+ const persisted = Number(usage.percentUsed);
357
+ if (Number.isFinite(persisted) && persisted > 1 && persisted <= 100) {
358
+ return { ...usage, percentUsed: persisted / 100 };
359
+ }
360
+ return usage;
361
+ }
350
362
  function getStoredAccounts(accountData) {
351
363
  return isPlainRecord(accountData.accounts) ? accountData.accounts : {};
352
364
  }
@@ -366,7 +378,7 @@ function applyRefreshDataToStoredAccount(id, account, data, now) {
366
378
  let usage = account.usage;
367
379
  if (isPlainRecord(data?.usage)) {
368
380
  const currentUsage = isPlainRecord(account.usage) ? account.usage : {};
369
- usage = { ...currentUsage, ...data.usage, lastUpdated: now };
381
+ usage = normalizeStoredUsagePercent({ ...currentUsage, ...data.usage, lastUpdated: now });
370
382
  }
371
383
  let subscription = account.subscription;
372
384
  if (isPlainRecord(data?.subscription)) {
@@ -418,7 +418,7 @@ async function checkAccountStatus(account) {
418
418
  profileArn: resolvedProfileArn,
419
419
  usage: {
420
420
  ...normalized.usage,
421
- percentUsed: normalized.usage.limit ? (normalized.usage.current / normalized.usage.limit) * 100 : 0,
421
+ percentUsed: normalized.usage.limit ? normalized.usage.current / normalized.usage.limit : 0,
422
422
  lastUpdated: Date.now()
423
423
  },
424
424
  newCredentials: refreshResult?.data ? {
@@ -27,13 +27,13 @@ function defaultProxyConfig(saved) {
27
27
  retryDelayMs: 1000,
28
28
  tokenRefreshBeforeExpiry: 300,
29
29
  clientDrivenToolExecution: true,
30
- accountSelectionStrategy: 'round-robin',
30
+ accountSelectionStrategy: 'smart',
31
31
  sessionAffinityEnabled: false,
32
32
  ...saved
33
33
  });
34
34
  }
35
35
  function normalizeProxyConfig(config) {
36
- const strategy = config.accountSelectionStrategy || 'round-robin';
36
+ const strategy = config.accountSelectionStrategy || 'smart';
37
37
  const normalized = {
38
38
  ...config,
39
39
  accountSelectionStrategy: strategy