waygate-open 0.0.0-stage → 1.6.14
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.
- package/LICENSE +21 -0
- package/README.md +393 -2
- package/bin/waygate-open.mjs +218 -0
- package/package.json +57 -4
- package/server/README.md +136 -0
- package/server/dist/api/api.filter.js +118 -0
- package/server/dist/api/api.module.js +91 -0
- package/server/dist/api/api.types.js +25 -0
- package/server/dist/api/auth/admin-credential.service.js +278 -0
- package/server/dist/api/auth/admin-password-policy.js +64 -0
- package/server/dist/api/auth/auth.controller.js +203 -0
- package/server/dist/api/auth/origin.js +111 -0
- package/server/dist/api/auth/password.js +124 -0
- package/server/dist/api/auth/rate-limit.js +77 -0
- package/server/dist/api/auth/session.guard.js +89 -0
- package/server/dist/api/auth/session.js +94 -0
- package/server/dist/api/client-harness.js +97 -0
- package/server/dist/api/client-name.js +52 -0
- package/server/dist/api/clients.service.js +197 -0
- package/server/dist/api/console.controller.js +447 -0
- package/server/dist/api/credential-import.service.js +126 -0
- package/server/dist/api/credential.service.js +218 -0
- package/server/dist/api/device-import.service.js +408 -0
- package/server/dist/api/keys.parsing.js +154 -0
- package/server/dist/api/keys.service.js +280 -0
- package/server/dist/api/models.service.harness.js +67 -0
- package/server/dist/api/models.service.js +187 -0
- package/server/dist/api/overview.service.js +557 -0
- package/server/dist/api/providers.controller.js +453 -0
- package/server/dist/api/providers.service.js +640 -0
- package/server/dist/api/request-body.js +71 -0
- package/server/dist/api/requests.service.js +255 -0
- package/server/dist/api/settings.catalog/config-mapping.js +28 -0
- package/server/dist/api/settings.catalog/danger.js +9 -0
- package/server/dist/api/settings.catalog/group-credential.js +85 -0
- package/server/dist/api/settings.catalog/group-danger.js +33 -0
- package/server/dist/api/settings.catalog/group-limits.js +52 -0
- package/server/dist/api/settings.catalog/group-logs.js +48 -0
- package/server/dist/api/settings.catalog/group-mapping.js +21 -0
- package/server/dist/api/settings.catalog/group-quota.js +73 -0
- package/server/dist/api/settings.catalog/group-service.js +48 -0
- package/server/dist/api/settings.catalog/group-signin.js +31 -0
- package/server/dist/api/settings.catalog/index.js +49 -0
- package/server/dist/api/settings.catalog/types.js +8 -0
- package/server/dist/api/settings.service.js +136 -0
- package/server/dist/api/token-metrics.js +33 -0
- package/server/dist/api/window.js +138 -0
- package/server/dist/app.module.js +35 -0
- package/server/dist/auth/key.guard.js +237 -0
- package/server/dist/auth/keys.js +88 -0
- package/server/dist/catalog/catalog-sync.js +450 -0
- package/server/dist/catalog/model-catalog.json +144 -0
- package/server/dist/catalog/probe.js +127 -0
- package/server/dist/catalog/pull.js +226 -0
- package/server/dist/catalog/qoder-models.js +117 -0
- package/server/dist/catalog/seed-models.js +28 -0
- package/server/dist/catalog/v3-config.js +115 -0
- package/server/dist/common/assets.js +75 -0
- package/server/dist/common/client-ip.js +105 -0
- package/server/dist/common/crypto.js +31 -0
- package/server/dist/common/errors.js +90 -0
- package/server/dist/common/file-cache.js +32 -0
- package/server/dist/common/ip-policy.js +104 -0
- package/server/dist/common/mask.js +27 -0
- package/server/dist/common/semaphore.js +38 -0
- package/server/dist/config/config.module.js +26 -0
- package/server/dist/config/config.service.js +256 -0
- package/server/dist/config/listen.js +31 -0
- package/server/dist/config/settings.defaults.js +105 -0
- package/server/dist/config/settings.validate.js +134 -0
- package/server/dist/identify/ua-table.js +136 -0
- package/server/dist/main.js +133 -0
- package/server/dist/proxy/accumulate.js +127 -0
- package/server/dist/proxy/anthropic/anthropic-key.guard.js +50 -0
- package/server/dist/proxy/anthropic/anthropic.controller.js +112 -0
- package/server/dist/proxy/anthropic/anthropic.filter.js +39 -0
- package/server/dist/proxy/anthropic/controller.harness.js +159 -0
- package/server/dist/proxy/anthropic/errors.js +77 -0
- package/server/dist/proxy/anthropic/respond.js +159 -0
- package/server/dist/proxy/anthropic/stream-events.js +156 -0
- package/server/dist/proxy/anthropic/translate-request.js +206 -0
- package/server/dist/proxy/anthropic/translate-response.js +80 -0
- package/server/dist/proxy/anthropic/types.js +12 -0
- package/server/dist/proxy/anthropic-upstream/minimax-credit.service.js +221 -0
- package/server/dist/proxy/anthropic-upstream/minimax-signin.service.js +212 -0
- package/server/dist/proxy/anthropic-upstream/minimax-token.service.js +203 -0
- package/server/dist/proxy/anthropic-upstream/stream-decode.js +214 -0
- package/server/dist/proxy/anthropic-upstream/translate.js +211 -0
- package/server/dist/proxy/body-capture.js +32 -0
- package/server/dist/proxy/concurrency.js +21 -0
- package/server/dist/proxy/headers.js +132 -0
- package/server/dist/proxy/model-scope.js +102 -0
- package/server/dist/proxy/openai-upstream/wb-claim.service.js +243 -0
- package/server/dist/proxy/openai-upstream/wb-oauth.harness.js +68 -0
- package/server/dist/proxy/openai-upstream/wb-oauth.service.js +207 -0
- package/server/dist/proxy/openai-upstream/wb-signin.service.js +273 -0
- package/server/dist/proxy/pipeline.js +76 -0
- package/server/dist/proxy/proxy-log.js +213 -0
- package/server/dist/proxy/proxy.controller.js +315 -0
- package/server/dist/proxy/proxy.module.js +53 -0
- package/server/dist/proxy/proxy.service.js +852 -0
- package/server/dist/proxy/qoder-upstream/cosy.js +168 -0
- package/server/dist/proxy/qoder-upstream/credential.js +79 -0
- package/server/dist/proxy/qoder-upstream/qoder-activity.service.js +427 -0
- package/server/dist/proxy/qoder-upstream/stream-decode.js +175 -0
- package/server/dist/proxy/qoder-upstream/translate.js +157 -0
- package/server/dist/proxy/quota/quota-block.js +123 -0
- package/server/dist/proxy/quota/quota-parse.js +180 -0
- package/server/dist/proxy/quota/quota-source.service.js +313 -0
- package/server/dist/proxy/rewrite.js +194 -0
- package/server/dist/proxy/sse-stream.js +273 -0
- package/server/dist/proxy/types.js +3 -0
- package/server/dist/proxy/upstream-errors.js +120 -0
- package/server/dist/proxy/upstream-fetch.js +55 -0
- package/server/dist/proxy/upstream-url.js +66 -0
- package/server/dist/proxy/usage-tokens.js +104 -0
- package/server/dist/proxy/usage.controller.js +94 -0
- package/server/dist/store/cached-stmt.js +24 -0
- package/server/dist/store/db.js +57 -0
- package/server/dist/store/key-usage-cache.js +89 -0
- package/server/dist/store/migrate.js +65 -0
- package/server/dist/store/repositories/admin-credential.repo.js +70 -0
- package/server/dist/store/repositories/aggregates.repo.js +107 -0
- package/server/dist/store/repositories/client-keys.repo.js +217 -0
- package/server/dist/store/repositories/model-catalog.repo.js +274 -0
- package/server/dist/store/repositories/provider-model-prefs.repo.js +81 -0
- package/server/dist/store/repositories/providers.repo.js +124 -0
- package/server/dist/store/repositories/requests.repo.js +209 -0
- package/server/dist/store/repositories/stats.repo.js +102 -0
- package/server/dist/store/retention.service.js +121 -0
- package/server/dist/store/schema.sql +336 -0
- package/server/dist/store/self-check.js +53 -0
- package/server/dist/store/store.module.js +62 -0
- package/server/dist/version.js +15 -0
- package/server/migrations/002-admin-credential.sql +44 -0
- package/server/migrations/003-client-id.sql +63 -0
- package/server/migrations/004-model-catalog-source.sql +65 -0
- package/server/migrations/005-cached-tokens.sql +43 -0
- package/server/migrations/006-reasoning-tokens.sql +37 -0
- package/server/migrations/007-providers.sql +57 -0
- package/server/migrations/008-provider-model-prefs.sql +45 -0
- package/server/migrations/009-key-clients.sql +31 -0
- package/server/migrations/010-key-policy.sql +35 -0
- package/server/migrations/011-provider-protocol.sql +33 -0
- package/server/migrations/012-model-catalog-provider.sql +56 -0
- package/server/migrations/013-provider-signin.sql +26 -0
- package/server/migrations/014-credits-nullable.sql +84 -0
- package/server/migrations/015-provider-protocol-qoder.sql +58 -0
- package/server/migrations/016-wb-signin.sql +29 -0
- package/server/migrations/017-wb-oauth.sql +23 -0
- package/server/package.json +39 -0
- package/web/README.md +116 -0
- package/web/dist/assets/ApiKeysPage-LOAdUyHT.js +5 -0
- package/web/dist/assets/ClientMetricTip-D-pXFJpS.js +1 -0
- package/web/dist/assets/ClientsPage-D3z8xy1H.js +1 -0
- package/web/dist/assets/Drawer-CleQGhRU.js +1 -0
- package/web/dist/assets/LandingPage-BXVLs2Ni.js +15 -0
- package/web/dist/assets/LandingPage-DIP_XFhP.css +1 -0
- package/web/dist/assets/Modal-B-rIrvXS.js +1 -0
- package/web/dist/assets/OverviewPage-xTsnlyEP.js +1 -0
- package/web/dist/assets/PageSkeleton-DG-Dj4_9.js +1 -0
- package/web/dist/assets/RequestsPage-B2ThbdoO.js +5 -0
- package/web/dist/assets/Select-BxZGnteq.js +1 -0
- package/web/dist/assets/SettingsPage-CfuPWZ4_.js +3 -0
- package/web/dist/assets/Switch-0KA1AbiV.js +1 -0
- package/web/dist/assets/UsagePage-UatftkHb.js +1 -0
- package/web/dist/assets/accessUrl-DbL5DLDz.js +1 -0
- package/web/dist/assets/index-BtuqaoxX.css +1 -0
- package/web/dist/assets/index-VH6Gc3-s.js +2 -0
- package/web/dist/assets/useFocusTrap-Cj84bnpH.js +1 -0
- package/web/dist/assets/useFx-xcrp_LHB.js +1 -0
- package/web/dist/assets/vendor-befEYGHM.js +9 -0
- package/web/dist/favicon.svg +10 -0
- package/web/dist/fonts/jetbrains-mono-412.woff2 +0 -0
- package/web/dist/fonts/jetbrains-mono-413.woff2 +0 -0
- package/web/dist/fonts/jetbrains-mono-414.woff2 +0 -0
- package/web/dist/fonts/jetbrains-mono-415.woff2 +0 -0
- package/web/dist/fonts/jetbrains-mono-416.woff2 +0 -0
- package/web/dist/fonts/jetbrains-mono-417.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-10.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-100.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-101.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-102.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-103.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-104.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-105.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-106.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-107.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-108.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-11.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-12.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-13.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-14.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-15.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-16.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-17.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-18.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-19.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-20.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-21.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-22.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-23.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-24.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-25.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-26.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-27.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-28.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-29.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-30.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-31.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-32.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-33.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-34.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-35.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-36.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-37.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-38.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-39.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-40.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-41.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-42.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-43.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-44.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-45.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-46.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-47.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-48.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-49.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-50.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-51.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-52.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-53.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-54.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-55.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-56.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-57.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-58.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-59.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-60.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-61.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-62.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-63.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-64.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-65.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-66.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-67.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-68.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-69.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-70.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-71.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-72.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-73.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-74.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-75.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-76.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-77.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-78.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-79.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-8.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-80.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-81.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-82.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-83.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-84.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-85.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-86.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-87.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-88.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-89.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-9.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-90.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-91.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-92.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-93.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-94.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-95.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-96.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-97.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-98.woff2 +0 -0
- package/web/dist/fonts/noto-sans-sc-99.woff2 +0 -0
- package/web/dist/fonts/sora-0.woff2 +0 -0
- package/web/dist/fonts/sora-1.woff2 +0 -0
- package/web/dist/icon.svg +10 -0
- package/web/dist/index.html +67 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 zhanghaojie513
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,394 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
# 途关 · Waygate
|
|
4
|
+
|
|
5
|
+
**把订阅积分,变成一个 `base_url`**
|
|
6
|
+
|
|
7
|
+
[](https://react.dev)
|
|
8
|
+
[](https://nestjs.com)
|
|
9
|
+
[](https://github.com/WiseLibs/better-sqlite3)
|
|
10
|
+
[](https://nodejs.org)
|
|
11
|
+
[](https://github.com/zhanghaojie513/waygate-open/releases)
|
|
12
|
+
[](./server)
|
|
13
|
+
|
|
14
|
+

|
|
15
|
+
|
|
16
|
+
[快速开始](#快速开始) · [功能](#功能) · [接入](#接入) · [多提供商与路由](#多提供商与路由) · [架构](#架构) · [部署](#部署自用单机)
|
|
17
|
+
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
自托管的「客户端订阅」反向代理:把 **WorkBuddy、MiniMax Code、Qoder CN** 这类**按客户端订阅计费**的服务,转成标准的 **OpenAI / Anthropic 兼容 `base_url`**。
|
|
21
|
+
|
|
22
|
+
- **任意工具直连** —— Claude Code、Cursor、Cherry Studio、OpenAI SDK……只要能自定义接入地址,就能消费订阅额度
|
|
23
|
+
- **三家同时生效** —— 按模型 id 路由到对应提供商,互不干扰(见「多提供商与路由」)
|
|
24
|
+
- **控制台对账** —— 逐请求留痕、积分账本、用量趋势,每一分消耗都对得上上游实扣
|
|
25
|
+
|
|
26
|
+
> [!NOTE]
|
|
27
|
+
> 名字:Waygate / 途关 —— 中转关隘。仓库内常见 `relay` 字样同义。
|
|
28
|
+
|
|
29
|
+
## 控制台一览
|
|
30
|
+
|
|
31
|
+
| 概览(按提供商归属的用量摘要) | 积分与用量(趋势 / 模型明细) |
|
|
32
|
+
| --- | --- |
|
|
33
|
+
|  |  |
|
|
34
|
+
|
|
35
|
+
| 请求日志(逐条留痕,SSE 流也在) |
|
|
36
|
+
| --- |
|
|
37
|
+
|  |
|
|
38
|
+
|
|
39
|
+
## 功能
|
|
40
|
+
|
|
41
|
+
- **双方言出口**:`/v1/chat/completions`(OpenAI 兼容)与 `/v1/messages`(Anthropic 兼容),流式/非流式;三种上游方言自动翻译
|
|
42
|
+
- **多提供商**:目录按 `(provider_id, id)` 分区;来源分 `seed / upstream / observed / manual` 四类,observed 由真实流量并入;三家可同时生效
|
|
43
|
+
- **导入客户端**(v1.6.11,本版起为**唯一**凭证入口):控制台选客户端 → 跳各自**官方授权页** → 回来自动落盘凭证。三家的授权机制各不相同(WorkBuddy 业务码判等待 / MiniMax PKCE 设备码 / Qoder PKCE + 404 判等待),服务端按协议分派;私有状态(`device_code` / `code_verifier`)只存服务端,**绝不回传浏览器**。三家凭证都在客户端登录态里、用户拿不到,因此**不提供手填**(WorkBuddy 原用的 `ck_` 密钥已退役,转发也改走 OAuth)——行内「重新导入授权」用于换账号 / 刷新令牌
|
|
44
|
+
- **API 密钥**:绑定提供商优先级、权限范围(全部/按族/按模型/按标签)、IP 上限、token 配额、IP 白名单
|
|
45
|
+
- **请求日志**:提供商列按「模型 id → 目录归属」解析(`requests` 无 provider_id,绑定名只代表路由偏好);SSE 流也留痕;**游标(keyset)分页**——翻页期间的新写入不产生重复行;按 `logs.retainDays` 每日自动清理过期日志(启动即补跑停机期间欠的账)
|
|
46
|
+
- **积分与用量**:趋势图(Token / 请求数双口径,**等距时间轴**,空桶补零)、按模型 / 按提供商拆分、缓存命中率、缓存 token 计量(Anthropic 口径:prompt = input + cache_creation + cache_read)
|
|
47
|
+
- **积分账本**:MiniMax `credit/details` 真实余额(精确到 0.01);凭证 refresh token 自动续期 + 每日签到(专用账号后即可代签);WorkBuddy 资源包聚合余额(`get-user-resource` POST 实测口径)
|
|
48
|
+
- **Qoder 签到与积分**(v1.6.0,积分口径 v1.6.1 更正):`campaigns` 签到代领(每日 100 Credits,进**资源包**)+ 积分账本并入概览。走 `openapi.qoder.com.cn`,Bearer 令牌鉴权(**无需 COSY 签名**——那套只用于聊天路径),不顶掉桌面登录。**余额口径**:`v2/me/usage` 给真实余额(`userQuota` 套餐内 + `addOnQuota` 资源包),账本「发放/剩余/已消耗」为真实值;该端点**不提供到期时刻**,故「最近过期」列显示 `—`。另注意 `credits-summary.totalCredits` 是**历史累计消耗**(等于逐日用量之和),与余额不是一回事,单列展示不混算
|
|
49
|
+
- **WorkBuddy 领取域**(v1.4.0;v1.6.11 起与转发统一):签到代领 + 成长任务 + 连登兑换走**用户会话域**(OAuth,`providers/<id>.oauth`,多会话共存不互踢)——正是这条链路让 `ck_` 得以整体退役。诚实边界:任务多为「体验 XX」型,前置条件不满足就如实记 `skipped`,**绝不伪造行为去凑任务**;补登卡/抽奖等资源消耗型操作不自动化
|
|
50
|
+
- **容错**:Qoder 零内容断流**自动重试一次**(对客户端无感);上游 11128 渠道校验自动降级重试
|
|
51
|
+
- **控制台**:React 19 单页,明暗双主题(跟随系统/手动,侧栏底部切换;官网与控制台同套主题——棱镜场景浅色下以 `multiply` 显形、深色下以 `screen`),CSS 伪元素 tooltip 体系,同形状骨架屏;页间数据先旧后新(SWR:重进页面即刻渲染上次数据、后台刷新无缝替换,自己的写操作写穿缓存不闪旧态);「提供商健康」只读总览
|
|
52
|
+
- **性能**:路由级代码分割:官网与控制台互不加载对方代码,首屏 gzip <100K;vendor 独立 chunk 享 30d `immutable` 长缓存(发版只重下业务 chunk);概览 5s TTL 缓存(写操作即清)、SQL 语句按文本缓存复用、凭证文件 stat 失效缓存、last_seen 节流落库——同进程转发 SSE,省下的每次查询/写库都是还给流的事件循环时间;Nginx gzip(level 5)压缩传输
|
|
53
|
+
|
|
54
|
+
## 快速开始
|
|
55
|
+
|
|
56
|
+
### 方式一:npm 包(推荐,一条命令)
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
npx waygate-open
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
起本地服务并**自动打开浏览器**到控制台(默认 <http://127.0.0.1:8787/console>)。
|
|
63
|
+
首次启动的管理口令默认是 **`waygate`** —— 仅当只监听回环(`127.0.0.1`)时套用这个
|
|
64
|
+
默认值;对外监听时改为随机生成并打印在启动日志里。登录后建议去「设置 → 管理口令」改掉。
|
|
65
|
+
|
|
66
|
+
数据默认落在 `~/.waygate/open/`(库与密钥都在那里)。
|
|
67
|
+
|
|
68
|
+
常用参数(都有等价环境变量):
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
npx waygate-open --port 9000 # 换端口(RELAY_PORT)
|
|
72
|
+
npx waygate-open --password 我的口令 # 换初始口令(RELAY_ADMIN_PASSWORD)
|
|
73
|
+
npx waygate-open --data-dir ./data # 换数据目录(RELAY_DATA_DIR)
|
|
74
|
+
npx waygate-open --no-open # 不开浏览器(服务器 / CI)
|
|
75
|
+
npx waygate-open --help # 全部选项
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
> ⚠️ 默认只监听 `127.0.0.1`。要对外提供(别的机器也能连)需显式 `--host 0.0.0.0`,
|
|
79
|
+
> 并自担公网暴露后果 —— 本进程持有你的订阅凭证与控制台口令。
|
|
80
|
+
|
|
81
|
+
### 方式二:从源码跑(改代码 / 开发用)
|
|
82
|
+
|
|
83
|
+
环境:Node ≥ 22.9(server)/ ≥ 20.19(web)。
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
# 后端(默认 127.0.0.1:8787)
|
|
87
|
+
cd server && npm i
|
|
88
|
+
npm run dev
|
|
89
|
+
|
|
90
|
+
# 控制台(另开终端)
|
|
91
|
+
cd web && npm i
|
|
92
|
+
npm run dev # 真实联调,代理到 127.0.0.1:8787
|
|
93
|
+
npm run dev:mock # 或:纯前端开发,内置 Mock 数据(口令 relay-dev)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
> 源码形态下**后端也会同源托管 `web/dist`**:先 `cd web && npm run build`,
|
|
97
|
+
> 之后只跑后端、直接访问 `http://127.0.0.1:8787/console` 即可(不必再开 vite)。
|
|
98
|
+
> 没构建过前端时后端只提供 API,并在日志里提醒。
|
|
99
|
+
|
|
100
|
+
关键环境变量(完整说明见 `server/.env.example`):
|
|
101
|
+
|
|
102
|
+
| 变量 | 作用 |
|
|
103
|
+
| --- | --- |
|
|
104
|
+
| `RELAY_UPSTREAM_KEY` | 可选:无 OAuth 时代的全局出站凭证(**v1.6.11 起 WorkBuddy 走 OAuth,不再需要**) |
|
|
105
|
+
| `RELAY_SECRET` | HMAC:密钥哈希 + 会话令牌签名,丢失等于全部密钥作废 |
|
|
106
|
+
| `RELAY_ADMIN_PASSWORD_HASH` | 管理口令 scrypt 哈希(不设则四级回落,首启打印一次性口令) |
|
|
107
|
+
| `RELAY_DATA_DIR` / `RELAY_PORT` | 数据目录(默认 `./data`)与端口(默认 8787) |
|
|
108
|
+
|
|
109
|
+
**Qoder CN 凭证**(若要用第三家):从本机 Qoder 客户端登录态一键导入,无需手抄令牌:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
cd server && node tools/qoder-import-credential.mjs <provider_id>
|
|
113
|
+
# 例:node tools/qoder-import-credential.mjs p_qoder01
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
> [!NOTE]
|
|
117
|
+
> Qoder 凭证**不自动刷新**(本服务用导入的令牌;令牌约 30 天过期时重跑本脚本即可)。
|
|
118
|
+
> 桌面客户端与它**互不影响**(Qoder 多会话并存,实测 2026-10-16)。
|
|
119
|
+
|
|
120
|
+
## 接入
|
|
121
|
+
|
|
122
|
+
两种方言共用同一把密钥(`sk-waygate-…`,控制台「API Keys」页创建;更名前签发的 `sk-relay-…` 已退役,不再可用):
|
|
123
|
+
|
|
124
|
+
**OpenAI 兼容客户端**(Cursor / Cherry Studio / OpenAI SDK……)
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
base_url = http://<你的服务器>:8787/v1
|
|
128
|
+
api_key = sk-waygate-…
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
curl http://127.0.0.1:8787/v1/chat/completions \
|
|
133
|
+
-H "Authorization: Bearer sk-waygate-…" -H "Content-Type: application/json" \
|
|
134
|
+
-d '{"model":"kimi-k3","messages":[{"role":"user","content":"你好"}]}'
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
**Anthropic 兼容客户端**(Claude Code 等,走 `x-api-key`)
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
export ANTHROPIC_BASE_URL=http://<你的服务器>:8787
|
|
141
|
+
export ANTHROPIC_API_KEY=sk-waygate-…
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
curl http://127.0.0.1:8787/v1/messages \
|
|
146
|
+
-H "x-api-key: sk-waygate-…" -H "anthropic-version: 2023-06-01" -H "Content-Type: application/json" \
|
|
147
|
+
-d '{"model":"MiniMax-M3","max_tokens":64,"messages":[{"role":"user","content":"你好"}]}'
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
**Qoder CN**(免费档)用同一套出口,模型 id 填网关内部 key:
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
curl http://127.0.0.1:8787/v1/chat/completions \
|
|
154
|
+
-H "Authorization: Bearer sk-waygate-…" -H "Content-Type: application/json" \
|
|
155
|
+
-d '{"model":"qfmodel","messages":[{"role":"user","content":"你好"}]}'
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
模型 id 即上游目录里的真实 id,**按 id 自动分流到对应提供商**:
|
|
159
|
+
|
|
160
|
+
| 提供商 | 模型 id 形态 | 示例 |
|
|
161
|
+
| --- | --- | --- |
|
|
162
|
+
| WorkBuddy | 小写 | `minimax-m3`、`kimi-k3` |
|
|
163
|
+
| MiniMax Code | 大写 | `MiniMax-M3`、`MiniMax-M3.1-Flash-Preview` |
|
|
164
|
+
| Qoder CN | 网关内部 key | `qfmodel` = Qwen3.8-Flash、`dmodel` = DeepSeek-V4-Pro(共 13 个) |
|
|
165
|
+
|
|
166
|
+
密钥可绑定多个客户端作为**路由优先级**(只排序、从不过滤)。
|
|
167
|
+
|
|
168
|
+
## 多提供商与路由
|
|
169
|
+
|
|
170
|
+
| 提供商 | 协议 | 凭证形态 | 目录来源 | 积分口径 |
|
|
171
|
+
| --- | --- | --- | --- | --- |
|
|
172
|
+
| **WorkBuddy** | `openai`(`/v2/chat/completions`) | **官方 OAuth 设备授权**(转发与领取域同一凭证,v1.6.11 起;原 `ck_` 密钥已退役) | 上游拉取 + 逐项实测 | 按次回报 `usage.credit`(实扣)+ 资源包聚合余额 |
|
|
173
|
+
| **MiniMax Code** | `anthropic`(`/mavis/api/v1/llm/v1/messages`) | OAuth refresh token(`mmort_`,自动换新) | 内置种子(网关无清单端点) | **独立账本** `credit/details`(精确到 0.01);单次不回报 → 记 `null` 不编造 |
|
|
174
|
+
| **Qoder CN** | `qoder`(COSY 协议,`/algo/…/agent_chat_generation`) | 客户端登录态身份 JSON(`dt-`/`drt-`) | 网关 `/model/list` 真拉取(13 模型) | SSE `usage.credits` × `billable`(免费档实扣 0);余额走 `openapi` 的 `v2/me/usage`(套餐内 + 资源包,Bearer 无 COSY 签名);到期时刻上游不提供 |
|
|
175
|
+
|
|
176
|
+
**路由规则**:模型 id 先跨全部生效提供商**精确匹配(大小写敏感)**,再按提供商内模糊回落。声明行(seed/upstream/manual)优先于 observed 行——observed 泄漏曾导致幽灵目录行抢走别家请求(已修)。
|
|
177
|
+
|
|
178
|
+
> [!IMPORTANT]
|
|
179
|
+
> **凭证口径(两家不同,别混为一谈)**:
|
|
180
|
+
> · **MiniMax Code — 真互斥**:refresh token 一次性,且同一账号**同时只有一个有效会话** ——
|
|
181
|
+
> 桌面客户端登录会把本服务顶下线,反之亦然。**务必配专用账号**,否则凭证反复失效。
|
|
182
|
+
> · **Qoder CN — 不互斥**:支持多会话并存,本服务与桌面客户端可同时用**同一账号**
|
|
183
|
+
> (实测 2026-10-16:双方令牌都有效)。**无需**专用账号。
|
|
184
|
+
> 设置页「提供商健康」有常驻提醒。
|
|
185
|
+
|
|
186
|
+
## 架构
|
|
187
|
+
|
|
188
|
+
> 交互式版本(单文件页,支持明暗主题与链路高亮):[技术架构图](docs/architecture.html) · [功能架构图](docs/features.html)
|
|
189
|
+
|
|
190
|
+
### 技术架构(请求流向)
|
|
191
|
+
|
|
192
|
+
```mermaid
|
|
193
|
+
%%{init: {"flowchart":{"curve":"linear","nodeSpacing":34,"rankSpacing":64,"padding":12},"themeVariables":{"fontFamily":"ui-sans-serif, system-ui, 'Segoe UI', sans-serif","fontSize":"13px"}}}%%
|
|
194
|
+
flowchart LR
|
|
195
|
+
subgraph L0["调用端"]
|
|
196
|
+
direction TB
|
|
197
|
+
A1["编码 Agent / 工具<br/>OpenAI 兼容"]
|
|
198
|
+
A2["Claude 系客户端<br/>Anthropic"]
|
|
199
|
+
A3["Web 管理控制台<br/>React 19"]
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
subgraph L1["Waygate 网关 · NestJS + Fastify · :8787"]
|
|
203
|
+
direction TB
|
|
204
|
+
B1["KeyGuard 鉴权<br/>密钥 · IP 策略 · 模型范围"]
|
|
205
|
+
B2["协议翻译<br/>三种上游方言互译"]
|
|
206
|
+
B3["反代管线<br/>模型路由 · 并发闸 · SSE 透传"]
|
|
207
|
+
B4["Admin API<br/>概览 · 用量 · 密钥 · 设置"]
|
|
208
|
+
B5["模型目录<br/>seed · upstream · observed"]
|
|
209
|
+
B6["活动采集<br/>MiniMax 签到/账本 · Qoder 签到/积分"]
|
|
210
|
+
DB[("SQLite<br/>requests · catalog · aggregates")]
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
subgraph L2["上游 · 三家同时生效"]
|
|
214
|
+
direction TB
|
|
215
|
+
C1["WorkBuddy<br/>openai"]
|
|
216
|
+
C2["MiniMax Code<br/>anthropic"]
|
|
217
|
+
C3["Qoder CN<br/>COSY"]
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
A1 --> B1
|
|
221
|
+
A2 --> B1
|
|
222
|
+
A3 --> B4
|
|
223
|
+
B1 --> B2
|
|
224
|
+
B2 --> B3
|
|
225
|
+
B3 -->|精确匹配 → 模糊回落| C1
|
|
226
|
+
B3 --> C2
|
|
227
|
+
B3 --> C3
|
|
228
|
+
B5 -.-> B3
|
|
229
|
+
B6 -.-> C2
|
|
230
|
+
B6 -.-> C3
|
|
231
|
+
B3 -.-> DB
|
|
232
|
+
B4 -.-> DB
|
|
233
|
+
B5 -.-> DB
|
|
234
|
+
|
|
235
|
+
classDef card fill:#FFFFFF,stroke:#D0D7DE,stroke-width:1px,color:#1F2328
|
|
236
|
+
classDef data fill:#F6F8FA,stroke:#8C959F,stroke-width:1px,stroke-dasharray:4 3,color:#424A53
|
|
237
|
+
class A1,A2,A3,B1,B2,B3,B4,B5,B6,C1,C2,C3 card
|
|
238
|
+
class DB data
|
|
239
|
+
style L0 fill:#F6F8FA,stroke:#D0D7DE,color:#656D76
|
|
240
|
+
style L2 fill:#F6F8FA,stroke:#D0D7DE,color:#656D76
|
|
241
|
+
style L1 fill:#FAF7FF,stroke:#8250DF,stroke-width:1.5px,color:#6639BA
|
|
242
|
+
linkStyle 0,1,2,3,4,5,6,7 stroke:#8C959F
|
|
243
|
+
linkStyle 8,9,10,11,12,13 stroke:#BABBC0
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### 功能架构
|
|
247
|
+
|
|
248
|
+
```mermaid
|
|
249
|
+
mindmap
|
|
250
|
+
root((Waygate))
|
|
251
|
+
Web 控制台
|
|
252
|
+
总览
|
|
253
|
+
用量分析
|
|
254
|
+
趋势图等距时间轴补空桶
|
|
255
|
+
模型明细表
|
|
256
|
+
积分与额度
|
|
257
|
+
提供商管理
|
|
258
|
+
API Keys
|
|
259
|
+
请求日志
|
|
260
|
+
设置
|
|
261
|
+
提供商面板与健康
|
|
262
|
+
监听 · 管理口令
|
|
263
|
+
外观
|
|
264
|
+
明暗双主题 · 跟随系统
|
|
265
|
+
核心能力
|
|
266
|
+
双方言出口
|
|
267
|
+
OpenAI 兼容
|
|
268
|
+
Anthropic 兼容
|
|
269
|
+
多提供商路由
|
|
270
|
+
模型 id 精确匹配
|
|
271
|
+
提供商内模糊回落
|
|
272
|
+
模型目录
|
|
273
|
+
seed / upstream / observed / manual
|
|
274
|
+
密钥与策略
|
|
275
|
+
IP 上限与白名单
|
|
276
|
+
token 配额与范围
|
|
277
|
+
用量归因
|
|
278
|
+
Token 口径占比
|
|
279
|
+
byProvider 目录归因
|
|
280
|
+
活动代领
|
|
281
|
+
签到 · 成长任务 · 连登兑换
|
|
282
|
+
容错
|
|
283
|
+
断流自动重试
|
|
284
|
+
渠道校验自动降级
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
## 核心原则:反代保真
|
|
288
|
+
|
|
289
|
+
这是全项目的最高准则,一切数据口径由它裁决:
|
|
290
|
+
|
|
291
|
+
- **不做自己的标定**。积分 = 上游实扣 `usage.credit`;上游没回报就是 `null`(界面显示 `—`),绝不估算成 0 或按倍率折算。
|
|
292
|
+
- **失败行两种口径**:本地拒绝(限流/鉴权失败)记 `0` 积分(这是事实);上游路径上的失败记 `null`(花了多少不可知)。
|
|
293
|
+
- **路由只有两级真相**:模型 id 先按各生效提供商目录精确匹配(声明行优先于 observed),再按提供商内模糊回落。
|
|
294
|
+
- 上游接口取不到的数(如 WorkBuddy 无额度接口)就明说取不到,不摆一个永远 `--` 的占位假装在加载。
|
|
295
|
+
|
|
296
|
+
## 测试与质量门禁
|
|
297
|
+
|
|
298
|
+
```bash
|
|
299
|
+
cd server
|
|
300
|
+
npm test # 727 项单测(vitest):路由优先级、口径不变式、三种方言翻译、失败落库、趋势桶补齐…
|
|
301
|
+
npm run typecheck # tsc --noEmit
|
|
302
|
+
npm run smoke:boot # 真起 dist:healthz / 登录 / 目录 / 设置四关
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
仓库级门禁在 `tools/`:文件行数上限、色值字面量检查(必须走 CSS 令牌)、构建产物清洁校验等。
|
|
306
|
+
|
|
307
|
+
## 部署
|
|
308
|
+
|
|
309
|
+
### 本地启动(推荐先这样跑通)
|
|
310
|
+
|
|
311
|
+
后端与控制台都跑在本机,数据落在 `server/data/`:
|
|
312
|
+
|
|
313
|
+
```bash
|
|
314
|
+
# 1) 后端:拉依赖、生成配置、起服务(默认 127.0.0.1:8787)
|
|
315
|
+
cd server
|
|
316
|
+
npm i
|
|
317
|
+
cp .env.example .env # 首启会打印一次性管理口令;凭证在控制台导入
|
|
318
|
+
npm run dev # 开发模式(tsc watch + 起 dist)
|
|
319
|
+
|
|
320
|
+
# 2) 控制台:另开一个终端
|
|
321
|
+
cd web
|
|
322
|
+
npm i
|
|
323
|
+
npm run dev # 联调:代理到 127.0.0.1:8787(推荐)
|
|
324
|
+
# npm run dev:mock # 或纯前端开发,内置假数据(口令 relay-dev)
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
打开 http://localhost:5173 进控制台,用首启打印的管理口令登录。
|
|
328
|
+
|
|
329
|
+
**产物模式**(想跑构建后的单体):`cd server && npm run build && npm start`,
|
|
330
|
+
再用任意静态服务器托管 `web/dist/`。
|
|
331
|
+
|
|
332
|
+
**同机一键导入凭证**:relay 与客户端在同一台机器时,「提供商管理 → 配置 → 自动导入」
|
|
333
|
+
按钮可直接读本机客户端登录态完成导入(无需手抄令牌)。
|
|
334
|
+
|
|
335
|
+
### 放到服务器(可选)
|
|
336
|
+
|
|
337
|
+
本项目不绑定任何特定部署方式 —— 它是一个普通的 Node 服务 + 静态前端:
|
|
338
|
+
|
|
339
|
+
- **服务端**:`npm run build` 后用 systemd / pm2 / docker 跑 `node dist/main.js`;
|
|
340
|
+
数据目录用 `RELAY_DATA_DIR` 指定,SQLite 单文件可直接 `VACUUM INTO` 备份。
|
|
341
|
+
- **前端**:`web/dist/` 是纯静态产物,交给 Nginx / Caddy 托管即可。
|
|
342
|
+
- **TLS / 反代**:`server/deploy/nginx-relay.conf` 是一份可参考的 Nginx 配置样例
|
|
343
|
+
(注意:CSRF 校验要求 `Origin` 与 `Host` 同源,反代时 `proxy_set_header Host $host;`
|
|
344
|
+
必须保留原 Host,否则控制台登录会 403 —— 原因写在配置样例注释里)。
|
|
345
|
+
- **凭证安全**:`server/.env`、`server/data/`(SQLite 与凭证文件)**绝不入库**,
|
|
346
|
+
已在 `.gitignore` 覆盖。
|
|
347
|
+
|
|
348
|
+
## 附录
|
|
349
|
+
|
|
350
|
+
<details>
|
|
351
|
+
<summary><b>目录结构</b></summary>
|
|
352
|
+
|
|
353
|
+
```
|
|
354
|
+
server/ NestJS + Fastify + better-sqlite3(单文件 SQLite)
|
|
355
|
+
src/proxy/ 转发核心:路由解析、三种方言出口(openai/anthropic/qoder)、失败落库
|
|
356
|
+
anthropic-upstream/ MiniMax Code 出站(oauth 令牌、账本、SSE 解码、签到)
|
|
357
|
+
qoder-upstream/ Qoder CN 出站(COSY 会话/签名/自定义 base64、SSE 解码、签到与积分账本)
|
|
358
|
+
src/api/ 控制台 API:概览聚合、密钥、目录、提供商、设置(含设置元数据目录)
|
|
359
|
+
src/catalog/ 目录同步(上游拉取 / observed 并入 / 幽灵行防护 / Qoder 拉取)
|
|
360
|
+
src/store/ 迁移、仓储与日志留存清理(schema.sql 即迁移事实源)
|
|
361
|
+
src/common/ 跨模块小件(凭证文件的 stat 失效进程内缓存等)
|
|
362
|
+
tools/ 运维与验收脚本(smoke:boot、凭证导入、迁移…)
|
|
363
|
+
web/ React 19 + Vite 控制台与官网(dev:mock 内置全套假数据)
|
|
364
|
+
tools/ 仓库级工具(lint 门禁、token 合并、截图)
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
前后端**同版本联动**(配套发版),所以是一个库而不是两个。
|
|
368
|
+
|
|
369
|
+
</details>
|
|
370
|
+
|
|
371
|
+
<details>
|
|
372
|
+
<summary><b>控制台口径速查</b></summary>
|
|
373
|
+
|
|
374
|
+
- **已消耗积分**:仅统计经本服务转发的成功行,取上游实扣;`credits IS NULL` 的行不计入,条数单列(`creditsUnknownCount`)。
|
|
375
|
+
- **客户端归属**(概览摘要 / 指标卡 tooltip):按「模型 id → 目录 provider」解析——双绑定密钥的流量会正确分到各自客户端;目录外的模型回落密钥绑定名。
|
|
376
|
+
- **不变式**:`ΣbyModel === ΣbyClient === metrics.creditsUsed`,服务端同源聚合并断言相等后才返回。
|
|
377
|
+
|
|
378
|
+
</details>
|
|
379
|
+
|
|
380
|
+
---
|
|
381
|
+
|
|
382
|
+
## 相关项目
|
|
383
|
+
|
|
384
|
+
同一作者还开源了三个**轻量单家反代**(各是一个 npm 包,一条命令起服务,无需控制台)——
|
|
385
|
+
只想接某一家时它们更轻;需要统一管理多家、看用量与请求日志时用本仓库:
|
|
386
|
+
|
|
387
|
+
- **[waygate-workbuddy](https://github.com/zhanghaojie513/waygate-workbuddy)** —— WorkBuddy(命令 `wb`,`npm i -g waygate-workbuddy`,走官方 OAuth 登录)
|
|
388
|
+
- **[waygate-qoder](https://github.com/zhanghaojie513/waygate-qoder)** —— Qoder CN(命令 `qoc`,`npm i -g waygate-qoder`)
|
|
389
|
+
- **[waygate-mmcode](https://github.com/zhanghaojie513/waygate-mmcode)** —— MiniMax Code(命令 `mmcode`,`npm i -g waygate-mmcode`)
|
|
390
|
+
- **[waygate-core](https://github.com/zhanghaojie513/waygate-core)** —— 上面三个共用的骨架(库,不面向最终用户)
|
|
391
|
+
|
|
392
|
+
---
|
|
393
|
+
|
|
394
|
+
版本约定:`1.0.0` 起 API 契约稳定(当前 `1.6.14`,见 `server/src/version.ts`;历史版本见 [Releases](https://github.com/zhanghaojie513/waygate-open/releases))。
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* `waygate-open` 的命令行入口(v1.6.14)。
|
|
4
|
+
*
|
|
5
|
+
* ## 它做什么
|
|
6
|
+
* `npx waygate-open` / `waygate-open` —— 起本地服务 → 等它就绪 → **自动打开浏览器**
|
|
7
|
+
* 到控制台。这是「本地启动」形态:自带控制台(同源托管),不需要另起前端。
|
|
8
|
+
*
|
|
9
|
+
* ## 为什么是这个形状(参考 Astra-Gate 的分发方式)
|
|
10
|
+
* Astra-Gate 也是「一条命令起服务 + 开浏览器」,但它要发**6 个平台的预编译二进制**
|
|
11
|
+
* (C# 应用)。本包是纯 Node:`better-sqlite3` 自带 N-API prebuild(`gypfile:false`
|
|
12
|
+
* 且无 install 脚本,安装不编译),所以**一个包全平台通用**。
|
|
13
|
+
*
|
|
14
|
+
* ## 参数(多可用环境变量等价替代)
|
|
15
|
+
* --port <n> 监听端口(默认 8787;等价 RELAY_PORT)
|
|
16
|
+
* --host <addr> 监听地址(默认 127.0.0.1;对外监听需 RELAY_ALLOW_PUBLIC_BIND=1)
|
|
17
|
+
* --data-dir <path> 数据目录(默认 ~/.waygate/open;等价 RELAY_DATA_DIR)
|
|
18
|
+
* --no-open 不自动开浏览器(服务器 / CI 场景)
|
|
19
|
+
* --help / -h
|
|
20
|
+
*
|
|
21
|
+
* ## 数据落在哪
|
|
22
|
+
* 默认 **`~/.waygate/open/`**(与家族的 wb / qoc / mmcode 一致的"用户主目录下"约定),
|
|
23
|
+
* 而**不是** cwd —— 全局安装后 cwd 是用户随便某个目录。
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { spawn } from 'node:child_process';
|
|
27
|
+
import { existsSync, mkdirSync } from 'node:fs';
|
|
28
|
+
import { homedir } from 'node:os';
|
|
29
|
+
import { dirname, join, resolve } from 'node:path';
|
|
30
|
+
import { fileURLToPath } from 'node:url';
|
|
31
|
+
|
|
32
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
33
|
+
/** 本包根目录:bin/ 的上一级。 */
|
|
34
|
+
const PKG_ROOT = resolve(HERE, '..');
|
|
35
|
+
|
|
36
|
+
// ---------------------------------------------------------------- 参数解析
|
|
37
|
+
|
|
38
|
+
function parseArgs(argv) {
|
|
39
|
+
const out = { open: true, help: false };
|
|
40
|
+
for (let i = 0; i < argv.length; i++) {
|
|
41
|
+
const a = argv[i];
|
|
42
|
+
if (a === '--help' || a === '-h') out.help = true;
|
|
43
|
+
else if (a === '--no-open') out.open = false;
|
|
44
|
+
else if (a === '--open') out.open = true;
|
|
45
|
+
else if (a === '--port') out.port = argv[++i];
|
|
46
|
+
else if (a.startsWith('--port=')) out.port = a.slice('--port='.length);
|
|
47
|
+
else if (a === '--host') out.host = argv[++i];
|
|
48
|
+
else if (a.startsWith('--host=')) out.host = a.slice('--host='.length);
|
|
49
|
+
else if (a === '--data-dir') out.dataDir = argv[++i];
|
|
50
|
+
else if (a.startsWith('--data-dir=')) out.dataDir = a.slice('--data-dir='.length);
|
|
51
|
+
else if (a === '--password') out.password = argv[++i];
|
|
52
|
+
else if (a.startsWith('--password=')) out.password = a.slice('--password='.length);
|
|
53
|
+
else {
|
|
54
|
+
console.error(`未知参数:${a}\n用 --help 看用法。`);
|
|
55
|
+
process.exit(2);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const HELP = `
|
|
62
|
+
途关 Waygate · 本地启动(自带控制台)
|
|
63
|
+
|
|
64
|
+
用法:
|
|
65
|
+
waygate-open [选项]
|
|
66
|
+
|
|
67
|
+
选项:
|
|
68
|
+
--port <n> 监听端口(默认 8787,环境变量 RELAY_PORT 同义)
|
|
69
|
+
--host <addr> 监听地址(默认 127.0.0.1;对外监听需 RELAY_ALLOW_PUBLIC_BIND=1)
|
|
70
|
+
--data-dir <path> 数据目录(默认 ~/.waygate/open,环境变量 RELAY_DATA_DIR 同义)
|
|
71
|
+
--password <pw> 首次启动的管理口令(默认 waygate;环境变量 RELAY_ADMIN_PASSWORD 同义)
|
|
72
|
+
只对「首次启动」生效,不改已改过的口令;对外监听时不套用默认值
|
|
73
|
+
--no-open 不自动打开浏览器(服务器 / CI)
|
|
74
|
+
-h, --help 显示本帮助
|
|
75
|
+
|
|
76
|
+
启动后:
|
|
77
|
+
· 控制台 http://127.0.0.1:<port>/console (浏览器会自动打开)
|
|
78
|
+
· 接入地址 http://127.0.0.1:<port>/v1 (填进任意 OpenAI / Anthropic 兼容客户端)
|
|
79
|
+
· 健康检查 http://127.0.0.1:<port>/healthz
|
|
80
|
+
|
|
81
|
+
默认管理口令是 waygate —— 仅本机可访问时使用,登录后建议改掉。
|
|
82
|
+
按 Ctrl+C 停止。
|
|
83
|
+
`.trim();
|
|
84
|
+
|
|
85
|
+
// ---------------------------------------------------------------- 开浏览器
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 用系统默认浏览器打开 URL。
|
|
89
|
+
*
|
|
90
|
+
* 三种平台各一条命令(与 Astra-Gate 的 cli/src/lib/browser.ts 同一套写法):
|
|
91
|
+
* darwin → open
|
|
92
|
+
* win32 → rundll32 url.dll,FileProtocolHandler(比 `start` 可靠:`start` 是 cmd 内建,
|
|
93
|
+
* 不经 shell 无法调用)
|
|
94
|
+
* 其他 → xdg-open
|
|
95
|
+
* 全部 `detached + unref + stdio:ignore`:不阻塞、不继承 stdout,且**失败也不影响服务**
|
|
96
|
+
* (URL 已打印,用户手点即可)。
|
|
97
|
+
*/
|
|
98
|
+
function openInBrowser(url) {
|
|
99
|
+
try {
|
|
100
|
+
if (process.platform === 'darwin') {
|
|
101
|
+
spawn('open', [url], { detached: true, stdio: 'ignore' }).unref();
|
|
102
|
+
} else if (process.platform === 'win32') {
|
|
103
|
+
spawn('rundll32', ['url.dll,FileProtocolHandler', url], {
|
|
104
|
+
detached: true,
|
|
105
|
+
stdio: 'ignore',
|
|
106
|
+
windowsHide: true,
|
|
107
|
+
}).unref();
|
|
108
|
+
} else {
|
|
109
|
+
spawn('xdg-open', [url], { detached: true, stdio: 'ignore' }).unref();
|
|
110
|
+
}
|
|
111
|
+
} catch {
|
|
112
|
+
/* 打不开就算了 —— URL 已打印 */
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ---------------------------------------------------------------- 等就绪
|
|
117
|
+
|
|
118
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
119
|
+
|
|
120
|
+
/** 轮询 /healthz 直到 200 或超时。 */
|
|
121
|
+
async function waitReady(base, timeoutMs = 30_000) {
|
|
122
|
+
const deadline = Date.now() + timeoutMs;
|
|
123
|
+
while (Date.now() < deadline) {
|
|
124
|
+
try {
|
|
125
|
+
const res = await fetch(`${base}/healthz`, { signal: AbortSignal.timeout(1500) });
|
|
126
|
+
if (res.ok) return true;
|
|
127
|
+
} catch {
|
|
128
|
+
/* 还没起来 */
|
|
129
|
+
}
|
|
130
|
+
await sleep(250);
|
|
131
|
+
}
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ---------------------------------------------------------------- 主流程
|
|
136
|
+
|
|
137
|
+
async function main() {
|
|
138
|
+
const args = parseArgs(process.argv.slice(2));
|
|
139
|
+
if (args.help) {
|
|
140
|
+
console.log(HELP);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/* 数据目录:显式参数 > 环境变量 > ~/.waygate/open。放主目录而非 cwd。 */
|
|
145
|
+
const dataDir = args.dataDir ?? process.env.RELAY_DATA_DIR ?? join(homedir(), '.waygate', 'open');
|
|
146
|
+
mkdirSync(dataDir, { recursive: true });
|
|
147
|
+
|
|
148
|
+
const entry = join(PKG_ROOT, 'server', 'dist', 'main.js');
|
|
149
|
+
if (!existsSync(entry)) {
|
|
150
|
+
console.error(
|
|
151
|
+
`未找到服务端产物:${entry}\n` +
|
|
152
|
+
`源码仓请先在 server/ 下跑 \`npm run build\`;npm 安装的正式包不应出现此项。`,
|
|
153
|
+
);
|
|
154
|
+
process.exit(1);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const port = args.port ?? process.env.RELAY_PORT ?? '8787';
|
|
158
|
+
const host = args.host ?? '127.0.0.1';
|
|
159
|
+
/* 对外监听(非回环)在服务端要显式放行 —— 把用户的 --host 意图翻译成那个环境变量,
|
|
160
|
+
否则服务端会静默降级回 127.0.0.1,用户以为监听成功了。 */
|
|
161
|
+
const allowPublicBind = ['127.0.0.1', '::1', 'localhost'].includes(host) ? undefined : '1';
|
|
162
|
+
|
|
163
|
+
/* 默认管理口令(v1.6.14):**只在回环绑定**(= 只有本机能访问)时注入。
|
|
164
|
+
理由:本地启动的诉求是"装上就能用",不该卡在"去日志里抄那串随机字符"。
|
|
165
|
+
而对外监听时,一个众所周知的口令等于把控制台公开 —— 那种情形仍走随机生成,
|
|
166
|
+
用户从启动日志里取(CLI 会把日志透传出来)。
|
|
167
|
+
用户显式给了 --password / RELAY_ADMIN_PASSWORD 时以他给的为准。 */
|
|
168
|
+
const isLoopback = !allowPublicBind;
|
|
169
|
+
const defaultPassword = isLoopback ? 'waygate' : undefined;
|
|
170
|
+
|
|
171
|
+
console.log('途关 Waygate · 本地启动');
|
|
172
|
+
console.log(` 数据目录 ${dataDir}`);
|
|
173
|
+
console.log(` 监听 http://${host}:${port}`);
|
|
174
|
+
if (args.password ?? process.env.RELAY_ADMIN_PASSWORD ?? defaultPassword) {
|
|
175
|
+
const pw = args.password ?? process.env.RELAY_ADMIN_PASSWORD ?? defaultPassword;
|
|
176
|
+
console.log(` 管理口令 ${pw}${isLoopback && pw === defaultPassword ? '(默认,建议登录后改掉)' : ''}`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const child = spawn(process.execPath, [entry], {
|
|
180
|
+
stdio: 'inherit', // 服务端日志(含首次启动的管理口令)直接透给用户
|
|
181
|
+
env: {
|
|
182
|
+
...process.env,
|
|
183
|
+
RELAY_DATA_DIR: dataDir,
|
|
184
|
+
RELAY_PORT: port,
|
|
185
|
+
...(allowPublicBind ? { RELAY_ALLOW_PUBLIC_BIND: allowPublicBind } : {}),
|
|
186
|
+
...((args.password ?? process.env.RELAY_ADMIN_PASSWORD ?? defaultPassword)
|
|
187
|
+
? { RELAY_ADMIN_PASSWORD: args.password ?? process.env.RELAY_ADMIN_PASSWORD ?? defaultPassword }
|
|
188
|
+
: {}),
|
|
189
|
+
},
|
|
190
|
+
cwd: join(PKG_ROOT, 'server'),
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
/* Ctrl+C:把信号转给子进程,让它走优雅关闭(WAL 检查点、在途请求收尾)。 */
|
|
194
|
+
const forward = (sig) => () => {
|
|
195
|
+
if (!child.killed) child.kill(sig);
|
|
196
|
+
};
|
|
197
|
+
process.on('SIGINT', forward('SIGINT'));
|
|
198
|
+
process.on('SIGTERM', forward('SIGTERM'));
|
|
199
|
+
|
|
200
|
+
child.on('exit', (code) => process.exit(code ?? 0));
|
|
201
|
+
|
|
202
|
+
const base = `http://${host === '0.0.0.0' ? '127.0.0.1' : host}:${port}`;
|
|
203
|
+
const ready = await waitReady(base);
|
|
204
|
+
if (!ready) {
|
|
205
|
+
console.error(`\n服务在 30 秒内没就绪,请查看上面的日志(端口 ${port} 可能被占用)。`);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const consoleUrl = `${base}/console`;
|
|
210
|
+
console.log(`\n就绪 ✓ 控制台 ${consoleUrl}`);
|
|
211
|
+
console.log(` 接入地址 ${base}/v1\n`);
|
|
212
|
+
if (args.open) openInBrowser(consoleUrl);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
main().catch((e) => {
|
|
216
|
+
console.error(`启动失败:${e && e.message ? e.message : String(e)}`);
|
|
217
|
+
process.exit(1);
|
|
218
|
+
});
|