@jsonstudio/rcc 0.89.1562 → 0.89.1803
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/README.md +97 -13
- package/configsamples/config.json +8 -8
- package/configsamples/config.reference.json +1 -1
- package/configsamples/provider/crs/config.v1.json +1 -1
- package/configsamples/provider/glm/config.v1.json +1 -1
- package/configsamples/provider/glm-anthropic/config.v1.json +1 -1
- package/configsamples/provider/kimi/config.v1.json +1 -1
- package/configsamples/provider/lmstudio/config.v1.json +2 -1
- package/configsamples/provider/mimo/config.v1.json +1 -1
- package/configsamples/provider/modelscope/config.v1.json +1 -1
- package/configsamples/provider/qwen/config.v1.json +1 -1
- package/configsamples/provider/tab/config.v1.json +2 -1
- package/configsamples/provider/tabglm/config.v1.json +1 -1
- package/dist/build-info.js +3 -3
- package/dist/build-info.js.map +1 -1
- package/dist/cli/commands/config.js +8 -9
- package/dist/cli/commands/config.js.map +1 -1
- package/dist/cli/commands/restart.d.ts +4 -12
- package/dist/cli/commands/restart.js +226 -120
- package/dist/cli/commands/restart.js.map +1 -1
- package/dist/cli/commands/start.d.ts +1 -0
- package/dist/cli/commands/start.js +28 -1
- package/dist/cli/commands/start.js.map +1 -1
- package/dist/cli/commands/status.js +12 -6
- package/dist/cli/commands/status.js.map +1 -1
- package/dist/cli/config/init-provider-catalog.js +12 -11
- package/dist/cli/config/init-provider-catalog.js.map +1 -1
- package/dist/cli.js +3 -14
- package/dist/cli.js.map +1 -1
- package/dist/client/anthropic/anthropic-protocol-client.d.ts +1 -0
- package/dist/client/anthropic/anthropic-protocol-client.js +25 -0
- package/dist/client/anthropic/anthropic-protocol-client.js.map +1 -1
- package/dist/commands/oauth.js +185 -9
- package/dist/commands/oauth.js.map +1 -1
- package/dist/commands/token-daemon.js +12 -2
- package/dist/commands/token-daemon.js.map +1 -1
- package/dist/docs/daemon-admin-ui.html +1242 -234
- package/dist/index.js +119 -0
- package/dist/index.js.map +1 -1
- package/dist/manager/index.d.ts +2 -0
- package/dist/manager/index.js +39 -2
- package/dist/manager/index.js.map +1 -1
- package/dist/manager/modules/quota/antigravity-quota-manager.d.ts +29 -5
- package/dist/manager/modules/quota/antigravity-quota-manager.js +369 -113
- package/dist/manager/modules/quota/antigravity-quota-manager.js.map +1 -1
- package/dist/manager/modules/quota/provider-quota-daemon.cooldown.d.ts +7 -0
- package/dist/manager/modules/quota/provider-quota-daemon.cooldown.js +61 -0
- package/dist/manager/modules/quota/provider-quota-daemon.cooldown.js.map +1 -1
- package/dist/manager/modules/quota/provider-quota-daemon.d.ts +1 -0
- package/dist/manager/modules/quota/provider-quota-daemon.events.js +134 -5
- package/dist/manager/modules/quota/provider-quota-daemon.events.js.map +1 -1
- package/dist/manager/modules/quota/provider-quota-daemon.js +19 -13
- package/dist/manager/modules/quota/provider-quota-daemon.js.map +1 -1
- package/dist/manager/modules/quota/provider-quota-daemon.model-backoff.d.ts +1 -0
- package/dist/manager/modules/quota/provider-quota-daemon.model-backoff.js +8 -3
- package/dist/manager/modules/quota/provider-quota-daemon.model-backoff.js.map +1 -1
- package/dist/manager/modules/token/index.js +2 -2
- package/dist/manager/modules/token/index.js.map +1 -1
- package/dist/manager/quota/provider-quota-center.d.ts +9 -0
- package/dist/manager/quota/provider-quota-center.js +19 -2
- package/dist/manager/quota/provider-quota-center.js.map +1 -1
- package/dist/modules/llmswitch/bridge.d.ts +33 -1
- package/dist/modules/llmswitch/bridge.js +170 -2
- package/dist/modules/llmswitch/bridge.js.map +1 -1
- package/dist/modules/llmswitch/core-loader.js +64 -11
- package/dist/modules/llmswitch/core-loader.js.map +1 -1
- package/dist/modules/pipeline/utils/debug-logger.d.ts +1 -0
- package/dist/modules/pipeline/utils/debug-logger.js +50 -3
- package/dist/modules/pipeline/utils/debug-logger.js.map +1 -1
- package/dist/providers/auth/apikey-auth.js +15 -3
- package/dist/providers/auth/apikey-auth.js.map +1 -1
- package/dist/providers/auth/oauth-auth.js +26 -2
- package/dist/providers/auth/oauth-auth.js.map +1 -1
- package/dist/providers/auth/oauth-lifecycle.d.ts +13 -1
- package/dist/providers/auth/oauth-lifecycle.js +346 -45
- package/dist/providers/auth/oauth-lifecycle.js.map +1 -1
- package/dist/providers/auth/oauth-repair-cooldown.d.ts +21 -0
- package/dist/providers/auth/oauth-repair-cooldown.js +100 -0
- package/dist/providers/auth/oauth-repair-cooldown.js.map +1 -0
- package/dist/providers/auth/oauth-repair-env.d.ts +1 -0
- package/dist/providers/auth/oauth-repair-env.js +79 -0
- package/dist/providers/auth/oauth-repair-env.js.map +1 -0
- package/dist/providers/auth/qwen-userinfo-helper.d.ts +2 -0
- package/dist/providers/auth/qwen-userinfo-helper.js +72 -40
- package/dist/providers/auth/qwen-userinfo-helper.js.map +1 -1
- package/dist/providers/auth/tokenfile-auth.d.ts +2 -0
- package/dist/providers/auth/tokenfile-auth.js +163 -21
- package/dist/providers/auth/tokenfile-auth.js.map +1 -1
- package/dist/providers/core/api/provider-types.d.ts +10 -0
- package/dist/providers/core/config/camoufox-launcher.d.ts +3 -0
- package/dist/providers/core/config/camoufox-launcher.js +190 -3
- package/dist/providers/core/config/camoufox-launcher.js.map +1 -1
- package/dist/providers/core/config/oauth-flows.js +50 -19
- package/dist/providers/core/config/oauth-flows.js.map +1 -1
- package/dist/providers/core/config/provider-oauth-configs.js +1 -1
- package/dist/providers/core/config/provider-oauth-configs.js.map +1 -1
- package/dist/providers/core/runtime/gemini-cli-http-provider.d.ts +5 -0
- package/dist/providers/core/runtime/gemini-cli-http-provider.js +172 -15
- package/dist/providers/core/runtime/gemini-cli-http-provider.js.map +1 -1
- package/dist/providers/core/runtime/gemini-http-provider.d.ts +11 -0
- package/dist/providers/core/runtime/gemini-http-provider.js +281 -3
- package/dist/providers/core/runtime/gemini-http-provider.js.map +1 -1
- package/dist/providers/core/runtime/http-request-executor.js +55 -0
- package/dist/providers/core/runtime/http-request-executor.js.map +1 -1
- package/dist/providers/core/runtime/http-transport-provider.js +10 -14
- package/dist/providers/core/runtime/http-transport-provider.js.map +1 -1
- package/dist/providers/core/runtime/provider-factory.d.ts +1 -0
- package/dist/providers/core/runtime/provider-factory.js +40 -2
- package/dist/providers/core/runtime/provider-factory.js.map +1 -1
- package/dist/providers/core/strategies/oauth-auth-code-flow.js +45 -2
- package/dist/providers/core/strategies/oauth-auth-code-flow.js.map +1 -1
- package/dist/providers/core/strategies/oauth-device-flow.js +13 -2
- package/dist/providers/core/strategies/oauth-device-flow.js.map +1 -1
- package/dist/providers/core/strategies/oauth-refresh-errors.d.ts +1 -0
- package/dist/providers/core/strategies/oauth-refresh-errors.js +26 -0
- package/dist/providers/core/strategies/oauth-refresh-errors.js.map +1 -0
- package/dist/providers/core/utils/snapshot-writer.d.ts +4 -2
- package/dist/providers/core/utils/snapshot-writer.js +86 -23
- package/dist/providers/core/utils/snapshot-writer.js.map +1 -1
- package/dist/scripts/camoufox/launch-auth.mjs +545 -49
- package/dist/server/handlers/chat-handler.js +1 -1
- package/dist/server/handlers/chat-handler.js.map +1 -1
- package/dist/server/handlers/handler-utils.d.ts +1 -0
- package/dist/server/handlers/handler-utils.js +231 -3
- package/dist/server/handlers/handler-utils.js.map +1 -1
- package/dist/server/handlers/messages-handler.js +1 -1
- package/dist/server/handlers/messages-handler.js.map +1 -1
- package/dist/server/handlers/responses-handler.js +17 -5
- package/dist/server/handlers/responses-handler.js.map +1 -1
- package/dist/server/handlers/sse-dispatcher.js +10 -1
- package/dist/server/handlers/sse-dispatcher.js.map +1 -1
- package/dist/server/runtime/http-server/daemon-admin/control-handler.d.ts +3 -0
- package/dist/server/runtime/http-server/daemon-admin/control-handler.js +389 -0
- package/dist/server/runtime/http-server/daemon-admin/control-handler.js.map +1 -0
- package/dist/server/runtime/http-server/daemon-admin/credentials-handler.js +190 -5
- package/dist/server/runtime/http-server/daemon-admin/credentials-handler.js.map +1 -1
- package/dist/server/runtime/http-server/daemon-admin/providers-handler.js +2 -1
- package/dist/server/runtime/http-server/daemon-admin/providers-handler.js.map +1 -1
- package/dist/server/runtime/http-server/daemon-admin/quota-handler.js +116 -14
- package/dist/server/runtime/http-server/daemon-admin/quota-handler.js.map +1 -1
- package/dist/server/runtime/http-server/daemon-admin/routing-policy.d.ts +30 -0
- package/dist/server/runtime/http-server/daemon-admin/routing-policy.js +133 -0
- package/dist/server/runtime/http-server/daemon-admin/routing-policy.js.map +1 -0
- package/dist/server/runtime/http-server/daemon-admin/status-handler.js +40 -1
- package/dist/server/runtime/http-server/daemon-admin/status-handler.js.map +1 -1
- package/dist/server/runtime/http-server/daemon-admin-routes.d.ts +5 -0
- package/dist/server/runtime/http-server/daemon-admin-routes.js +3 -0
- package/dist/server/runtime/http-server/daemon-admin-routes.js.map +1 -1
- package/dist/server/runtime/http-server/executor-pipeline.d.ts +10 -0
- package/dist/server/runtime/http-server/executor-pipeline.js +6 -0
- package/dist/server/runtime/http-server/executor-pipeline.js.map +1 -1
- package/dist/server/runtime/http-server/executor-response.js +26 -0
- package/dist/server/runtime/http-server/executor-response.js.map +1 -1
- package/dist/server/runtime/http-server/hub-shadow-compare.js +41 -3
- package/dist/server/runtime/http-server/hub-shadow-compare.js.map +1 -1
- package/dist/server/runtime/http-server/index.d.ts +9 -0
- package/dist/server/runtime/http-server/index.js +337 -91
- package/dist/server/runtime/http-server/index.js.map +1 -1
- package/dist/server/runtime/http-server/middleware.js +27 -1
- package/dist/server/runtime/http-server/middleware.js.map +1 -1
- package/dist/server/runtime/http-server/request-executor.js +159 -24
- package/dist/server/runtime/http-server/request-executor.js.map +1 -1
- package/dist/server/runtime/http-server/routes.d.ts +1 -0
- package/dist/server/runtime/http-server/routes.js +36 -3
- package/dist/server/runtime/http-server/routes.js.map +1 -1
- package/dist/server/runtime/http-server/server-id.d.ts +1 -0
- package/dist/server/runtime/http-server/server-id.js +18 -0
- package/dist/server/runtime/http-server/server-id.js.map +1 -0
- package/dist/server/runtime/http-server/stats-manager.d.ts +2 -0
- package/dist/server/runtime/http-server/stats-manager.js +63 -7
- package/dist/server/runtime/http-server/stats-manager.js.map +1 -1
- package/dist/server/runtime/http-server/types.d.ts +2 -0
- package/dist/server/utils/stage-logger.js +54 -9
- package/dist/server/utils/stage-logger.js.map +1 -1
- package/dist/token-daemon/history-store.d.ts +8 -3
- package/dist/token-daemon/history-store.js +41 -20
- package/dist/token-daemon/history-store.js.map +1 -1
- package/dist/token-daemon/index.d.ts +5 -1
- package/dist/token-daemon/index.js +191 -11
- package/dist/token-daemon/index.js.map +1 -1
- package/dist/token-daemon/quota-auth-issue.d.ts +7 -0
- package/dist/token-daemon/quota-auth-issue.js +231 -0
- package/dist/token-daemon/quota-auth-issue.js.map +1 -0
- package/dist/token-daemon/token-daemon.d.ts +2 -0
- package/dist/token-daemon/token-daemon.js +177 -14
- package/dist/token-daemon/token-daemon.js.map +1 -1
- package/dist/token-portal/local-token-portal.js +6 -0
- package/dist/token-portal/local-token-portal.js.map +1 -1
- package/docs/ANTIGRAVITY_IDE_FORWARD_PROXY.md +61 -0
- package/docs/ANTIGRAVITY_THOUGHT_SIGNATURE_BOOTSTRAP_429.md +80 -0
- package/docs/CLOCK.md +94 -0
- package/docs/DAEMON_CONTROL_PLANE.md +34 -0
- package/docs/OAUTH.md +172 -0
- package/docs/PROVIDERS_BUILTIN.md +5 -3
- package/docs/PROVIDER_TYPES.md +6 -4
- package/docs/QUOTA_MANAGER_V3.md +54 -0
- package/docs/ROUTING_POLICY_SCHEMA.md +47 -0
- package/docs/ROUTING_POLICY_UI.md +11 -0
- package/docs/SERVERTOOL_CLOCK_DESIGN.md +56 -25
- package/docs/antigravity-routing-contract.md +17 -11
- package/docs/config-secrets.md +49 -0
- package/docs/daemon-admin-ui.html +1242 -234
- package/docs/oauth-authentication-guide.md +4 -0
- package/docs/oauth-iflow-implementation.md +4 -0
- package/docs/provider-quota-design.md +11 -0
- package/docs/providers/antigravity-gemini-provider-compat.md +1 -0
- package/docs/providers/antigravity-thought-signature.md +127 -0
- package/docs/providers/tabglm-claude-code-compat.md +11 -3
- package/docs/refactoring/host-sharedmodule-safe-migration-plan.md +164 -0
- package/docs/token-daemon-preview.html +2 -2
- package/docs/token-refresh-daemon-plan.md +6 -6
- package/package.json +4 -3
- package/scripts/antigravity-ide-forward-proxy.mjs +362 -0
- package/scripts/backfill-apply-patch-exec-errorsamples.mjs +19 -0
- package/scripts/camoufox/launch-auth.mjs +545 -49
- package/scripts/ci/repo-sanity.mjs +2 -0
- package/scripts/install-global.sh +46 -0
- package/scripts/migrate-antigravity-session-signatures-alias.mjs +193 -0
- package/scripts/migrate-antigravity-session-signatures.mjs +165 -0
- package/scripts/tests/blackbox-rcc-vs-routecodex-antigravity.mjs +44 -9
- package/scripts/tests/ci-jest.mjs +3 -0
- package/scripts/verify-client-headers.mjs +33 -5
- package/scripts/virtual-router-dryrun.mjs +333 -0
|
@@ -557,6 +557,40 @@
|
|
|
557
557
|
color: rgba(255, 255, 255, 0.82);
|
|
558
558
|
}
|
|
559
559
|
|
|
560
|
+
|
|
561
|
+
.auth-selection-summary {
|
|
562
|
+
display: flex;
|
|
563
|
+
flex-wrap: wrap;
|
|
564
|
+
gap: 8px;
|
|
565
|
+
margin-top: 6px;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
.auth-mode-panels {
|
|
569
|
+
border: 1px solid var(--border);
|
|
570
|
+
border-radius: 12px;
|
|
571
|
+
padding: 10px;
|
|
572
|
+
background: rgba(0, 0, 0, 0.16);
|
|
573
|
+
margin-bottom: 10px;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
.auth-mode-panel .section-sub {
|
|
577
|
+
margin-bottom: 8px;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
.auth-verify-url {
|
|
581
|
+
margin: 8px 0;
|
|
582
|
+
white-space: pre-wrap;
|
|
583
|
+
word-break: break-all;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
.auth-mode-tabs {
|
|
587
|
+
margin: 10px 0;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
.auth-mode-tabs .tab {
|
|
591
|
+
min-width: 120px;
|
|
592
|
+
}
|
|
593
|
+
|
|
560
594
|
.log {
|
|
561
595
|
white-space: pre-wrap;
|
|
562
596
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
|
|
@@ -604,6 +638,7 @@
|
|
|
604
638
|
<div class="statusline">
|
|
605
639
|
<div class="pill"><span id="statusDot" class="dot"></span><span id="statusText">connecting…</span></div>
|
|
606
640
|
<div class="pill"><span class="mono" id="serverId">serverId: —</span></div>
|
|
641
|
+
<div class="pill"><span class="mono" id="serverVersion">version: —</span></div>
|
|
607
642
|
<button id="restartRuntimeBtn" class="primary">Restart runtime</button>
|
|
608
643
|
</div>
|
|
609
644
|
</header>
|
|
@@ -660,13 +695,105 @@
|
|
|
660
695
|
</div>
|
|
661
696
|
|
|
662
697
|
<div class="tabs">
|
|
698
|
+
<button class="tab" data-tab="control">Control</button>
|
|
663
699
|
<button class="tab active" data-tab="providers">Provider Pool</button>
|
|
700
|
+
<button class="tab" data-tab="stats">Stats</button>
|
|
664
701
|
<button class="tab" data-tab="tokens">Token Stats</button>
|
|
665
702
|
<button class="tab" data-tab="credentials">Auth Provider Pool</button>
|
|
666
703
|
<button class="tab" data-tab="quota">Quota Pool</button>
|
|
667
704
|
<button class="tab" data-tab="routing">Runtime Routing Pool</button>
|
|
668
705
|
</div>
|
|
669
706
|
|
|
707
|
+
<section id="panelControl" data-panel="control" style="display:none;">
|
|
708
|
+
<div class="grid grid-wide-left">
|
|
709
|
+
<div class="card" style="box-shadow: none;">
|
|
710
|
+
<p class="section-title">Control plane (single entry)</p>
|
|
711
|
+
<p class="section-sub">
|
|
712
|
+
This panel uses <span class="mono">/daemon/control/snapshot</span> and <span class="mono">/daemon/control/mutate</span>
|
|
713
|
+
to manage all local servers (broadcast restart) and quota operations from one port.
|
|
714
|
+
</p>
|
|
715
|
+
<div class="row" style="margin-bottom: 10px;">
|
|
716
|
+
<button id="controlRefreshBtn" class="primary">Refresh</button>
|
|
717
|
+
<button id="controlRestartAllBtn" class="danger">Restart all servers</button>
|
|
718
|
+
<button id="controlQuotaRefreshBtn">Refresh quota</button>
|
|
719
|
+
<span id="controlHint" class="muted" style="margin-left:auto; font-size:12px;"></span>
|
|
720
|
+
</div>
|
|
721
|
+
<div class="table-wrap">
|
|
722
|
+
<table class="table">
|
|
723
|
+
<thead>
|
|
724
|
+
<tr>
|
|
725
|
+
<th>port</th>
|
|
726
|
+
<th>version</th>
|
|
727
|
+
<th>ready</th>
|
|
728
|
+
<th>pids</th>
|
|
729
|
+
</tr>
|
|
730
|
+
</thead>
|
|
731
|
+
<tbody id="controlServersTbody"></tbody>
|
|
732
|
+
</table>
|
|
733
|
+
</div>
|
|
734
|
+
<p class="section-title" style="margin-top: 12px;">Routing hits (llmswitch-core)</p>
|
|
735
|
+
<div class="table-wrap">
|
|
736
|
+
<table class="table">
|
|
737
|
+
<thead>
|
|
738
|
+
<tr>
|
|
739
|
+
<th>route</th>
|
|
740
|
+
<th>hits</th>
|
|
741
|
+
</tr>
|
|
742
|
+
</thead>
|
|
743
|
+
<tbody id="controlHitsTbody"></tbody>
|
|
744
|
+
</table>
|
|
745
|
+
</div>
|
|
746
|
+
<div id="controlOpLog" class="log" style="margin-top: 10px; display:none;"></div>
|
|
747
|
+
</div>
|
|
748
|
+
|
|
749
|
+
<div class="card" style="box-shadow: none;">
|
|
750
|
+
<p class="section-title">Quota actions</p>
|
|
751
|
+
<p class="section-sub">
|
|
752
|
+
Offline/Recover/Reset are executed by control plane (no direct writes from UI).
|
|
753
|
+
</p>
|
|
754
|
+
<div class="row" style="margin-bottom: 10px;">
|
|
755
|
+
<label for="controlQuotaKeyInput">providerKey</label>
|
|
756
|
+
<input id="controlQuotaKeyInput" type="text" placeholder="tab.key1.gpt-5.2-codex" style="flex: 1; min-width: 320px;" />
|
|
757
|
+
</div>
|
|
758
|
+
<div class="row" style="margin-bottom: 10px;">
|
|
759
|
+
<label for="controlQuotaModeSelect">mode</label>
|
|
760
|
+
<select id="controlQuotaModeSelect" style="width: 140px;">
|
|
761
|
+
<option value="cooldown" selected>cooldown</option>
|
|
762
|
+
<option value="blacklist">blacklist</option>
|
|
763
|
+
</select>
|
|
764
|
+
<label for="controlQuotaDurationSelect">time</label>
|
|
765
|
+
<select id="controlQuotaDurationSelect" style="width: 160px;">
|
|
766
|
+
<option value="5">5m</option>
|
|
767
|
+
<option value="15">15m</option>
|
|
768
|
+
<option value="30">30m</option>
|
|
769
|
+
<option value="60" selected>1h</option>
|
|
770
|
+
<option value="180">3h</option>
|
|
771
|
+
<option value="360">6h</option>
|
|
772
|
+
<option value="720">12h</option>
|
|
773
|
+
<option value="1440">24h</option>
|
|
774
|
+
</select>
|
|
775
|
+
<button id="controlQuotaOfflineBtn" class="danger">Offline</button>
|
|
776
|
+
<button id="controlQuotaRecoverBtn">Recover</button>
|
|
777
|
+
<button id="controlQuotaResetBtn">Reset</button>
|
|
778
|
+
</div>
|
|
779
|
+
<div class="table-wrap">
|
|
780
|
+
<table class="table">
|
|
781
|
+
<thead>
|
|
782
|
+
<tr>
|
|
783
|
+
<th>providerKey</th>
|
|
784
|
+
<th>inPool</th>
|
|
785
|
+
<th>reason</th>
|
|
786
|
+
<th>cooldownUntil</th>
|
|
787
|
+
<th>blacklistUntil</th>
|
|
788
|
+
</tr>
|
|
789
|
+
</thead>
|
|
790
|
+
<tbody id="controlQuotaProvidersTbody"></tbody>
|
|
791
|
+
</table>
|
|
792
|
+
</div>
|
|
793
|
+
</div>
|
|
794
|
+
</div>
|
|
795
|
+
</section>
|
|
796
|
+
|
|
670
797
|
<section id="panelProviders" data-panel="providers">
|
|
671
798
|
<div class="grid grid-wide-left">
|
|
672
799
|
<div class="card" style="box-shadow: none;">
|
|
@@ -801,6 +928,132 @@
|
|
|
801
928
|
</div>
|
|
802
929
|
</section>
|
|
803
930
|
|
|
931
|
+
<section id="panelStats" data-panel="stats" style="display:none;">
|
|
932
|
+
<div class="grid">
|
|
933
|
+
<div class="card" style="box-shadow:none;">
|
|
934
|
+
<p class="section-title">Stats (session)</p>
|
|
935
|
+
<p class="section-sub">
|
|
936
|
+
Real-time request/error counters aggregated from in-memory buckets (counts provider attempts, including failover retries). Resets on server restart.
|
|
937
|
+
</p>
|
|
938
|
+
<div class="row" style="margin-bottom: 10px;">
|
|
939
|
+
<button id="refreshStatsBtn" class="primary">Refresh</button>
|
|
940
|
+
<label class="muted" style="display:inline-flex; align-items:center; gap:8px;">
|
|
941
|
+
<input id="statsAutoRefresh" type="checkbox" checked />
|
|
942
|
+
auto refresh (2s)
|
|
943
|
+
</label>
|
|
944
|
+
<span class="mono muted" id="statsLastUpdated"></span>
|
|
945
|
+
</div>
|
|
946
|
+
<div class="notice mono" id="statsSessionTotalsBox" style="white-space: pre-wrap;"></div>
|
|
947
|
+
<div class="table-wrap" style="margin-top: 10px;">
|
|
948
|
+
<table class="table">
|
|
949
|
+
<thead>
|
|
950
|
+
<tr>
|
|
951
|
+
<th>providerId</th>
|
|
952
|
+
<th>req</th>
|
|
953
|
+
<th>err</th>
|
|
954
|
+
</tr>
|
|
955
|
+
</thead>
|
|
956
|
+
<tbody id="statsSessionProvidersTbody"></tbody>
|
|
957
|
+
</table>
|
|
958
|
+
</div>
|
|
959
|
+
<div class="table-wrap" style="margin-top: 10px;">
|
|
960
|
+
<table class="table">
|
|
961
|
+
<thead>
|
|
962
|
+
<tr>
|
|
963
|
+
<th>providerRuntime</th>
|
|
964
|
+
<th>req</th>
|
|
965
|
+
<th>err</th>
|
|
966
|
+
</tr>
|
|
967
|
+
</thead>
|
|
968
|
+
<tbody id="statsSessionRuntimesTbody"></tbody>
|
|
969
|
+
</table>
|
|
970
|
+
</div>
|
|
971
|
+
<div class="table-wrap" style="margin-top: 10px;">
|
|
972
|
+
<table class="table">
|
|
973
|
+
<thead>
|
|
974
|
+
<tr>
|
|
975
|
+
<th>model</th>
|
|
976
|
+
<th>req</th>
|
|
977
|
+
<th>err</th>
|
|
978
|
+
</tr>
|
|
979
|
+
</thead>
|
|
980
|
+
<tbody id="statsSessionModelsTbody"></tbody>
|
|
981
|
+
</table>
|
|
982
|
+
</div>
|
|
983
|
+
<div class="table-wrap" style="margin-top: 10px;">
|
|
984
|
+
<table class="table">
|
|
985
|
+
<thead>
|
|
986
|
+
<tr>
|
|
987
|
+
<th>providerKey</th>
|
|
988
|
+
<th>model</th>
|
|
989
|
+
<th>req</th>
|
|
990
|
+
<th>err</th>
|
|
991
|
+
</tr>
|
|
992
|
+
</thead>
|
|
993
|
+
<tbody id="statsSessionErrorsTbody"></tbody>
|
|
994
|
+
</table>
|
|
995
|
+
</div>
|
|
996
|
+
</div>
|
|
997
|
+
|
|
998
|
+
<div class="card" style="box-shadow:none;">
|
|
999
|
+
<p class="section-title">Stats (historical)</p>
|
|
1000
|
+
<p class="section-sub">
|
|
1001
|
+
Aggregated from <span class="mono">~/.routecodex/logs/provider-stats.jsonl</span> (best-effort).
|
|
1002
|
+
</p>
|
|
1003
|
+
<div class="notice mono" id="statsHistoricalTotalsBox" style="white-space: pre-wrap;"></div>
|
|
1004
|
+
<div class="table-wrap" style="margin-top: 10px;">
|
|
1005
|
+
<table class="table">
|
|
1006
|
+
<thead>
|
|
1007
|
+
<tr>
|
|
1008
|
+
<th>providerId</th>
|
|
1009
|
+
<th>req</th>
|
|
1010
|
+
<th>err</th>
|
|
1011
|
+
</tr>
|
|
1012
|
+
</thead>
|
|
1013
|
+
<tbody id="statsHistoricalProvidersTbody"></tbody>
|
|
1014
|
+
</table>
|
|
1015
|
+
</div>
|
|
1016
|
+
<div class="table-wrap" style="margin-top: 10px;">
|
|
1017
|
+
<table class="table">
|
|
1018
|
+
<thead>
|
|
1019
|
+
<tr>
|
|
1020
|
+
<th>providerRuntime</th>
|
|
1021
|
+
<th>req</th>
|
|
1022
|
+
<th>err</th>
|
|
1023
|
+
</tr>
|
|
1024
|
+
</thead>
|
|
1025
|
+
<tbody id="statsHistoricalRuntimesTbody"></tbody>
|
|
1026
|
+
</table>
|
|
1027
|
+
</div>
|
|
1028
|
+
<div class="table-wrap" style="margin-top: 10px;">
|
|
1029
|
+
<table class="table">
|
|
1030
|
+
<thead>
|
|
1031
|
+
<tr>
|
|
1032
|
+
<th>model</th>
|
|
1033
|
+
<th>req</th>
|
|
1034
|
+
<th>err</th>
|
|
1035
|
+
</tr>
|
|
1036
|
+
</thead>
|
|
1037
|
+
<tbody id="statsHistoricalModelsTbody"></tbody>
|
|
1038
|
+
</table>
|
|
1039
|
+
</div>
|
|
1040
|
+
<div class="table-wrap" style="margin-top: 10px;">
|
|
1041
|
+
<table class="table">
|
|
1042
|
+
<thead>
|
|
1043
|
+
<tr>
|
|
1044
|
+
<th>providerKey</th>
|
|
1045
|
+
<th>model</th>
|
|
1046
|
+
<th>req</th>
|
|
1047
|
+
<th>err</th>
|
|
1048
|
+
</tr>
|
|
1049
|
+
</thead>
|
|
1050
|
+
<tbody id="statsHistoricalErrorsTbody"></tbody>
|
|
1051
|
+
</table>
|
|
1052
|
+
</div>
|
|
1053
|
+
</div>
|
|
1054
|
+
</div>
|
|
1055
|
+
</section>
|
|
1056
|
+
|
|
804
1057
|
<section id="panelTokens" data-panel="tokens" style="display:none;">
|
|
805
1058
|
<div class="grid">
|
|
806
1059
|
<div class="card" style="box-shadow:none;">
|
|
@@ -833,9 +1086,9 @@
|
|
|
833
1086
|
</section>
|
|
834
1087
|
|
|
835
1088
|
<section id="panelCredentials" data-panel="credentials" style="display:none;">
|
|
836
|
-
<div class="grid">
|
|
1089
|
+
<div class="grid grid-wide-right">
|
|
837
1090
|
<div class="card" style="box-shadow:none;">
|
|
838
|
-
<p class="section-title">
|
|
1091
|
+
<p class="section-title">Auth Inventory</p>
|
|
839
1092
|
<p class="section-sub">
|
|
840
1093
|
Token files + API key authfiles in <span class="mono">~/.routecodex/auth</span>.
|
|
841
1094
|
</p>
|
|
@@ -860,10 +1113,11 @@
|
|
|
860
1113
|
</div>
|
|
861
1114
|
|
|
862
1115
|
<div class="card" style="box-shadow:none;">
|
|
863
|
-
<p class="section-title">
|
|
1116
|
+
<p class="section-title">Auth Workbench</p>
|
|
864
1117
|
<p class="section-sub">
|
|
865
|
-
|
|
1118
|
+
Manual and Auto flows are isolated. Use Manual for explicit user-driven login; use Auto only for provider auto-mode replay.
|
|
866
1119
|
</p>
|
|
1120
|
+
|
|
867
1121
|
<div class="row" style="margin-bottom: 10px;">
|
|
868
1122
|
<label for="oauthBrowserSelect">oauthBrowser</label>
|
|
869
1123
|
<select id="oauthBrowserSelect">
|
|
@@ -873,8 +1127,8 @@
|
|
|
873
1127
|
<button id="saveOauthBrowserBtn" class="primary">Save</button>
|
|
874
1128
|
</div>
|
|
875
1129
|
|
|
876
|
-
<p class="section-title" style="margin-top: 10px;">
|
|
877
|
-
<div class="row" style="margin-bottom:
|
|
1130
|
+
<p class="section-title" style="margin-top: 10px;">Auth Context</p>
|
|
1131
|
+
<div class="row" style="margin-bottom: 8px;">
|
|
878
1132
|
<label for="oauthProviderSelect">provider</label>
|
|
879
1133
|
<select id="oauthProviderSelect">
|
|
880
1134
|
<option value="qwen">qwen</option>
|
|
@@ -885,13 +1139,52 @@
|
|
|
885
1139
|
<label for="oauthAuthAliasInput">alias</label>
|
|
886
1140
|
<input id="oauthAuthAliasInput" type="text" placeholder="default" style="width: 240px;" />
|
|
887
1141
|
</div>
|
|
1142
|
+
<div class="auth-selection-summary" style="margin-bottom: 10px;">
|
|
1143
|
+
<span id="oauthSelectionStatusPill" class="pill">status: —</span>
|
|
1144
|
+
<span id="oauthSelectionExpiryPill" class="pill">expires: —</span>
|
|
1145
|
+
<span id="oauthSelectionIssuePill" class="pill">issue: —</span>
|
|
1146
|
+
</div>
|
|
1147
|
+
|
|
1148
|
+
<div class="tabs auth-mode-tabs">
|
|
1149
|
+
<button id="oauthModeManualBtn" class="tab active" type="button">Manual Auth</button>
|
|
1150
|
+
<button id="oauthModeAutoBtn" class="tab" type="button">Auto Auth</button>
|
|
1151
|
+
</div>
|
|
1152
|
+
|
|
1153
|
+
<div class="auth-mode-panels">
|
|
1154
|
+
<div id="oauthManualPanel" class="auth-mode-panel">
|
|
1155
|
+
<p class="section-sub">Pure manual mode (no auto selector / no auto confirm injection).</p>
|
|
1156
|
+
<div class="row" style="margin-bottom: 6px;">
|
|
1157
|
+
<label><input id="oauthManualOpenBrowser" type="checkbox" checked /> open browser</label>
|
|
1158
|
+
<label><input id="oauthManualForceReauth" type="checkbox" checked /> force reauthorize</label>
|
|
1159
|
+
<label><input id="oauthManualHeadful" type="checkbox" checked /> headed window</label>
|
|
1160
|
+
<button id="oauthAuthorizeManualBtn" class="primary" type="button">Start Manual Auth</button>
|
|
1161
|
+
</div>
|
|
1162
|
+
</div>
|
|
1163
|
+
<div id="oauthAutoPanel" class="auth-mode-panel" style="display:none;">
|
|
1164
|
+
<p class="section-sub">Auto mode (provider-specific Camoufox auto-mode, fallback handled by runtime).</p>
|
|
1165
|
+
<div class="row" style="margin-bottom: 6px;">
|
|
1166
|
+
<label><input id="oauthAutoOpenBrowser" type="checkbox" checked /> open browser</label>
|
|
1167
|
+
<label><input id="oauthAutoForceReauth" type="checkbox" checked /> force reauthorize</label>
|
|
1168
|
+
<label><input id="oauthAutoHeadful" type="checkbox" /> debug headful</label>
|
|
1169
|
+
<button id="oauthAuthorizeAutoBtn" class="primary" type="button">Run Auto Auth</button>
|
|
1170
|
+
</div>
|
|
1171
|
+
</div>
|
|
1172
|
+
</div>
|
|
1173
|
+
|
|
888
1174
|
<div id="oauthAuthIssueHint" class="notice" style="display:none; margin-bottom: 10px; white-space: pre-wrap;"></div>
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
<
|
|
892
|
-
<
|
|
1175
|
+
|
|
1176
|
+
<div id="oauthVerifyCenter" class="notice" style="display:none; margin-bottom: 10px;">
|
|
1177
|
+
<p class="section-title" style="margin-bottom: 4px;">Verify Center</p>
|
|
1178
|
+
<p class="section-sub" style="margin-bottom: 6px;">If upstream requires account verification, complete it here before re-authorizing.</p>
|
|
1179
|
+
<div id="oauthVerifyUrl" class="mono auth-verify-url">—</div>
|
|
1180
|
+
<div class="row">
|
|
1181
|
+
<button id="oauthVerifyOpenBtn" type="button">Open Verify in Camoufox</button>
|
|
1182
|
+
<button id="oauthVerifyCopyBtn" type="button">Copy Verify URL</button>
|
|
1183
|
+
<button id="oauthVerifyRecheckBtn" type="button">Recheck Status</button>
|
|
1184
|
+
</div>
|
|
893
1185
|
</div>
|
|
894
1186
|
|
|
1187
|
+
<p class="section-title" style="margin-top: 10px;">Run Console</p>
|
|
895
1188
|
<div id="credentialOpLog" class="log" style="display:none;"></div>
|
|
896
1189
|
</div>
|
|
897
1190
|
</div>
|
|
@@ -905,16 +1198,20 @@
|
|
|
905
1198
|
VirtualRouter consumes this via <span class="mono">quotaView</span>. When
|
|
906
1199
|
<span class="mono">inPool=false</span>, the provider is treated as removed from the route pool.
|
|
907
1200
|
</p>
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
1201
|
+
<div class="row" style="margin-bottom: 10px;">
|
|
1202
|
+
<label for="quotaFilterInput">filter</label>
|
|
1203
|
+
<input id="quotaFilterInput" type="text" placeholder="providerKey contains…" style="width: 320px;" />
|
|
1204
|
+
<label><input id="quotaHideOkToggle" type="checkbox" /> hide ok</label>
|
|
1205
|
+
<label><input id="quotaOnlyRoutedTargetsToggle" type="checkbox" checked /> only routed targets</label>
|
|
1206
|
+
<button id="quotaSelectVisibleBtn">Select visible</button>
|
|
1207
|
+
<button id="quotaClearSelectionBtn">Clear selection</button>
|
|
1208
|
+
<label><input id="quotaAutoRefreshToggle" type="checkbox" checked /> auto refresh</label>
|
|
1209
|
+
<span id="quotaSelectionHint" class="muted" style="font-size:12px;">selected 0</span>
|
|
1210
|
+
<span class="muted" style="font-size:12px;">Tip: click a row to fill the offline box.</span>
|
|
1211
|
+
</div>
|
|
1212
|
+
<div class="row" style="margin-bottom: 10px;">
|
|
1213
|
+
<label for="quotaKeyInput">providerKey</label>
|
|
1214
|
+
<input id="quotaKeyInput" type="text" placeholder="tab.key1.gpt-5.2-codex" style="width: 420px;" />
|
|
918
1215
|
<label for="quotaModeSelect">offline mode</label>
|
|
919
1216
|
<select id="quotaModeSelect" style="width: 140px;">
|
|
920
1217
|
<option value="cooldown">cooldown</option>
|
|
@@ -934,25 +1231,28 @@
|
|
|
934
1231
|
<button id="quotaApplyDisableBtn" class="danger">Offline</button>
|
|
935
1232
|
<button id="quotaApplyRecoverBtn">Recover</button>
|
|
936
1233
|
<button id="quotaApplyResetBtn">Reset</button>
|
|
937
|
-
|
|
1234
|
+
</div>
|
|
938
1235
|
<div class="muted" style="font-size:12px; margin: -6px 0 10px;">
|
|
939
1236
|
Offline removes the provider from the route pool for the selected minutes. Recover brings it back online immediately.
|
|
1237
|
+
If selection is non-empty, actions apply to selected providerKeys.
|
|
940
1238
|
</div>
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
1239
|
+
<div class="row" style="margin-bottom: 10px;">
|
|
1240
|
+
<button id="refreshQuotaBtn" class="primary">Refresh provider pool</button>
|
|
1241
|
+
<button id="refreshQuotaSnapshotBtn" class="primary">Refresh antigravity snapshot</button>
|
|
1242
|
+
<button id="resetQuotaBtn" class="danger">Reset provider-quota module</button>
|
|
945
1243
|
</div>
|
|
946
1244
|
<div class="table-wrap">
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
1245
|
+
<table class="table">
|
|
1246
|
+
<thead>
|
|
1247
|
+
<tr>
|
|
1248
|
+
<th style="width: 34px;">
|
|
1249
|
+
<input id="quotaSelectAllVisibleToggle" type="checkbox" title="Select all visible" />
|
|
1250
|
+
</th>
|
|
1251
|
+
<th>key</th>
|
|
1252
|
+
<th>model</th>
|
|
1253
|
+
<th>inPool</th>
|
|
1254
|
+
<th>reason</th>
|
|
1255
|
+
<th>until</th>
|
|
956
1256
|
<th>errCount</th>
|
|
957
1257
|
<th></th>
|
|
958
1258
|
</tr>
|
|
@@ -1056,19 +1356,26 @@
|
|
|
1056
1356
|
|
|
1057
1357
|
<script>
|
|
1058
1358
|
const $ = (id) => document.getElementById(id);
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1359
|
+
const UI = {
|
|
1360
|
+
selectedProviderId: "",
|
|
1361
|
+
lastUnauthorizedToastAt: 0,
|
|
1362
|
+
adminAuth: null,
|
|
1363
|
+
controlSnapshot: null,
|
|
1364
|
+
controlSnapshotUpdatedAt: 0,
|
|
1365
|
+
quotaProviders: [],
|
|
1366
|
+
quotaProvidersUpdatedAt: 0,
|
|
1367
|
+
quotaProviderMap: null,
|
|
1368
|
+
quotaSelection: new Set(),
|
|
1369
|
+
quotaRenderedVisibleKeys: [],
|
|
1370
|
+
quotaRenderedGroups: new Map(),
|
|
1371
|
+
routingTargets: null,
|
|
1372
|
+
routingTargetsUpdatedAt: 0,
|
|
1373
|
+
routingSources: [],
|
|
1374
|
+
routingSourcesUpdatedAt: 0,
|
|
1375
|
+
routingLocation: "virtualrouter.routing",
|
|
1376
|
+
credentials: [],
|
|
1377
|
+
oauthMode: "manual"
|
|
1378
|
+
};
|
|
1072
1379
|
let toastTimer = null;
|
|
1073
1380
|
|
|
1074
1381
|
function toast(msg, kind = "err") {
|
|
@@ -1157,7 +1464,9 @@
|
|
|
1157
1464
|
btn.classList.toggle("active", btn.getAttribute("data-tab") === name);
|
|
1158
1465
|
});
|
|
1159
1466
|
const panels = [
|
|
1467
|
+
{ name: "control", el: $("panelControl") },
|
|
1160
1468
|
{ name: "providers", el: $("panelProviders") },
|
|
1469
|
+
{ name: "stats", el: $("panelStats") },
|
|
1161
1470
|
{ name: "tokens", el: $("panelTokens") },
|
|
1162
1471
|
{ name: "credentials", el: $("panelCredentials") },
|
|
1163
1472
|
{ name: "quota", el: $("panelQuota") },
|
|
@@ -1167,6 +1476,7 @@
|
|
|
1167
1476
|
|
|
1168
1477
|
// Light auto-refresh on tab switch to avoid showing stale "Unauthorized" after login.
|
|
1169
1478
|
void maybeRefreshTab(name);
|
|
1479
|
+
syncStatsAutoRefresh();
|
|
1170
1480
|
}
|
|
1171
1481
|
|
|
1172
1482
|
function getActiveTab() {
|
|
@@ -1176,7 +1486,9 @@
|
|
|
1176
1486
|
}
|
|
1177
1487
|
|
|
1178
1488
|
const tabLastRefreshedAt = {
|
|
1489
|
+
control: 0,
|
|
1179
1490
|
providers: 0,
|
|
1491
|
+
stats: 0,
|
|
1180
1492
|
tokens: 0,
|
|
1181
1493
|
credentials: 0,
|
|
1182
1494
|
quota: 0,
|
|
@@ -1192,6 +1504,8 @@
|
|
|
1192
1504
|
tabLastRefreshedAt[key] = now;
|
|
1193
1505
|
try {
|
|
1194
1506
|
if (key === "providers") await refreshProviders();
|
|
1507
|
+
else if (key === "control") await refreshControl();
|
|
1508
|
+
else if (key === "stats") await refreshStats();
|
|
1195
1509
|
else if (key === "tokens") await refreshTokens();
|
|
1196
1510
|
else if (key === "credentials") await refreshCredentials();
|
|
1197
1511
|
else if (key === "quota") {
|
|
@@ -1207,6 +1521,32 @@
|
|
|
1207
1521
|
}
|
|
1208
1522
|
}
|
|
1209
1523
|
|
|
1524
|
+
let statsAutoRefreshTimer = null;
|
|
1525
|
+
function syncStatsAutoRefresh() {
|
|
1526
|
+
if (statsAutoRefreshTimer) {
|
|
1527
|
+
try { clearInterval(statsAutoRefreshTimer); } catch {}
|
|
1528
|
+
statsAutoRefreshTimer = null;
|
|
1529
|
+
}
|
|
1530
|
+
const active = getActiveTab();
|
|
1531
|
+
if (active !== "stats") {
|
|
1532
|
+
return;
|
|
1533
|
+
}
|
|
1534
|
+
const checkbox = $("statsAutoRefresh");
|
|
1535
|
+
const enabled = checkbox && checkbox.checked;
|
|
1536
|
+
if (!enabled) {
|
|
1537
|
+
return;
|
|
1538
|
+
}
|
|
1539
|
+
if (UI && UI.adminAuth && UI.adminAuth.authenticated === false) {
|
|
1540
|
+
return;
|
|
1541
|
+
}
|
|
1542
|
+
statsAutoRefreshTimer = setInterval(() => {
|
|
1543
|
+
if (getActiveTab() !== "stats") return;
|
|
1544
|
+
const checkboxNow = $("statsAutoRefresh");
|
|
1545
|
+
if (!checkboxNow || !checkboxNow.checked) return;
|
|
1546
|
+
void refreshStats();
|
|
1547
|
+
}, 2000);
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1210
1550
|
function textOf(value) {
|
|
1211
1551
|
if (value === null || value === undefined) return "";
|
|
1212
1552
|
return String(value);
|
|
@@ -1231,6 +1571,16 @@
|
|
|
1231
1571
|
return tr;
|
|
1232
1572
|
}
|
|
1233
1573
|
|
|
1574
|
+
function createInfoRow(colSpan, message) {
|
|
1575
|
+
const tr = document.createElement("tr");
|
|
1576
|
+
const td = document.createElement("td");
|
|
1577
|
+
td.colSpan = colSpan;
|
|
1578
|
+
td.className = "mono muted";
|
|
1579
|
+
td.textContent = textOf(message);
|
|
1580
|
+
tr.appendChild(td);
|
|
1581
|
+
return tr;
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1234
1584
|
function setSelectedProviderId(id) {
|
|
1235
1585
|
UI.selectedProviderId = textOf(id || "");
|
|
1236
1586
|
const tbody = $("providersTbody");
|
|
@@ -1751,6 +2101,14 @@
|
|
|
1751
2101
|
}
|
|
1752
2102
|
|
|
1753
2103
|
async function refreshStatus() {
|
|
2104
|
+
// /health is public; use it to always show version even before admin login.
|
|
2105
|
+
try {
|
|
2106
|
+
const res = await fetch("/health");
|
|
2107
|
+
const data = res && res.ok ? await res.json().catch(() => null) : null;
|
|
2108
|
+
$("serverVersion").textContent = `version: ${data && data.version ? data.version : "—"}`;
|
|
2109
|
+
} catch {
|
|
2110
|
+
$("serverVersion").textContent = "version: —";
|
|
2111
|
+
}
|
|
1754
2112
|
try {
|
|
1755
2113
|
const data = await apiFetch("/daemon/status");
|
|
1756
2114
|
$("serverId").textContent = `serverId: ${data.serverId || "—"}`;
|
|
@@ -1761,9 +2119,136 @@
|
|
|
1761
2119
|
$("statusText").textContent = e && e.status === 401 ? "401 (set API key above)" : "disconnected";
|
|
1762
2120
|
$("statusDot").classList.remove("ok");
|
|
1763
2121
|
$("statusDot").classList.add("err");
|
|
2122
|
+
$("serverId").textContent = "serverId: —";
|
|
1764
2123
|
}
|
|
1765
2124
|
}
|
|
1766
2125
|
|
|
2126
|
+
function formatTs(ms) {
|
|
2127
|
+
if (ms === null || ms === undefined) return "—";
|
|
2128
|
+
const n = Number(ms);
|
|
2129
|
+
if (!Number.isFinite(n) || n <= 0) return "—";
|
|
2130
|
+
try {
|
|
2131
|
+
return new Date(n).toLocaleString();
|
|
2132
|
+
} catch {
|
|
2133
|
+
return String(n);
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2137
|
+
function renderControlSnapshot() {
|
|
2138
|
+
const snap = UI.controlSnapshot;
|
|
2139
|
+
const serversBody = $("controlServersTbody");
|
|
2140
|
+
const hitsBody = $("controlHitsTbody");
|
|
2141
|
+
const quotaBody = $("controlQuotaProvidersTbody");
|
|
2142
|
+
if (serversBody) serversBody.replaceChildren();
|
|
2143
|
+
if (hitsBody) hitsBody.replaceChildren();
|
|
2144
|
+
if (quotaBody) quotaBody.replaceChildren();
|
|
2145
|
+
if (!snap || typeof snap !== "object") {
|
|
2146
|
+
if (serversBody) serversBody.appendChild(createErrorRow(4, "No snapshot"));
|
|
2147
|
+
if (hitsBody) hitsBody.appendChild(createErrorRow(2, "No snapshot"));
|
|
2148
|
+
if (quotaBody) quotaBody.appendChild(createErrorRow(5, "No snapshot"));
|
|
2149
|
+
return;
|
|
2150
|
+
}
|
|
2151
|
+
const servers = Array.isArray(snap.servers) ? snap.servers : [];
|
|
2152
|
+
if (!servers.length) {
|
|
2153
|
+
if (serversBody) serversBody.appendChild(createErrorRow(4, "No local servers discovered"));
|
|
2154
|
+
} else if (serversBody) {
|
|
2155
|
+
for (const s of servers) {
|
|
2156
|
+
const tr = document.createElement("tr");
|
|
2157
|
+
tr.appendChild(createCell("td", String(s.port ?? "—"), "mono"));
|
|
2158
|
+
tr.appendChild(createCell("td", textOf(s.version || "—"), "mono"));
|
|
2159
|
+
tr.appendChild(createCell("td", s.ready === true ? "true" : s.ready === false ? "false" : "—", ""));
|
|
2160
|
+
tr.appendChild(createCell("td", Array.isArray(s.pids) ? s.pids.join(" ") : "", "mono"));
|
|
2161
|
+
serversBody.appendChild(tr);
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2165
|
+
try {
|
|
2166
|
+
const llms = snap.llmsStats && typeof snap.llmsStats === "object" ? snap.llmsStats : null;
|
|
2167
|
+
const routeHits =
|
|
2168
|
+
llms && llms.router && llms.router.global && llms.router.global.routeHitCount
|
|
2169
|
+
? llms.router.global.routeHitCount
|
|
2170
|
+
: null;
|
|
2171
|
+
const entries = routeHits && typeof routeHits === "object" ? Object.entries(routeHits) : [];
|
|
2172
|
+
entries.sort((a, b) => Number(b[1] || 0) - Number(a[1] || 0));
|
|
2173
|
+
const top = entries.slice(0, 20);
|
|
2174
|
+
if (!top.length) {
|
|
2175
|
+
if (hitsBody) hitsBody.appendChild(createErrorRow(2, "No routing hits recorded yet"));
|
|
2176
|
+
} else if (hitsBody) {
|
|
2177
|
+
for (const [route, count] of top) {
|
|
2178
|
+
const tr = document.createElement("tr");
|
|
2179
|
+
tr.appendChild(createCell("td", String(route || "—"), "mono"));
|
|
2180
|
+
tr.appendChild(createCell("td", String(count ?? 0), "mono"));
|
|
2181
|
+
hitsBody.appendChild(tr);
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
} catch {
|
|
2185
|
+
if (hitsBody) hitsBody.appendChild(createErrorRow(2, "Failed to read routing hits"));
|
|
2186
|
+
}
|
|
2187
|
+
|
|
2188
|
+
const quota = snap.quota && typeof snap.quota === "object" ? snap.quota : null;
|
|
2189
|
+
const providers = quota && Array.isArray(quota.providers) ? quota.providers : [];
|
|
2190
|
+
const list = providers
|
|
2191
|
+
.map((p) => ({
|
|
2192
|
+
providerKey: textOf(p.providerKey),
|
|
2193
|
+
inPool: Boolean(p.inPool),
|
|
2194
|
+
reason: textOf(p.reason),
|
|
2195
|
+
cooldownUntil: p.cooldownUntil ?? null,
|
|
2196
|
+
blacklistUntil: p.blacklistUntil ?? null
|
|
2197
|
+
}))
|
|
2198
|
+
.filter((p) => p.providerKey);
|
|
2199
|
+
list.sort((a, b) => a.providerKey.localeCompare(b.providerKey));
|
|
2200
|
+
const max = 60;
|
|
2201
|
+
const visible = list.slice(0, max);
|
|
2202
|
+
if (!visible.length) {
|
|
2203
|
+
if (quotaBody) quotaBody.appendChild(createErrorRow(5, "No quota providers"));
|
|
2204
|
+
} else if (quotaBody) {
|
|
2205
|
+
for (const p of visible) {
|
|
2206
|
+
const tr = document.createElement("tr");
|
|
2207
|
+
tr.appendChild(createCell("td", p.providerKey, "mono truncate", { title: true }));
|
|
2208
|
+
tr.appendChild(createCell("td", p.inPool ? "true" : "false", p.inPool ? "" : "danger"));
|
|
2209
|
+
tr.appendChild(createCell("td", p.reason || "—", "mono truncate", { title: true }));
|
|
2210
|
+
tr.appendChild(createCell("td", formatTs(p.cooldownUntil), "mono"));
|
|
2211
|
+
tr.appendChild(createCell("td", formatTs(p.blacklistUntil), "mono"));
|
|
2212
|
+
quotaBody.appendChild(tr);
|
|
2213
|
+
}
|
|
2214
|
+
if (list.length > max) {
|
|
2215
|
+
const tr = document.createElement("tr");
|
|
2216
|
+
const td = document.createElement("td");
|
|
2217
|
+
td.colSpan = 5;
|
|
2218
|
+
td.className = "muted";
|
|
2219
|
+
td.textContent = `… ${list.length - max} more hidden`;
|
|
2220
|
+
tr.appendChild(td);
|
|
2221
|
+
quotaBody.appendChild(tr);
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
|
|
2225
|
+
const hint = $("controlHint");
|
|
2226
|
+
if (hint) {
|
|
2227
|
+
hint.textContent = `servers: ${servers.length} · quota keys: ${list.length} · updated: ${formatTs(snap.nowMs)}`;
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
|
|
2231
|
+
async function refreshControl() {
|
|
2232
|
+
setLog("controlOpLog", "");
|
|
2233
|
+
try {
|
|
2234
|
+
const snap = await apiFetch("/daemon/control/snapshot");
|
|
2235
|
+
UI.controlSnapshot = snap;
|
|
2236
|
+
UI.controlSnapshotUpdatedAt = Date.now();
|
|
2237
|
+
renderControlSnapshot();
|
|
2238
|
+
} catch (e) {
|
|
2239
|
+
if (e && e.status === 401) notifyUnauthorizedOnce("control");
|
|
2240
|
+
setLog("controlOpLog", `Control snapshot failed: ${e.message || e}`);
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2244
|
+
async function controlMutate(action, payload = {}) {
|
|
2245
|
+
const out = await apiFetch("/daemon/control/mutate", {
|
|
2246
|
+
method: "POST",
|
|
2247
|
+
body: JSON.stringify({ action, ...payload })
|
|
2248
|
+
});
|
|
2249
|
+
return out;
|
|
2250
|
+
}
|
|
2251
|
+
|
|
1767
2252
|
async function refreshProviders() {
|
|
1768
2253
|
const body = $("providersTbody");
|
|
1769
2254
|
body.replaceChildren();
|
|
@@ -1976,27 +2461,150 @@
|
|
|
1976
2461
|
return null;
|
|
1977
2462
|
}
|
|
1978
2463
|
|
|
2464
|
+
function findCredentialForSelection(providerRaw, aliasRaw) {
|
|
2465
|
+
const provider = textOf(providerRaw || "").trim().toLowerCase();
|
|
2466
|
+
const alias = textOf(aliasRaw || "default").trim().toLowerCase() || "default";
|
|
2467
|
+
const list = Array.isArray(UI.credentials) ? UI.credentials : [];
|
|
2468
|
+
for (const item of list) {
|
|
2469
|
+
const itemProvider = textOf(item && item.provider ? item.provider : "").trim().toLowerCase();
|
|
2470
|
+
const itemAlias = textOf(item && item.alias ? item.alias : "default").trim().toLowerCase() || "default";
|
|
2471
|
+
const kind = textOf(item && item.kind ? item.kind : "").trim().toLowerCase();
|
|
2472
|
+
if (kind !== "oauth") continue;
|
|
2473
|
+
if (itemProvider === provider && itemAlias === alias) {
|
|
2474
|
+
return item;
|
|
2475
|
+
}
|
|
2476
|
+
}
|
|
2477
|
+
return null;
|
|
2478
|
+
}
|
|
2479
|
+
|
|
2480
|
+
function formatCredentialExpiry(cred) {
|
|
2481
|
+
if (!cred || cred.expiresInSec == null) return "—";
|
|
2482
|
+
const sec = Number(cred.expiresInSec);
|
|
2483
|
+
if (!Number.isFinite(sec)) return "—";
|
|
2484
|
+
if (sec <= 0) return "expired";
|
|
2485
|
+
if (sec < 60) return `${sec}s`;
|
|
2486
|
+
const min = Math.round(sec / 60);
|
|
2487
|
+
if (min < 60) return `${min}m`;
|
|
2488
|
+
const hour = Math.round(min / 60);
|
|
2489
|
+
return `${hour}h`;
|
|
2490
|
+
}
|
|
2491
|
+
|
|
2492
|
+
function setOauthMode(modeRaw) {
|
|
2493
|
+
const mode = modeRaw === "auto" ? "auto" : "manual";
|
|
2494
|
+
UI.oauthMode = mode;
|
|
2495
|
+
const manualBtn = $("oauthModeManualBtn");
|
|
2496
|
+
const autoBtn = $("oauthModeAutoBtn");
|
|
2497
|
+
const manualPanel = $("oauthManualPanel");
|
|
2498
|
+
const autoPanel = $("oauthAutoPanel");
|
|
2499
|
+
if (manualBtn) manualBtn.classList.toggle("active", mode === "manual");
|
|
2500
|
+
if (autoBtn) autoBtn.classList.toggle("active", mode === "auto");
|
|
2501
|
+
if (manualPanel) manualPanel.style.display = mode === "manual" ? "block" : "none";
|
|
2502
|
+
if (autoPanel) autoPanel.style.display = mode === "auto" ? "block" : "none";
|
|
2503
|
+
}
|
|
2504
|
+
|
|
1979
2505
|
function updateOauthAuthIssueHint() {
|
|
1980
|
-
const
|
|
1981
|
-
|
|
2506
|
+
const issueHint = $("oauthAuthIssueHint");
|
|
2507
|
+
const verifyCenter = $("oauthVerifyCenter");
|
|
2508
|
+
const verifyUrlEl = $("oauthVerifyUrl");
|
|
1982
2509
|
const provider = textOf($("oauthProviderSelect").value || "").trim().toLowerCase();
|
|
1983
|
-
const alias = textOf($("oauthAuthAliasInput").value || "default").trim().toLowerCase();
|
|
2510
|
+
const alias = textOf($("oauthAuthAliasInput").value || "default").trim().toLowerCase() || "default";
|
|
1984
2511
|
const issue = findAuthIssueForProviderAlias(provider, alias);
|
|
2512
|
+
const cred = findCredentialForSelection(provider, alias);
|
|
2513
|
+
|
|
2514
|
+
const statusPill = $("oauthSelectionStatusPill");
|
|
2515
|
+
const expiryPill = $("oauthSelectionExpiryPill");
|
|
2516
|
+
const issuePill = $("oauthSelectionIssuePill");
|
|
2517
|
+
if (statusPill) {
|
|
2518
|
+
statusPill.textContent = `status: ${cred ? textOf(cred.status || "—") : "not found"}`;
|
|
2519
|
+
statusPill.className = "pill";
|
|
2520
|
+
const statusRaw = textOf(cred && cred.status ? cred.status : "").trim().toLowerCase();
|
|
2521
|
+
if (statusRaw === "valid") statusPill.classList.add("ok");
|
|
2522
|
+
else if (statusRaw === "expired" || statusRaw === "invalid") statusPill.classList.add("bad");
|
|
2523
|
+
else if (statusRaw) statusPill.classList.add("warn");
|
|
2524
|
+
}
|
|
2525
|
+
if (expiryPill) {
|
|
2526
|
+
expiryPill.textContent = `expires: ${formatCredentialExpiry(cred)}`;
|
|
2527
|
+
expiryPill.className = "pill";
|
|
2528
|
+
const exp = Number(cred && cred.expiresInSec != null ? cred.expiresInSec : NaN);
|
|
2529
|
+
if (Number.isFinite(exp) && exp > 0 && exp < 600) {
|
|
2530
|
+
expiryPill.classList.add("warn");
|
|
2531
|
+
}
|
|
2532
|
+
}
|
|
2533
|
+
if (issuePill) {
|
|
2534
|
+
const hasIssue = Boolean(issue);
|
|
2535
|
+
issuePill.textContent = hasIssue ? "issue: verify required" : "issue: —";
|
|
2536
|
+
issuePill.className = `pill${hasIssue ? " bad" : ""}`;
|
|
2537
|
+
}
|
|
2538
|
+
|
|
1985
2539
|
if (!issue) {
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
2540
|
+
if (issueHint) {
|
|
2541
|
+
issueHint.style.display = "none";
|
|
2542
|
+
issueHint.textContent = "";
|
|
2543
|
+
}
|
|
2544
|
+
if (verifyCenter) verifyCenter.style.display = "none";
|
|
2545
|
+
if (verifyUrlEl) verifyUrlEl.textContent = "—";
|
|
2546
|
+
return null;
|
|
1989
2547
|
}
|
|
2548
|
+
|
|
1990
2549
|
if (issue.kind === "google_account_verification") {
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
2550
|
+
if (issueHint) {
|
|
2551
|
+
issueHint.textContent =
|
|
2552
|
+
`⚠ Google requires account verification for alias "${alias}".\n` +
|
|
2553
|
+
`Use Verify Center to open the exact verify URL in Camoufox, then re-run auth.`;
|
|
2554
|
+
issueHint.style.display = "block";
|
|
2555
|
+
}
|
|
2556
|
+
if (verifyCenter) verifyCenter.style.display = "block";
|
|
2557
|
+
if (verifyUrlEl) verifyUrlEl.textContent = issue.url || "(missing verify url from quota state)";
|
|
2558
|
+
return issue;
|
|
2559
|
+
}
|
|
2560
|
+
|
|
2561
|
+
if (issueHint) {
|
|
2562
|
+
issueHint.style.display = "none";
|
|
2563
|
+
issueHint.textContent = "";
|
|
2564
|
+
}
|
|
2565
|
+
if (verifyCenter) verifyCenter.style.display = "none";
|
|
2566
|
+
if (verifyUrlEl) verifyUrlEl.textContent = "—";
|
|
2567
|
+
return issue;
|
|
2568
|
+
}
|
|
2569
|
+
|
|
2570
|
+
async function openAuthIssueInCamoufox(provider, alias, url) {
|
|
2571
|
+
setLog("credentialOpLog", "");
|
|
2572
|
+
try {
|
|
2573
|
+
await apiFetch("/daemon/oauth/open", {
|
|
2574
|
+
method: "POST",
|
|
2575
|
+
body: JSON.stringify({ provider, alias, url })
|
|
2576
|
+
});
|
|
2577
|
+
setLog("credentialOpLog", `Opened verification URL in Camoufox for ${provider}.${alias}.`);
|
|
2578
|
+
} catch (e) {
|
|
2579
|
+
setLog("credentialOpLog", `Open failed: ${e.message}`);
|
|
2580
|
+
}
|
|
2581
|
+
}
|
|
2582
|
+
|
|
2583
|
+
async function openSelectedVerifyUrl() {
|
|
2584
|
+
const provider = textOf($("oauthProviderSelect").value || "").trim().toLowerCase();
|
|
2585
|
+
const alias = textOf($("oauthAuthAliasInput").value || "default").trim().toLowerCase() || "default";
|
|
2586
|
+
const issue = findAuthIssueForProviderAlias(provider, alias);
|
|
2587
|
+
if (!issue || !issue.url) {
|
|
2588
|
+
setLog("credentialOpLog", "No verify URL available for current provider/alias.");
|
|
2589
|
+
return;
|
|
2590
|
+
}
|
|
2591
|
+
await openAuthIssueInCamoufox(provider, alias, issue.url);
|
|
2592
|
+
}
|
|
2593
|
+
|
|
2594
|
+
async function copySelectedVerifyUrl() {
|
|
2595
|
+
const provider = textOf($("oauthProviderSelect").value || "").trim().toLowerCase();
|
|
2596
|
+
const alias = textOf($("oauthAuthAliasInput").value || "default").trim().toLowerCase() || "default";
|
|
2597
|
+
const issue = findAuthIssueForProviderAlias(provider, alias);
|
|
2598
|
+
if (!issue || !issue.url) {
|
|
2599
|
+
setLog("credentialOpLog", "No verify URL available for current provider/alias.");
|
|
1996
2600
|
return;
|
|
1997
2601
|
}
|
|
1998
|
-
|
|
1999
|
-
|
|
2602
|
+
try {
|
|
2603
|
+
await navigator.clipboard.writeText(issue.url);
|
|
2604
|
+
setLog("credentialOpLog", "Verify URL copied.");
|
|
2605
|
+
} catch (e) {
|
|
2606
|
+
setLog("credentialOpLog", `Copy failed: ${e && e.message ? e.message : String(e)}`);
|
|
2607
|
+
}
|
|
2000
2608
|
}
|
|
2001
2609
|
|
|
2002
2610
|
function listAntigravityProviderKeysByAlias(aliasRaw) {
|
|
@@ -2045,6 +2653,7 @@
|
|
|
2045
2653
|
body.replaceChildren();
|
|
2046
2654
|
try {
|
|
2047
2655
|
const items = await apiFetch("/daemon/credentials");
|
|
2656
|
+
UI.credentials = Array.isArray(items) ? items : [];
|
|
2048
2657
|
// Best-effort: load quota provider state so we can surface upstream auth issues (e.g. Google verify required).
|
|
2049
2658
|
try {
|
|
2050
2659
|
const quota = await apiFetch("/quota/providers");
|
|
@@ -2056,7 +2665,6 @@
|
|
|
2056
2665
|
// ignore
|
|
2057
2666
|
}
|
|
2058
2667
|
|
|
2059
|
-
updateOauthAuthIssueHint();
|
|
2060
2668
|
for (const c of items || []) {
|
|
2061
2669
|
const tr = document.createElement("tr");
|
|
2062
2670
|
const exp = c.expiresInSec == null ? "—" : `${c.expiresInSec}s`;
|
|
@@ -2072,11 +2680,13 @@
|
|
|
2072
2680
|
if (issue.url) {
|
|
2073
2681
|
statusTd.appendChild(document.createTextNode(" "));
|
|
2074
2682
|
const a = document.createElement("a");
|
|
2075
|
-
a.href =
|
|
2076
|
-
a.target = "_blank";
|
|
2077
|
-
a.rel = "noreferrer";
|
|
2683
|
+
a.href = "#";
|
|
2078
2684
|
a.textContent = "open";
|
|
2079
2685
|
a.className = "mono";
|
|
2686
|
+
a.addEventListener("click", (ev) => {
|
|
2687
|
+
ev.preventDefault();
|
|
2688
|
+
void openAuthIssueInCamoufox(c.provider, c.alias, issue.url);
|
|
2689
|
+
});
|
|
2080
2690
|
statusTd.appendChild(a);
|
|
2081
2691
|
}
|
|
2082
2692
|
}
|
|
@@ -2085,8 +2695,10 @@
|
|
|
2085
2695
|
tr.appendChild(createCell("td", c.secretRef || "—", "mono"));
|
|
2086
2696
|
body.appendChild(tr);
|
|
2087
2697
|
}
|
|
2698
|
+
updateOauthAuthIssueHint();
|
|
2088
2699
|
} catch (e) {
|
|
2089
2700
|
body.appendChild(createErrorRow(6, e && e.message ? e.message : e));
|
|
2701
|
+
updateOauthAuthIssueHint();
|
|
2090
2702
|
}
|
|
2091
2703
|
}
|
|
2092
2704
|
|
|
@@ -2112,37 +2724,45 @@
|
|
|
2112
2724
|
}
|
|
2113
2725
|
}
|
|
2114
2726
|
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
if (textOf(provider).trim().toLowerCase() === "antigravity") {
|
|
2132
|
-
try {
|
|
2133
|
-
await recoverAntigravityAliasIfAuthVerify(alias);
|
|
2134
|
-
} catch {}
|
|
2135
|
-
try {
|
|
2136
|
-
await refreshQuota();
|
|
2137
|
-
await refreshCredentials();
|
|
2138
|
-
} catch {}
|
|
2139
|
-
}
|
|
2140
|
-
} catch (e) {
|
|
2141
|
-
setLog("credentialOpLog", `Authorize failed: ${e.message}`);
|
|
2142
|
-
}
|
|
2143
|
-
}
|
|
2727
|
+
async function authorizeOauth(mode = "manual") {
|
|
2728
|
+
const safeMode = mode === "auto" ? "auto" : "manual";
|
|
2729
|
+
setLog("credentialOpLog", "");
|
|
2730
|
+
const provider = $("oauthProviderSelect").value;
|
|
2731
|
+
const alias = ($("oauthAuthAliasInput").value || "default").trim() || "default";
|
|
2732
|
+
updateOauthAuthIssueHint();
|
|
2733
|
+
|
|
2734
|
+
const openBrowser = safeMode === "auto"
|
|
2735
|
+
? Boolean($("oauthAutoOpenBrowser") && $("oauthAutoOpenBrowser").checked)
|
|
2736
|
+
: Boolean($("oauthManualOpenBrowser") && $("oauthManualOpenBrowser").checked);
|
|
2737
|
+
const forceReauthorize = safeMode === "auto"
|
|
2738
|
+
? Boolean($("oauthAutoForceReauth") && $("oauthAutoForceReauth").checked)
|
|
2739
|
+
: Boolean($("oauthManualForceReauth") && $("oauthManualForceReauth").checked);
|
|
2740
|
+
const headful = safeMode === "auto"
|
|
2741
|
+
? Boolean($("oauthAutoHeadful") && $("oauthAutoHeadful").checked)
|
|
2742
|
+
: Boolean($("oauthManualHeadful") && $("oauthManualHeadful").checked);
|
|
2144
2743
|
|
|
2145
|
-
|
|
2744
|
+
try {
|
|
2745
|
+
const out = await apiFetch("/daemon/oauth/authorize", {
|
|
2746
|
+
method: "POST",
|
|
2747
|
+
body: JSON.stringify({ provider, alias, openBrowser, forceReauthorize, mode: safeMode, headful })
|
|
2748
|
+
});
|
|
2749
|
+
setLog("credentialOpLog", `[${safeMode}] OK. tokenFile: ${out.tokenFile || "—"}`);
|
|
2750
|
+
await refreshCredentials();
|
|
2751
|
+
if (textOf(provider).trim().toLowerCase() === "antigravity") {
|
|
2752
|
+
try {
|
|
2753
|
+
await recoverAntigravityAliasIfAuthVerify(alias);
|
|
2754
|
+
} catch {}
|
|
2755
|
+
try {
|
|
2756
|
+
await refreshQuota();
|
|
2757
|
+
await refreshCredentials();
|
|
2758
|
+
} catch {}
|
|
2759
|
+
}
|
|
2760
|
+
} catch (e) {
|
|
2761
|
+
setLog("credentialOpLog", `[${safeMode}] Authorize failed: ${e.message}`);
|
|
2762
|
+
}
|
|
2763
|
+
}
|
|
2764
|
+
|
|
2765
|
+
const routingEditorState = {
|
|
2146
2766
|
value: {},
|
|
2147
2767
|
dirty: false
|
|
2148
2768
|
};
|
|
@@ -2799,13 +3419,14 @@
|
|
|
2799
3419
|
}
|
|
2800
3420
|
}
|
|
2801
3421
|
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
3422
|
+
async function refreshQuota() {
|
|
3423
|
+
const body = $("quotaTbody");
|
|
3424
|
+
body.replaceChildren();
|
|
3425
|
+
setLog("quotaOpLog", "");
|
|
3426
|
+
clearQuotaRenderedGroups();
|
|
3427
|
+
try {
|
|
3428
|
+
// Load routing targets so we can filter out providers not referenced by routing pools.
|
|
3429
|
+
await refreshRoutingTargets();
|
|
2809
3430
|
// Force refresh quota first so UI doesn't show stale pool state.
|
|
2810
3431
|
try {
|
|
2811
3432
|
await apiFetch("/daemon/modules/quota/refresh", { method: "POST" });
|
|
@@ -2824,23 +3445,119 @@
|
|
|
2824
3445
|
const out = await apiFetch("/quota/providers");
|
|
2825
3446
|
UI.quotaProviders = Array.isArray(out.providers) ? out.providers : [];
|
|
2826
3447
|
UI.quotaProvidersUpdatedAt = Date.now();
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
3448
|
+
UI.quotaProviderMap = new Map(UI.quotaProviders.map((q) => [textOf(q.providerKey), q]));
|
|
3449
|
+
updateOauthAuthIssueHint();
|
|
3450
|
+
renderQuotaProviders();
|
|
3451
|
+
updateQuotaSelectionSummary();
|
|
3452
|
+
} catch (e) {
|
|
3453
|
+
if (e && e.status === 401) notifyUnauthorizedOnce("quota");
|
|
3454
|
+
else toast(e && e.message ? e.message : String(e || "quota refresh failed"));
|
|
3455
|
+
body.appendChild(createErrorRow(8, e && e.message ? e.message : e));
|
|
3456
|
+
}
|
|
3457
|
+
}
|
|
3458
|
+
|
|
3459
|
+
function normalizeProviderKeyList(value) {
|
|
3460
|
+
if (Array.isArray(value)) {
|
|
3461
|
+
return value.map((k) => textOf(k)).map((k) => k.trim()).filter(Boolean);
|
|
3462
|
+
}
|
|
3463
|
+
const single = textOf(value || "").trim();
|
|
3464
|
+
return single ? [single] : [];
|
|
2834
3465
|
}
|
|
2835
|
-
}
|
|
2836
3466
|
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
3467
|
+
function clearQuotaRenderedGroups() {
|
|
3468
|
+
UI.quotaRenderedVisibleKeys = [];
|
|
3469
|
+
UI.quotaRenderedGroups = new Map();
|
|
3470
|
+
}
|
|
2840
3471
|
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
3472
|
+
function computeSelectionState(keys) {
|
|
3473
|
+
const sel = UI.quotaSelection instanceof Set ? UI.quotaSelection : new Set();
|
|
3474
|
+
let any = false;
|
|
3475
|
+
let all = true;
|
|
3476
|
+
for (const k of keys) {
|
|
3477
|
+
const hit = sel.has(k);
|
|
3478
|
+
if (hit) any = true;
|
|
3479
|
+
else all = false;
|
|
3480
|
+
}
|
|
3481
|
+
return { any, all };
|
|
3482
|
+
}
|
|
3483
|
+
|
|
3484
|
+
function setGroupCheckboxState(checkbox, keys) {
|
|
3485
|
+
if (!checkbox) return;
|
|
3486
|
+
const { any, all } = computeSelectionState(keys);
|
|
3487
|
+
checkbox.indeterminate = Boolean(any && !all);
|
|
3488
|
+
checkbox.checked = Boolean(all && keys.length > 0);
|
|
3489
|
+
}
|
|
3490
|
+
|
|
3491
|
+
function syncQuotaRowCheckboxes() {
|
|
3492
|
+
const sel = UI.quotaSelection instanceof Set ? UI.quotaSelection : new Set();
|
|
3493
|
+
const body = $("quotaTbody");
|
|
3494
|
+
if (!body) return;
|
|
3495
|
+
body.querySelectorAll("input[data-select-key]").forEach((el) => {
|
|
3496
|
+
const key = el.getAttribute("data-select-key");
|
|
3497
|
+
if (!key) return;
|
|
3498
|
+
el.checked = sel.has(key);
|
|
3499
|
+
});
|
|
3500
|
+
}
|
|
3501
|
+
|
|
3502
|
+
function updateQuotaSelectionSummary() {
|
|
3503
|
+
syncQuotaRowCheckboxes();
|
|
3504
|
+
const hint = $("quotaSelectionHint");
|
|
3505
|
+
if (hint) {
|
|
3506
|
+
const n = UI.quotaSelection instanceof Set ? UI.quotaSelection.size : 0;
|
|
3507
|
+
hint.textContent = `selected ${n}`;
|
|
3508
|
+
}
|
|
3509
|
+
const master = $("quotaSelectAllVisibleToggle");
|
|
3510
|
+
if (master) {
|
|
3511
|
+
setGroupCheckboxState(master, Array.isArray(UI.quotaRenderedVisibleKeys) ? UI.quotaRenderedVisibleKeys : []);
|
|
3512
|
+
}
|
|
3513
|
+
if (UI.quotaRenderedGroups instanceof Map) {
|
|
3514
|
+
for (const entry of UI.quotaRenderedGroups.values()) {
|
|
3515
|
+
if (!entry || !entry.checkbox || !Array.isArray(entry.keys)) continue;
|
|
3516
|
+
setGroupCheckboxState(entry.checkbox, entry.keys);
|
|
3517
|
+
}
|
|
3518
|
+
}
|
|
3519
|
+
}
|
|
3520
|
+
|
|
3521
|
+
function setQuotaSelection(keys, selected) {
|
|
3522
|
+
if (!(UI.quotaSelection instanceof Set)) UI.quotaSelection = new Set();
|
|
3523
|
+
for (const k of keys) {
|
|
3524
|
+
if (!k) continue;
|
|
3525
|
+
if (selected) UI.quotaSelection.add(k);
|
|
3526
|
+
else UI.quotaSelection.delete(k);
|
|
3527
|
+
}
|
|
3528
|
+
updateQuotaSelectionSummary();
|
|
3529
|
+
}
|
|
3530
|
+
|
|
3531
|
+
function clearQuotaSelection() {
|
|
3532
|
+
if (UI.quotaSelection instanceof Set) UI.quotaSelection.clear();
|
|
3533
|
+
updateQuotaSelectionSummary();
|
|
3534
|
+
}
|
|
3535
|
+
|
|
3536
|
+
function resolveQuotaActionTargets() {
|
|
3537
|
+
const selected = UI.quotaSelection instanceof Set ? Array.from(UI.quotaSelection) : [];
|
|
3538
|
+
if (selected.length) return selected;
|
|
3539
|
+
return normalizeProviderKeyList($("quotaKeyInput")?.value);
|
|
3540
|
+
}
|
|
3541
|
+
|
|
3542
|
+
let quotaRefreshTimer = null;
|
|
3543
|
+
function scheduleQuotaRefresh() {
|
|
3544
|
+
const auto = $("quotaAutoRefreshToggle");
|
|
3545
|
+
if (auto && auto.checked === false) return;
|
|
3546
|
+
if (quotaRefreshTimer) clearTimeout(quotaRefreshTimer);
|
|
3547
|
+
quotaRefreshTimer = setTimeout(() => {
|
|
3548
|
+
quotaRefreshTimer = null;
|
|
3549
|
+
void refreshQuota();
|
|
3550
|
+
}, 550);
|
|
3551
|
+
}
|
|
3552
|
+
|
|
3553
|
+
function renderQuotaProviders() {
|
|
3554
|
+
const body = $("quotaTbody");
|
|
3555
|
+
body.replaceChildren();
|
|
3556
|
+
clearQuotaRenderedGroups();
|
|
3557
|
+
|
|
3558
|
+
const filter = textOf($("quotaFilterInput").value || "").trim().toLowerCase();
|
|
3559
|
+
const hideOk = Boolean($("quotaHideOkToggle").checked);
|
|
3560
|
+
const onlyRoutedTargets = Boolean($("quotaOnlyRoutedTargetsToggle").checked);
|
|
2844
3561
|
const routedProviderKeys = onlyRoutedTargets ? resolveRoutedProviderKeys(UI.routingTargets, UI.quotaProviders) : null;
|
|
2845
3562
|
|
|
2846
3563
|
const list = Array.isArray(UI.quotaProviders) ? UI.quotaProviders : [];
|
|
@@ -2854,20 +3571,13 @@
|
|
|
2854
3571
|
})
|
|
2855
3572
|
.slice();
|
|
2856
3573
|
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
const bIn = b && b.inPool === true ? 1 : 0;
|
|
2860
|
-
if (aIn !== bIn) return aIn - bIn; // inPool=false first
|
|
2861
|
-
const aUntil = Math.max(Number(a?.blacklistUntil || 0), Number(a?.cooldownUntil || 0));
|
|
2862
|
-
const bUntil = Math.max(Number(b?.blacklistUntil || 0), Number(b?.cooldownUntil || 0));
|
|
2863
|
-
if (aUntil !== bUntil) return bUntil - aUntil;
|
|
2864
|
-
return textOf(a?.providerKey).localeCompare(textOf(b?.providerKey));
|
|
2865
|
-
});
|
|
3574
|
+
// Keep order stable across polling refreshes: avoid health-based sorting which makes the table jump.
|
|
3575
|
+
next.sort((a, b) => textOf(a?.providerKey).localeCompare(textOf(b?.providerKey)));
|
|
2866
3576
|
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
3577
|
+
if (!next.length) {
|
|
3578
|
+
body.appendChild(createErrorRow(8, "No providers matched filter."));
|
|
3579
|
+
return;
|
|
3580
|
+
}
|
|
2871
3581
|
|
|
2872
3582
|
function splitProviderKey(providerKey) {
|
|
2873
3583
|
const raw = textOf(providerKey || "");
|
|
@@ -2891,52 +3601,86 @@
|
|
|
2891
3601
|
byAlias.get(authAlias).push({ q, pk, providerId, authAlias, model });
|
|
2892
3602
|
}
|
|
2893
3603
|
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
3604
|
+
const providerIds = Array.from(byProvider.keys()).sort((a, b) => a.localeCompare(b));
|
|
3605
|
+
const groupCols = 8;
|
|
3606
|
+
|
|
3607
|
+
const appendGroupRow = (label, opts = {}) => {
|
|
3608
|
+
const tr = document.createElement("tr");
|
|
3609
|
+
tr.className = "group-row";
|
|
3610
|
+
const keys = Array.isArray(opts.keys) ? opts.keys : [];
|
|
3611
|
+
const canSelect = Boolean(keys.length);
|
|
3612
|
+
const td0 = document.createElement("td");
|
|
3613
|
+
td0.className = "check-cell";
|
|
3614
|
+
if (canSelect) {
|
|
3615
|
+
const cb = document.createElement("input");
|
|
3616
|
+
cb.type = "checkbox";
|
|
3617
|
+
cb.title = "Select group";
|
|
3618
|
+
cb.addEventListener("change", () => setQuotaSelection(keys, cb.checked));
|
|
3619
|
+
td0.appendChild(cb);
|
|
3620
|
+
if (UI.quotaRenderedGroups instanceof Map && typeof opts.groupId === "string" && opts.groupId) {
|
|
3621
|
+
UI.quotaRenderedGroups.set(opts.groupId, { checkbox: cb, keys });
|
|
3622
|
+
}
|
|
3623
|
+
setGroupCheckboxState(cb, keys);
|
|
3624
|
+
}
|
|
3625
|
+
tr.appendChild(td0);
|
|
3626
|
+
|
|
3627
|
+
const td = document.createElement("td");
|
|
3628
|
+
td.colSpan = groupCols - 1;
|
|
3629
|
+
td.textContent = label;
|
|
3630
|
+
if (opts.indent) td.className = "indent";
|
|
3631
|
+
tr.appendChild(td);
|
|
3632
|
+
body.appendChild(tr);
|
|
3633
|
+
};
|
|
3634
|
+
|
|
3635
|
+
for (const providerId of providerIds) {
|
|
3636
|
+
const byAlias = byProvider.get(providerId);
|
|
3637
|
+
let modelsCount = 0;
|
|
3638
|
+
for (const v of byAlias.values()) modelsCount += v.length;
|
|
3639
|
+
const providerKeys = Array.from(byAlias.values()).flatMap((items) => items.map((it) => it.pk));
|
|
3640
|
+
appendGroupRow(`${providerId} (${modelsCount})`, { keys: providerKeys, groupId: `provider:${providerId}` });
|
|
3641
|
+
|
|
3642
|
+
const aliases = Array.from(byAlias.keys()).sort((a, b) => a.localeCompare(b));
|
|
3643
|
+
for (const alias of aliases) {
|
|
3644
|
+
const items = byAlias.get(alias);
|
|
2917
3645
|
const fpSuffix = (() => {
|
|
2918
3646
|
if (providerId !== "antigravity") return "";
|
|
2919
3647
|
if (!items || !items.length) return "";
|
|
2920
3648
|
const q0 = items[0] && items[0].q ? items[0].q : null;
|
|
2921
3649
|
const suffix = textOf(q0 && (q0.fpSuffix || q0.fingerprintSuffix) ? (q0.fpSuffix || q0.fingerprintSuffix) : "");
|
|
2922
3650
|
return suffix;
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
3651
|
+
})();
|
|
3652
|
+
const fpTag = fpSuffix ? ` · fp=${fpSuffix}` : "";
|
|
3653
|
+
const aliasKeys = Array.isArray(items) ? items.map((it) => it.pk) : [];
|
|
3654
|
+
appendGroupRow(`${alias || "(no-key)"} (${items.length})${fpTag}`, {
|
|
3655
|
+
indent: true,
|
|
3656
|
+
keys: aliasKeys,
|
|
3657
|
+
groupId: `alias:${providerId}.${alias || "(no-key)"}`
|
|
3658
|
+
});
|
|
3659
|
+
|
|
3660
|
+
for (const item of items) {
|
|
3661
|
+
const q = item.q;
|
|
3662
|
+
const tr = document.createElement("tr");
|
|
3663
|
+
tr.className = "provider-row";
|
|
3664
|
+
tr.setAttribute("data-provider-key", item.pk);
|
|
3665
|
+
|
|
3666
|
+
const selTd = document.createElement("td");
|
|
3667
|
+
selTd.className = "check-cell";
|
|
3668
|
+
const cb = document.createElement("input");
|
|
3669
|
+
cb.type = "checkbox";
|
|
3670
|
+
cb.setAttribute("data-select-key", item.pk);
|
|
3671
|
+
cb.checked = UI.quotaSelection instanceof Set ? UI.quotaSelection.has(item.pk) : false;
|
|
3672
|
+
cb.addEventListener("change", () => {
|
|
3673
|
+
setQuotaSelection([item.pk], cb.checked);
|
|
3674
|
+
});
|
|
3675
|
+
selTd.appendChild(cb);
|
|
3676
|
+
tr.appendChild(selTd);
|
|
3677
|
+
tr.appendChild(createCell("td", "", "mono"));
|
|
3678
|
+
tr.appendChild(createCell("td", item.model || item.pk, "mono indent", { title: true }));
|
|
3679
|
+
UI.quotaRenderedVisibleKeys.push(item.pk);
|
|
3680
|
+
|
|
3681
|
+
const inPool = Boolean(q.inPool);
|
|
3682
|
+
const inTd = document.createElement("td");
|
|
3683
|
+
inTd.appendChild(pill(inPool ? "true" : "false", inPool ? "ok" : "bad"));
|
|
2940
3684
|
tr.appendChild(inTd);
|
|
2941
3685
|
|
|
2942
3686
|
const reason = textOf(q.reason || "");
|
|
@@ -2993,11 +3737,12 @@
|
|
|
2993
3737
|
actionsTd.appendChild(box);
|
|
2994
3738
|
tr.appendChild(actionsTd);
|
|
2995
3739
|
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3740
|
+
body.appendChild(tr);
|
|
3741
|
+
}
|
|
3742
|
+
}
|
|
3743
|
+
}
|
|
3744
|
+
updateQuotaSelectionSummary();
|
|
3745
|
+
}
|
|
3001
3746
|
|
|
3002
3747
|
function formatRemainingFraction(v) {
|
|
3003
3748
|
if (typeof v !== "number" || !Number.isFinite(v)) return "—";
|
|
@@ -3097,6 +3842,166 @@
|
|
|
3097
3842
|
return `${label}: requests=${req} (err=${err}) tokens in/out/total=${inTok}/${outTok}/${totTok}`;
|
|
3098
3843
|
}
|
|
3099
3844
|
|
|
3845
|
+
function summarizeStatsRows(rows) {
|
|
3846
|
+
const byProvider = new Map();
|
|
3847
|
+
const byRuntime = new Map();
|
|
3848
|
+
const byModel = new Map();
|
|
3849
|
+
let totalReq = 0;
|
|
3850
|
+
let totalErr = 0;
|
|
3851
|
+
for (const r of rows) {
|
|
3852
|
+
if (!r || !r.providerKey) continue;
|
|
3853
|
+
const providerKey = textOf(r.providerKey);
|
|
3854
|
+
const providerId = (providerKey.split(".")[0] || providerKey).trim() || providerKey;
|
|
3855
|
+
const runtime = (() => {
|
|
3856
|
+
const parts = providerKey.split(".").filter(Boolean);
|
|
3857
|
+
if (parts.length >= 2) return `${parts[0]}.${parts[1]}`;
|
|
3858
|
+
return providerKey;
|
|
3859
|
+
})();
|
|
3860
|
+
const model = textOf(r.model || "");
|
|
3861
|
+
const req = typeof r.requestCount === "number" ? r.requestCount : 0;
|
|
3862
|
+
const err = typeof r.errorCount === "number" ? r.errorCount : 0;
|
|
3863
|
+
totalReq += req;
|
|
3864
|
+
totalErr += err;
|
|
3865
|
+
const p = byProvider.get(providerId) || { providerId, requestCount: 0, errorCount: 0 };
|
|
3866
|
+
p.requestCount += req;
|
|
3867
|
+
p.errorCount += err;
|
|
3868
|
+
byProvider.set(providerId, p);
|
|
3869
|
+
const rt = byRuntime.get(runtime) || { runtime, requestCount: 0, errorCount: 0 };
|
|
3870
|
+
rt.requestCount += req;
|
|
3871
|
+
rt.errorCount += err;
|
|
3872
|
+
byRuntime.set(runtime, rt);
|
|
3873
|
+
const mKey = model || "—";
|
|
3874
|
+
const m = byModel.get(mKey) || { model: mKey, requestCount: 0, errorCount: 0 };
|
|
3875
|
+
m.requestCount += req;
|
|
3876
|
+
m.errorCount += err;
|
|
3877
|
+
byModel.set(mKey, m);
|
|
3878
|
+
}
|
|
3879
|
+
return {
|
|
3880
|
+
totalReq,
|
|
3881
|
+
totalErr,
|
|
3882
|
+
byProvider: Array.from(byProvider.values()).sort((a, b) => b.requestCount - a.requestCount),
|
|
3883
|
+
byRuntime: Array.from(byRuntime.values()).sort((a, b) => b.requestCount - a.requestCount),
|
|
3884
|
+
byModel: Array.from(byModel.values()).sort((a, b) => b.requestCount - a.requestCount)
|
|
3885
|
+
};
|
|
3886
|
+
}
|
|
3887
|
+
|
|
3888
|
+
function renderStatsTable(bodyEl, rows, colCount, emptyMsg) {
|
|
3889
|
+
bodyEl.replaceChildren();
|
|
3890
|
+
if (!rows.length) {
|
|
3891
|
+
bodyEl.appendChild(createInfoRow(colCount, emptyMsg));
|
|
3892
|
+
return;
|
|
3893
|
+
}
|
|
3894
|
+
for (const r of rows) {
|
|
3895
|
+
const tr = document.createElement("tr");
|
|
3896
|
+
for (const cell of r) {
|
|
3897
|
+
tr.appendChild(createCell("td", cell, "mono truncate", { title: true }));
|
|
3898
|
+
}
|
|
3899
|
+
bodyEl.appendChild(tr);
|
|
3900
|
+
}
|
|
3901
|
+
}
|
|
3902
|
+
|
|
3903
|
+
async function refreshStats() {
|
|
3904
|
+
$("statsSessionTotalsBox").textContent = "";
|
|
3905
|
+
$("statsHistoricalTotalsBox").textContent = "";
|
|
3906
|
+
const sessionProvidersBody = $("statsSessionProvidersTbody");
|
|
3907
|
+
const sessionRuntimesBody = $("statsSessionRuntimesTbody");
|
|
3908
|
+
const sessionModelsBody = $("statsSessionModelsTbody");
|
|
3909
|
+
const sessionErrorsBody = $("statsSessionErrorsTbody");
|
|
3910
|
+
const histProvidersBody = $("statsHistoricalProvidersTbody");
|
|
3911
|
+
const histRuntimesBody = $("statsHistoricalRuntimesTbody");
|
|
3912
|
+
const histModelsBody = $("statsHistoricalModelsTbody");
|
|
3913
|
+
const histErrorsBody = $("statsHistoricalErrorsTbody");
|
|
3914
|
+
sessionProvidersBody.replaceChildren();
|
|
3915
|
+
sessionRuntimesBody.replaceChildren();
|
|
3916
|
+
sessionModelsBody.replaceChildren();
|
|
3917
|
+
sessionErrorsBody.replaceChildren();
|
|
3918
|
+
histProvidersBody.replaceChildren();
|
|
3919
|
+
histRuntimesBody.replaceChildren();
|
|
3920
|
+
histModelsBody.replaceChildren();
|
|
3921
|
+
histErrorsBody.replaceChildren();
|
|
3922
|
+
|
|
3923
|
+
try {
|
|
3924
|
+
const out = await apiFetch("/daemon/stats");
|
|
3925
|
+
const session = out && out.session ? out.session : null;
|
|
3926
|
+
const historical = out && out.historical ? out.historical : null;
|
|
3927
|
+
const sessionRows = session && Array.isArray(session.totals) ? session.totals : [];
|
|
3928
|
+
const histRows = historical && Array.isArray(historical.totals) ? historical.totals : [];
|
|
3929
|
+
|
|
3930
|
+
const s = summarizeStatsRows(sessionRows);
|
|
3931
|
+
const h = summarizeStatsRows(histRows);
|
|
3932
|
+
|
|
3933
|
+
$("statsSessionTotalsBox").textContent = `ALL (session): requests=${formatInt(s.totalReq)} (err=${formatInt(s.totalErr)})`;
|
|
3934
|
+
$("statsHistoricalTotalsBox").textContent = `ALL (historical): requests=${formatInt(h.totalReq)} (err=${formatInt(h.totalErr)})`;
|
|
3935
|
+
|
|
3936
|
+
renderStatsTable(
|
|
3937
|
+
sessionProvidersBody,
|
|
3938
|
+
s.byProvider.map((p) => [p.providerId, formatInt(p.requestCount), formatInt(p.errorCount)]),
|
|
3939
|
+
3,
|
|
3940
|
+
"No provider stats recorded in this session."
|
|
3941
|
+
);
|
|
3942
|
+
renderStatsTable(
|
|
3943
|
+
sessionRuntimesBody,
|
|
3944
|
+
s.byRuntime.map((r) => [r.runtime, formatInt(r.requestCount), formatInt(r.errorCount)]),
|
|
3945
|
+
3,
|
|
3946
|
+
"No runtime stats recorded in this session."
|
|
3947
|
+
);
|
|
3948
|
+
renderStatsTable(
|
|
3949
|
+
sessionModelsBody,
|
|
3950
|
+
s.byModel.map((m) => [m.model, formatInt(m.requestCount), formatInt(m.errorCount)]),
|
|
3951
|
+
3,
|
|
3952
|
+
"No model stats recorded in this session."
|
|
3953
|
+
);
|
|
3954
|
+
|
|
3955
|
+
renderStatsTable(
|
|
3956
|
+
histProvidersBody,
|
|
3957
|
+
h.byProvider.map((p) => [p.providerId, formatInt(p.requestCount), formatInt(p.errorCount)]),
|
|
3958
|
+
3,
|
|
3959
|
+
"No historical stats recorded."
|
|
3960
|
+
);
|
|
3961
|
+
renderStatsTable(
|
|
3962
|
+
histRuntimesBody,
|
|
3963
|
+
h.byRuntime.map((r) => [r.runtime, formatInt(r.requestCount), formatInt(r.errorCount)]),
|
|
3964
|
+
3,
|
|
3965
|
+
"No historical stats recorded."
|
|
3966
|
+
);
|
|
3967
|
+
renderStatsTable(
|
|
3968
|
+
histModelsBody,
|
|
3969
|
+
h.byModel.map((m) => [m.model, formatInt(m.requestCount), formatInt(m.errorCount)]),
|
|
3970
|
+
3,
|
|
3971
|
+
"No historical stats recorded."
|
|
3972
|
+
);
|
|
3973
|
+
|
|
3974
|
+
const toProviderKeys = (rows) =>
|
|
3975
|
+
rows
|
|
3976
|
+
.filter((r) => r && r.providerKey)
|
|
3977
|
+
.map((r) => ({
|
|
3978
|
+
providerKey: textOf(r.providerKey),
|
|
3979
|
+
model: textOf(r.model || "—") || "—",
|
|
3980
|
+
req: typeof r.requestCount === "number" ? r.requestCount : 0,
|
|
3981
|
+
err: typeof r.errorCount === "number" ? r.errorCount : 0
|
|
3982
|
+
}))
|
|
3983
|
+
.sort((a, b) => b.req - a.req || b.err - a.err || a.providerKey.localeCompare(b.providerKey))
|
|
3984
|
+
.slice(0, 50)
|
|
3985
|
+
.map((x) => [x.providerKey, x.model, formatInt(x.req), formatInt(x.err)]);
|
|
3986
|
+
|
|
3987
|
+
renderStatsTable(sessionErrorsBody, toProviderKeys(sessionRows), 4, "No provider stats recorded in this session.");
|
|
3988
|
+
renderStatsTable(histErrorsBody, toProviderKeys(histRows), 4, "No historical provider stats recorded.");
|
|
3989
|
+
|
|
3990
|
+
$("statsLastUpdated").textContent = `updated ${formatEpochMs(Date.now())}`;
|
|
3991
|
+
} catch (e) {
|
|
3992
|
+
if (e && e.status === 401) notifyUnauthorizedOnce("stats");
|
|
3993
|
+
const msg = e && e.message ? e.message : e;
|
|
3994
|
+
renderStatsTable(sessionProvidersBody, [], 3, textOf(msg));
|
|
3995
|
+
renderStatsTable(sessionRuntimesBody, [], 3, textOf(msg));
|
|
3996
|
+
renderStatsTable(sessionModelsBody, [], 3, textOf(msg));
|
|
3997
|
+
renderStatsTable(sessionErrorsBody, [], 4, textOf(msg));
|
|
3998
|
+
renderStatsTable(histProvidersBody, [], 3, textOf(msg));
|
|
3999
|
+
renderStatsTable(histRuntimesBody, [], 3, textOf(msg));
|
|
4000
|
+
renderStatsTable(histModelsBody, [], 3, textOf(msg));
|
|
4001
|
+
renderStatsTable(histErrorsBody, [], 4, textOf(msg));
|
|
4002
|
+
}
|
|
4003
|
+
}
|
|
4004
|
+
|
|
3100
4005
|
async function refreshTokens() {
|
|
3101
4006
|
const body = $("tokensTbody");
|
|
3102
4007
|
body.replaceChildren();
|
|
@@ -3198,47 +4103,59 @@
|
|
|
3198
4103
|
}
|
|
3199
4104
|
}
|
|
3200
4105
|
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
}
|
|
3215
|
-
if (kind === "reset") {
|
|
3216
|
-
await apiFetch(`/quota/providers/${encodeURIComponent(providerKey)}/reset`, { method: "POST" });
|
|
3217
|
-
await refreshQuota();
|
|
3218
|
-
toast("Reset.", "ok");
|
|
3219
|
-
return;
|
|
3220
|
-
}
|
|
3221
|
-
if (kind === "disable") {
|
|
3222
|
-
const minutes = Number.parseFloat(textOf($("quotaDurationSelect").value || "60"));
|
|
3223
|
-
if (!Number.isFinite(minutes) || minutes <= 0) {
|
|
3224
|
-
throw new Error("Invalid minutes");
|
|
4106
|
+
async function quotaAction(kind, providerKeys) {
|
|
4107
|
+
setLog("quotaOpLog", "");
|
|
4108
|
+
const keys = normalizeProviderKeyList(providerKeys);
|
|
4109
|
+
if (!keys.length) {
|
|
4110
|
+
setLog("quotaOpLog", "providerKey required (or select rows)");
|
|
4111
|
+
toast("providerKey required (or select rows)");
|
|
4112
|
+
return;
|
|
4113
|
+
}
|
|
4114
|
+
try {
|
|
4115
|
+
const many = keys.length > 1;
|
|
4116
|
+
if (many) {
|
|
4117
|
+
const ok = confirm(`${kind} on ${keys.length} providerKeys?`);
|
|
4118
|
+
if (!ok) return;
|
|
3225
4119
|
}
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
4120
|
+
if (kind === "recover") {
|
|
4121
|
+
for (const key of keys) {
|
|
4122
|
+
await apiFetch(`/quota/providers/${encodeURIComponent(key)}/recover`, { method: "POST" });
|
|
4123
|
+
}
|
|
4124
|
+
scheduleQuotaRefresh();
|
|
4125
|
+
toast(many ? `Recovered (${keys.length}).` : "Recovered.", "ok");
|
|
4126
|
+
return;
|
|
4127
|
+
}
|
|
4128
|
+
if (kind === "reset") {
|
|
4129
|
+
for (const key of keys) {
|
|
4130
|
+
await apiFetch(`/quota/providers/${encodeURIComponent(key)}/reset`, { method: "POST" });
|
|
4131
|
+
}
|
|
4132
|
+
scheduleQuotaRefresh();
|
|
4133
|
+
toast(many ? `Reset (${keys.length}).` : "Reset.", "ok");
|
|
4134
|
+
return;
|
|
4135
|
+
}
|
|
4136
|
+
if (kind === "disable") {
|
|
4137
|
+
const minutes = Number.parseFloat(textOf($("quotaDurationSelect").value || "60"));
|
|
4138
|
+
if (!Number.isFinite(minutes) || minutes <= 0) {
|
|
4139
|
+
throw new Error("Invalid minutes");
|
|
4140
|
+
}
|
|
4141
|
+
const modeRaw = (textOf($("quotaModeSelect").value) || "cooldown").trim().toLowerCase();
|
|
4142
|
+
const mode = modeRaw === "blacklist" ? "blacklist" : "cooldown";
|
|
4143
|
+
for (const key of keys) {
|
|
4144
|
+
await apiFetch(`/quota/providers/${encodeURIComponent(key)}/disable`, {
|
|
4145
|
+
method: "POST",
|
|
4146
|
+
body: JSON.stringify({ mode, durationMinutes: minutes })
|
|
4147
|
+
});
|
|
4148
|
+
}
|
|
4149
|
+
scheduleQuotaRefresh();
|
|
4150
|
+
toast(many ? `Applied (${keys.length}).` : "Applied.", "ok");
|
|
4151
|
+
return;
|
|
4152
|
+
}
|
|
4153
|
+
} catch (e) {
|
|
4154
|
+
if (e && e.status === 401) notifyUnauthorizedOnce("quota action");
|
|
4155
|
+
setLog("quotaOpLog", `Action failed: ${e && e.message ? e.message : e}`);
|
|
4156
|
+
toast(`Action failed: ${e && e.message ? e.message : String(e)}`);
|
|
4157
|
+
}
|
|
4158
|
+
}
|
|
3242
4159
|
|
|
3243
4160
|
async function resetQuotaModule() {
|
|
3244
4161
|
setLog("quotaOpLog", "");
|
|
@@ -3410,7 +4327,73 @@
|
|
|
3410
4327
|
}
|
|
3411
4328
|
});
|
|
3412
4329
|
|
|
4330
|
+
$("controlRefreshBtn").addEventListener("click", refreshControl);
|
|
4331
|
+
$("controlRestartAllBtn").addEventListener("click", async () => {
|
|
4332
|
+
setLog("controlOpLog", "");
|
|
4333
|
+
if (!confirm("Broadcast restart all local RouteCodex servers now?")) return;
|
|
4334
|
+
try {
|
|
4335
|
+
const out = await controlMutate("servers.restart", {});
|
|
4336
|
+
setLog("controlOpLog", `Restart requested.\n${JSON.stringify(out, null, 2)}`);
|
|
4337
|
+
await refreshControl();
|
|
4338
|
+
await refreshStatus();
|
|
4339
|
+
} catch (e) {
|
|
4340
|
+
setLog("controlOpLog", `Restart failed: ${e.message || e}`);
|
|
4341
|
+
}
|
|
4342
|
+
});
|
|
4343
|
+
$("controlQuotaRefreshBtn").addEventListener("click", async () => {
|
|
4344
|
+
setLog("controlOpLog", "");
|
|
4345
|
+
try {
|
|
4346
|
+
const out = await controlMutate("quota.refresh", {});
|
|
4347
|
+
setLog("controlOpLog", `Quota refresh requested.\n${JSON.stringify(out, null, 2)}`);
|
|
4348
|
+
await refreshControl();
|
|
4349
|
+
} catch (e) {
|
|
4350
|
+
setLog("controlOpLog", `Quota refresh failed: ${e.message || e}`);
|
|
4351
|
+
}
|
|
4352
|
+
});
|
|
4353
|
+
$("controlQuotaOfflineBtn").addEventListener("click", async () => {
|
|
4354
|
+
setLog("controlOpLog", "");
|
|
4355
|
+
const providerKey = ($("controlQuotaKeyInput").value || "").trim();
|
|
4356
|
+
if (!providerKey) return toast("providerKey required");
|
|
4357
|
+
const mode = $("controlQuotaModeSelect").value === "blacklist" ? "blacklist" : "cooldown";
|
|
4358
|
+
const minutes = Number($("controlQuotaDurationSelect").value || 0);
|
|
4359
|
+
const durationMs = Number.isFinite(minutes) && minutes > 0 ? minutes * 60 * 1000 : 0;
|
|
4360
|
+
if (!durationMs) return toast("duration required");
|
|
4361
|
+
try {
|
|
4362
|
+
const out = await controlMutate("quota.disable", { providerKey, mode, durationMs });
|
|
4363
|
+
setLog("controlOpLog", `Offline requested.\n${JSON.stringify(out, null, 2)}`);
|
|
4364
|
+
await refreshControl();
|
|
4365
|
+
} catch (e) {
|
|
4366
|
+
setLog("controlOpLog", `Offline failed: ${e.message || e}`);
|
|
4367
|
+
}
|
|
4368
|
+
});
|
|
4369
|
+
$("controlQuotaRecoverBtn").addEventListener("click", async () => {
|
|
4370
|
+
setLog("controlOpLog", "");
|
|
4371
|
+
const providerKey = ($("controlQuotaKeyInput").value || "").trim();
|
|
4372
|
+
if (!providerKey) return toast("providerKey required");
|
|
4373
|
+
try {
|
|
4374
|
+
const out = await controlMutate("quota.recover", { providerKey });
|
|
4375
|
+
setLog("controlOpLog", `Recover requested.\n${JSON.stringify(out, null, 2)}`);
|
|
4376
|
+
await refreshControl();
|
|
4377
|
+
} catch (e) {
|
|
4378
|
+
setLog("controlOpLog", `Recover failed: ${e.message || e}`);
|
|
4379
|
+
}
|
|
4380
|
+
});
|
|
4381
|
+
$("controlQuotaResetBtn").addEventListener("click", async () => {
|
|
4382
|
+
setLog("controlOpLog", "");
|
|
4383
|
+
const providerKey = ($("controlQuotaKeyInput").value || "").trim();
|
|
4384
|
+
if (!providerKey) return toast("providerKey required");
|
|
4385
|
+
try {
|
|
4386
|
+
const out = await controlMutate("quota.reset", { providerKey });
|
|
4387
|
+
setLog("controlOpLog", `Reset requested.\n${JSON.stringify(out, null, 2)}`);
|
|
4388
|
+
await refreshControl();
|
|
4389
|
+
} catch (e) {
|
|
4390
|
+
setLog("controlOpLog", `Reset failed: ${e.message || e}`);
|
|
4391
|
+
}
|
|
4392
|
+
});
|
|
4393
|
+
|
|
3413
4394
|
$("refreshProvidersBtn").addEventListener("click", refreshProviders);
|
|
4395
|
+
$("refreshStatsBtn").addEventListener("click", refreshStats);
|
|
4396
|
+
$("statsAutoRefresh").addEventListener("change", syncStatsAutoRefresh);
|
|
3414
4397
|
$("refreshTokensBtn").addEventListener("click", refreshTokens);
|
|
3415
4398
|
$("newProviderBtn").addEventListener("click", () => {
|
|
3416
4399
|
$("providerEditorTitle").textContent = "Provider editor (new)";
|
|
@@ -3472,33 +4455,58 @@
|
|
|
3472
4455
|
|
|
3473
4456
|
$("refreshCredentialsBtn").addEventListener("click", refreshCredentials);
|
|
3474
4457
|
$("saveOauthBrowserBtn").addEventListener("click", saveSettings);
|
|
3475
|
-
$("
|
|
4458
|
+
$("oauthModeManualBtn").addEventListener("click", () => setOauthMode("manual"));
|
|
4459
|
+
$("oauthModeAutoBtn").addEventListener("click", () => setOauthMode("auto"));
|
|
4460
|
+
$("oauthAuthorizeManualBtn").addEventListener("click", () => void authorizeOauth("manual"));
|
|
4461
|
+
$("oauthAuthorizeAutoBtn").addEventListener("click", () => void authorizeOauth("auto"));
|
|
4462
|
+
$("oauthVerifyOpenBtn").addEventListener("click", () => void openSelectedVerifyUrl());
|
|
4463
|
+
$("oauthVerifyCopyBtn").addEventListener("click", () => void copySelectedVerifyUrl());
|
|
4464
|
+
$("oauthVerifyRecheckBtn").addEventListener("click", async () => {
|
|
4465
|
+
try {
|
|
4466
|
+
await refreshQuota();
|
|
4467
|
+
await refreshCredentials();
|
|
4468
|
+
setLog("credentialOpLog", "Rechecked quota/credential state.");
|
|
4469
|
+
} catch (e) {
|
|
4470
|
+
setLog("credentialOpLog", `Recheck failed: ${e && e.message ? e.message : String(e)}`);
|
|
4471
|
+
}
|
|
4472
|
+
});
|
|
3476
4473
|
$("oauthProviderSelect").addEventListener("change", updateOauthAuthIssueHint);
|
|
3477
4474
|
$("oauthAuthAliasInput").addEventListener("input", updateOauthAuthIssueHint);
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
4475
|
+
setOauthMode("manual");
|
|
4476
|
+
|
|
4477
|
+
$("refreshQuotaBtn").addEventListener("click", refreshQuota);
|
|
4478
|
+
$("refreshQuotaSnapshotBtn").addEventListener("click", refreshQuotaSnapshotNow);
|
|
4479
|
+
$("quotaFilterInput").addEventListener("input", renderQuotaProviders);
|
|
4480
|
+
$("quotaHideOkToggle").addEventListener("change", renderQuotaProviders);
|
|
4481
|
+
$("quotaOnlyRoutedTargetsToggle").addEventListener("change", renderQuotaProviders);
|
|
4482
|
+
$("quotaSelectVisibleBtn").addEventListener("click", () => {
|
|
4483
|
+
const keys = Array.isArray(UI.quotaRenderedVisibleKeys) ? UI.quotaRenderedVisibleKeys : [];
|
|
4484
|
+
setQuotaSelection(keys, true);
|
|
4485
|
+
});
|
|
4486
|
+
$("quotaClearSelectionBtn").addEventListener("click", clearQuotaSelection);
|
|
4487
|
+
$("quotaSelectAllVisibleToggle").addEventListener("change", () => {
|
|
4488
|
+
const master = $("quotaSelectAllVisibleToggle");
|
|
4489
|
+
const keys = Array.isArray(UI.quotaRenderedVisibleKeys) ? UI.quotaRenderedVisibleKeys : [];
|
|
4490
|
+
setQuotaSelection(keys, Boolean(master && master.checked));
|
|
4491
|
+
});
|
|
4492
|
+
$("quotaApplyDisableBtn").addEventListener("click", () => void quotaAction("disable", resolveQuotaActionTargets()));
|
|
4493
|
+
$("quotaApplyRecoverBtn").addEventListener("click", () => void quotaAction("recover", resolveQuotaActionTargets()));
|
|
4494
|
+
$("quotaApplyResetBtn").addEventListener("click", () => void quotaAction("reset", resolveQuotaActionTargets()));
|
|
4495
|
+
$("quotaTbody").addEventListener("click", (ev) => {
|
|
4496
|
+
const tr = ev.target.closest("tr");
|
|
4497
|
+
if (tr && tr.getAttribute) {
|
|
4498
|
+
const pk = tr.getAttribute("data-provider-key");
|
|
3491
4499
|
if (pk) $("quotaKeyInput").value = pk;
|
|
3492
4500
|
}
|
|
3493
4501
|
const el = ev.target.closest("button");
|
|
3494
4502
|
if (!el) return;
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
4503
|
+
const action = el.getAttribute("data-action");
|
|
4504
|
+
const key = el.getAttribute("data-key");
|
|
4505
|
+
if (key) $("quotaKeyInput").value = key;
|
|
4506
|
+
if (action === "quota-recover") void quotaAction("recover", [key]);
|
|
4507
|
+
else if (action === "quota-reset") void quotaAction("reset", [key]);
|
|
4508
|
+
else if (action === "quota-disable") void quotaAction("disable", [key]);
|
|
4509
|
+
});
|
|
3502
4510
|
$("resetQuotaBtn").addEventListener("click", resetQuotaModule);
|
|
3503
4511
|
|
|
3504
4512
|
$("loadRoutingBtn").addEventListener("click", loadRouting);
|