@jsonstudio/rcc 0.89.1205 → 0.89.1348
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 +17 -0
- package/configsamples/config.json +426 -0
- package/configsamples/config.reference.json +58 -0
- package/configsamples/provider/crs/config.v1.json +46 -0
- package/configsamples/provider/glm/config.v1.json +81 -0
- package/configsamples/provider/glm-anthropic/config.v1.json +45 -0
- package/configsamples/provider/iflow/config.v1.json +74 -0
- package/configsamples/provider/kimi/config.v1.json +41 -0
- package/configsamples/provider/lmstudio/config.v1.json +101 -0
- package/configsamples/provider/mimo/config.v1.json +35 -0
- package/configsamples/provider/modelscope/config.v1.json +96 -0
- package/configsamples/provider/qwen/config.v1.json +38 -0
- package/configsamples/provider/tab/config.v1.json +50 -0
- package/configsamples/provider/tabglm/config.v1.json +49 -0
- package/dist/build-info.js +2 -2
- package/dist/cli/commands/code.js +12 -6
- package/dist/cli/commands/code.js.map +1 -1
- package/dist/cli/commands/config.d.ts +2 -1
- package/dist/cli/commands/config.js +74 -103
- package/dist/cli/commands/config.js.map +1 -1
- package/dist/cli/commands/examples.js +6 -6
- package/dist/cli/commands/examples.js.map +1 -1
- package/dist/cli/commands/init.d.ts +28 -0
- package/dist/cli/commands/init.js +91 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/port.js +10 -2
- package/dist/cli/commands/port.js.map +1 -1
- package/dist/cli/commands/restart.js +5 -2
- package/dist/cli/commands/restart.js.map +1 -1
- package/dist/cli/commands/start.js +25 -22
- package/dist/cli/commands/start.js.map +1 -1
- package/dist/cli/commands/status.js +1 -0
- package/dist/cli/commands/status.js.map +1 -1
- package/dist/cli/commands/stop.js +1 -0
- package/dist/cli/commands/stop.js.map +1 -1
- package/dist/cli/config/bundled-docs.d.ts +20 -0
- package/dist/cli/config/bundled-docs.js +91 -0
- package/dist/cli/config/bundled-docs.js.map +1 -0
- package/dist/cli/config/init-config.d.ts +36 -0
- package/dist/cli/config/init-config.js +180 -0
- package/dist/cli/config/init-config.js.map +1 -0
- package/dist/cli/config/init-provider-catalog.d.ts +8 -0
- package/dist/cli/config/init-provider-catalog.js +187 -0
- package/dist/cli/config/init-provider-catalog.js.map +1 -0
- package/dist/cli/register/init-command.d.ts +3 -0
- package/dist/cli/register/init-command.js +5 -0
- package/dist/cli/register/init-command.js.map +1 -0
- package/dist/cli.js +28 -3
- package/dist/cli.js.map +1 -1
- package/dist/client/gemini-cli/gemini-cli-protocol-client.js +1 -1
- package/dist/client/gemini-cli/gemini-cli-protocol-client.js.map +1 -1
- package/dist/config/risk-control-config.d.ts +94 -0
- package/dist/config/risk-control-config.js +196 -0
- package/dist/config/risk-control-config.js.map +1 -0
- package/dist/constants/index.d.ts +6 -0
- package/dist/constants/index.js +13 -0
- package/dist/constants/index.js.map +1 -1
- package/dist/docs/daemon-admin-ui.html +2113 -190
- package/dist/index.js +0 -1
- package/dist/index.js.map +1 -1
- package/dist/manager/modules/health/index.d.ts +1 -1
- package/dist/manager/modules/quota/antigravity-quota-manager.d.ts +70 -0
- package/dist/manager/modules/quota/antigravity-quota-manager.js +442 -0
- package/dist/manager/modules/quota/antigravity-quota-manager.js.map +1 -0
- package/dist/manager/modules/quota/index.d.ts +3 -127
- package/dist/manager/modules/quota/index.js +2 -1093
- package/dist/manager/modules/quota/index.js.map +1 -1
- package/dist/manager/modules/quota/provider-key-normalization.d.ts +3 -0
- package/dist/manager/modules/quota/provider-key-normalization.js +155 -0
- package/dist/manager/modules/quota/provider-key-normalization.js.map +1 -0
- package/dist/manager/modules/quota/provider-quota-daemon.cooldown.d.ts +9 -0
- package/dist/manager/modules/quota/provider-quota-daemon.cooldown.js +115 -0
- package/dist/manager/modules/quota/provider-quota-daemon.cooldown.js.map +1 -0
- package/dist/manager/modules/quota/provider-quota-daemon.d.ts +77 -0
- package/dist/manager/modules/quota/provider-quota-daemon.events.d.ts +12 -0
- package/dist/manager/modules/quota/provider-quota-daemon.events.js +237 -0
- package/dist/manager/modules/quota/provider-quota-daemon.events.js.map +1 -0
- package/dist/manager/modules/quota/provider-quota-daemon.js +404 -0
- package/dist/manager/modules/quota/provider-quota-daemon.js.map +1 -0
- package/dist/manager/modules/quota/provider-quota-daemon.model-backoff.d.ts +11 -0
- package/dist/manager/modules/quota/provider-quota-daemon.model-backoff.js +189 -0
- package/dist/manager/modules/quota/provider-quota-daemon.model-backoff.js.map +1 -0
- package/dist/manager/modules/quota/provider-quota-daemon.snapshot.d.ts +8 -0
- package/dist/manager/modules/quota/provider-quota-daemon.snapshot.js +96 -0
- package/dist/manager/modules/quota/provider-quota-daemon.snapshot.js.map +1 -0
- package/dist/manager/modules/quota/provider-quota-daemon.view.d.ts +19 -0
- package/dist/manager/modules/quota/provider-quota-daemon.view.js +37 -0
- package/dist/manager/modules/quota/provider-quota-daemon.view.js.map +1 -0
- package/dist/manager/modules/routing/index.d.ts +1 -0
- package/dist/manager/modules/routing/index.js +11 -25
- package/dist/manager/modules/routing/index.js.map +1 -1
- package/dist/manager/quota/provider-quota-center.d.ts +2 -0
- package/dist/manager/quota/provider-quota-center.js +80 -82
- package/dist/manager/quota/provider-quota-center.js.map +1 -1
- package/dist/modules/llmswitch/bridge.d.ts +16 -18
- package/dist/modules/llmswitch/bridge.js +293 -94
- package/dist/modules/llmswitch/bridge.js.map +1 -1
- package/dist/modules/llmswitch/core-loader.d.ts +4 -2
- package/dist/modules/llmswitch/core-loader.js +32 -20
- package/dist/modules/llmswitch/core-loader.js.map +1 -1
- package/dist/modules/pipeline/utils/colored-logger.js +3 -2
- package/dist/modules/pipeline/utils/colored-logger.js.map +1 -1
- package/dist/modules/pipeline/utils/debug-logger.js +1 -1
- package/dist/modules/pipeline/utils/debug-logger.js.map +1 -1
- package/dist/providers/auth/iflow-cookie-auth.js +0 -2
- package/dist/providers/auth/iflow-cookie-auth.js.map +1 -1
- package/dist/providers/auth/oauth-lifecycle.js +2 -23
- package/dist/providers/auth/oauth-lifecycle.js.map +1 -1
- package/dist/providers/core/config/camoufox-launcher.js +35 -4
- package/dist/providers/core/config/camoufox-launcher.js.map +1 -1
- package/dist/providers/core/runtime/antigravity-quota-client.js +6 -3
- package/dist/providers/core/runtime/antigravity-quota-client.js.map +1 -1
- package/dist/providers/core/runtime/base-provider.d.ts +2 -2
- package/dist/providers/core/runtime/base-provider.js +74 -69
- package/dist/providers/core/runtime/base-provider.js.map +1 -1
- package/dist/providers/core/runtime/gemini-cli-http-provider.js +6 -4
- package/dist/providers/core/runtime/gemini-cli-http-provider.js.map +1 -1
- package/dist/providers/core/runtime/http-request-executor.js +2 -2
- package/dist/providers/core/runtime/http-request-executor.js.map +1 -1
- package/dist/providers/core/runtime/http-transport-provider.d.ts +14 -0
- package/dist/providers/core/runtime/http-transport-provider.js +111 -5
- package/dist/providers/core/runtime/http-transport-provider.js.map +1 -1
- package/dist/providers/core/runtime/provider-error-classifier.js +10 -0
- package/dist/providers/core/runtime/provider-error-classifier.js.map +1 -1
- package/dist/providers/core/runtime/provider-factory.js +7 -5
- package/dist/providers/core/runtime/provider-factory.js.map +1 -1
- package/dist/providers/core/runtime/provider-runtime-metadata.d.ts +6 -0
- package/dist/providers/core/runtime/provider-runtime-metadata.js.map +1 -1
- package/dist/providers/core/runtime/responses-provider.d.ts +1 -7
- package/dist/providers/core/runtime/responses-provider.js +12 -93
- package/dist/providers/core/runtime/responses-provider.js.map +1 -1
- package/dist/providers/core/strategies/oauth-auth-code-flow.js +12 -8
- package/dist/providers/core/strategies/oauth-auth-code-flow.js.map +1 -1
- package/dist/providers/core/utils/http-client.js +16 -3
- package/dist/providers/core/utils/http-client.js.map +1 -1
- package/dist/providers/core/utils/provider-error-logger.d.ts +1 -1
- package/dist/providers/core/utils/provider-error-reporter.d.ts +3 -1
- package/dist/providers/core/utils/provider-error-reporter.js +3 -0
- package/dist/providers/core/utils/provider-error-reporter.js.map +1 -1
- package/dist/providers/core/utils/snapshot-writer.js +1 -4
- package/dist/providers/core/utils/snapshot-writer.js.map +1 -1
- package/dist/providers/mock/mock-provider-runtime.js +57 -27
- package/dist/providers/mock/mock-provider-runtime.js.map +1 -1
- package/dist/scripts/camoufox/launch-auth.mjs +193 -58
- package/dist/server/handlers/handler-utils.js +3 -2
- package/dist/server/handlers/handler-utils.js.map +1 -1
- package/dist/server/runtime/http-server/daemon-admin/auth-handler.d.ts +2 -0
- package/dist/server/runtime/http-server/daemon-admin/auth-handler.js +103 -0
- package/dist/server/runtime/http-server/daemon-admin/auth-handler.js.map +1 -0
- package/dist/server/runtime/http-server/daemon-admin/auth-session.d.ts +5 -0
- package/dist/server/runtime/http-server/daemon-admin/auth-session.js +77 -0
- package/dist/server/runtime/http-server/daemon-admin/auth-session.js.map +1 -0
- package/dist/server/runtime/http-server/daemon-admin/auth-store.d.ts +18 -0
- package/dist/server/runtime/http-server/daemon-admin/auth-store.js +89 -0
- package/dist/server/runtime/http-server/daemon-admin/auth-store.js.map +1 -0
- package/dist/server/runtime/http-server/daemon-admin/credentials-handler.js +1 -2
- 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 +226 -24
- 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 +47 -8
- package/dist/server/runtime/http-server/daemon-admin/quota-handler.js.map +1 -1
- package/dist/server/runtime/http-server/daemon-admin/restart-handler.js +1 -1
- package/dist/server/runtime/http-server/daemon-admin/restart-handler.js.map +1 -1
- package/dist/server/runtime/http-server/daemon-admin/stats-handler.js +1 -1
- package/dist/server/runtime/http-server/daemon-admin/stats-handler.js.map +1 -1
- package/dist/server/runtime/http-server/daemon-admin/status-handler.js +68 -4
- 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 +3 -4
- package/dist/server/runtime/http-server/daemon-admin-routes.js +9 -14
- package/dist/server/runtime/http-server/daemon-admin-routes.js.map +1 -1
- package/dist/server/runtime/http-server/executor-metadata.js +1 -1
- package/dist/server/runtime/http-server/executor-metadata.js.map +1 -1
- package/dist/server/runtime/http-server/executor-response.js +0 -16
- package/dist/server/runtime/http-server/executor-response.js.map +1 -1
- package/dist/server/runtime/http-server/hub-shadow-compare.js +110 -34
- package/dist/server/runtime/http-server/hub-shadow-compare.js.map +1 -1
- package/dist/server/runtime/http-server/index.d.ts +5 -3
- package/dist/server/runtime/http-server/index.js +215 -109
- package/dist/server/runtime/http-server/index.js.map +1 -1
- package/dist/server/runtime/http-server/middleware.js +19 -1
- package/dist/server/runtime/http-server/middleware.js.map +1 -1
- package/dist/server/runtime/http-server/request-executor.js +10 -19
- package/dist/server/runtime/http-server/request-executor.js.map +1 -1
- package/dist/server/runtime/http-server/routes.js +8 -2
- package/dist/server/runtime/http-server/routes.js.map +1 -1
- package/dist/server/runtime/http-server/session-dir.d.ts +2 -0
- package/dist/server/runtime/http-server/session-dir.js +59 -0
- package/dist/server/runtime/http-server/session-dir.js.map +1 -0
- package/dist/server/runtime/http-server/types.d.ts +0 -4
- package/dist/server/utils/utf8-chunk-buffer.js +6 -3
- package/dist/server/utils/utf8-chunk-buffer.js.map +1 -1
- package/dist/server/utils/warmup-storm-tracker.js +1 -1
- package/dist/server/utils/warmup-storm-tracker.js.map +1 -1
- package/dist/server-factory.d.ts +6 -28
- package/dist/server-factory.js +8 -93
- package/dist/server-factory.js.map +1 -1
- package/dist/token-daemon/index.js +2 -2
- package/dist/token-daemon/index.js.map +1 -1
- package/dist/token-daemon/provider-registry.js +0 -1
- package/dist/token-daemon/provider-registry.js.map +1 -1
- package/dist/token-daemon/server-utils.js +8 -9
- package/dist/token-daemon/server-utils.js.map +1 -1
- package/dist/token-daemon/token-utils.js +1 -1
- package/dist/token-daemon/token-utils.js.map +1 -1
- package/dist/tools/semantic-replay.js +2 -2
- package/dist/tools/semantic-replay.js.map +1 -1
- package/dist/tools/stats-request-events.d.ts +1 -1
- package/dist/tools/stats-usage.js +6 -3
- package/dist/tools/stats-usage.js.map +1 -1
- package/dist/utils/llms-engine-shadow.d.ts +19 -0
- package/dist/utils/llms-engine-shadow.js +209 -0
- package/dist/utils/llms-engine-shadow.js.map +1 -0
- package/dist/utils/runtime-versions.js +2 -1
- package/dist/utils/runtime-versions.js.map +1 -1
- package/docs/ARCHITECTURE.md +402 -0
- package/docs/CODEX_AND_CLAUDE_CODE.md +69 -0
- package/docs/CONFIG_ARCHITECTURE.md +517 -0
- package/docs/ERROR_HANDLING_AUDIT.md +0 -0
- package/docs/GCLI2API_PARITY_GAPS.md +98 -0
- package/docs/INSTALLATION_AND_QUICKSTART.md +74 -0
- package/docs/INSTRUCTION_MARKUP.md +89 -0
- package/docs/MODULE_ENHANCEMENT_SYSTEM.md +666 -0
- package/docs/PORTS.md +36 -0
- package/docs/PROVIDERS_BUILTIN.md +111 -0
- package/docs/PROVIDER_TYPES.md +55 -0
- package/docs/SERVERTOOL_CLOCK_DESIGN.md +233 -0
- package/docs/USAGE_HANDLING_ANALYSIS.md +335 -0
- package/docs/USER_CONFIG_PARSER_CHANGES.md +175 -0
- package/docs/V3_INBOUND_OUTBOUND_DESIGN.md +86 -0
- package/docs/VIRTUAL_ROUTER_PRIORITY_AND_HEALTH.md +125 -0
- package/docs/anthropic-request-golden-samples.md +50 -0
- package/docs/ccr-alignment-enhancetool.md +105 -0
- package/docs/chat-glm-500-analysis.md +79 -0
- package/docs/chat-request-golden-samples.md +42 -0
- package/docs/chat-semantic-expansion-plan.md +82 -0
- package/docs/cli-command-inventory.md +76 -0
- package/docs/codex-samples-replay.md +50 -0
- package/docs/daemon-admin-api-design.md +350 -0
- package/docs/daemon-admin-module-structure.md +169 -0
- package/docs/daemon-admin-ui.html +3394 -0
- package/docs/debug-system-design.md +734 -0
- package/docs/debugging/gemini-sse-root-cause.md +52 -0
- package/docs/debugging/sse_encoding_failure_analysis.md +53 -0
- package/docs/dry-run/README.md +721 -0
- package/docs/error-handling-v2.md +92 -0
- package/docs/exec-command-guard-policy.example.v1.json +42 -0
- package/docs/fixes/gemini-protocol-mapping.md +57 -0
- package/docs/fixes/oauth-portal-timing-fix.md +202 -0
- package/docs/fixes/web-search-hop3-fix.md +265 -0
- package/docs/glm-api-reference.md +390 -0
- package/docs/glm-chat-completions.md +1779 -0
- package/docs/glm-history-inline-images.md +44 -0
- package/docs/golden-ci-library.md +66 -0
- package/docs/lmstudio-dry-run-summary.md +203 -0
- package/docs/lmstudio-tool-calling.md +214 -0
- package/docs/mapping-tables/anthropic-to-openai.json +290 -0
- package/docs/mapping-tables/iflow-to-openai.json +215 -0
- package/docs/mapping-tables/openai-passthrough.json +190 -0
- package/docs/mapping-tables/openai-to-iflow.json +227 -0
- package/docs/monitoring/Design.md +61 -0
- package/docs/multi-token-auth-guide.md +66 -0
- package/docs/oauth-authentication-guide.md +168 -0
- package/docs/oauth-iflow-implementation.md +153 -0
- package/docs/pipeline-routing-report.md +209 -0
- package/docs/plans/manager-daemon/PLAN.md +86 -0
- package/docs/plans/provider-config-v2-plan.md +176 -0
- package/docs/plans/provider-runtime-manager-plan.md +209 -0
- package/docs/plans/transparent-429-failover.md +89 -0
- package/docs/plans/unified-hub-framework-v1.md +245 -0
- package/docs/provider-config-v2-ui-design.md +181 -0
- package/docs/provider-quota-design.md +129 -0
- package/docs/providers/gemini-provider.md +62 -0
- package/docs/providers/lmstudio-v2-migration-report.md +102 -0
- package/docs/providers/provider-composite-design.md +142 -0
- package/docs/providers/provider-composite-testing.md +98 -0
- package/docs/providers/provider-type-only-migration.md +111 -0
- package/docs/rccx-wasm-migration.md +74 -0
- package/docs/refactoring/architecture-comparison-diagram.md +140 -0
- package/docs/refactoring/compatibility-v2-architecture-design.md +738 -0
- package/docs/refactoring/workflow-compatibility-refactoring-design.md +361 -0
- package/docs/reports/routing-classification-report.json +24 -0
- package/docs/reports/routing-classification-report.md +18 -0
- package/docs/reports/thinking-keywords-report.json +19 -0
- package/docs/responses/README.md +156 -0
- package/docs/responses-generic-provider.md +86 -0
- package/docs/responses-passthrough-provider-design.md +202 -0
- package/docs/routing-awrr-health-weighted-round-robin.md +179 -0
- package/docs/routing-instructions.md +393 -0
- package/docs/stop-message-auto.md +225 -0
- package/docs/streaming-flow.html +30 -0
- package/docs/streaming-flow.md +182 -0
- package/docs/token-daemon-preview.html +490 -0
- package/docs/token-refresh-daemon-plan.md +269 -0
- package/docs/transformation-tables/Gemini-FinishReason/345/256/214/346/225/264/350/275/254/346/215/242/350/241/250.json +233 -0
- package/docs/transformation-tables/README.md +225 -0
- package/docs/transformation-tables/claude-code-router-anthropic-to-gemini.json +283 -0
- package/docs/transformation-tables/claude-code-router-anthropic-to-openai.json +208 -0
- package/docs/transformation-tables/claude-code-router-openai-to-anthropic.json +261 -0
- package/docs/transformation-tables/claude-code-router-openai-to-gemini.json +208 -0
- package/docs/transformation-tables/claude-code-router-openai-to-lmstudio.json +182 -0
- package/docs/transformation-tables/claude-code-router-openai-to-ollama.json +250 -0
- package/docs/transformation-tables/claude-code-router-openai-to-textgenwebui.json +295 -0
- package/docs/transformation-tables/claude-code-router-provider-conversions.json +193 -0
- package/docs/transformation-tables//345/256/214/346/225/264/347/232/204/345/267/245/345/205/267/346/211/247/350/241/214/346/265/201/347/250/213/350/275/254/346/215/242/350/241/250.json +299 -0
- package/docs/transformation-tables//345/257/271/350/257/235/345/216/206/345/217/262/347/273/264/346/212/244/345/210/206/346/236/220.md +134 -0
- package/docs/transformation-tables//345/267/245/345/205/267/350/260/203/347/224/250/346/250/241/345/274/217/345/210/206/346/236/220.md +158 -0
- package/docs/transformation-tables//347/212/266/346/200/201/347/256/241/347/220/206/351/234/200/346/261/202/345/210/206/346/236/220.md +175 -0
- package/docs/transformation-tables//351/235/231/346/200/201/350/241/250vs/345/212/250/346/200/201/345/210/206/346/236/220.md +189 -0
- package/docs/transformation-tables//351/235/231/346/200/201/350/241/250/345/207/206/347/241/256/346/200/247/350/257/204/344/274/260.md +179 -0
- package/docs/transformation-tables//351/235/236/346/265/201/345/274/217/345/234/272/346/231/257/345/210/206/346/236/220.md +189 -0
- package/docs/v2-architecture/IMPLEMENTATION-ROADMAP.md +367 -0
- package/docs/v2-architecture/OPTIMIZED-DESIGN.md +827 -0
- package/docs/v2-architecture/PRERUN-CONNECTION-DESIGN.md +716 -0
- package/docs/v2-architecture/README.md +551 -0
- package/docs/verification/modelscope-verify.md +59 -0
- package/docs/web-search-service-design.md +322 -0
- package/package.json +12 -7
- package/scripts/camoufox/launch-auth.mjs +193 -58
- package/scripts/monitor-diff.mjs +126 -0
- package/scripts/pack-mode.mjs +19 -1
- package/scripts/pack-rcc.mjs +63 -0
- package/scripts/unified-hub-shadow-compare.mjs +33 -13
- package/scripts/verify-e2e-toolcall.mjs +115 -26
- package/dist/modules/llmswitch/pipeline-registry.d.ts +0 -57
- package/dist/modules/llmswitch/pipeline-registry.js +0 -229
- package/dist/modules/llmswitch/pipeline-registry.js.map +0 -1
- package/dist/server/RouteCodexServer.d.ts +0 -13
- package/dist/server/RouteCodexServer.js +0 -25
- package/dist/server/RouteCodexServer.js.map +0 -1
- package/dist/v2/conversion/hub/snapshot-recorder.d.ts +0 -12
- package/dist/v2/conversion/hub/snapshot-recorder.js +0 -22
- package/dist/v2/conversion/hub/snapshot-recorder.js.map +0 -1
|
@@ -0,0 +1,3394 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<title>RouteCodex Daemon Admin</title>
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
--bg: #070a14;
|
|
10
|
+
--panel: rgba(255, 255, 255, 0.05);
|
|
11
|
+
--panel-2: rgba(255, 255, 255, 0.03);
|
|
12
|
+
--border: rgba(255, 255, 255, 0.08);
|
|
13
|
+
--text: rgba(255, 255, 255, 0.92);
|
|
14
|
+
--muted: rgba(255, 255, 255, 0.62);
|
|
15
|
+
--accent: #4ea1ff;
|
|
16
|
+
--ok: #4cd964;
|
|
17
|
+
--warn: #ffb547;
|
|
18
|
+
--err: #ff5f5f;
|
|
19
|
+
--radius: 12px;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
* {
|
|
23
|
+
box-sizing: border-box;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
body {
|
|
27
|
+
margin: 0;
|
|
28
|
+
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
|
|
29
|
+
sans-serif;
|
|
30
|
+
background: radial-gradient(circle at 25% 0, #131a36 0, var(--bg) 50%, #03040a 100%);
|
|
31
|
+
color: var(--text);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.container {
|
|
35
|
+
max-width: 1680px;
|
|
36
|
+
margin: 22px auto 40px;
|
|
37
|
+
padding: 0 16px;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.card {
|
|
41
|
+
border: 1px solid var(--border);
|
|
42
|
+
background: linear-gradient(180deg, var(--panel), var(--panel-2));
|
|
43
|
+
border-radius: var(--radius);
|
|
44
|
+
padding: 14px;
|
|
45
|
+
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.35);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
header {
|
|
49
|
+
display: flex;
|
|
50
|
+
align-items: center;
|
|
51
|
+
justify-content: space-between;
|
|
52
|
+
gap: 14px;
|
|
53
|
+
flex-wrap: wrap;
|
|
54
|
+
margin-bottom: 14px;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.title h1 {
|
|
58
|
+
margin: 0;
|
|
59
|
+
font-size: 16px;
|
|
60
|
+
font-weight: 650;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.title p {
|
|
64
|
+
margin: 3px 0 0;
|
|
65
|
+
font-size: 12px;
|
|
66
|
+
color: var(--muted);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.statusline {
|
|
70
|
+
display: flex;
|
|
71
|
+
align-items: center;
|
|
72
|
+
gap: 10px;
|
|
73
|
+
flex-wrap: wrap;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.pill {
|
|
77
|
+
display: inline-flex;
|
|
78
|
+
align-items: center;
|
|
79
|
+
gap: 8px;
|
|
80
|
+
border: 1px solid var(--border);
|
|
81
|
+
background: rgba(255, 255, 255, 0.03);
|
|
82
|
+
border-radius: 999px;
|
|
83
|
+
padding: 4px 10px;
|
|
84
|
+
font-size: 12px;
|
|
85
|
+
color: var(--muted);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.toast {
|
|
89
|
+
position: fixed;
|
|
90
|
+
right: 14px;
|
|
91
|
+
bottom: 14px;
|
|
92
|
+
max-width: 520px;
|
|
93
|
+
padding: 10px 12px;
|
|
94
|
+
border-radius: 10px;
|
|
95
|
+
border: 1px solid var(--border);
|
|
96
|
+
background: rgba(20, 20, 20, 0.92);
|
|
97
|
+
color: var(--fg);
|
|
98
|
+
box-shadow: 0 12px 34px rgba(0,0,0,0.45);
|
|
99
|
+
z-index: 99999;
|
|
100
|
+
font-size: 13px;
|
|
101
|
+
line-height: 1.35;
|
|
102
|
+
display: none;
|
|
103
|
+
white-space: pre-wrap;
|
|
104
|
+
word-break: break-word;
|
|
105
|
+
}
|
|
106
|
+
.toast.show { display: block; }
|
|
107
|
+
.toast.ok { border-color: rgba(38, 200, 120, 0.45); }
|
|
108
|
+
.toast.err { border-color: rgba(255, 90, 90, 0.55); }
|
|
109
|
+
|
|
110
|
+
.kv {
|
|
111
|
+
border: 1px solid rgba(255, 255, 255, 0.10);
|
|
112
|
+
border-radius: 12px;
|
|
113
|
+
padding: 8px 10px;
|
|
114
|
+
background: rgba(0, 0, 0, 0.18);
|
|
115
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
|
116
|
+
font-size: 12px;
|
|
117
|
+
line-height: 1.45;
|
|
118
|
+
color: rgba(255, 255, 255, 0.86);
|
|
119
|
+
overflow: visible;
|
|
120
|
+
max-height: none;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.kv details {
|
|
124
|
+
border-left: 1px solid rgba(255, 255, 255, 0.10);
|
|
125
|
+
margin-left: 10px;
|
|
126
|
+
padding-left: 10px;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.kv summary {
|
|
130
|
+
cursor: pointer;
|
|
131
|
+
list-style: none;
|
|
132
|
+
user-select: none;
|
|
133
|
+
display: flex;
|
|
134
|
+
gap: 10px;
|
|
135
|
+
align-items: baseline;
|
|
136
|
+
padding: 2px 0;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.kv summary::-webkit-details-marker { display: none; }
|
|
140
|
+
|
|
141
|
+
.kv .kv-key {
|
|
142
|
+
color: rgba(255, 255, 255, 0.92);
|
|
143
|
+
min-width: 180px;
|
|
144
|
+
max-width: 520px;
|
|
145
|
+
overflow: hidden;
|
|
146
|
+
text-overflow: ellipsis;
|
|
147
|
+
white-space: nowrap;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.kv .kv-meta {
|
|
151
|
+
color: rgba(255, 255, 255, 0.55);
|
|
152
|
+
flex: 1;
|
|
153
|
+
overflow: hidden;
|
|
154
|
+
text-overflow: ellipsis;
|
|
155
|
+
white-space: nowrap;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.kv .kv-leaf {
|
|
159
|
+
display: flex;
|
|
160
|
+
gap: 10px;
|
|
161
|
+
padding: 2px 0;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.kv .kv-val {
|
|
165
|
+
color: rgba(255, 255, 255, 0.78);
|
|
166
|
+
flex: 1;
|
|
167
|
+
overflow: hidden;
|
|
168
|
+
text-overflow: ellipsis;
|
|
169
|
+
white-space: nowrap;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.kv-tools {
|
|
173
|
+
display: flex;
|
|
174
|
+
gap: 8px;
|
|
175
|
+
flex-wrap: wrap;
|
|
176
|
+
margin: 0 0 8px;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.kv-editor .kv-leaf,
|
|
180
|
+
.kv-editor summary {
|
|
181
|
+
align-items: center;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.kv-editor .kv-type {
|
|
185
|
+
color: rgba(255, 255, 255, 0.55);
|
|
186
|
+
min-width: 92px;
|
|
187
|
+
max-width: 140px;
|
|
188
|
+
overflow: hidden;
|
|
189
|
+
text-overflow: ellipsis;
|
|
190
|
+
white-space: nowrap;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.kv-editor .kv-actions {
|
|
194
|
+
display: inline-flex;
|
|
195
|
+
gap: 6px;
|
|
196
|
+
margin-left: auto;
|
|
197
|
+
flex: 0 0 auto;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.kv-editor .kv-actions button {
|
|
201
|
+
padding: 4px 8px;
|
|
202
|
+
border-radius: 8px;
|
|
203
|
+
font-size: 12px;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.kv-editor input[type="text"],
|
|
207
|
+
.kv-editor input[type="number"],
|
|
208
|
+
.kv-editor select {
|
|
209
|
+
padding: 4px 6px;
|
|
210
|
+
border-radius: 8px;
|
|
211
|
+
font-size: 12px;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.kv-editor .kv-path {
|
|
215
|
+
color: rgba(255, 255, 255, 0.42);
|
|
216
|
+
margin-left: 6px;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
#routingKvEditor.kv-editor .kv-val {
|
|
220
|
+
white-space: normal;
|
|
221
|
+
overflow: visible;
|
|
222
|
+
text-overflow: unset;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
#routingKvEditor.kv-editor .kv-actions .kv-path {
|
|
226
|
+
display: none;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
body.show-routing-paths #routingKvEditor.kv-editor .kv-actions .kv-path {
|
|
230
|
+
display: inline-flex;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
#routingKvEditor.kv-editor .kv-actions button.danger {
|
|
234
|
+
display: none;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
#routingKvEditor.kv-editor .kv-leaf:hover .kv-actions button.danger,
|
|
238
|
+
#routingKvEditor.kv-editor summary:hover .kv-actions button.danger {
|
|
239
|
+
display: inline-flex;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
#routingKvEditor .routing-target-actions {
|
|
243
|
+
margin-top: 6px;
|
|
244
|
+
display: flex;
|
|
245
|
+
flex-direction: column;
|
|
246
|
+
gap: 6px;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
#routingKvEditor .routing-target-row {
|
|
250
|
+
display: flex;
|
|
251
|
+
gap: 8px;
|
|
252
|
+
flex-wrap: wrap;
|
|
253
|
+
align-items: center;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
#routingKvEditor .routing-target-key {
|
|
257
|
+
padding: 2px 6px;
|
|
258
|
+
border: 1px solid rgba(255, 255, 255, 0.12);
|
|
259
|
+
border-radius: 999px;
|
|
260
|
+
background: rgba(255, 255, 255, 0.04);
|
|
261
|
+
max-width: 520px;
|
|
262
|
+
overflow: hidden;
|
|
263
|
+
text-overflow: ellipsis;
|
|
264
|
+
white-space: nowrap;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
#routingKvEditor .routing-target-meta {
|
|
268
|
+
color: rgba(255, 255, 255, 0.55);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
#routingKvEditor .routing-target-row button {
|
|
272
|
+
padding: 4px 8px;
|
|
273
|
+
border-radius: 8px;
|
|
274
|
+
font-size: 12px;
|
|
275
|
+
}
|
|
276
|
+
.dot {
|
|
277
|
+
width: 8px;
|
|
278
|
+
height: 8px;
|
|
279
|
+
border-radius: 999px;
|
|
280
|
+
background: var(--warn);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.dot.ok {
|
|
284
|
+
background: var(--ok);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.dot.err {
|
|
288
|
+
background: var(--err);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.row {
|
|
292
|
+
display: flex;
|
|
293
|
+
gap: 10px;
|
|
294
|
+
flex-wrap: wrap;
|
|
295
|
+
align-items: center;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
label {
|
|
299
|
+
font-size: 12px;
|
|
300
|
+
color: var(--muted);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
input[type="text"],
|
|
304
|
+
input[type="password"],
|
|
305
|
+
select,
|
|
306
|
+
textarea {
|
|
307
|
+
border: 1px solid var(--border);
|
|
308
|
+
background: rgba(0, 0, 0, 0.25);
|
|
309
|
+
color: var(--text);
|
|
310
|
+
border-radius: 10px;
|
|
311
|
+
padding: 8px 10px;
|
|
312
|
+
font-size: 12px;
|
|
313
|
+
outline: none;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
textarea {
|
|
317
|
+
width: 100%;
|
|
318
|
+
min-height: 220px;
|
|
319
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
|
|
320
|
+
"Courier New", monospace;
|
|
321
|
+
line-height: 1.45;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
button {
|
|
325
|
+
border: 1px solid var(--border);
|
|
326
|
+
background: rgba(255, 255, 255, 0.04);
|
|
327
|
+
color: var(--text);
|
|
328
|
+
border-radius: 10px;
|
|
329
|
+
padding: 8px 10px;
|
|
330
|
+
font-size: 12px;
|
|
331
|
+
cursor: pointer;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
button.primary {
|
|
335
|
+
border-color: rgba(78, 161, 255, 0.55);
|
|
336
|
+
background: rgba(78, 161, 255, 0.14);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
button.danger {
|
|
340
|
+
border-color: rgba(255, 95, 95, 0.55);
|
|
341
|
+
background: rgba(255, 95, 95, 0.14);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
.tabs {
|
|
345
|
+
display: flex;
|
|
346
|
+
gap: 6px;
|
|
347
|
+
margin: 14px 0 10px;
|
|
348
|
+
flex-wrap: wrap;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
.tab {
|
|
352
|
+
padding: 8px 12px;
|
|
353
|
+
border-radius: 999px;
|
|
354
|
+
border: 1px solid var(--border);
|
|
355
|
+
background: rgba(255, 255, 255, 0.02);
|
|
356
|
+
color: var(--muted);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
.tab.active {
|
|
360
|
+
color: var(--text);
|
|
361
|
+
border-color: rgba(78, 161, 255, 0.55);
|
|
362
|
+
background: rgba(78, 161, 255, 0.12);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
.grid {
|
|
366
|
+
display: grid;
|
|
367
|
+
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
|
368
|
+
gap: 12px;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
.grid.grid-wide-left {
|
|
372
|
+
grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
.grid.grid-wide-right {
|
|
376
|
+
grid-template-columns: minmax(0, 1fr) minmax(0, 1.6fr);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
.grid.grid-one {
|
|
380
|
+
grid-template-columns: minmax(0, 1fr);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/* Ensure grid children can shrink without overflowing into the next column */
|
|
384
|
+
.grid > .card {
|
|
385
|
+
min-width: 0;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
@media (max-width: 980px) {
|
|
389
|
+
.grid {
|
|
390
|
+
grid-template-columns: minmax(0, 1fr);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
.section-title {
|
|
395
|
+
font-size: 13px;
|
|
396
|
+
font-weight: 650;
|
|
397
|
+
margin: 0 0 6px;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
.section-sub {
|
|
401
|
+
margin: 0 0 10px;
|
|
402
|
+
font-size: 12px;
|
|
403
|
+
color: var(--muted);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
.table {
|
|
407
|
+
width: 100%;
|
|
408
|
+
border-collapse: collapse;
|
|
409
|
+
table-layout: fixed;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
.table th,
|
|
413
|
+
.table td {
|
|
414
|
+
padding: 8px 10px;
|
|
415
|
+
font-size: 12px;
|
|
416
|
+
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
|
417
|
+
vertical-align: top;
|
|
418
|
+
overflow-wrap: anywhere;
|
|
419
|
+
word-break: break-word;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
.table th {
|
|
423
|
+
text-align: left;
|
|
424
|
+
color: var(--muted);
|
|
425
|
+
font-weight: 600;
|
|
426
|
+
background: rgba(255, 255, 255, 0.03);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
.table.table-runtime th:nth-child(1),
|
|
430
|
+
.table.table-runtime td:nth-child(1) {
|
|
431
|
+
width: 22%;
|
|
432
|
+
}
|
|
433
|
+
.table.table-runtime th:nth-child(2),
|
|
434
|
+
.table.table-runtime td:nth-child(2) {
|
|
435
|
+
width: 19%;
|
|
436
|
+
}
|
|
437
|
+
.table.table-runtime th:nth-child(3),
|
|
438
|
+
.table.table-runtime td:nth-child(3) {
|
|
439
|
+
width: 9%;
|
|
440
|
+
}
|
|
441
|
+
.table.table-runtime th:nth-child(4),
|
|
442
|
+
.table.table-runtime td:nth-child(4) {
|
|
443
|
+
width: 12%;
|
|
444
|
+
}
|
|
445
|
+
.table.table-runtime th:nth-child(5),
|
|
446
|
+
.table.table-runtime td:nth-child(5) {
|
|
447
|
+
width: 12%;
|
|
448
|
+
}
|
|
449
|
+
.table.table-runtime th:nth-child(6),
|
|
450
|
+
.table.table-runtime td:nth-child(6) {
|
|
451
|
+
width: 7%;
|
|
452
|
+
}
|
|
453
|
+
.table.table-runtime th:nth-child(7),
|
|
454
|
+
.table.table-runtime td:nth-child(7) {
|
|
455
|
+
width: 11%;
|
|
456
|
+
}
|
|
457
|
+
.table.table-runtime th:nth-child(8),
|
|
458
|
+
.table.table-runtime td:nth-child(8) {
|
|
459
|
+
width: 8%;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
.table tr.group-row td {
|
|
463
|
+
background: rgba(255, 255, 255, 0.02);
|
|
464
|
+
color: rgba(255, 255, 255, 0.86);
|
|
465
|
+
font-weight: 650;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
.table tr.provider-row:hover td {
|
|
469
|
+
background: rgba(78, 161, 255, 0.06);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
.table tr.provider-row.selected td {
|
|
473
|
+
background: rgba(78, 161, 255, 0.12);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
.indent {
|
|
477
|
+
padding-left: 22px !important;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
.table-wrap {
|
|
481
|
+
width: 100%;
|
|
482
|
+
max-width: 100%;
|
|
483
|
+
overflow: visible;
|
|
484
|
+
border-radius: 12px;
|
|
485
|
+
border: 1px solid var(--border);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
.table-wrap .table {
|
|
489
|
+
border: 0;
|
|
490
|
+
min-width: 0;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
.table td.actions-cell {
|
|
494
|
+
width: 220px;
|
|
495
|
+
overflow: visible;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
.actions {
|
|
499
|
+
display: flex;
|
|
500
|
+
gap: 8px;
|
|
501
|
+
flex-wrap: wrap;
|
|
502
|
+
justify-content: flex-end;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
.truncate {
|
|
506
|
+
white-space: nowrap;
|
|
507
|
+
overflow: hidden;
|
|
508
|
+
text-overflow: ellipsis;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
.mono {
|
|
512
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
|
|
513
|
+
"Courier New", monospace;
|
|
514
|
+
color: var(--muted);
|
|
515
|
+
word-break: break-all;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
.pill {
|
|
519
|
+
display: inline-block;
|
|
520
|
+
padding: 2px 8px;
|
|
521
|
+
border-radius: 999px;
|
|
522
|
+
border: 1px solid rgba(255, 255, 255, 0.12);
|
|
523
|
+
background: rgba(255, 255, 255, 0.04);
|
|
524
|
+
font-size: 11px;
|
|
525
|
+
line-height: 1.4;
|
|
526
|
+
white-space: nowrap;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
.pill.ok {
|
|
530
|
+
border-color: rgba(52, 211, 153, 0.35);
|
|
531
|
+
background: rgba(52, 211, 153, 0.10);
|
|
532
|
+
color: rgba(210, 255, 236, 0.92);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
.pill.warn {
|
|
536
|
+
border-color: rgba(255, 181, 71, 0.35);
|
|
537
|
+
background: rgba(255, 181, 71, 0.10);
|
|
538
|
+
color: rgba(255, 236, 210, 0.92);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
.pill.bad {
|
|
542
|
+
border-color: rgba(239, 68, 68, 0.35);
|
|
543
|
+
background: rgba(239, 68, 68, 0.10);
|
|
544
|
+
color: rgba(255, 220, 220, 0.92);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
.muted {
|
|
548
|
+
color: var(--muted);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
.notice {
|
|
552
|
+
border: 1px solid rgba(255, 181, 71, 0.35);
|
|
553
|
+
background: rgba(255, 181, 71, 0.08);
|
|
554
|
+
border-radius: 12px;
|
|
555
|
+
padding: 10px 12px;
|
|
556
|
+
font-size: 12px;
|
|
557
|
+
color: rgba(255, 255, 255, 0.82);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
.log {
|
|
561
|
+
white-space: pre-wrap;
|
|
562
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
|
|
563
|
+
"Courier New", monospace;
|
|
564
|
+
font-size: 12px;
|
|
565
|
+
line-height: 1.45;
|
|
566
|
+
color: rgba(255, 255, 255, 0.84);
|
|
567
|
+
background: rgba(0, 0, 0, 0.25);
|
|
568
|
+
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
569
|
+
border-radius: 12px;
|
|
570
|
+
padding: 10px 12px;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
@media (max-width: 640px) {
|
|
574
|
+
header {
|
|
575
|
+
flex-direction: column;
|
|
576
|
+
align-items: flex-start;
|
|
577
|
+
}
|
|
578
|
+
.statusline {
|
|
579
|
+
width: 100%;
|
|
580
|
+
justify-content: flex-start;
|
|
581
|
+
}
|
|
582
|
+
.row {
|
|
583
|
+
align-items: stretch;
|
|
584
|
+
}
|
|
585
|
+
.row > input,
|
|
586
|
+
.row > select,
|
|
587
|
+
.row > button {
|
|
588
|
+
max-width: 100%;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
</style>
|
|
592
|
+
</head>
|
|
593
|
+
<body>
|
|
594
|
+
<div class="container">
|
|
595
|
+
<div class="card">
|
|
596
|
+
<header>
|
|
597
|
+
<div class="title">
|
|
598
|
+
<h1>RouteCodex Daemon Admin</h1>
|
|
599
|
+
<p>
|
|
600
|
+
Writes to <span class="mono">~/.routecodex/config.json</span>. This UI requires an admin password:
|
|
601
|
+
first visit will ask you to set one (stored at <span class="mono">~/.routecodex/login</span>), then you login with it.
|
|
602
|
+
</p>
|
|
603
|
+
</div>
|
|
604
|
+
<div class="statusline">
|
|
605
|
+
<div class="pill"><span id="statusDot" class="dot"></span><span id="statusText">connecting…</span></div>
|
|
606
|
+
<div class="pill"><span class="mono" id="serverId">serverId: —</span></div>
|
|
607
|
+
<button id="restartRuntimeBtn" class="primary">Restart runtime</button>
|
|
608
|
+
</div>
|
|
609
|
+
</header>
|
|
610
|
+
|
|
611
|
+
<div class="row" style="margin-bottom: 10px;">
|
|
612
|
+
<label for="adminPasswordInput">Admin password</label>
|
|
613
|
+
<input
|
|
614
|
+
id="adminPasswordInput"
|
|
615
|
+
type="password"
|
|
616
|
+
placeholder="set (first time) / login"
|
|
617
|
+
style="flex: 1; min-width: 260px;"
|
|
618
|
+
/>
|
|
619
|
+
<button id="setupPasswordBtn" class="primary">Set</button>
|
|
620
|
+
<button id="loginPasswordBtn" class="primary">Login</button>
|
|
621
|
+
<button id="logoutPasswordBtn">Logout</button>
|
|
622
|
+
<span class="mono" id="adminAuthHint"></span>
|
|
623
|
+
</div>
|
|
624
|
+
|
|
625
|
+
<details id="changePasswordDetails" style="margin: 0 0 10px; display:none;">
|
|
626
|
+
<summary class="muted" style="font-size:12px; cursor:pointer; user-select:none;">Change admin password</summary>
|
|
627
|
+
<div class="row" style="margin-top: 10px;">
|
|
628
|
+
<label for="oldAdminPasswordInput">old</label>
|
|
629
|
+
<input
|
|
630
|
+
id="oldAdminPasswordInput"
|
|
631
|
+
type="password"
|
|
632
|
+
placeholder="old password"
|
|
633
|
+
style="flex: 1; min-width: 240px;"
|
|
634
|
+
/>
|
|
635
|
+
<label for="newAdminPasswordInput">new</label>
|
|
636
|
+
<input
|
|
637
|
+
id="newAdminPasswordInput"
|
|
638
|
+
type="password"
|
|
639
|
+
placeholder="new password (8+ chars)"
|
|
640
|
+
style="flex: 1; min-width: 240px;"
|
|
641
|
+
/>
|
|
642
|
+
<button id="changePasswordBtn" class="primary">Change</button>
|
|
643
|
+
</div>
|
|
644
|
+
<div class="muted" style="font-size:12px; margin-top: 6px;">
|
|
645
|
+
Localhost-only. Requires current authenticated session.
|
|
646
|
+
</div>
|
|
647
|
+
</details>
|
|
648
|
+
|
|
649
|
+
<div class="row" style="margin-bottom: 10px;">
|
|
650
|
+
<label for="apiKeyInput">Server API key (optional, for /v1/* tests)</label>
|
|
651
|
+
<input
|
|
652
|
+
id="apiKeyInput"
|
|
653
|
+
type="password"
|
|
654
|
+
placeholder="x-api-key / Authorization: Bearer …"
|
|
655
|
+
style="flex: 1; min-width: 260px;"
|
|
656
|
+
/>
|
|
657
|
+
<button id="saveApiKeyBtn" class="primary">Save</button>
|
|
658
|
+
<button id="clearApiKeyBtn">Clear</button>
|
|
659
|
+
<span class="mono" id="apiKeyHint"></span>
|
|
660
|
+
</div>
|
|
661
|
+
|
|
662
|
+
<div class="tabs">
|
|
663
|
+
<button class="tab active" data-tab="providers">Provider Pool</button>
|
|
664
|
+
<button class="tab" data-tab="tokens">Token Stats</button>
|
|
665
|
+
<button class="tab" data-tab="credentials">Auth Provider Pool</button>
|
|
666
|
+
<button class="tab" data-tab="quota">Quota Pool</button>
|
|
667
|
+
<button class="tab" data-tab="routing">Runtime Routing Pool</button>
|
|
668
|
+
</div>
|
|
669
|
+
|
|
670
|
+
<section id="panelProviders" data-panel="providers">
|
|
671
|
+
<div class="grid grid-wide-left">
|
|
672
|
+
<div class="card" style="box-shadow: none;">
|
|
673
|
+
<p class="section-title">Providers in <span class="mono">virtualrouter.providers</span></p>
|
|
674
|
+
<p class="section-sub">
|
|
675
|
+
API keys are stored as authfiles and referenced with <span class="mono">authfile-…</span>. The UI never reads or returns secrets.
|
|
676
|
+
</p>
|
|
677
|
+
<div class="row" style="margin-bottom: 10px;">
|
|
678
|
+
<button id="refreshProvidersBtn" class="primary">Refresh</button>
|
|
679
|
+
<button id="newProviderBtn">New provider…</button>
|
|
680
|
+
</div>
|
|
681
|
+
<div class="table-wrap">
|
|
682
|
+
<table class="table">
|
|
683
|
+
<thead>
|
|
684
|
+
<tr>
|
|
685
|
+
<th>id</th>
|
|
686
|
+
<th>type</th>
|
|
687
|
+
<th>enabled</th>
|
|
688
|
+
<th>baseURL</th>
|
|
689
|
+
<th>models</th>
|
|
690
|
+
<th>compat</th>
|
|
691
|
+
<th>auth</th>
|
|
692
|
+
<th></th>
|
|
693
|
+
</tr>
|
|
694
|
+
</thead>
|
|
695
|
+
<tbody id="providersTbody"></tbody>
|
|
696
|
+
</table>
|
|
697
|
+
</div>
|
|
698
|
+
</div>
|
|
699
|
+
|
|
700
|
+
<div class="card" style="box-shadow: none;">
|
|
701
|
+
<p class="section-title" id="providerEditorTitle">Provider editor</p>
|
|
702
|
+
<p class="section-sub">
|
|
703
|
+
Edit providers as key/value entries to avoid JSON syntax errors. Save writes to disk and creates a backup.
|
|
704
|
+
</p>
|
|
705
|
+
|
|
706
|
+
<div class="notice" style="margin-bottom: 10px;">
|
|
707
|
+
Admin API calls use the password login above (cookie session). The optional server API key is only needed for testing proxy endpoints like <span class="mono">/v1/responses</span>.
|
|
708
|
+
</div>
|
|
709
|
+
|
|
710
|
+
<div class="row" style="margin-bottom: 10px;">
|
|
711
|
+
<label for="providerIdInput">provider id</label>
|
|
712
|
+
<input id="providerIdInput" type="text" placeholder="e.g. tab, glm, qwen" style="width: 240px;" />
|
|
713
|
+
<label for="providerPreset">preset</label>
|
|
714
|
+
<select id="providerPreset">
|
|
715
|
+
<option value="responses">responses (OpenAI /v1/responses)</option>
|
|
716
|
+
<option value="openai">openai (OpenAI /v1/chat/completions)</option>
|
|
717
|
+
<option value="openai-standard">openai-standard</option>
|
|
718
|
+
<option value="iflow">iflow (cookieFile)</option>
|
|
719
|
+
<option value="custom" selected>custom (start empty)</option>
|
|
720
|
+
</select>
|
|
721
|
+
</div>
|
|
722
|
+
|
|
723
|
+
<div class="row" style="margin-bottom: 10px;">
|
|
724
|
+
<label for="authMode">auth</label>
|
|
725
|
+
<select id="authMode">
|
|
726
|
+
<option value="apikey">apikey (store as authfile)</option>
|
|
727
|
+
<option value="oauth">oauth (alias-only + authorize in Auth tab)</option>
|
|
728
|
+
<option value="cookie">cookieFile</option>
|
|
729
|
+
<option value="none">none</option>
|
|
730
|
+
</select>
|
|
731
|
+
</div>
|
|
732
|
+
|
|
733
|
+
<div id="authApikeyBox" class="card" style="box-shadow:none; padding: 10px; margin-bottom: 10px;">
|
|
734
|
+
<p class="section-title" style="margin-bottom: 8px;">API key</p>
|
|
735
|
+
<div class="row" style="margin-bottom: 8px;">
|
|
736
|
+
<label for="apikeyAliasInput">alias</label>
|
|
737
|
+
<input id="apikeyAliasInput" type="text" placeholder="default" style="width: 220px;" />
|
|
738
|
+
<label for="apikeyValueInput">apiKey</label>
|
|
739
|
+
<input
|
|
740
|
+
id="apikeyValueInput"
|
|
741
|
+
type="password"
|
|
742
|
+
placeholder="will be written to ~/.routecodex/auth/*.key"
|
|
743
|
+
style="flex: 1; min-width: 260px;"
|
|
744
|
+
/>
|
|
745
|
+
</div>
|
|
746
|
+
<div class="row">
|
|
747
|
+
<span class="mono" id="apikeySecretRefOut"></span>
|
|
748
|
+
<button id="createApiKeyCredentialBtn">Create authfile</button>
|
|
749
|
+
</div>
|
|
750
|
+
</div>
|
|
751
|
+
|
|
752
|
+
<div id="authOauthBox" class="card" style="box-shadow:none; padding: 10px; margin-bottom: 10px; display:none;">
|
|
753
|
+
<p class="section-title" style="margin-bottom: 8px;">OAuth</p>
|
|
754
|
+
<p class="section-sub" style="margin-bottom: 8px;">
|
|
755
|
+
Use Auth tab to authorize. Here we only reference <span class="mono">tokenFile</span> as an alias.
|
|
756
|
+
</p>
|
|
757
|
+
<div class="row">
|
|
758
|
+
<label for="oauthTypeInput">auth.type</label>
|
|
759
|
+
<input
|
|
760
|
+
id="oauthTypeInput"
|
|
761
|
+
type="text"
|
|
762
|
+
placeholder="qwen-oauth / iflow-oauth / gemini-cli-oauth / antigravity-oauth"
|
|
763
|
+
style="flex: 1; min-width: 260px;"
|
|
764
|
+
/>
|
|
765
|
+
<label for="oauthAliasInput">tokenFile alias</label>
|
|
766
|
+
<input id="oauthAliasInput" type="text" placeholder="default" style="width: 240px;" />
|
|
767
|
+
</div>
|
|
768
|
+
</div>
|
|
769
|
+
|
|
770
|
+
<div id="authCookieBox" class="card" style="box-shadow:none; padding: 10px; margin-bottom: 10px; display:none;">
|
|
771
|
+
<p class="section-title" style="margin-bottom: 8px;">Cookie file</p>
|
|
772
|
+
<div class="row">
|
|
773
|
+
<label for="cookieFileInput">cookieFile</label>
|
|
774
|
+
<input
|
|
775
|
+
id="cookieFileInput"
|
|
776
|
+
type="text"
|
|
777
|
+
placeholder="~/.routecodex/auth/iflow-work.cookie"
|
|
778
|
+
style="flex: 1; min-width: 280px;"
|
|
779
|
+
/>
|
|
780
|
+
</div>
|
|
781
|
+
</div>
|
|
782
|
+
|
|
783
|
+
<p class="section-title" style="margin-top: 10px;">Provider config</p>
|
|
784
|
+
<p class="section-sub">Tree editor. Use “Apply preset” to populate common templates.</p>
|
|
785
|
+
<div class="kv-tools">
|
|
786
|
+
<button id="providerEditorExpandBtn">Expand all</button>
|
|
787
|
+
<button id="providerEditorCollapseBtn">Collapse all</button>
|
|
788
|
+
<span class="mono" id="providerEditorDirtyHint"></span>
|
|
789
|
+
</div>
|
|
790
|
+
<div id="providerKvEditor" class="kv kv-editor"></div>
|
|
791
|
+
|
|
792
|
+
<div class="row" style="margin-top: 10px;">
|
|
793
|
+
<button id="loadProviderBtn">Load</button>
|
|
794
|
+
<button id="applyPresetBtn">Apply preset</button>
|
|
795
|
+
<button id="saveProviderBtn" class="primary">Save</button>
|
|
796
|
+
<button id="deleteProviderBtn" class="danger">Delete</button>
|
|
797
|
+
</div>
|
|
798
|
+
|
|
799
|
+
<div id="providerOpLog" class="log" style="margin-top: 10px; display:none;"></div>
|
|
800
|
+
</div>
|
|
801
|
+
</div>
|
|
802
|
+
</section>
|
|
803
|
+
|
|
804
|
+
<section id="panelTokens" data-panel="tokens" style="display:none;">
|
|
805
|
+
<div class="grid">
|
|
806
|
+
<div class="card" style="box-shadow:none;">
|
|
807
|
+
<p class="section-title">Token usage (session + historical)</p>
|
|
808
|
+
<p class="section-sub">
|
|
809
|
+
Session stats reset on server restart. Historical totals are aggregated from
|
|
810
|
+
<span class="mono">~/.routecodex/logs/provider-stats.jsonl</span> (best-effort).
|
|
811
|
+
</p>
|
|
812
|
+
<div class="row" style="margin-bottom: 10px;">
|
|
813
|
+
<button id="refreshTokensBtn" class="primary">Refresh</button>
|
|
814
|
+
</div>
|
|
815
|
+
<div class="notice mono" id="tokenTotalsBox" style="white-space: pre-wrap;"></div>
|
|
816
|
+
<div class="table-wrap" style="margin-top: 10px;">
|
|
817
|
+
<table class="table">
|
|
818
|
+
<thead>
|
|
819
|
+
<tr>
|
|
820
|
+
<th>providerKey</th>
|
|
821
|
+
<th>model</th>
|
|
822
|
+
<th>session req/err</th>
|
|
823
|
+
<th>session tokens in/out/total</th>
|
|
824
|
+
<th>historical req/err</th>
|
|
825
|
+
<th>historical tokens in/out/total</th>
|
|
826
|
+
</tr>
|
|
827
|
+
</thead>
|
|
828
|
+
<tbody id="tokensTbody"></tbody>
|
|
829
|
+
</table>
|
|
830
|
+
</div>
|
|
831
|
+
</div>
|
|
832
|
+
</div>
|
|
833
|
+
</section>
|
|
834
|
+
|
|
835
|
+
<section id="panelCredentials" data-panel="credentials" style="display:none;">
|
|
836
|
+
<div class="grid">
|
|
837
|
+
<div class="card" style="box-shadow:none;">
|
|
838
|
+
<p class="section-title">Credentials</p>
|
|
839
|
+
<p class="section-sub">
|
|
840
|
+
Token files + API key authfiles in <span class="mono">~/.routecodex/auth</span>.
|
|
841
|
+
</p>
|
|
842
|
+
<div class="row" style="margin-bottom: 10px;">
|
|
843
|
+
<button id="refreshCredentialsBtn" class="primary">Refresh</button>
|
|
844
|
+
</div>
|
|
845
|
+
<div class="table-wrap">
|
|
846
|
+
<table class="table">
|
|
847
|
+
<thead>
|
|
848
|
+
<tr>
|
|
849
|
+
<th>kind</th>
|
|
850
|
+
<th>provider</th>
|
|
851
|
+
<th>alias</th>
|
|
852
|
+
<th>status</th>
|
|
853
|
+
<th>expires</th>
|
|
854
|
+
<th>secretRef</th>
|
|
855
|
+
</tr>
|
|
856
|
+
</thead>
|
|
857
|
+
<tbody id="credentialsTbody"></tbody>
|
|
858
|
+
</table>
|
|
859
|
+
</div>
|
|
860
|
+
</div>
|
|
861
|
+
|
|
862
|
+
<div class="card" style="box-shadow:none;">
|
|
863
|
+
<p class="section-title">OAuth / Browser settings</p>
|
|
864
|
+
<p class="section-sub">
|
|
865
|
+
Set <span class="mono">ROUTECODEX_OAUTH_BROWSER</span> via config so “Authorize” can auto-open with Camoufox.
|
|
866
|
+
</p>
|
|
867
|
+
<div class="row" style="margin-bottom: 10px;">
|
|
868
|
+
<label for="oauthBrowserSelect">oauthBrowser</label>
|
|
869
|
+
<select id="oauthBrowserSelect">
|
|
870
|
+
<option value="default">default</option>
|
|
871
|
+
<option value="camoufox">camoufox</option>
|
|
872
|
+
</select>
|
|
873
|
+
<button id="saveOauthBrowserBtn" class="primary">Save</button>
|
|
874
|
+
</div>
|
|
875
|
+
|
|
876
|
+
<p class="section-title" style="margin-top: 10px;">Authorize OAuth</p>
|
|
877
|
+
<div class="row" style="margin-bottom: 10px;">
|
|
878
|
+
<label for="oauthProviderSelect">provider</label>
|
|
879
|
+
<select id="oauthProviderSelect">
|
|
880
|
+
<option value="qwen">qwen</option>
|
|
881
|
+
<option value="iflow">iflow</option>
|
|
882
|
+
<option value="gemini-cli">gemini-cli</option>
|
|
883
|
+
<option value="antigravity">antigravity</option>
|
|
884
|
+
</select>
|
|
885
|
+
<label for="oauthAuthAliasInput">alias</label>
|
|
886
|
+
<input id="oauthAuthAliasInput" type="text" placeholder="default" style="width: 240px;" />
|
|
887
|
+
</div>
|
|
888
|
+
<div class="row" style="margin-bottom: 10px;">
|
|
889
|
+
<label><input id="oauthOpenBrowser" type="checkbox" checked /> open browser</label>
|
|
890
|
+
<label><input id="oauthForceReauth" type="checkbox" /> force reauthorize</label>
|
|
891
|
+
<button id="oauthAuthorizeBtn" class="primary">Authorize</button>
|
|
892
|
+
</div>
|
|
893
|
+
|
|
894
|
+
<div id="credentialOpLog" class="log" style="display:none;"></div>
|
|
895
|
+
</div>
|
|
896
|
+
</div>
|
|
897
|
+
</section>
|
|
898
|
+
|
|
899
|
+
<section id="panelQuota" data-panel="quota" style="display:none;">
|
|
900
|
+
<div class="grid">
|
|
901
|
+
<div class="card" style="box-shadow:none;">
|
|
902
|
+
<p class="section-title">Quota (daemon)</p>
|
|
903
|
+
<p class="section-sub">
|
|
904
|
+
VirtualRouter consumes this via <span class="mono">quotaView</span>. When
|
|
905
|
+
<span class="mono">inPool=false</span>, the provider is treated as removed from the route pool.
|
|
906
|
+
</p>
|
|
907
|
+
<div class="row" style="margin-bottom: 10px;">
|
|
908
|
+
<label for="quotaFilterInput">filter</label>
|
|
909
|
+
<input id="quotaFilterInput" type="text" placeholder="providerKey contains…" style="width: 320px;" />
|
|
910
|
+
<label><input id="quotaHideOkToggle" type="checkbox" /> hide ok</label>
|
|
911
|
+
<label><input id="quotaOnlyRoutedTargetsToggle" type="checkbox" checked /> only routed targets</label>
|
|
912
|
+
<span class="muted" style="font-size:12px;">Tip: click a row to fill the offline box.</span>
|
|
913
|
+
</div>
|
|
914
|
+
<div class="row" style="margin-bottom: 10px;">
|
|
915
|
+
<label for="quotaKeyInput">providerKey</label>
|
|
916
|
+
<input id="quotaKeyInput" type="text" placeholder="tab.key1.gpt-5.2-codex" style="width: 420px;" />
|
|
917
|
+
<label for="quotaModeSelect">offline mode</label>
|
|
918
|
+
<select id="quotaModeSelect" style="width: 140px;">
|
|
919
|
+
<option value="cooldown">cooldown</option>
|
|
920
|
+
<option value="blacklist">blacklist</option>
|
|
921
|
+
</select>
|
|
922
|
+
<label for="quotaDurationSelect">offline time</label>
|
|
923
|
+
<select id="quotaDurationSelect" style="width: 160px;">
|
|
924
|
+
<option value="5">5m</option>
|
|
925
|
+
<option value="15">15m</option>
|
|
926
|
+
<option value="30">30m</option>
|
|
927
|
+
<option value="60" selected>1h</option>
|
|
928
|
+
<option value="180">3h</option>
|
|
929
|
+
<option value="360">6h</option>
|
|
930
|
+
<option value="720">12h</option>
|
|
931
|
+
<option value="1440">24h</option>
|
|
932
|
+
</select>
|
|
933
|
+
<button id="quotaApplyDisableBtn" class="danger">Offline</button>
|
|
934
|
+
<button id="quotaApplyRecoverBtn">Recover</button>
|
|
935
|
+
<button id="quotaApplyResetBtn">Reset</button>
|
|
936
|
+
</div>
|
|
937
|
+
<div class="muted" style="font-size:12px; margin: -6px 0 10px;">
|
|
938
|
+
Offline removes the provider from the route pool for the selected minutes. Recover brings it back online immediately.
|
|
939
|
+
</div>
|
|
940
|
+
<div class="row" style="margin-bottom: 10px;">
|
|
941
|
+
<button id="refreshQuotaBtn" class="primary">Refresh provider pool</button>
|
|
942
|
+
<button id="refreshQuotaSnapshotBtn" class="primary">Refresh antigravity snapshot</button>
|
|
943
|
+
<button id="resetQuotaBtn" class="danger">Reset provider-quota module</button>
|
|
944
|
+
</div>
|
|
945
|
+
<div class="table-wrap">
|
|
946
|
+
<table class="table">
|
|
947
|
+
<thead>
|
|
948
|
+
<tr>
|
|
949
|
+
<th>provider</th>
|
|
950
|
+
<th>key</th>
|
|
951
|
+
<th>model</th>
|
|
952
|
+
<th>inPool</th>
|
|
953
|
+
<th>reason</th>
|
|
954
|
+
<th>until</th>
|
|
955
|
+
<th>errCount</th>
|
|
956
|
+
<th></th>
|
|
957
|
+
</tr>
|
|
958
|
+
</thead>
|
|
959
|
+
<tbody id="quotaTbody"></tbody>
|
|
960
|
+
</table>
|
|
961
|
+
</div>
|
|
962
|
+
<div id="quotaOpLog" class="log" style="margin-top: 10px; display:none;"></div>
|
|
963
|
+
</div>
|
|
964
|
+
|
|
965
|
+
<div class="card" style="box-shadow:none;">
|
|
966
|
+
<p class="section-title">Antigravity quota snapshot</p>
|
|
967
|
+
<p class="section-sub">
|
|
968
|
+
Snapshot fetched by <span class="mono">QuotaManagerModule</span> (antigravity quota API). Used to gate antigravity providers entering the route pool.
|
|
969
|
+
</p>
|
|
970
|
+
<div class="row" style="margin-bottom: 10px;">
|
|
971
|
+
<label><input id="quotaSnapshotOnlyRoutedToggle" type="checkbox" checked /> only routed models</label>
|
|
972
|
+
<span class="muted" style="font-size:12px;">(filters by current provider pool keys)</span>
|
|
973
|
+
</div>
|
|
974
|
+
<div class="table-wrap">
|
|
975
|
+
<table class="table">
|
|
976
|
+
<thead>
|
|
977
|
+
<tr>
|
|
978
|
+
<th>alias</th>
|
|
979
|
+
<th>model</th>
|
|
980
|
+
<th>remaining</th>
|
|
981
|
+
<th>resetAt</th>
|
|
982
|
+
<th>fetchedAt</th>
|
|
983
|
+
</tr>
|
|
984
|
+
</thead>
|
|
985
|
+
<tbody id="quotaSnapshotTbody"></tbody>
|
|
986
|
+
</table>
|
|
987
|
+
</div>
|
|
988
|
+
<div id="quotaSnapshotLog" class="log" style="margin-top: 10px; display:none;"></div>
|
|
989
|
+
</div>
|
|
990
|
+
|
|
991
|
+
<div class="card" style="box-shadow:none;">
|
|
992
|
+
<p class="section-title">Notes</p>
|
|
993
|
+
<div class="notice">
|
|
994
|
+
<div style="margin-bottom: 6px;">
|
|
995
|
+
Use this view to confirm 429/backoff/blacklist decisions and whether a provider is currently eligible.
|
|
996
|
+
</div>
|
|
997
|
+
<div>
|
|
998
|
+
If a provider looks stuck, try <span class="mono">Reset provider-quota module</span>, then <span class="mono">Restart runtime</span>.
|
|
999
|
+
</div>
|
|
1000
|
+
</div>
|
|
1001
|
+
</div>
|
|
1002
|
+
</div>
|
|
1003
|
+
</section>
|
|
1004
|
+
|
|
1005
|
+
<section id="panelRouting" data-panel="routing" style="display:none;">
|
|
1006
|
+
<div class="grid grid-one">
|
|
1007
|
+
<div class="card" style="box-shadow:none;">
|
|
1008
|
+
<p class="section-title">Routing editor</p>
|
|
1009
|
+
<p class="section-sub">
|
|
1010
|
+
Edits routing in a selected config file (auto-detects <span class="mono">virtualrouter.routing</span> or <span class="mono">routing</span>).
|
|
1011
|
+
</p>
|
|
1012
|
+
<div class="row" style="margin-bottom: 10px;">
|
|
1013
|
+
<label for="routingSourceSelect">source</label>
|
|
1014
|
+
<select id="routingSourceSelect" style="width: 420px;"></select>
|
|
1015
|
+
<button id="refreshRoutingSourcesBtn">Refresh sources</button>
|
|
1016
|
+
</div>
|
|
1017
|
+
<div class="row" style="margin-bottom: 10px;">
|
|
1018
|
+
<label for="routingQuotaModeSelect">mode</label>
|
|
1019
|
+
<select id="routingQuotaModeSelect" style="width: 160px;">
|
|
1020
|
+
<option value="cooldown" selected>cooldown</option>
|
|
1021
|
+
<option value="blacklist">blacklist</option>
|
|
1022
|
+
</select>
|
|
1023
|
+
<label for="routingQuotaDurationSelect">offline time</label>
|
|
1024
|
+
<select id="routingQuotaDurationSelect" style="width: 160px;">
|
|
1025
|
+
<option value="5">5m</option>
|
|
1026
|
+
<option value="15">15m</option>
|
|
1027
|
+
<option value="30">30m</option>
|
|
1028
|
+
<option value="60" selected>1h</option>
|
|
1029
|
+
<option value="180">3h</option>
|
|
1030
|
+
<option value="360">6h</option>
|
|
1031
|
+
<option value="720">12h</option>
|
|
1032
|
+
<option value="1440">24h</option>
|
|
1033
|
+
</select>
|
|
1034
|
+
</div>
|
|
1035
|
+
<div class="row" style="margin-bottom: 10px;">
|
|
1036
|
+
<button id="loadRoutingBtn" class="primary">Load</button>
|
|
1037
|
+
<button id="saveRoutingBtn" class="primary">Save</button>
|
|
1038
|
+
<button id="refreshRoutingPoolBtn">Refresh pool status</button>
|
|
1039
|
+
<button id="routingRegExpandBtn">Expand all</button>
|
|
1040
|
+
<button id="routingRegCollapseBtn">Collapse all</button>
|
|
1041
|
+
<label style="margin-left:auto;"><input id="routingShowPathsToggle" type="checkbox" /> show debug paths</label>
|
|
1042
|
+
</div>
|
|
1043
|
+
<div id="routingOpLog" class="log" style="margin-top: 10px; display:none;"></div>
|
|
1044
|
+
<div class="muted" style="font-size:12px; margin: -4px 0 10px;">
|
|
1045
|
+
Tip: edit routing here (CRUD). For tool targets, use the inline <span class="mono">Offline/Recover</span> controls to manage runtime provider keys.
|
|
1046
|
+
</div>
|
|
1047
|
+
<div id="routingKvEditor" class="kv kv-editor"></div>
|
|
1048
|
+
</div>
|
|
1049
|
+
</div>
|
|
1050
|
+
</section>
|
|
1051
|
+
</div>
|
|
1052
|
+
</div>
|
|
1053
|
+
|
|
1054
|
+
<div id="toast" class="toast" role="status" aria-live="polite"></div>
|
|
1055
|
+
|
|
1056
|
+
<script>
|
|
1057
|
+
const $ = (id) => document.getElementById(id);
|
|
1058
|
+
const UI = {
|
|
1059
|
+
selectedProviderId: "",
|
|
1060
|
+
lastUnauthorizedToastAt: 0,
|
|
1061
|
+
adminAuth: null,
|
|
1062
|
+
quotaProviders: [],
|
|
1063
|
+
quotaProvidersUpdatedAt: 0,
|
|
1064
|
+
quotaProviderMap: null,
|
|
1065
|
+
routingTargets: null,
|
|
1066
|
+
routingTargetsUpdatedAt: 0,
|
|
1067
|
+
routingSources: [],
|
|
1068
|
+
routingSourcesUpdatedAt: 0,
|
|
1069
|
+
routingLocation: "virtualrouter.routing"
|
|
1070
|
+
};
|
|
1071
|
+
let toastTimer = null;
|
|
1072
|
+
|
|
1073
|
+
function toast(msg, kind = "err") {
|
|
1074
|
+
const el = $("toast");
|
|
1075
|
+
if (!el) return;
|
|
1076
|
+
el.classList.remove("ok", "err", "show");
|
|
1077
|
+
el.classList.add(kind === "ok" ? "ok" : "err");
|
|
1078
|
+
el.textContent = String(msg || "");
|
|
1079
|
+
el.classList.add("show");
|
|
1080
|
+
if (toastTimer) clearTimeout(toastTimer);
|
|
1081
|
+
toastTimer = setTimeout(() => {
|
|
1082
|
+
try { el.classList.remove("show"); } catch {}
|
|
1083
|
+
}, 4200);
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
function onClick(id, handler) {
|
|
1087
|
+
const el = $(id);
|
|
1088
|
+
if (!el) {
|
|
1089
|
+
console.warn(`[daemon-admin-ui] missing #${id}`);
|
|
1090
|
+
return false;
|
|
1091
|
+
}
|
|
1092
|
+
el.addEventListener("click", handler);
|
|
1093
|
+
return true;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
function notifyUnauthorizedOnce(context) {
|
|
1097
|
+
const now = Date.now();
|
|
1098
|
+
if (now - UI.lastUnauthorizedToastAt < 1800) return;
|
|
1099
|
+
UI.lastUnauthorizedToastAt = now;
|
|
1100
|
+
const label = context ? ` (${context})` : "";
|
|
1101
|
+
toast(`Unauthorized${label}. Login required.`);
|
|
1102
|
+
try { void refreshAdminAuthStatus(); } catch {}
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
function setLog(id, value) {
|
|
1106
|
+
const el = $(id);
|
|
1107
|
+
if (!el) return;
|
|
1108
|
+
el.style.display = value ? "block" : "none";
|
|
1109
|
+
const raw = value || "";
|
|
1110
|
+
const max = 12000;
|
|
1111
|
+
const out = raw.length > max ? raw.slice(0, max) + "\n…(truncated)" : raw;
|
|
1112
|
+
el.textContent = out;
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
function getApiKey() {
|
|
1116
|
+
try {
|
|
1117
|
+
return sessionStorage.getItem("routecodex:apikey") || "";
|
|
1118
|
+
} catch {
|
|
1119
|
+
return "";
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
function setApiKey(value) {
|
|
1124
|
+
try {
|
|
1125
|
+
if (!value) sessionStorage.removeItem("routecodex:apikey");
|
|
1126
|
+
else sessionStorage.setItem("routecodex:apikey", value);
|
|
1127
|
+
} catch {}
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
async function apiFetch(path, opts = {}) {
|
|
1131
|
+
const headers = new Headers(opts.headers || {});
|
|
1132
|
+
if (!headers.has("content-type") && opts.body) headers.set("content-type", "application/json");
|
|
1133
|
+
const res = await fetch(path, { ...opts, headers, credentials: "same-origin" });
|
|
1134
|
+
const text = await res.text();
|
|
1135
|
+
let json = null;
|
|
1136
|
+
try {
|
|
1137
|
+
json = text ? JSON.parse(text) : null;
|
|
1138
|
+
} catch {
|
|
1139
|
+
json = null;
|
|
1140
|
+
}
|
|
1141
|
+
if (!res.ok) {
|
|
1142
|
+
const msg =
|
|
1143
|
+
(json && json.error && (json.error.message || json.error.code)) ||
|
|
1144
|
+
`HTTP ${res.status} ${res.statusText}`;
|
|
1145
|
+
const err = new Error(msg);
|
|
1146
|
+
err.status = res.status;
|
|
1147
|
+
err.path = path;
|
|
1148
|
+
err.payload = json;
|
|
1149
|
+
throw err;
|
|
1150
|
+
}
|
|
1151
|
+
return json;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
function selectTab(name) {
|
|
1155
|
+
document.querySelectorAll(".tab").forEach((btn) => {
|
|
1156
|
+
btn.classList.toggle("active", btn.getAttribute("data-tab") === name);
|
|
1157
|
+
});
|
|
1158
|
+
const panels = [
|
|
1159
|
+
{ name: "providers", el: $("panelProviders") },
|
|
1160
|
+
{ name: "tokens", el: $("panelTokens") },
|
|
1161
|
+
{ name: "credentials", el: $("panelCredentials") },
|
|
1162
|
+
{ name: "quota", el: $("panelQuota") },
|
|
1163
|
+
{ name: "routing", el: $("panelRouting") }
|
|
1164
|
+
];
|
|
1165
|
+
for (const p of panels) p.el.style.display = p.name === name ? "block" : "none";
|
|
1166
|
+
|
|
1167
|
+
// Light auto-refresh on tab switch to avoid showing stale "Unauthorized" after login.
|
|
1168
|
+
void maybeRefreshTab(name);
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
function getActiveTab() {
|
|
1172
|
+
const active = document.querySelector(".tab.active");
|
|
1173
|
+
const name = active ? active.getAttribute("data-tab") : null;
|
|
1174
|
+
return name || "providers";
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
const tabLastRefreshedAt = {
|
|
1178
|
+
providers: 0,
|
|
1179
|
+
tokens: 0,
|
|
1180
|
+
credentials: 0,
|
|
1181
|
+
quota: 0,
|
|
1182
|
+
routing: 0
|
|
1183
|
+
};
|
|
1184
|
+
|
|
1185
|
+
async function maybeRefreshTab(name) {
|
|
1186
|
+
const key = name in tabLastRefreshedAt ? name : "providers";
|
|
1187
|
+
const now = Date.now();
|
|
1188
|
+
if (now - tabLastRefreshedAt[key] < 1500) {
|
|
1189
|
+
return;
|
|
1190
|
+
}
|
|
1191
|
+
tabLastRefreshedAt[key] = now;
|
|
1192
|
+
try {
|
|
1193
|
+
if (key === "providers") await refreshProviders();
|
|
1194
|
+
else if (key === "tokens") await refreshTokens();
|
|
1195
|
+
else if (key === "credentials") await refreshCredentials();
|
|
1196
|
+
else if (key === "quota") {
|
|
1197
|
+
await refreshQuota();
|
|
1198
|
+
await refreshQuotaSnapshot();
|
|
1199
|
+
}
|
|
1200
|
+
else if (key === "routing") {
|
|
1201
|
+
await refreshRuntimes();
|
|
1202
|
+
await refreshRoutingSources();
|
|
1203
|
+
}
|
|
1204
|
+
} catch {
|
|
1205
|
+
// ignore refresh failures on tab switch
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
function textOf(value) {
|
|
1210
|
+
if (value === null || value === undefined) return "";
|
|
1211
|
+
return String(value);
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
function createCell(tag, text, className, opts = {}) {
|
|
1215
|
+
const el = document.createElement(tag);
|
|
1216
|
+
if (className) el.className = className;
|
|
1217
|
+
const s = textOf(text);
|
|
1218
|
+
el.textContent = s;
|
|
1219
|
+
if (opts.title && s) el.title = s;
|
|
1220
|
+
return el;
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
function createErrorRow(colSpan, message) {
|
|
1224
|
+
const tr = document.createElement("tr");
|
|
1225
|
+
const td = document.createElement("td");
|
|
1226
|
+
td.colSpan = colSpan;
|
|
1227
|
+
td.className = "mono";
|
|
1228
|
+
td.textContent = `Failed to load: ${textOf(message)}`;
|
|
1229
|
+
tr.appendChild(td);
|
|
1230
|
+
return tr;
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
function setSelectedProviderId(id) {
|
|
1234
|
+
UI.selectedProviderId = textOf(id || "");
|
|
1235
|
+
const tbody = $("providersTbody");
|
|
1236
|
+
if (!tbody) return;
|
|
1237
|
+
tbody.querySelectorAll("tr.provider-row").forEach((tr) => {
|
|
1238
|
+
const pid = tr.getAttribute("data-provider-id") || "";
|
|
1239
|
+
tr.classList.toggle("selected", pid && pid === UI.selectedProviderId);
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
function kvTypeMeta(value) {
|
|
1244
|
+
if (value === null) return "null";
|
|
1245
|
+
if (value === undefined) return "undefined";
|
|
1246
|
+
if (Array.isArray(value)) return `array(${value.length})`;
|
|
1247
|
+
const t = typeof value;
|
|
1248
|
+
if (t === "string") return `string(${value.length})`;
|
|
1249
|
+
if (t === "number") return "number";
|
|
1250
|
+
if (t === "boolean") return "boolean";
|
|
1251
|
+
if (t === "bigint") return "bigint";
|
|
1252
|
+
if (t === "function") return "function";
|
|
1253
|
+
if (t === "symbol") return "symbol";
|
|
1254
|
+
if (t === "object") {
|
|
1255
|
+
try {
|
|
1256
|
+
const keys = Object.keys(value);
|
|
1257
|
+
return `object(${keys.length})`;
|
|
1258
|
+
} catch {
|
|
1259
|
+
return "object";
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
return t;
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
const providerEditorState = {
|
|
1266
|
+
value: {},
|
|
1267
|
+
dirty: false
|
|
1268
|
+
};
|
|
1269
|
+
|
|
1270
|
+
function providerEditorClone(value) {
|
|
1271
|
+
try {
|
|
1272
|
+
return value == null ? value : JSON.parse(JSON.stringify(value));
|
|
1273
|
+
} catch {
|
|
1274
|
+
return value;
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
function providerEditorSetDirty(dirty) {
|
|
1279
|
+
providerEditorState.dirty = Boolean(dirty);
|
|
1280
|
+
const hint = $("providerEditorDirtyHint");
|
|
1281
|
+
if (hint) hint.textContent = providerEditorState.dirty ? "unsaved changes" : "";
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
function providerEditorSetValue(value) {
|
|
1285
|
+
providerEditorState.value = providerEditorClone(value) || {};
|
|
1286
|
+
providerEditorSetDirty(false);
|
|
1287
|
+
providerEditorRender();
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
function providerEditorGetValue() {
|
|
1291
|
+
return providerEditorClone(providerEditorState.value) || {};
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
function providerEditorPathToString(path) {
|
|
1295
|
+
if (!path || !path.length) return "";
|
|
1296
|
+
return path.map((p) => String(p)).join(".");
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
function providerEditorGetByPath(root, path) {
|
|
1300
|
+
let cur = root;
|
|
1301
|
+
for (const part of path || []) {
|
|
1302
|
+
if (cur == null) return undefined;
|
|
1303
|
+
cur = cur[part];
|
|
1304
|
+
}
|
|
1305
|
+
return cur;
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
function providerEditorSetByPath(root, path, nextValue) {
|
|
1309
|
+
if (!root || typeof root !== "object") return;
|
|
1310
|
+
if (!path || !path.length) return;
|
|
1311
|
+
const last = path[path.length - 1];
|
|
1312
|
+
let cur = root;
|
|
1313
|
+
for (let i = 0; i < path.length - 1; i += 1) {
|
|
1314
|
+
const part = path[i];
|
|
1315
|
+
const existing = cur[part];
|
|
1316
|
+
if (existing == null || typeof existing !== "object") cur[part] = {};
|
|
1317
|
+
cur = cur[part];
|
|
1318
|
+
}
|
|
1319
|
+
cur[last] = nextValue;
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
function providerEditorDeleteByPath(root, path) {
|
|
1323
|
+
if (!root || typeof root !== "object") return;
|
|
1324
|
+
if (!path || !path.length) return;
|
|
1325
|
+
const last = path[path.length - 1];
|
|
1326
|
+
let cur = root;
|
|
1327
|
+
for (let i = 0; i < path.length - 1; i += 1) {
|
|
1328
|
+
const part = path[i];
|
|
1329
|
+
if (cur == null) return;
|
|
1330
|
+
cur = cur[part];
|
|
1331
|
+
}
|
|
1332
|
+
if (Array.isArray(cur)) {
|
|
1333
|
+
const idx = Number(last);
|
|
1334
|
+
if (Number.isFinite(idx) && idx >= 0 && idx < cur.length) cur.splice(idx, 1);
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1337
|
+
try { delete cur[last]; } catch {}
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
function providerEditorKind(value) {
|
|
1341
|
+
if (value === null) return "null";
|
|
1342
|
+
if (Array.isArray(value)) return "array";
|
|
1343
|
+
const t = typeof value;
|
|
1344
|
+
if (t === "string") return "string";
|
|
1345
|
+
if (t === "number") return "number";
|
|
1346
|
+
if (t === "boolean") return "boolean";
|
|
1347
|
+
if (t === "object") return "object";
|
|
1348
|
+
return "string";
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
function providerEditorCoerce(kind, raw) {
|
|
1352
|
+
if (kind === "null") return null;
|
|
1353
|
+
if (kind === "boolean") return raw === true || raw === "true";
|
|
1354
|
+
if (kind === "number") {
|
|
1355
|
+
const n = typeof raw === "number" ? raw : Number.parseFloat(String(raw));
|
|
1356
|
+
return Number.isFinite(n) ? n : 0;
|
|
1357
|
+
}
|
|
1358
|
+
if (kind === "array") return [];
|
|
1359
|
+
if (kind === "object") return {};
|
|
1360
|
+
return String(raw ?? "");
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
function providerEditorRender() {
|
|
1364
|
+
const container = $("providerKvEditor");
|
|
1365
|
+
if (!container) return;
|
|
1366
|
+
try {
|
|
1367
|
+
container.replaceChildren();
|
|
1368
|
+
} catch {
|
|
1369
|
+
try { container.innerHTML = ""; } catch {}
|
|
1370
|
+
}
|
|
1371
|
+
try {
|
|
1372
|
+
container.appendChild(providerEditorRenderNode("provider", providerEditorState.value || {}, [], 0, true));
|
|
1373
|
+
} catch (e) {
|
|
1374
|
+
const msg = e && e.message ? e.message : String(e);
|
|
1375
|
+
try { container.textContent = `Render failed: ${msg}`; } catch {}
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
function providerEditorRenderNode(label, value, path, depth, isRoot = false) {
|
|
1380
|
+
const isObj = value !== null && typeof value === "object";
|
|
1381
|
+
const isArr = Array.isArray(value);
|
|
1382
|
+
const kind = providerEditorKind(value);
|
|
1383
|
+
|
|
1384
|
+
if (!isObj) {
|
|
1385
|
+
const row = document.createElement("div");
|
|
1386
|
+
row.className = "kv-leaf";
|
|
1387
|
+
|
|
1388
|
+
const keyEl = document.createElement("div");
|
|
1389
|
+
keyEl.className = "kv-key";
|
|
1390
|
+
keyEl.textContent = label;
|
|
1391
|
+
|
|
1392
|
+
const typeSel = document.createElement("select");
|
|
1393
|
+
typeSel.className = "kv-type";
|
|
1394
|
+
typeSel.innerHTML = [
|
|
1395
|
+
"<option value=\"string\">string</option>",
|
|
1396
|
+
"<option value=\"number\">number</option>",
|
|
1397
|
+
"<option value=\"boolean\">boolean</option>",
|
|
1398
|
+
"<option value=\"null\">null</option>",
|
|
1399
|
+
"<option value=\"object\">object</option>",
|
|
1400
|
+
"<option value=\"array\">array</option>"
|
|
1401
|
+
].join("");
|
|
1402
|
+
typeSel.value = kind;
|
|
1403
|
+
typeSel.addEventListener("click", (e) => e.stopPropagation());
|
|
1404
|
+
typeSel.addEventListener("change", () => {
|
|
1405
|
+
const root = providerEditorState.value || {};
|
|
1406
|
+
providerEditorSetByPath(root, path, providerEditorCoerce(typeSel.value, ""));
|
|
1407
|
+
providerEditorSetDirty(true);
|
|
1408
|
+
providerEditorRender();
|
|
1409
|
+
});
|
|
1410
|
+
|
|
1411
|
+
const valWrap = document.createElement("div");
|
|
1412
|
+
valWrap.className = "kv-val";
|
|
1413
|
+
|
|
1414
|
+
if (kind === "boolean") {
|
|
1415
|
+
const sel = document.createElement("select");
|
|
1416
|
+
sel.innerHTML = `<option value="true">true</option><option value="false">false</option>`;
|
|
1417
|
+
sel.value = value ? "true" : "false";
|
|
1418
|
+
sel.addEventListener("click", (e) => e.stopPropagation());
|
|
1419
|
+
sel.addEventListener("change", () => {
|
|
1420
|
+
const root = providerEditorState.value || {};
|
|
1421
|
+
providerEditorSetByPath(root, path, sel.value === "true");
|
|
1422
|
+
providerEditorSetDirty(true);
|
|
1423
|
+
providerEditorRender();
|
|
1424
|
+
});
|
|
1425
|
+
valWrap.appendChild(sel);
|
|
1426
|
+
} else if (kind === "number") {
|
|
1427
|
+
const inp = document.createElement("input");
|
|
1428
|
+
inp.type = "number";
|
|
1429
|
+
inp.value = String(value);
|
|
1430
|
+
inp.addEventListener("click", (e) => e.stopPropagation());
|
|
1431
|
+
inp.addEventListener("change", () => {
|
|
1432
|
+
const root = providerEditorState.value || {};
|
|
1433
|
+
const n = Number.parseFloat(inp.value);
|
|
1434
|
+
providerEditorSetByPath(root, path, Number.isFinite(n) ? n : 0);
|
|
1435
|
+
providerEditorSetDirty(true);
|
|
1436
|
+
providerEditorRender();
|
|
1437
|
+
});
|
|
1438
|
+
valWrap.appendChild(inp);
|
|
1439
|
+
} else if (kind === "null") {
|
|
1440
|
+
const span = document.createElement("span");
|
|
1441
|
+
span.className = "mono";
|
|
1442
|
+
span.textContent = "null";
|
|
1443
|
+
valWrap.appendChild(span);
|
|
1444
|
+
} else {
|
|
1445
|
+
const inp = document.createElement("input");
|
|
1446
|
+
inp.type = "text";
|
|
1447
|
+
inp.value = value == null ? "" : String(value);
|
|
1448
|
+
inp.addEventListener("click", (e) => e.stopPropagation());
|
|
1449
|
+
inp.addEventListener("change", () => {
|
|
1450
|
+
const root = providerEditorState.value || {};
|
|
1451
|
+
providerEditorSetByPath(root, path, String(inp.value));
|
|
1452
|
+
providerEditorSetDirty(true);
|
|
1453
|
+
providerEditorRender();
|
|
1454
|
+
});
|
|
1455
|
+
valWrap.appendChild(inp);
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
const actions = document.createElement("div");
|
|
1459
|
+
actions.className = "kv-actions";
|
|
1460
|
+
if (!isRoot) {
|
|
1461
|
+
const del = document.createElement("button");
|
|
1462
|
+
del.textContent = "Del";
|
|
1463
|
+
del.className = "danger";
|
|
1464
|
+
del.addEventListener("click", (e) => {
|
|
1465
|
+
e.preventDefault();
|
|
1466
|
+
e.stopPropagation();
|
|
1467
|
+
const root = providerEditorState.value || {};
|
|
1468
|
+
providerEditorDeleteByPath(root, path);
|
|
1469
|
+
providerEditorSetDirty(true);
|
|
1470
|
+
providerEditorRender();
|
|
1471
|
+
});
|
|
1472
|
+
actions.appendChild(del);
|
|
1473
|
+
}
|
|
1474
|
+
const pathEl = document.createElement("span");
|
|
1475
|
+
pathEl.className = "kv-path mono";
|
|
1476
|
+
pathEl.textContent = providerEditorPathToString(path);
|
|
1477
|
+
actions.appendChild(pathEl);
|
|
1478
|
+
|
|
1479
|
+
row.appendChild(keyEl);
|
|
1480
|
+
row.appendChild(typeSel);
|
|
1481
|
+
row.appendChild(valWrap);
|
|
1482
|
+
row.appendChild(actions);
|
|
1483
|
+
return row;
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
const details = document.createElement("details");
|
|
1487
|
+
details.open = isRoot || depth < 1;
|
|
1488
|
+
|
|
1489
|
+
const summary = document.createElement("summary");
|
|
1490
|
+
|
|
1491
|
+
const keyEl = document.createElement("div");
|
|
1492
|
+
keyEl.className = "kv-key";
|
|
1493
|
+
keyEl.textContent = label;
|
|
1494
|
+
|
|
1495
|
+
const metaEl = document.createElement("div");
|
|
1496
|
+
metaEl.className = "kv-meta";
|
|
1497
|
+
metaEl.textContent = kvTypeMeta(value);
|
|
1498
|
+
|
|
1499
|
+
const actions = document.createElement("div");
|
|
1500
|
+
actions.className = "kv-actions";
|
|
1501
|
+
|
|
1502
|
+
if (isArr) {
|
|
1503
|
+
const add = document.createElement("button");
|
|
1504
|
+
add.textContent = "+Item";
|
|
1505
|
+
add.addEventListener("click", (e) => {
|
|
1506
|
+
e.preventDefault();
|
|
1507
|
+
e.stopPropagation();
|
|
1508
|
+
const t = (prompt("Item type: string/number/boolean/null/object/array", "string") || "string").trim().toLowerCase();
|
|
1509
|
+
const k = ["string","number","boolean","null","object","array"].includes(t) ? t : "string";
|
|
1510
|
+
let init = "";
|
|
1511
|
+
if (k === "boolean") init = prompt("Value: true/false", "false") || "false";
|
|
1512
|
+
else if (k === "number") init = prompt("Value (number)", "0") || "0";
|
|
1513
|
+
else if (k === "string") init = prompt("Value (string)", "") || "";
|
|
1514
|
+
const v = providerEditorCoerce(k, k === "boolean" ? init === "true" : init);
|
|
1515
|
+
const root = providerEditorState.value || {};
|
|
1516
|
+
const arr = providerEditorGetByPath(root, path);
|
|
1517
|
+
if (Array.isArray(arr)) {
|
|
1518
|
+
arr.push(v);
|
|
1519
|
+
providerEditorSetDirty(true);
|
|
1520
|
+
providerEditorRender();
|
|
1521
|
+
}
|
|
1522
|
+
});
|
|
1523
|
+
actions.appendChild(add);
|
|
1524
|
+
} else {
|
|
1525
|
+
const add = document.createElement("button");
|
|
1526
|
+
add.textContent = "+Key";
|
|
1527
|
+
add.addEventListener("click", (e) => {
|
|
1528
|
+
e.preventDefault();
|
|
1529
|
+
e.stopPropagation();
|
|
1530
|
+
const name = (prompt("Key name", "") || "").trim();
|
|
1531
|
+
if (!name) return;
|
|
1532
|
+
const t = (prompt("Value type: string/number/boolean/null/object/array", "string") || "string").trim().toLowerCase();
|
|
1533
|
+
const k = ["string","number","boolean","null","object","array"].includes(t) ? t : "string";
|
|
1534
|
+
let init = "";
|
|
1535
|
+
if (k === "boolean") init = prompt("Value: true/false", "false") || "false";
|
|
1536
|
+
else if (k === "number") init = prompt("Value (number)", "0") || "0";
|
|
1537
|
+
else if (k === "string") init = prompt("Value (string)", "") || "";
|
|
1538
|
+
const v = providerEditorCoerce(k, k === "boolean" ? init === "true" : init);
|
|
1539
|
+
const root = providerEditorState.value || {};
|
|
1540
|
+
const obj = providerEditorGetByPath(root, path);
|
|
1541
|
+
if (obj && typeof obj === "object" && !Array.isArray(obj)) {
|
|
1542
|
+
obj[name] = v;
|
|
1543
|
+
providerEditorSetDirty(true);
|
|
1544
|
+
providerEditorRender();
|
|
1545
|
+
}
|
|
1546
|
+
});
|
|
1547
|
+
actions.appendChild(add);
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
if (!isRoot) {
|
|
1551
|
+
const del = document.createElement("button");
|
|
1552
|
+
del.textContent = "Del";
|
|
1553
|
+
del.className = "danger";
|
|
1554
|
+
del.addEventListener("click", (e) => {
|
|
1555
|
+
e.preventDefault();
|
|
1556
|
+
e.stopPropagation();
|
|
1557
|
+
const root = providerEditorState.value || {};
|
|
1558
|
+
providerEditorDeleteByPath(root, path);
|
|
1559
|
+
providerEditorSetDirty(true);
|
|
1560
|
+
providerEditorRender();
|
|
1561
|
+
});
|
|
1562
|
+
actions.appendChild(del);
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
const pathEl = document.createElement("span");
|
|
1566
|
+
pathEl.className = "kv-path mono";
|
|
1567
|
+
pathEl.textContent = providerEditorPathToString(path);
|
|
1568
|
+
actions.appendChild(pathEl);
|
|
1569
|
+
|
|
1570
|
+
summary.appendChild(keyEl);
|
|
1571
|
+
summary.appendChild(metaEl);
|
|
1572
|
+
summary.appendChild(actions);
|
|
1573
|
+
details.appendChild(summary);
|
|
1574
|
+
|
|
1575
|
+
if (isArr) {
|
|
1576
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
1577
|
+
details.appendChild(providerEditorRenderNode(String(i), value[i], path.concat([i]), depth + 1));
|
|
1578
|
+
}
|
|
1579
|
+
return details;
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
let keys = [];
|
|
1583
|
+
try {
|
|
1584
|
+
keys = Object.keys(value);
|
|
1585
|
+
keys.sort((a, b) => a.localeCompare(b));
|
|
1586
|
+
} catch {
|
|
1587
|
+
keys = [];
|
|
1588
|
+
}
|
|
1589
|
+
for (const childKey of keys) {
|
|
1590
|
+
details.appendChild(providerEditorRenderNode(childKey, value[childKey], path.concat([childKey]), depth + 1));
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
return details;
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
function providerEditorSetAuthApiKey(secretRef) {
|
|
1597
|
+
const ref = textOf(secretRef).trim();
|
|
1598
|
+
if (!ref) return;
|
|
1599
|
+
const root = providerEditorState.value || {};
|
|
1600
|
+
if (!root.auth || typeof root.auth !== "object" || Array.isArray(root.auth)) root.auth = {};
|
|
1601
|
+
root.auth.type = "apikey";
|
|
1602
|
+
root.auth.apiKey = ref;
|
|
1603
|
+
providerEditorSetDirty(true);
|
|
1604
|
+
providerEditorRender();
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
function kvPreview(value) {
|
|
1608
|
+
if (value === null || value === undefined) return String(value);
|
|
1609
|
+
const t = typeof value;
|
|
1610
|
+
if (t === "string") {
|
|
1611
|
+
const max = 140;
|
|
1612
|
+
const s = value.length > max ? value.slice(0, max) + "…" : value;
|
|
1613
|
+
return JSON.stringify(s);
|
|
1614
|
+
}
|
|
1615
|
+
if (t === "number" || t === "boolean" || t === "bigint") return String(value);
|
|
1616
|
+
if (Array.isArray(value)) return "";
|
|
1617
|
+
if (t === "object") return "";
|
|
1618
|
+
return String(value);
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
function renderRegistry(container, value, opts = {}) {
|
|
1622
|
+
if (!container) return;
|
|
1623
|
+
container.replaceChildren();
|
|
1624
|
+
const rootLabel = (opts.rootLabel || "root").trim() || "root";
|
|
1625
|
+
try {
|
|
1626
|
+
container.appendChild(renderRegistryNode(rootLabel, value, 0, true));
|
|
1627
|
+
} catch (e) {
|
|
1628
|
+
const box = document.createElement("div");
|
|
1629
|
+
box.className = "mono";
|
|
1630
|
+
box.textContent = `Render failed: ${e && e.message ? e.message : String(e)}`;
|
|
1631
|
+
container.appendChild(box);
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
function renderRegistryNode(key, value, depth, isRoot = false) {
|
|
1636
|
+
const isObj = value !== null && typeof value === "object";
|
|
1637
|
+
const isArr = Array.isArray(value);
|
|
1638
|
+
|
|
1639
|
+
if (!isObj) {
|
|
1640
|
+
const row = document.createElement("div");
|
|
1641
|
+
row.className = "kv-leaf";
|
|
1642
|
+
const k = document.createElement("div");
|
|
1643
|
+
k.className = "kv-key";
|
|
1644
|
+
k.textContent = key;
|
|
1645
|
+
const v = document.createElement("div");
|
|
1646
|
+
v.className = "kv-val";
|
|
1647
|
+
v.textContent = kvPreview(value);
|
|
1648
|
+
row.appendChild(k);
|
|
1649
|
+
row.appendChild(v);
|
|
1650
|
+
return row;
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
const details = document.createElement("details");
|
|
1654
|
+
details.open = isRoot || depth < 1;
|
|
1655
|
+
|
|
1656
|
+
const summary = document.createElement("summary");
|
|
1657
|
+
const k = document.createElement("div");
|
|
1658
|
+
k.className = "kv-key";
|
|
1659
|
+
k.textContent = key;
|
|
1660
|
+
const m = document.createElement("div");
|
|
1661
|
+
m.className = "kv-meta";
|
|
1662
|
+
const meta = kvTypeMeta(value);
|
|
1663
|
+
const preview = kvPreview(value);
|
|
1664
|
+
m.textContent = preview ? `${meta} ${preview}` : meta;
|
|
1665
|
+
summary.appendChild(k);
|
|
1666
|
+
summary.appendChild(m);
|
|
1667
|
+
details.appendChild(summary);
|
|
1668
|
+
|
|
1669
|
+
if (isArr) {
|
|
1670
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
1671
|
+
details.appendChild(renderRegistryNode(String(i), value[i], depth + 1));
|
|
1672
|
+
}
|
|
1673
|
+
return details;
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
let keys = [];
|
|
1677
|
+
try {
|
|
1678
|
+
keys = Object.keys(value);
|
|
1679
|
+
keys.sort((a, b) => a.localeCompare(b));
|
|
1680
|
+
} catch {
|
|
1681
|
+
keys = [];
|
|
1682
|
+
}
|
|
1683
|
+
for (const childKey of keys) {
|
|
1684
|
+
let childValue;
|
|
1685
|
+
try {
|
|
1686
|
+
childValue = value[childKey];
|
|
1687
|
+
} catch (e) {
|
|
1688
|
+
childValue = `[[unreadable: ${e && e.message ? e.message : String(e)}]]`;
|
|
1689
|
+
}
|
|
1690
|
+
details.appendChild(renderRegistryNode(childKey, childValue, depth + 1));
|
|
1691
|
+
}
|
|
1692
|
+
return details;
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
function setAllDetailsOpen(container, open, keepRootOpen = true) {
|
|
1696
|
+
if (!container) return;
|
|
1697
|
+
const nodes = Array.from(container.querySelectorAll("details"));
|
|
1698
|
+
nodes.forEach((d, idx) => {
|
|
1699
|
+
d.open = open || (keepRootOpen && idx === 0);
|
|
1700
|
+
});
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
function presetFor(type) {
|
|
1704
|
+
if (type === "responses") {
|
|
1705
|
+
return {
|
|
1706
|
+
enabled: true,
|
|
1707
|
+
type: "responses",
|
|
1708
|
+
baseURL: "https://api.openai.com/v1",
|
|
1709
|
+
auth: { type: "apikey", apiKey: "authfile-REPLACE_ME" },
|
|
1710
|
+
responses: { process: "chat", streaming: "always" },
|
|
1711
|
+
config: { responses: { process: "chat", streaming: "always" } },
|
|
1712
|
+
models: {}
|
|
1713
|
+
};
|
|
1714
|
+
}
|
|
1715
|
+
if (type === "openai") {
|
|
1716
|
+
return {
|
|
1717
|
+
enabled: true,
|
|
1718
|
+
type: "openai",
|
|
1719
|
+
baseURL: "https://api.openai.com/v1",
|
|
1720
|
+
auth: { type: "apikey", apiKey: "authfile-REPLACE_ME" },
|
|
1721
|
+
models: {}
|
|
1722
|
+
};
|
|
1723
|
+
}
|
|
1724
|
+
if (type === "openai-standard") {
|
|
1725
|
+
return {
|
|
1726
|
+
enabled: true,
|
|
1727
|
+
type: "openai-standard",
|
|
1728
|
+
baseURL: "https://api.openai.com/v1",
|
|
1729
|
+
auth: { type: "apikey", apiKey: "authfile-REPLACE_ME" },
|
|
1730
|
+
models: {}
|
|
1731
|
+
};
|
|
1732
|
+
}
|
|
1733
|
+
if (type === "iflow") {
|
|
1734
|
+
return {
|
|
1735
|
+
enabled: true,
|
|
1736
|
+
type: "iflow",
|
|
1737
|
+
baseURL: "https://apis.iflow.cn/v1",
|
|
1738
|
+
compatibilityProfile: "chat:iflow",
|
|
1739
|
+
auth: { type: "iflow-cookie", cookieFile: "~/.routecodex/auth/iflow-work.cookie" },
|
|
1740
|
+
models: {}
|
|
1741
|
+
};
|
|
1742
|
+
}
|
|
1743
|
+
return {
|
|
1744
|
+
enabled: true,
|
|
1745
|
+
type: "responses",
|
|
1746
|
+
baseURL: "",
|
|
1747
|
+
auth: { type: "apikey", apiKey: "authfile-REPLACE_ME" },
|
|
1748
|
+
models: {}
|
|
1749
|
+
};
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
async function refreshStatus() {
|
|
1753
|
+
try {
|
|
1754
|
+
const data = await apiFetch("/daemon/status");
|
|
1755
|
+
$("serverId").textContent = `serverId: ${data.serverId || "—"}`;
|
|
1756
|
+
$("statusText").textContent = "connected";
|
|
1757
|
+
$("statusDot").classList.add("ok");
|
|
1758
|
+
$("statusDot").classList.remove("err");
|
|
1759
|
+
} catch (e) {
|
|
1760
|
+
$("statusText").textContent = e && e.status === 401 ? "401 (set API key above)" : "disconnected";
|
|
1761
|
+
$("statusDot").classList.remove("ok");
|
|
1762
|
+
$("statusDot").classList.add("err");
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
async function refreshProviders() {
|
|
1767
|
+
const body = $("providersTbody");
|
|
1768
|
+
body.replaceChildren();
|
|
1769
|
+
try {
|
|
1770
|
+
const data = await apiFetch("/config/providers");
|
|
1771
|
+
const list = Array.isArray(data.providers) ? data.providers : [];
|
|
1772
|
+
const grouped = new Map();
|
|
1773
|
+
for (const p of list) {
|
|
1774
|
+
const t = textOf(p.type || "unknown") || "unknown";
|
|
1775
|
+
if (!grouped.has(t)) grouped.set(t, []);
|
|
1776
|
+
grouped.get(t).push(p);
|
|
1777
|
+
}
|
|
1778
|
+
const types = Array.from(grouped.keys()).sort((a, b) => a.localeCompare(b));
|
|
1779
|
+
for (const type of types) {
|
|
1780
|
+
const groupRow = document.createElement("tr");
|
|
1781
|
+
groupRow.className = "group-row";
|
|
1782
|
+
const groupCell = document.createElement("td");
|
|
1783
|
+
groupCell.colSpan = 8;
|
|
1784
|
+
groupCell.textContent = `${type} (${grouped.get(type).length})`;
|
|
1785
|
+
groupRow.appendChild(groupCell);
|
|
1786
|
+
body.appendChild(groupRow);
|
|
1787
|
+
|
|
1788
|
+
const items = grouped.get(type);
|
|
1789
|
+
items.sort((a, b) => textOf(a.id).localeCompare(textOf(b.id)));
|
|
1790
|
+
for (const p of items) {
|
|
1791
|
+
const pid = textOf(p.id);
|
|
1792
|
+
const tr = document.createElement("tr");
|
|
1793
|
+
tr.className = "provider-row";
|
|
1794
|
+
tr.setAttribute("data-provider-id", pid);
|
|
1795
|
+
if (pid && pid === UI.selectedProviderId) tr.classList.add("selected");
|
|
1796
|
+
tr.appendChild(createCell("td", pid || "", "mono indent"));
|
|
1797
|
+
tr.appendChild(createCell("td", p.type || "", ""));
|
|
1798
|
+
tr.appendChild(createCell("td", String(Boolean(p.enabled)), ""));
|
|
1799
|
+
tr.appendChild(createCell("td", p.baseURL || "", "mono truncate", { title: true }));
|
|
1800
|
+
const preview = Array.isArray(p.modelsPreview) ? p.modelsPreview.map((x) => textOf(x)).filter(Boolean) : [];
|
|
1801
|
+
const modelSummary = preview.length ? `${p.modelCount || 0}: ${preview.join(", ")}${(p.modelCount || 0) > preview.length ? ", …" : ""}` : String(p.modelCount || 0);
|
|
1802
|
+
tr.appendChild(createCell("td", modelSummary, "mono truncate", { title: true }));
|
|
1803
|
+
tr.appendChild(createCell("td", p.compatibilityProfile || "", "mono truncate", { title: true }));
|
|
1804
|
+
tr.appendChild(createCell("td", p.authType || "", ""));
|
|
1805
|
+
const actionsTd = document.createElement("td");
|
|
1806
|
+
actionsTd.className = "actions-cell";
|
|
1807
|
+
const box = document.createElement("div");
|
|
1808
|
+
box.className = "actions";
|
|
1809
|
+
const edit = document.createElement("button");
|
|
1810
|
+
edit.textContent = "Edit";
|
|
1811
|
+
edit.setAttribute("data-action", "edit");
|
|
1812
|
+
edit.setAttribute("data-id", textOf(p.id));
|
|
1813
|
+
const test = document.createElement("button");
|
|
1814
|
+
test.textContent = "Test";
|
|
1815
|
+
test.setAttribute("data-action", "test");
|
|
1816
|
+
test.setAttribute("data-id", textOf(p.id));
|
|
1817
|
+
test.disabled = !(p && p.enabled !== false && Number(p.modelCount || 0) > 0);
|
|
1818
|
+
const del = document.createElement("button");
|
|
1819
|
+
del.textContent = "Delete";
|
|
1820
|
+
del.className = "danger";
|
|
1821
|
+
del.setAttribute("data-action", "delete");
|
|
1822
|
+
del.setAttribute("data-id", textOf(p.id));
|
|
1823
|
+
box.appendChild(edit);
|
|
1824
|
+
box.appendChild(test);
|
|
1825
|
+
box.appendChild(del);
|
|
1826
|
+
actionsTd.appendChild(box);
|
|
1827
|
+
tr.appendChild(actionsTd);
|
|
1828
|
+
body.appendChild(tr);
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
} catch (e) {
|
|
1832
|
+
if (e && e.status === 401) notifyUnauthorizedOnce("providers");
|
|
1833
|
+
body.appendChild(createErrorRow(8, e && e.message ? e.message : e));
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
async function loadProvider(id) {
|
|
1838
|
+
setLog("providerOpLog", "");
|
|
1839
|
+
setSelectedProviderId(id);
|
|
1840
|
+
try {
|
|
1841
|
+
const data = await apiFetch(`/config/providers/${encodeURIComponent(id)}`);
|
|
1842
|
+
$("providerIdInput").value = id;
|
|
1843
|
+
providerEditorSetValue(data.provider || {});
|
|
1844
|
+
$("providerEditorTitle").textContent = `Provider editor: ${id}`;
|
|
1845
|
+
} catch (e) {
|
|
1846
|
+
if (e && e.status === 401) notifyUnauthorizedOnce("provider load");
|
|
1847
|
+
setLog("providerOpLog", `Load failed: ${e.message}`);
|
|
1848
|
+
toast(`Load failed: ${e.message}`);
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
async function saveProvider() {
|
|
1853
|
+
setLog("providerOpLog", "");
|
|
1854
|
+
const id = ($("providerIdInput").value || "").trim();
|
|
1855
|
+
if (!id) {
|
|
1856
|
+
setLog("providerOpLog", "provider id is required");
|
|
1857
|
+
toast("provider id is required");
|
|
1858
|
+
return;
|
|
1859
|
+
}
|
|
1860
|
+
try {
|
|
1861
|
+
const provider = providerEditorGetValue() || {};
|
|
1862
|
+
if (provider && typeof provider === "object") provider.id = id;
|
|
1863
|
+
const result = await apiFetch(`/config/providers/${encodeURIComponent(id)}`, {
|
|
1864
|
+
method: "PUT",
|
|
1865
|
+
body: JSON.stringify({ provider })
|
|
1866
|
+
});
|
|
1867
|
+
setLog("providerOpLog", `Saved. Path: ${result.path || "—"}\nRestart required to apply.`);
|
|
1868
|
+
toast("Provider saved.", "ok");
|
|
1869
|
+
providerEditorSetDirty(false);
|
|
1870
|
+
await refreshProviders();
|
|
1871
|
+
} catch (e) {
|
|
1872
|
+
if (e && e.status === 401) notifyUnauthorizedOnce("provider save");
|
|
1873
|
+
setLog("providerOpLog", `Save failed: ${e.message}`);
|
|
1874
|
+
toast(`Save failed: ${e.message}`);
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
async function deleteProvider(id) {
|
|
1879
|
+
setLog("providerOpLog", "");
|
|
1880
|
+
if (!id) {
|
|
1881
|
+
setLog("providerOpLog", "provider id is required");
|
|
1882
|
+
toast("provider id is required");
|
|
1883
|
+
return;
|
|
1884
|
+
}
|
|
1885
|
+
if (!confirm(`Delete provider "${id}" from user config?`)) return;
|
|
1886
|
+
try {
|
|
1887
|
+
const result = await apiFetch(`/config/providers/${encodeURIComponent(id)}`, { method: "DELETE" });
|
|
1888
|
+
setLog("providerOpLog", `Deleted. Path: ${result.path || "—"}\nRestart required to apply.`);
|
|
1889
|
+
toast("Provider deleted.", "ok");
|
|
1890
|
+
await refreshProviders();
|
|
1891
|
+
} catch (e) {
|
|
1892
|
+
if (e && e.status === 401) notifyUnauthorizedOnce("provider delete");
|
|
1893
|
+
setLog("providerOpLog", `Delete failed: ${e.message}`);
|
|
1894
|
+
toast(`Delete failed: ${e.message}`);
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
async function createApiKeyCredential() {
|
|
1899
|
+
setLog("providerOpLog", "");
|
|
1900
|
+
$("apikeySecretRefOut").textContent = "";
|
|
1901
|
+
const provider = ($("providerIdInput").value || "").trim();
|
|
1902
|
+
const alias = ($("apikeyAliasInput").value || "default").trim() || "default";
|
|
1903
|
+
const apiKey = ($("apikeyValueInput").value || "").trim();
|
|
1904
|
+
if (!provider) {
|
|
1905
|
+
setLog("providerOpLog", "provider id is required before creating an authfile");
|
|
1906
|
+
return null;
|
|
1907
|
+
}
|
|
1908
|
+
if (!apiKey) {
|
|
1909
|
+
setLog("providerOpLog", "apiKey is required");
|
|
1910
|
+
return null;
|
|
1911
|
+
}
|
|
1912
|
+
try {
|
|
1913
|
+
const out = await apiFetch("/daemon/credentials/apikey", {
|
|
1914
|
+
method: "POST",
|
|
1915
|
+
body: JSON.stringify({ provider, alias, apiKey })
|
|
1916
|
+
});
|
|
1917
|
+
$("apikeySecretRefOut").textContent = `secretRef: ${out.secretRef}`;
|
|
1918
|
+
return out.secretRef;
|
|
1919
|
+
} catch (e) {
|
|
1920
|
+
if (e && e.status === 401) notifyUnauthorizedOnce("authfile create");
|
|
1921
|
+
setLog("providerOpLog", `Create authfile failed: ${e.message}`);
|
|
1922
|
+
toast(`Create authfile failed: ${e.message}`);
|
|
1923
|
+
return null;
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
function applyPresetToEditor() {
|
|
1928
|
+
const preset = $("providerPreset").value;
|
|
1929
|
+
const base = presetFor(preset);
|
|
1930
|
+
const authMode = $("authMode").value;
|
|
1931
|
+
if (authMode === "none") {
|
|
1932
|
+
delete base.auth;
|
|
1933
|
+
}
|
|
1934
|
+
if (authMode === "cookie") {
|
|
1935
|
+
base.auth = {
|
|
1936
|
+
type: "iflow-cookie",
|
|
1937
|
+
cookieFile: ($("cookieFileInput").value || "").trim() || "~/.routecodex/auth/iflow.cookie"
|
|
1938
|
+
};
|
|
1939
|
+
}
|
|
1940
|
+
if (authMode === "oauth") {
|
|
1941
|
+
const t = ($("oauthTypeInput").value || "").trim() || "qwen-oauth";
|
|
1942
|
+
const alias = ($("oauthAliasInput").value || "default").trim() || "default";
|
|
1943
|
+
base.auth = { type: t, tokenFile: alias };
|
|
1944
|
+
}
|
|
1945
|
+
if (authMode === "apikey") {
|
|
1946
|
+
const secretRef = ($("apikeySecretRefOut").textContent || "").replace(/^secretRef:\\s*/i, "").trim();
|
|
1947
|
+
base.auth = { type: "apikey", apiKey: secretRef || "authfile-REPLACE_ME" };
|
|
1948
|
+
}
|
|
1949
|
+
providerEditorSetValue(base);
|
|
1950
|
+
providerEditorSetDirty(true);
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
function updateAuthModeUi() {
|
|
1954
|
+
const mode = $("authMode").value;
|
|
1955
|
+
$("authApikeyBox").style.display = mode === "apikey" ? "block" : "none";
|
|
1956
|
+
$("authOauthBox").style.display = mode === "oauth" ? "block" : "none";
|
|
1957
|
+
$("authCookieBox").style.display = mode === "cookie" ? "block" : "none";
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
async function refreshCredentials() {
|
|
1961
|
+
const body = $("credentialsTbody");
|
|
1962
|
+
body.replaceChildren();
|
|
1963
|
+
try {
|
|
1964
|
+
const items = await apiFetch("/daemon/credentials");
|
|
1965
|
+
for (const c of items || []) {
|
|
1966
|
+
const tr = document.createElement("tr");
|
|
1967
|
+
const exp = c.expiresInSec == null ? "—" : `${c.expiresInSec}s`;
|
|
1968
|
+
tr.appendChild(createCell("td", c.kind || "", ""));
|
|
1969
|
+
tr.appendChild(createCell("td", c.provider || "", "mono"));
|
|
1970
|
+
tr.appendChild(createCell("td", c.alias || "", "mono"));
|
|
1971
|
+
tr.appendChild(createCell("td", c.status || "", ""));
|
|
1972
|
+
tr.appendChild(createCell("td", exp, "mono"));
|
|
1973
|
+
tr.appendChild(createCell("td", c.secretRef || "—", "mono"));
|
|
1974
|
+
body.appendChild(tr);
|
|
1975
|
+
}
|
|
1976
|
+
} catch (e) {
|
|
1977
|
+
body.appendChild(createErrorRow(6, e && e.message ? e.message : e));
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
async function loadSettings() {
|
|
1982
|
+
try {
|
|
1983
|
+
const data = await apiFetch("/config/settings");
|
|
1984
|
+
const v = (data.oauthBrowser || "default").toLowerCase();
|
|
1985
|
+
$("oauthBrowserSelect").value = v === "camoufox" ? "camoufox" : "default";
|
|
1986
|
+
} catch {}
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
async function saveSettings() {
|
|
1990
|
+
setLog("credentialOpLog", "");
|
|
1991
|
+
const oauthBrowser = $("oauthBrowserSelect").value;
|
|
1992
|
+
try {
|
|
1993
|
+
const out = await apiFetch("/config/settings", {
|
|
1994
|
+
method: "PUT",
|
|
1995
|
+
body: JSON.stringify({ oauthBrowser })
|
|
1996
|
+
});
|
|
1997
|
+
setLog("credentialOpLog", `Saved oauthBrowser=${out.oauthBrowser}.`);
|
|
1998
|
+
} catch (e) {
|
|
1999
|
+
setLog("credentialOpLog", `Save failed: ${e.message}`);
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
async function authorizeOauth() {
|
|
2004
|
+
setLog("credentialOpLog", "");
|
|
2005
|
+
const provider = $("oauthProviderSelect").value;
|
|
2006
|
+
const alias = ($("oauthAuthAliasInput").value || "default").trim() || "default";
|
|
2007
|
+
const openBrowser = $("oauthOpenBrowser").checked;
|
|
2008
|
+
const forceReauthorize = $("oauthForceReauth").checked;
|
|
2009
|
+
try {
|
|
2010
|
+
const out = await apiFetch("/daemon/oauth/authorize", {
|
|
2011
|
+
method: "POST",
|
|
2012
|
+
body: JSON.stringify({ provider, alias, openBrowser, forceReauthorize })
|
|
2013
|
+
});
|
|
2014
|
+
setLog("credentialOpLog", `OK. tokenFile: ${out.tokenFile || "—"}`);
|
|
2015
|
+
await refreshCredentials();
|
|
2016
|
+
} catch (e) {
|
|
2017
|
+
setLog("credentialOpLog", `Authorize failed: ${e.message}`);
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
const routingEditorState = {
|
|
2022
|
+
value: {},
|
|
2023
|
+
dirty: false
|
|
2024
|
+
};
|
|
2025
|
+
|
|
2026
|
+
function routingEditorClone(value) {
|
|
2027
|
+
try {
|
|
2028
|
+
return value == null ? value : JSON.parse(JSON.stringify(value));
|
|
2029
|
+
} catch {
|
|
2030
|
+
return value;
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
function routingEditorSetDirty(dirty) {
|
|
2035
|
+
routingEditorState.dirty = Boolean(dirty);
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
function routingEditorSetValue(value) {
|
|
2039
|
+
routingEditorState.value = routingEditorClone(value) || {};
|
|
2040
|
+
routingEditorSetDirty(false);
|
|
2041
|
+
routingEditorRender();
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
function routingEditorGetValue() {
|
|
2045
|
+
return routingEditorClone(routingEditorState.value) || {};
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
function routingEditorPathToString(path) {
|
|
2049
|
+
if (!path || !path.length) return "";
|
|
2050
|
+
return path.map((p) => String(p)).join(".");
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
function routingEditorGetByPath(root, path) {
|
|
2054
|
+
let cur = root;
|
|
2055
|
+
for (const part of path || []) {
|
|
2056
|
+
if (cur == null) return undefined;
|
|
2057
|
+
cur = cur[part];
|
|
2058
|
+
}
|
|
2059
|
+
return cur;
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
function routingEditorSetByPath(root, path, nextValue) {
|
|
2063
|
+
if (!root || typeof root !== "object") return;
|
|
2064
|
+
if (!path || !path.length) return;
|
|
2065
|
+
const last = path[path.length - 1];
|
|
2066
|
+
let cur = root;
|
|
2067
|
+
for (let i = 0; i < path.length - 1; i += 1) {
|
|
2068
|
+
const part = path[i];
|
|
2069
|
+
const existing = cur[part];
|
|
2070
|
+
if (existing == null || typeof existing !== "object") cur[part] = {};
|
|
2071
|
+
cur = cur[part];
|
|
2072
|
+
}
|
|
2073
|
+
cur[last] = nextValue;
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
function routingEditorDeleteByPath(root, path) {
|
|
2077
|
+
if (!root || typeof root !== "object") return;
|
|
2078
|
+
if (!path || !path.length) return;
|
|
2079
|
+
const last = path[path.length - 1];
|
|
2080
|
+
let cur = root;
|
|
2081
|
+
for (let i = 0; i < path.length - 1; i += 1) {
|
|
2082
|
+
const part = path[i];
|
|
2083
|
+
if (cur == null) return;
|
|
2084
|
+
cur = cur[part];
|
|
2085
|
+
}
|
|
2086
|
+
if (Array.isArray(cur)) {
|
|
2087
|
+
const idx = Number(last);
|
|
2088
|
+
if (Number.isFinite(idx) && idx >= 0 && idx < cur.length) cur.splice(idx, 1);
|
|
2089
|
+
return;
|
|
2090
|
+
}
|
|
2091
|
+
try { delete cur[last]; } catch {}
|
|
2092
|
+
}
|
|
2093
|
+
|
|
2094
|
+
function routingEditorKind(value) {
|
|
2095
|
+
if (value === null) return "null";
|
|
2096
|
+
if (Array.isArray(value)) return "array";
|
|
2097
|
+
const t = typeof value;
|
|
2098
|
+
if (t === "string") return "string";
|
|
2099
|
+
if (t === "number") return "number";
|
|
2100
|
+
if (t === "boolean") return "boolean";
|
|
2101
|
+
if (t === "object") return "object";
|
|
2102
|
+
return "string";
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
function routingEditorCoerce(kind, raw) {
|
|
2106
|
+
if (kind === "null") return null;
|
|
2107
|
+
if (kind === "boolean") return raw === true || raw === "true";
|
|
2108
|
+
if (kind === "number") {
|
|
2109
|
+
const n = typeof raw === "number" ? raw : Number.parseFloat(String(raw));
|
|
2110
|
+
return Number.isFinite(n) ? n : 0;
|
|
2111
|
+
}
|
|
2112
|
+
if (kind === "array") return [];
|
|
2113
|
+
if (kind === "object") return {};
|
|
2114
|
+
return String(raw ?? "");
|
|
2115
|
+
}
|
|
2116
|
+
|
|
2117
|
+
function looksLikeRoutingTargetString(path, value) {
|
|
2118
|
+
const s = textOf(value).trim();
|
|
2119
|
+
if (!s) return false;
|
|
2120
|
+
if (s.length > 320) return false;
|
|
2121
|
+
if (s.includes(" ")) return false;
|
|
2122
|
+
if (!s.includes(".")) return false;
|
|
2123
|
+
const root = routingEditorState.value || {};
|
|
2124
|
+
const parentPath = path.slice(0, -1);
|
|
2125
|
+
const parent = routingEditorGetByPath(root, parentPath);
|
|
2126
|
+
const parentKey = path.length >= 2 ? path[path.length - 2] : null;
|
|
2127
|
+
if (parentKey === "targets") return true;
|
|
2128
|
+
if (Array.isArray(parent) && path.length === 2 && typeof path[0] === "string") return true;
|
|
2129
|
+
return false;
|
|
2130
|
+
}
|
|
2131
|
+
|
|
2132
|
+
function resolveTargetToProviderKeys(target) {
|
|
2133
|
+
const raw = textOf(target).trim();
|
|
2134
|
+
if (!raw) return [];
|
|
2135
|
+
const list = Array.isArray(UI.quotaProviders) ? UI.quotaProviders : [];
|
|
2136
|
+
const keys = list.map((p) => textOf(p && p.providerKey ? p.providerKey : "")).filter(Boolean);
|
|
2137
|
+
const known = new Set(keys);
|
|
2138
|
+
if (known.has(raw)) return [raw];
|
|
2139
|
+
const dot = raw.indexOf(".");
|
|
2140
|
+
if (dot <= 0) return [];
|
|
2141
|
+
const providerId = raw.slice(0, dot);
|
|
2142
|
+
const modelId = raw.slice(dot + 1);
|
|
2143
|
+
if (!providerId || !modelId) return [];
|
|
2144
|
+
const prefix = `${providerId}.`;
|
|
2145
|
+
const suffix = `.${modelId}`;
|
|
2146
|
+
const out = [];
|
|
2147
|
+
for (const k of keys) {
|
|
2148
|
+
if (k.startsWith(prefix) && k.endsWith(suffix)) out.push(k);
|
|
2149
|
+
}
|
|
2150
|
+
out.sort((a, b) => a.localeCompare(b));
|
|
2151
|
+
return out;
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2154
|
+
function getQuotaStateByProviderKey(providerKey) {
|
|
2155
|
+
const map = UI.quotaProviderMap instanceof Map ? UI.quotaProviderMap : null;
|
|
2156
|
+
if (map && map.has(providerKey)) return map.get(providerKey);
|
|
2157
|
+
const list = Array.isArray(UI.quotaProviders) ? UI.quotaProviders : [];
|
|
2158
|
+
return list.find((p) => textOf(p && p.providerKey ? p.providerKey : "") === providerKey) || null;
|
|
2159
|
+
}
|
|
2160
|
+
|
|
2161
|
+
async function routingQuotaAction(kind, providerKey) {
|
|
2162
|
+
if (!providerKey) return;
|
|
2163
|
+
if (!UI.adminAuth || !UI.adminAuth.authenticated) {
|
|
2164
|
+
notifyUnauthorizedOnce("routing quota action");
|
|
2165
|
+
return;
|
|
2166
|
+
}
|
|
2167
|
+
try {
|
|
2168
|
+
if (kind === "recover") {
|
|
2169
|
+
await apiFetch(`/quota/providers/${encodeURIComponent(providerKey)}/recover`, { method: "POST" });
|
|
2170
|
+
} else if (kind === "disable") {
|
|
2171
|
+
const minutes = Number.parseFloat(textOf($("routingQuotaDurationSelect").value || "60"));
|
|
2172
|
+
if (!Number.isFinite(minutes) || minutes <= 0) throw new Error("Invalid minutes");
|
|
2173
|
+
const modeRaw = (textOf($("routingQuotaModeSelect").value) || "cooldown").trim().toLowerCase();
|
|
2174
|
+
const mode = modeRaw === "blacklist" ? "blacklist" : "cooldown";
|
|
2175
|
+
await apiFetch(`/quota/providers/${encodeURIComponent(providerKey)}/disable`, {
|
|
2176
|
+
method: "POST",
|
|
2177
|
+
body: JSON.stringify({ mode, durationMinutes: minutes })
|
|
2178
|
+
});
|
|
2179
|
+
} else {
|
|
2180
|
+
return;
|
|
2181
|
+
}
|
|
2182
|
+
await refreshRuntimes();
|
|
2183
|
+
routingEditorRender();
|
|
2184
|
+
} catch (e) {
|
|
2185
|
+
if (e && e.status === 401) notifyUnauthorizedOnce("routing quota action");
|
|
2186
|
+
toast(`Action failed: ${e && e.message ? e.message : String(e)}`);
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2190
|
+
function routingEditorRenderTargetActions(target) {
|
|
2191
|
+
const providerKeys = resolveTargetToProviderKeys(target);
|
|
2192
|
+
if (!providerKeys.length) return null;
|
|
2193
|
+
|
|
2194
|
+
const box = document.createElement("div");
|
|
2195
|
+
box.className = "routing-target-actions";
|
|
2196
|
+
|
|
2197
|
+
for (const providerKey of providerKeys) {
|
|
2198
|
+
const row = document.createElement("div");
|
|
2199
|
+
row.className = "routing-target-row";
|
|
2200
|
+
|
|
2201
|
+
const key = document.createElement("span");
|
|
2202
|
+
key.className = "routing-target-key mono";
|
|
2203
|
+
key.textContent = providerKey;
|
|
2204
|
+
|
|
2205
|
+
const state = getQuotaStateByProviderKey(providerKey);
|
|
2206
|
+
const inPool = state ? Boolean(state.inPool) : null;
|
|
2207
|
+
const untilMs = state ? Math.max(Number(state.blacklistUntil || 0), Number(state.cooldownUntil || 0)) : 0;
|
|
2208
|
+
const meta = document.createElement("span");
|
|
2209
|
+
meta.className = "routing-target-meta";
|
|
2210
|
+
meta.textContent =
|
|
2211
|
+
inPool === null
|
|
2212
|
+
? "unknown"
|
|
2213
|
+
: inPool
|
|
2214
|
+
? "online"
|
|
2215
|
+
: untilMs
|
|
2216
|
+
? `offline ${formatEpochWithDelta(untilMs)}`
|
|
2217
|
+
: "offline";
|
|
2218
|
+
|
|
2219
|
+
const offBtn = document.createElement("button");
|
|
2220
|
+
offBtn.textContent = "Offline";
|
|
2221
|
+
offBtn.className = "danger";
|
|
2222
|
+
offBtn.addEventListener("click", (e) => {
|
|
2223
|
+
e.preventDefault();
|
|
2224
|
+
e.stopPropagation();
|
|
2225
|
+
void routingQuotaAction("disable", providerKey);
|
|
2226
|
+
});
|
|
2227
|
+
|
|
2228
|
+
const recBtn = document.createElement("button");
|
|
2229
|
+
recBtn.textContent = "Recover";
|
|
2230
|
+
recBtn.addEventListener("click", (e) => {
|
|
2231
|
+
e.preventDefault();
|
|
2232
|
+
e.stopPropagation();
|
|
2233
|
+
void routingQuotaAction("recover", providerKey);
|
|
2234
|
+
});
|
|
2235
|
+
|
|
2236
|
+
row.appendChild(key);
|
|
2237
|
+
row.appendChild(meta);
|
|
2238
|
+
row.appendChild(offBtn);
|
|
2239
|
+
row.appendChild(recBtn);
|
|
2240
|
+
box.appendChild(row);
|
|
2241
|
+
}
|
|
2242
|
+
|
|
2243
|
+
return box;
|
|
2244
|
+
}
|
|
2245
|
+
|
|
2246
|
+
function routingEditorRender() {
|
|
2247
|
+
const container = $("routingKvEditor");
|
|
2248
|
+
if (!container) return;
|
|
2249
|
+
try {
|
|
2250
|
+
container.replaceChildren();
|
|
2251
|
+
} catch {
|
|
2252
|
+
try { container.innerHTML = ""; } catch {}
|
|
2253
|
+
}
|
|
2254
|
+
try {
|
|
2255
|
+
container.appendChild(routingEditorRenderNode("routing", routingEditorState.value || {}, [], 0, true));
|
|
2256
|
+
} catch (e) {
|
|
2257
|
+
const msg = e && e.message ? e.message : String(e);
|
|
2258
|
+
try { container.textContent = `Render failed: ${msg}`; } catch {}
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
|
|
2262
|
+
function routingEditorRenderNode(label, value, path, depth, isRoot = false) {
|
|
2263
|
+
const isObj = value !== null && typeof value === "object";
|
|
2264
|
+
const isArr = Array.isArray(value);
|
|
2265
|
+
const kind = routingEditorKind(value);
|
|
2266
|
+
|
|
2267
|
+
if (!isObj) {
|
|
2268
|
+
const row = document.createElement("div");
|
|
2269
|
+
row.className = "kv-leaf";
|
|
2270
|
+
|
|
2271
|
+
const keyEl = document.createElement("div");
|
|
2272
|
+
keyEl.className = "kv-key";
|
|
2273
|
+
keyEl.textContent = label;
|
|
2274
|
+
|
|
2275
|
+
const typeSel = document.createElement("select");
|
|
2276
|
+
typeSel.className = "kv-type";
|
|
2277
|
+
typeSel.innerHTML = [
|
|
2278
|
+
"<option value=\"string\">string</option>",
|
|
2279
|
+
"<option value=\"number\">number</option>",
|
|
2280
|
+
"<option value=\"boolean\">boolean</option>",
|
|
2281
|
+
"<option value=\"null\">null</option>",
|
|
2282
|
+
"<option value=\"object\">object</option>",
|
|
2283
|
+
"<option value=\"array\">array</option>"
|
|
2284
|
+
].join("");
|
|
2285
|
+
typeSel.value = kind;
|
|
2286
|
+
typeSel.addEventListener("click", (e) => e.stopPropagation());
|
|
2287
|
+
typeSel.addEventListener("change", () => {
|
|
2288
|
+
const root = routingEditorState.value || {};
|
|
2289
|
+
routingEditorSetByPath(root, path, routingEditorCoerce(typeSel.value, ""));
|
|
2290
|
+
routingEditorSetDirty(true);
|
|
2291
|
+
routingEditorRender();
|
|
2292
|
+
});
|
|
2293
|
+
|
|
2294
|
+
const valWrap = document.createElement("div");
|
|
2295
|
+
valWrap.className = "kv-val";
|
|
2296
|
+
|
|
2297
|
+
if (kind === "boolean") {
|
|
2298
|
+
const sel = document.createElement("select");
|
|
2299
|
+
sel.innerHTML = `<option value="true">true</option><option value="false">false</option>`;
|
|
2300
|
+
sel.value = value ? "true" : "false";
|
|
2301
|
+
sel.addEventListener("click", (e) => e.stopPropagation());
|
|
2302
|
+
sel.addEventListener("change", () => {
|
|
2303
|
+
const root = routingEditorState.value || {};
|
|
2304
|
+
routingEditorSetByPath(root, path, sel.value === "true");
|
|
2305
|
+
routingEditorSetDirty(true);
|
|
2306
|
+
routingEditorRender();
|
|
2307
|
+
});
|
|
2308
|
+
valWrap.appendChild(sel);
|
|
2309
|
+
} else if (kind === "number") {
|
|
2310
|
+
const inp = document.createElement("input");
|
|
2311
|
+
inp.type = "number";
|
|
2312
|
+
inp.value = String(value);
|
|
2313
|
+
inp.addEventListener("click", (e) => e.stopPropagation());
|
|
2314
|
+
inp.addEventListener("change", () => {
|
|
2315
|
+
const root = routingEditorState.value || {};
|
|
2316
|
+
const n = Number.parseFloat(inp.value);
|
|
2317
|
+
routingEditorSetByPath(root, path, Number.isFinite(n) ? n : 0);
|
|
2318
|
+
routingEditorSetDirty(true);
|
|
2319
|
+
routingEditorRender();
|
|
2320
|
+
});
|
|
2321
|
+
valWrap.appendChild(inp);
|
|
2322
|
+
} else if (kind === "null") {
|
|
2323
|
+
const span = document.createElement("span");
|
|
2324
|
+
span.className = "mono";
|
|
2325
|
+
span.textContent = "null";
|
|
2326
|
+
valWrap.appendChild(span);
|
|
2327
|
+
} else {
|
|
2328
|
+
const inp = document.createElement("input");
|
|
2329
|
+
inp.type = "text";
|
|
2330
|
+
inp.value = value == null ? "" : String(value);
|
|
2331
|
+
inp.addEventListener("click", (e) => e.stopPropagation());
|
|
2332
|
+
inp.addEventListener("change", () => {
|
|
2333
|
+
const root = routingEditorState.value || {};
|
|
2334
|
+
routingEditorSetByPath(root, path, String(inp.value));
|
|
2335
|
+
routingEditorSetDirty(true);
|
|
2336
|
+
routingEditorRender();
|
|
2337
|
+
});
|
|
2338
|
+
valWrap.appendChild(inp);
|
|
2339
|
+
|
|
2340
|
+
if (looksLikeRoutingTargetString(path, value)) {
|
|
2341
|
+
const widget = routingEditorRenderTargetActions(inp.value);
|
|
2342
|
+
if (widget) valWrap.appendChild(widget);
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2346
|
+
const actions = document.createElement("div");
|
|
2347
|
+
actions.className = "kv-actions";
|
|
2348
|
+
if (!isRoot) {
|
|
2349
|
+
const del = document.createElement("button");
|
|
2350
|
+
del.textContent = "Del";
|
|
2351
|
+
del.className = "danger";
|
|
2352
|
+
del.addEventListener("click", (e) => {
|
|
2353
|
+
e.preventDefault();
|
|
2354
|
+
e.stopPropagation();
|
|
2355
|
+
const root = routingEditorState.value || {};
|
|
2356
|
+
routingEditorDeleteByPath(root, path);
|
|
2357
|
+
routingEditorSetDirty(true);
|
|
2358
|
+
routingEditorRender();
|
|
2359
|
+
});
|
|
2360
|
+
actions.appendChild(del);
|
|
2361
|
+
}
|
|
2362
|
+
const pathEl = document.createElement("span");
|
|
2363
|
+
pathEl.className = "kv-path mono";
|
|
2364
|
+
pathEl.textContent = routingEditorPathToString(path);
|
|
2365
|
+
actions.appendChild(pathEl);
|
|
2366
|
+
|
|
2367
|
+
row.appendChild(keyEl);
|
|
2368
|
+
row.appendChild(typeSel);
|
|
2369
|
+
row.appendChild(valWrap);
|
|
2370
|
+
row.appendChild(actions);
|
|
2371
|
+
return row;
|
|
2372
|
+
}
|
|
2373
|
+
|
|
2374
|
+
const details = document.createElement("details");
|
|
2375
|
+
details.open = isRoot || depth < 1;
|
|
2376
|
+
|
|
2377
|
+
const summary = document.createElement("summary");
|
|
2378
|
+
|
|
2379
|
+
const keyEl = document.createElement("div");
|
|
2380
|
+
keyEl.className = "kv-key";
|
|
2381
|
+
keyEl.textContent = label;
|
|
2382
|
+
|
|
2383
|
+
const metaEl = document.createElement("div");
|
|
2384
|
+
metaEl.className = "kv-meta";
|
|
2385
|
+
metaEl.textContent = kvTypeMeta(value);
|
|
2386
|
+
|
|
2387
|
+
const actions = document.createElement("div");
|
|
2388
|
+
actions.className = "kv-actions";
|
|
2389
|
+
|
|
2390
|
+
if (isArr) {
|
|
2391
|
+
const add = document.createElement("button");
|
|
2392
|
+
add.textContent = "+Item";
|
|
2393
|
+
add.addEventListener("click", (e) => {
|
|
2394
|
+
e.preventDefault();
|
|
2395
|
+
e.stopPropagation();
|
|
2396
|
+
const t = (prompt("Item type: string/number/boolean/null/object/array", "string") || "string").trim().toLowerCase();
|
|
2397
|
+
const k = ["string","number","boolean","null","object","array"].includes(t) ? t : "string";
|
|
2398
|
+
let init = "";
|
|
2399
|
+
if (k === "boolean") init = prompt("Value: true/false", "false") || "false";
|
|
2400
|
+
else if (k === "number") init = prompt("Value (number)", "0") || "0";
|
|
2401
|
+
else if (k === "string") init = prompt("Value (string)", "") || "";
|
|
2402
|
+
const v = routingEditorCoerce(k, k === "boolean" ? init === "true" : init);
|
|
2403
|
+
const root = routingEditorState.value || {};
|
|
2404
|
+
const arr = routingEditorGetByPath(root, path);
|
|
2405
|
+
if (Array.isArray(arr)) {
|
|
2406
|
+
arr.push(v);
|
|
2407
|
+
routingEditorSetDirty(true);
|
|
2408
|
+
routingEditorRender();
|
|
2409
|
+
}
|
|
2410
|
+
});
|
|
2411
|
+
actions.appendChild(add);
|
|
2412
|
+
} else {
|
|
2413
|
+
const add = document.createElement("button");
|
|
2414
|
+
add.textContent = "+Key";
|
|
2415
|
+
add.addEventListener("click", (e) => {
|
|
2416
|
+
e.preventDefault();
|
|
2417
|
+
e.stopPropagation();
|
|
2418
|
+
const name = (prompt("Key name", "") || "").trim();
|
|
2419
|
+
if (!name) return;
|
|
2420
|
+
const t = (prompt("Value type: string/number/boolean/null/object/array", "string") || "string").trim().toLowerCase();
|
|
2421
|
+
const k = ["string","number","boolean","null","object","array"].includes(t) ? t : "string";
|
|
2422
|
+
let init = "";
|
|
2423
|
+
if (k === "boolean") init = prompt("Value: true/false", "false") || "false";
|
|
2424
|
+
else if (k === "number") init = prompt("Value (number)", "0") || "0";
|
|
2425
|
+
else if (k === "string") init = prompt("Value (string)", "") || "";
|
|
2426
|
+
const v = routingEditorCoerce(k, k === "boolean" ? init === "true" : init);
|
|
2427
|
+
const root = routingEditorState.value || {};
|
|
2428
|
+
const obj = routingEditorGetByPath(root, path);
|
|
2429
|
+
if (obj && typeof obj === "object" && !Array.isArray(obj)) {
|
|
2430
|
+
obj[name] = v;
|
|
2431
|
+
routingEditorSetDirty(true);
|
|
2432
|
+
routingEditorRender();
|
|
2433
|
+
}
|
|
2434
|
+
});
|
|
2435
|
+
actions.appendChild(add);
|
|
2436
|
+
}
|
|
2437
|
+
|
|
2438
|
+
if (!isRoot) {
|
|
2439
|
+
const del = document.createElement("button");
|
|
2440
|
+
del.textContent = "Del";
|
|
2441
|
+
del.className = "danger";
|
|
2442
|
+
del.addEventListener("click", (e) => {
|
|
2443
|
+
e.preventDefault();
|
|
2444
|
+
e.stopPropagation();
|
|
2445
|
+
const root = routingEditorState.value || {};
|
|
2446
|
+
routingEditorDeleteByPath(root, path);
|
|
2447
|
+
routingEditorSetDirty(true);
|
|
2448
|
+
routingEditorRender();
|
|
2449
|
+
});
|
|
2450
|
+
actions.appendChild(del);
|
|
2451
|
+
}
|
|
2452
|
+
|
|
2453
|
+
const pathEl = document.createElement("span");
|
|
2454
|
+
pathEl.className = "kv-path mono";
|
|
2455
|
+
pathEl.textContent = routingEditorPathToString(path);
|
|
2456
|
+
actions.appendChild(pathEl);
|
|
2457
|
+
|
|
2458
|
+
summary.appendChild(keyEl);
|
|
2459
|
+
summary.appendChild(metaEl);
|
|
2460
|
+
summary.appendChild(actions);
|
|
2461
|
+
details.appendChild(summary);
|
|
2462
|
+
|
|
2463
|
+
if (isArr) {
|
|
2464
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
2465
|
+
details.appendChild(routingEditorRenderNode(String(i), value[i], path.concat([i]), depth + 1));
|
|
2466
|
+
}
|
|
2467
|
+
return details;
|
|
2468
|
+
}
|
|
2469
|
+
|
|
2470
|
+
let keys = [];
|
|
2471
|
+
try {
|
|
2472
|
+
keys = Object.keys(value);
|
|
2473
|
+
keys.sort((a, b) => a.localeCompare(b));
|
|
2474
|
+
} catch {
|
|
2475
|
+
keys = [];
|
|
2476
|
+
}
|
|
2477
|
+
for (const childKey of keys) {
|
|
2478
|
+
details.appendChild(routingEditorRenderNode(childKey, value[childKey], path.concat([childKey]), depth + 1));
|
|
2479
|
+
}
|
|
2480
|
+
|
|
2481
|
+
return details;
|
|
2482
|
+
}
|
|
2483
|
+
|
|
2484
|
+
async function loadRouting() {
|
|
2485
|
+
setLog("routingOpLog", "");
|
|
2486
|
+
const auth = UI.adminAuth ? UI.adminAuth : await refreshAdminAuthStatus();
|
|
2487
|
+
if (!auth || !auth.authenticated) {
|
|
2488
|
+
notifyUnauthorizedOnce("routing");
|
|
2489
|
+
return;
|
|
2490
|
+
}
|
|
2491
|
+
try {
|
|
2492
|
+
const selectedPath = textOf($("routingSourceSelect").value || "").trim();
|
|
2493
|
+
const query = selectedPath ? `?path=${encodeURIComponent(selectedPath)}` : "";
|
|
2494
|
+
const out = await apiFetch(`/config/routing${query}`);
|
|
2495
|
+
UI.routingLocation = out.location || "virtualrouter.routing";
|
|
2496
|
+
routingEditorSetValue(out.routing || {});
|
|
2497
|
+
setLog("routingOpLog", `Loaded. Path: ${out.path || "—"}\nLocation: ${UI.routingLocation}`);
|
|
2498
|
+
toast("Routing loaded.", "ok");
|
|
2499
|
+
} catch (e) {
|
|
2500
|
+
if (e && e.status === 401) notifyUnauthorizedOnce("routing");
|
|
2501
|
+
setLog("routingOpLog", `Load failed: ${e.message}`);
|
|
2502
|
+
toast(`Load failed: ${e.message}`);
|
|
2503
|
+
}
|
|
2504
|
+
}
|
|
2505
|
+
|
|
2506
|
+
async function saveRouting() {
|
|
2507
|
+
setLog("routingOpLog", "");
|
|
2508
|
+
const auth = UI.adminAuth ? UI.adminAuth : await refreshAdminAuthStatus();
|
|
2509
|
+
if (!auth || !auth.authenticated) {
|
|
2510
|
+
notifyUnauthorizedOnce("routing save");
|
|
2511
|
+
return;
|
|
2512
|
+
}
|
|
2513
|
+
try {
|
|
2514
|
+
const selectedPath = textOf($("routingSourceSelect").value || "").trim();
|
|
2515
|
+
const query = selectedPath ? `?path=${encodeURIComponent(selectedPath)}` : "";
|
|
2516
|
+
const routing = routingEditorGetValue() || {};
|
|
2517
|
+
const out = await apiFetch(`/config/routing${query}`, {
|
|
2518
|
+
method: "PUT",
|
|
2519
|
+
body: JSON.stringify({ routing, location: UI.routingLocation, path: selectedPath || undefined })
|
|
2520
|
+
});
|
|
2521
|
+
UI.routingLocation = out.location || UI.routingLocation;
|
|
2522
|
+
setLog("routingOpLog", `Saved. Path: ${out.path || "—"}\nLocation: ${UI.routingLocation}\nRestart required to apply.`);
|
|
2523
|
+
toast("Routing saved.", "ok");
|
|
2524
|
+
routingEditorSetDirty(false);
|
|
2525
|
+
} catch (e) {
|
|
2526
|
+
if (e && e.status === 401) notifyUnauthorizedOnce("routing save");
|
|
2527
|
+
setLog("routingOpLog", `Save failed: ${e.message}`);
|
|
2528
|
+
toast(`Save failed: ${e.message}`);
|
|
2529
|
+
}
|
|
2530
|
+
}
|
|
2531
|
+
|
|
2532
|
+
async function refreshRoutingSources() {
|
|
2533
|
+
const select = $("routingSourceSelect");
|
|
2534
|
+
if (!select) return;
|
|
2535
|
+
const prev = textOf(select.value || "");
|
|
2536
|
+
try {
|
|
2537
|
+
const out = await apiFetch("/config/routing/sources");
|
|
2538
|
+
const sources = Array.isArray(out && out.sources) ? out.sources : [];
|
|
2539
|
+
UI.routingSources = sources;
|
|
2540
|
+
UI.routingSourcesUpdatedAt = Date.now();
|
|
2541
|
+
|
|
2542
|
+
select.replaceChildren();
|
|
2543
|
+
for (const s of sources) {
|
|
2544
|
+
const opt = document.createElement("option");
|
|
2545
|
+
opt.value = textOf(s.path || "");
|
|
2546
|
+
const version = s.version ? ` v=${s.version}` : "";
|
|
2547
|
+
const loc = s.location ? ` (${s.location})` : "";
|
|
2548
|
+
const kind = s.kind ? `[${s.kind}] ` : "";
|
|
2549
|
+
opt.textContent = `${kind}${textOf(s.label || s.path || "")}${version}${loc}`;
|
|
2550
|
+
select.appendChild(opt);
|
|
2551
|
+
}
|
|
2552
|
+
|
|
2553
|
+
const active = textOf(out && out.activePath ? out.activePath : "");
|
|
2554
|
+
const hasPrev = sources.some((s) => textOf(s.path) === prev);
|
|
2555
|
+
const hasActive = sources.some((s) => textOf(s.path) === active);
|
|
2556
|
+
if (hasPrev) select.value = prev;
|
|
2557
|
+
else if (hasActive) select.value = active;
|
|
2558
|
+
|
|
2559
|
+
toast("Routing sources refreshed.", "ok");
|
|
2560
|
+
} catch (e) {
|
|
2561
|
+
if (e && e.status === 401) notifyUnauthorizedOnce("routing sources");
|
|
2562
|
+
toast(`Routing sources failed: ${e && e.message ? e.message : String(e)}`);
|
|
2563
|
+
}
|
|
2564
|
+
}
|
|
2565
|
+
|
|
2566
|
+
async function refreshRuntimes() {
|
|
2567
|
+
try {
|
|
2568
|
+
const quota = await apiFetch("/quota/providers");
|
|
2569
|
+
const quotaProviders = quota && Array.isArray(quota.providers) ? quota.providers : [];
|
|
2570
|
+
UI.quotaProviders = quotaProviders;
|
|
2571
|
+
UI.quotaProvidersUpdatedAt = Date.now();
|
|
2572
|
+
UI.quotaProviderMap = new Map(quotaProviders.map((q) => [textOf(q.providerKey), q]));
|
|
2573
|
+
} catch (e) {
|
|
2574
|
+
if (e && e.status === 401) notifyUnauthorizedOnce("pool status");
|
|
2575
|
+
throw e;
|
|
2576
|
+
}
|
|
2577
|
+
}
|
|
2578
|
+
|
|
2579
|
+
function formatEpochMs(ms) {
|
|
2580
|
+
if (typeof ms !== "number" || !Number.isFinite(ms) || ms <= 0) return "—";
|
|
2581
|
+
try {
|
|
2582
|
+
return new Date(ms).toLocaleString();
|
|
2583
|
+
} catch {
|
|
2584
|
+
return String(ms);
|
|
2585
|
+
}
|
|
2586
|
+
}
|
|
2587
|
+
|
|
2588
|
+
function formatDurationMs(ms) {
|
|
2589
|
+
if (typeof ms !== "number" || !Number.isFinite(ms)) return "—";
|
|
2590
|
+
const abs = Math.abs(ms);
|
|
2591
|
+
const sign = ms < 0 ? "-" : "";
|
|
2592
|
+
const s = Math.round(abs / 1000);
|
|
2593
|
+
if (s < 60) return `${sign}${s}s`;
|
|
2594
|
+
const m = Math.round(s / 60);
|
|
2595
|
+
if (m < 60) return `${sign}${m}m`;
|
|
2596
|
+
const h = Math.round(m / 60);
|
|
2597
|
+
if (h < 48) return `${sign}${h}h`;
|
|
2598
|
+
const d = Math.round(h / 24);
|
|
2599
|
+
return `${sign}${d}d`;
|
|
2600
|
+
}
|
|
2601
|
+
|
|
2602
|
+
function formatEpochWithDelta(ms) {
|
|
2603
|
+
if (typeof ms !== "number" || !Number.isFinite(ms) || ms <= 0) return "—";
|
|
2604
|
+
const delta = ms - Date.now();
|
|
2605
|
+
const tail = delta >= 0 ? `in ${formatDurationMs(delta)}` : `${formatDurationMs(delta)} ago`;
|
|
2606
|
+
return `${formatEpochMs(ms)} (${tail})`;
|
|
2607
|
+
}
|
|
2608
|
+
|
|
2609
|
+
function pill(text, kind) {
|
|
2610
|
+
const span = document.createElement("span");
|
|
2611
|
+
span.className = `pill ${kind || ""}`.trim();
|
|
2612
|
+
span.textContent = textOf(text);
|
|
2613
|
+
return span;
|
|
2614
|
+
}
|
|
2615
|
+
|
|
2616
|
+
function extractRoutingTargets(routing) {
|
|
2617
|
+
const out = new Set();
|
|
2618
|
+
if (!routing || typeof routing !== "object") return out;
|
|
2619
|
+
for (const routeName of Object.keys(routing)) {
|
|
2620
|
+
const pools = routing[routeName];
|
|
2621
|
+
if (!Array.isArray(pools)) continue;
|
|
2622
|
+
for (const pool of pools) {
|
|
2623
|
+
if (!pool || typeof pool !== "object") continue;
|
|
2624
|
+
const targets = pool.targets;
|
|
2625
|
+
if (!Array.isArray(targets)) continue;
|
|
2626
|
+
for (const t of targets) {
|
|
2627
|
+
if (typeof t === "string" && t.trim()) out.add(t.trim());
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
}
|
|
2631
|
+
return out;
|
|
2632
|
+
}
|
|
2633
|
+
|
|
2634
|
+
function resolveRoutedProviderKeys(routingTargets, providers) {
|
|
2635
|
+
const targets = routingTargets instanceof Set ? Array.from(routingTargets) : [];
|
|
2636
|
+
const list = Array.isArray(providers) ? providers : [];
|
|
2637
|
+
const keys = [];
|
|
2638
|
+
for (const p of list) {
|
|
2639
|
+
const k = textOf(p && p.providerKey ? p.providerKey : "");
|
|
2640
|
+
if (k) keys.push(k);
|
|
2641
|
+
}
|
|
2642
|
+
const known = new Set(keys);
|
|
2643
|
+
const resolved = new Set();
|
|
2644
|
+
if (!targets.length || !known.size) return resolved;
|
|
2645
|
+
|
|
2646
|
+
for (const t of targets) {
|
|
2647
|
+
const target = textOf(t);
|
|
2648
|
+
if (!target) continue;
|
|
2649
|
+
if (known.has(target)) {
|
|
2650
|
+
resolved.add(target);
|
|
2651
|
+
continue;
|
|
2652
|
+
}
|
|
2653
|
+
const dot = target.indexOf(".");
|
|
2654
|
+
if (dot <= 0) continue;
|
|
2655
|
+
const providerId = target.slice(0, dot);
|
|
2656
|
+
const modelId = target.slice(dot + 1);
|
|
2657
|
+
if (!providerId || !modelId) continue;
|
|
2658
|
+
const prefix = `${providerId}.`;
|
|
2659
|
+
const suffix = `.${modelId}`;
|
|
2660
|
+
for (const k of known) {
|
|
2661
|
+
if (k.startsWith(prefix) && k.endsWith(suffix)) resolved.add(k);
|
|
2662
|
+
}
|
|
2663
|
+
}
|
|
2664
|
+
return resolved;
|
|
2665
|
+
}
|
|
2666
|
+
|
|
2667
|
+
async function refreshRoutingTargets() {
|
|
2668
|
+
try {
|
|
2669
|
+
const routingOut = await apiFetch("/config/routing");
|
|
2670
|
+
UI.routingTargets = extractRoutingTargets(routingOut && routingOut.routing ? routingOut.routing : {});
|
|
2671
|
+
UI.routingTargetsUpdatedAt = Date.now();
|
|
2672
|
+
} catch {
|
|
2673
|
+
UI.routingTargets = null;
|
|
2674
|
+
UI.routingTargetsUpdatedAt = Date.now();
|
|
2675
|
+
}
|
|
2676
|
+
}
|
|
2677
|
+
|
|
2678
|
+
async function refreshQuota() {
|
|
2679
|
+
const body = $("quotaTbody");
|
|
2680
|
+
body.replaceChildren();
|
|
2681
|
+
setLog("quotaOpLog", "");
|
|
2682
|
+
try {
|
|
2683
|
+
// Load routing targets so we can filter out providers not referenced by routing pools.
|
|
2684
|
+
await refreshRoutingTargets();
|
|
2685
|
+
// Force refresh quota first so UI doesn't show stale pool state.
|
|
2686
|
+
try {
|
|
2687
|
+
await apiFetch("/daemon/modules/quota/refresh", { method: "POST" });
|
|
2688
|
+
} catch (e) {
|
|
2689
|
+
// Backwards compatibility: older servers may only support reset.
|
|
2690
|
+
if (e && e.status === 404) {
|
|
2691
|
+
try {
|
|
2692
|
+
await apiFetch("/daemon/modules/quota/reset", { method: "POST" });
|
|
2693
|
+
} catch (e2) {
|
|
2694
|
+
throw e2;
|
|
2695
|
+
}
|
|
2696
|
+
} else {
|
|
2697
|
+
throw e;
|
|
2698
|
+
}
|
|
2699
|
+
}
|
|
2700
|
+
const out = await apiFetch("/quota/providers");
|
|
2701
|
+
UI.quotaProviders = Array.isArray(out.providers) ? out.providers : [];
|
|
2702
|
+
UI.quotaProvidersUpdatedAt = Date.now();
|
|
2703
|
+
renderQuotaProviders();
|
|
2704
|
+
} catch (e) {
|
|
2705
|
+
if (e && e.status === 401) notifyUnauthorizedOnce("quota");
|
|
2706
|
+
else toast(e && e.message ? e.message : String(e || "quota refresh failed"));
|
|
2707
|
+
body.appendChild(createErrorRow(8, e && e.message ? e.message : e));
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2710
|
+
|
|
2711
|
+
function renderQuotaProviders() {
|
|
2712
|
+
const body = $("quotaTbody");
|
|
2713
|
+
body.replaceChildren();
|
|
2714
|
+
|
|
2715
|
+
const filter = textOf($("quotaFilterInput").value || "").trim().toLowerCase();
|
|
2716
|
+
const hideOk = Boolean($("quotaHideOkToggle").checked);
|
|
2717
|
+
const onlyRoutedTargets = Boolean($("quotaOnlyRoutedTargetsToggle").checked);
|
|
2718
|
+
const routedProviderKeys = onlyRoutedTargets ? resolveRoutedProviderKeys(UI.routingTargets, UI.quotaProviders) : null;
|
|
2719
|
+
|
|
2720
|
+
const list = Array.isArray(UI.quotaProviders) ? UI.quotaProviders : [];
|
|
2721
|
+
const next = list
|
|
2722
|
+
.filter((q) => {
|
|
2723
|
+
const key = textOf(q && q.providerKey ? q.providerKey : "").toLowerCase();
|
|
2724
|
+
if (filter && !key.includes(filter)) return false;
|
|
2725
|
+
if (hideOk && q && q.inPool === true) return false;
|
|
2726
|
+
if (routedProviderKeys && !routedProviderKeys.has(textOf(q && q.providerKey ? q.providerKey : ""))) return false;
|
|
2727
|
+
return true;
|
|
2728
|
+
})
|
|
2729
|
+
.slice();
|
|
2730
|
+
|
|
2731
|
+
next.sort((a, b) => {
|
|
2732
|
+
const aIn = a && a.inPool === true ? 1 : 0;
|
|
2733
|
+
const bIn = b && b.inPool === true ? 1 : 0;
|
|
2734
|
+
if (aIn !== bIn) return aIn - bIn; // inPool=false first
|
|
2735
|
+
const aUntil = Math.max(Number(a?.blacklistUntil || 0), Number(a?.cooldownUntil || 0));
|
|
2736
|
+
const bUntil = Math.max(Number(b?.blacklistUntil || 0), Number(b?.cooldownUntil || 0));
|
|
2737
|
+
if (aUntil !== bUntil) return bUntil - aUntil;
|
|
2738
|
+
return textOf(a?.providerKey).localeCompare(textOf(b?.providerKey));
|
|
2739
|
+
});
|
|
2740
|
+
|
|
2741
|
+
if (!next.length) {
|
|
2742
|
+
body.appendChild(createErrorRow(8, "No providers matched filter."));
|
|
2743
|
+
return;
|
|
2744
|
+
}
|
|
2745
|
+
|
|
2746
|
+
function splitProviderKey(providerKey) {
|
|
2747
|
+
const raw = textOf(providerKey || "");
|
|
2748
|
+
const parts = raw.split(".");
|
|
2749
|
+
if (parts.length >= 3) {
|
|
2750
|
+
return { providerId: parts[0], authAlias: parts[1], model: parts.slice(2).join(".") };
|
|
2751
|
+
}
|
|
2752
|
+
if (parts.length === 2) {
|
|
2753
|
+
return { providerId: parts[0], authAlias: "", model: parts[1] };
|
|
2754
|
+
}
|
|
2755
|
+
return { providerId: raw, authAlias: "", model: "" };
|
|
2756
|
+
}
|
|
2757
|
+
|
|
2758
|
+
const byProvider = new Map();
|
|
2759
|
+
for (const q of next) {
|
|
2760
|
+
const pk = textOf(q && q.providerKey ? q.providerKey : "");
|
|
2761
|
+
const { providerId, authAlias, model } = splitProviderKey(pk);
|
|
2762
|
+
if (!byProvider.has(providerId)) byProvider.set(providerId, new Map());
|
|
2763
|
+
const byAlias = byProvider.get(providerId);
|
|
2764
|
+
if (!byAlias.has(authAlias)) byAlias.set(authAlias, []);
|
|
2765
|
+
byAlias.get(authAlias).push({ q, pk, providerId, authAlias, model });
|
|
2766
|
+
}
|
|
2767
|
+
|
|
2768
|
+
const providerIds = Array.from(byProvider.keys()).sort((a, b) => a.localeCompare(b));
|
|
2769
|
+
const groupCols = 8;
|
|
2770
|
+
|
|
2771
|
+
const appendGroupRow = (label, indent = false) => {
|
|
2772
|
+
const tr = document.createElement("tr");
|
|
2773
|
+
tr.className = "group-row";
|
|
2774
|
+
const td = document.createElement("td");
|
|
2775
|
+
td.colSpan = groupCols;
|
|
2776
|
+
td.textContent = label;
|
|
2777
|
+
if (indent) td.className = "indent";
|
|
2778
|
+
tr.appendChild(td);
|
|
2779
|
+
body.appendChild(tr);
|
|
2780
|
+
};
|
|
2781
|
+
|
|
2782
|
+
for (const providerId of providerIds) {
|
|
2783
|
+
const byAlias = byProvider.get(providerId);
|
|
2784
|
+
let modelsCount = 0;
|
|
2785
|
+
for (const v of byAlias.values()) modelsCount += v.length;
|
|
2786
|
+
appendGroupRow(`${providerId} (${modelsCount})`);
|
|
2787
|
+
|
|
2788
|
+
const aliases = Array.from(byAlias.keys()).sort((a, b) => a.localeCompare(b));
|
|
2789
|
+
for (const alias of aliases) {
|
|
2790
|
+
const items = byAlias.get(alias);
|
|
2791
|
+
appendGroupRow(`${alias || "(no-key)"} (${items.length})`, true);
|
|
2792
|
+
|
|
2793
|
+
for (const item of items) {
|
|
2794
|
+
const q = item.q;
|
|
2795
|
+
const tr = document.createElement("tr");
|
|
2796
|
+
tr.className = "provider-row";
|
|
2797
|
+
tr.setAttribute("data-provider-key", item.pk);
|
|
2798
|
+
|
|
2799
|
+
tr.appendChild(createCell("td", "", "mono"));
|
|
2800
|
+
tr.appendChild(createCell("td", "", "mono"));
|
|
2801
|
+
tr.appendChild(createCell("td", item.model || item.pk, "mono indent", { title: true }));
|
|
2802
|
+
|
|
2803
|
+
const inPool = Boolean(q.inPool);
|
|
2804
|
+
const inTd = document.createElement("td");
|
|
2805
|
+
inTd.appendChild(pill(inPool ? "true" : "false", inPool ? "ok" : "bad"));
|
|
2806
|
+
tr.appendChild(inTd);
|
|
2807
|
+
|
|
2808
|
+
const reason = textOf(q.reason || "");
|
|
2809
|
+
const reasonKind =
|
|
2810
|
+
reason === "ok" ? "ok" :
|
|
2811
|
+
reason === "cooldown" ? "warn" :
|
|
2812
|
+
reason === "blacklist" || reason === "fatal" || reason === "quotaDepleted" ? "bad" : "warn";
|
|
2813
|
+
const reasonTd = document.createElement("td");
|
|
2814
|
+
reasonTd.appendChild(pill(reason || "—", reasonKind));
|
|
2815
|
+
tr.appendChild(reasonTd);
|
|
2816
|
+
|
|
2817
|
+
const cooldown = formatEpochWithDelta(q.cooldownUntil);
|
|
2818
|
+
const blacklist = formatEpochWithDelta(q.blacklistUntil);
|
|
2819
|
+
const until = `cooldown=${cooldown || "—"} blacklist=${blacklist || "—"}`;
|
|
2820
|
+
tr.appendChild(createCell("td", until, "mono", { title: true }));
|
|
2821
|
+
|
|
2822
|
+
tr.appendChild(createCell("td", q.consecutiveErrorCount ?? 0, "mono"));
|
|
2823
|
+
|
|
2824
|
+
const actionsTd = document.createElement("td");
|
|
2825
|
+
actionsTd.className = "actions-cell";
|
|
2826
|
+
const box = document.createElement("div");
|
|
2827
|
+
box.className = "actions";
|
|
2828
|
+
const recover = document.createElement("button");
|
|
2829
|
+
recover.textContent = "Recover";
|
|
2830
|
+
recover.setAttribute("data-action", "quota-recover");
|
|
2831
|
+
recover.setAttribute("data-key", item.pk);
|
|
2832
|
+
const reset = document.createElement("button");
|
|
2833
|
+
reset.textContent = "Reset";
|
|
2834
|
+
reset.setAttribute("data-action", "quota-reset");
|
|
2835
|
+
reset.setAttribute("data-key", item.pk);
|
|
2836
|
+
const disable = document.createElement("button");
|
|
2837
|
+
disable.textContent = "Offline…";
|
|
2838
|
+
disable.className = "danger";
|
|
2839
|
+
disable.setAttribute("data-action", "quota-disable");
|
|
2840
|
+
disable.setAttribute("data-key", item.pk);
|
|
2841
|
+
box.appendChild(recover);
|
|
2842
|
+
box.appendChild(reset);
|
|
2843
|
+
box.appendChild(disable);
|
|
2844
|
+
actionsTd.appendChild(box);
|
|
2845
|
+
tr.appendChild(actionsTd);
|
|
2846
|
+
|
|
2847
|
+
body.appendChild(tr);
|
|
2848
|
+
}
|
|
2849
|
+
}
|
|
2850
|
+
}
|
|
2851
|
+
}
|
|
2852
|
+
|
|
2853
|
+
function formatRemainingFraction(v) {
|
|
2854
|
+
if (typeof v !== "number" || !Number.isFinite(v)) return "—";
|
|
2855
|
+
const pct = Math.max(0, Math.min(1, v)) * 100;
|
|
2856
|
+
return `${pct.toFixed(1)}%`;
|
|
2857
|
+
}
|
|
2858
|
+
|
|
2859
|
+
async function refreshQuotaSnapshot() {
|
|
2860
|
+
const body = $("quotaSnapshotTbody");
|
|
2861
|
+
body.replaceChildren();
|
|
2862
|
+
setLog("quotaSnapshotLog", "");
|
|
2863
|
+
try {
|
|
2864
|
+
// Ensure routing + provider pool snapshots exist so we can filter routed models.
|
|
2865
|
+
await refreshRoutingTargets();
|
|
2866
|
+
if (!Array.isArray(UI.quotaProviders) || UI.quotaProviders.length === 0) {
|
|
2867
|
+
try {
|
|
2868
|
+
const outProviders = await apiFetch("/quota/providers");
|
|
2869
|
+
UI.quotaProviders = Array.isArray(outProviders.providers) ? outProviders.providers : [];
|
|
2870
|
+
UI.quotaProvidersUpdatedAt = Date.now();
|
|
2871
|
+
} catch {
|
|
2872
|
+
// ignore: snapshot view can still render unfiltered
|
|
2873
|
+
}
|
|
2874
|
+
}
|
|
2875
|
+
const out = await apiFetch("/quota/summary");
|
|
2876
|
+
let list = Array.isArray(out.records) ? out.records : [];
|
|
2877
|
+
|
|
2878
|
+
const onlyRouted = Boolean($("quotaSnapshotOnlyRoutedToggle").checked);
|
|
2879
|
+
if (onlyRouted) {
|
|
2880
|
+
const routedProviderKeys = resolveRoutedProviderKeys(UI.routingTargets, UI.quotaProviders);
|
|
2881
|
+
const allowedSnapshotKeys = new Set();
|
|
2882
|
+
for (const providerKey of routedProviderKeys) {
|
|
2883
|
+
if (!providerKey.toLowerCase().startsWith("antigravity.")) continue;
|
|
2884
|
+
const parts = providerKey.split(".");
|
|
2885
|
+
if (parts.length < 3) continue;
|
|
2886
|
+
const alias = parts[1];
|
|
2887
|
+
const modelId = parts.slice(2).join(".");
|
|
2888
|
+
if (!alias || !modelId) continue;
|
|
2889
|
+
allowedSnapshotKeys.add(`antigravity://${alias}/${modelId}`);
|
|
2890
|
+
}
|
|
2891
|
+
list = list.filter((r) => allowedSnapshotKeys.has(textOf(r && r.key ? r.key : "")));
|
|
2892
|
+
}
|
|
2893
|
+
|
|
2894
|
+
list.sort((a, b) => String(a.key || "").localeCompare(String(b.key || "")));
|
|
2895
|
+
for (const r of list) {
|
|
2896
|
+
const raw = textOf(r && r.key ? r.key : "");
|
|
2897
|
+
const prefix = "antigravity://";
|
|
2898
|
+
const rest = raw.startsWith(prefix) ? raw.slice(prefix.length) : raw;
|
|
2899
|
+
const parts = rest.split("/");
|
|
2900
|
+
const alias = parts.length >= 2 ? parts[0] : "";
|
|
2901
|
+
const model = parts.length >= 2 ? parts.slice(1).join("/") : rest;
|
|
2902
|
+
const tr = document.createElement("tr");
|
|
2903
|
+
tr.appendChild(createCell("td", alias || "—", "mono", { title: true }));
|
|
2904
|
+
tr.appendChild(createCell("td", model || "—", "mono", { title: true }));
|
|
2905
|
+
tr.appendChild(createCell("td", formatRemainingFraction(r.remainingFraction), "mono"));
|
|
2906
|
+
tr.appendChild(createCell("td", formatEpochWithDelta(r.resetAt), "mono", { title: true }));
|
|
2907
|
+
tr.appendChild(createCell("td", formatEpochMs(r.fetchedAt), "mono", { title: true }));
|
|
2908
|
+
body.appendChild(tr);
|
|
2909
|
+
}
|
|
2910
|
+
if (!list.length) {
|
|
2911
|
+
body.appendChild(createErrorRow(5, "No quota records to show (check filter or click Refresh antigravity snapshot)."));
|
|
2912
|
+
}
|
|
2913
|
+
} catch (e) {
|
|
2914
|
+
if (e && e.status === 401) notifyUnauthorizedOnce("quota snapshot");
|
|
2915
|
+
body.appendChild(createErrorRow(5, e && e.message ? e.message : e));
|
|
2916
|
+
}
|
|
2917
|
+
}
|
|
2918
|
+
|
|
2919
|
+
async function refreshQuotaSnapshotNow() {
|
|
2920
|
+
setLog("quotaSnapshotLog", "");
|
|
2921
|
+
try {
|
|
2922
|
+
const out = await apiFetch("/quota/refresh", { method: "POST" });
|
|
2923
|
+
const result = out && out.result ? out.result : null;
|
|
2924
|
+
setLog(
|
|
2925
|
+
"quotaSnapshotLog",
|
|
2926
|
+
`OK. refreshedAt=${result && result.refreshedAt ? formatEpochMs(result.refreshedAt) : "—"} tokenCount=${result && typeof result.tokenCount === "number" ? result.tokenCount : "—"} records=${result && typeof result.recordCount === "number" ? result.recordCount : "—"}`
|
|
2927
|
+
);
|
|
2928
|
+
await refreshQuotaSnapshot();
|
|
2929
|
+
toast("Quota snapshot refreshed.", "ok");
|
|
2930
|
+
} catch (e) {
|
|
2931
|
+
if (e && e.status === 401) notifyUnauthorizedOnce("quota refresh");
|
|
2932
|
+
setLog("quotaSnapshotLog", `Refresh failed: ${e && e.message ? e.message : e}`);
|
|
2933
|
+
toast(`Refresh failed: ${e && e.message ? e.message : String(e)}`);
|
|
2934
|
+
}
|
|
2935
|
+
}
|
|
2936
|
+
|
|
2937
|
+
function formatInt(n) {
|
|
2938
|
+
const v = typeof n === "number" && Number.isFinite(n) ? n : 0;
|
|
2939
|
+
try { return v.toLocaleString(); } catch { return String(v); }
|
|
2940
|
+
}
|
|
2941
|
+
|
|
2942
|
+
function formatTokensRow(label, totals) {
|
|
2943
|
+
const inTok = formatInt(totals.totalPromptTokens);
|
|
2944
|
+
const outTok = formatInt(totals.totalCompletionTokens);
|
|
2945
|
+
const totTok = formatInt(totals.totalOutputTokens);
|
|
2946
|
+
const req = formatInt(totals.requestCount);
|
|
2947
|
+
const err = formatInt(totals.errorCount);
|
|
2948
|
+
return `${label}: requests=${req} (err=${err}) tokens in/out/total=${inTok}/${outTok}/${totTok}`;
|
|
2949
|
+
}
|
|
2950
|
+
|
|
2951
|
+
async function refreshTokens() {
|
|
2952
|
+
const body = $("tokensTbody");
|
|
2953
|
+
body.replaceChildren();
|
|
2954
|
+
$("tokenTotalsBox").textContent = "";
|
|
2955
|
+
try {
|
|
2956
|
+
const out = await apiFetch("/daemon/stats");
|
|
2957
|
+
const session = out && out.session ? out.session : null;
|
|
2958
|
+
const historical = out && out.historical ? out.historical : null;
|
|
2959
|
+
const totals = out && out.totals ? out.totals : null;
|
|
2960
|
+
|
|
2961
|
+
const sessionTotals = totals && totals.session ? totals.session : { requestCount: 0, errorCount: 0, totalPromptTokens: 0, totalCompletionTokens: 0, totalOutputTokens: 0 };
|
|
2962
|
+
const historicalTotals = totals && totals.historical ? totals.historical : { requestCount: 0, errorCount: 0, totalPromptTokens: 0, totalCompletionTokens: 0, totalOutputTokens: 0 };
|
|
2963
|
+
|
|
2964
|
+
const lines = [];
|
|
2965
|
+
lines.push(formatTokensRow("ALL (session)", sessionTotals));
|
|
2966
|
+
lines.push(formatTokensRow("ALL (historical)", historicalTotals));
|
|
2967
|
+
$("tokenTotalsBox").textContent = lines.join("\n");
|
|
2968
|
+
|
|
2969
|
+
const sessionRows = session && Array.isArray(session.totals) ? session.totals : [];
|
|
2970
|
+
const histRows = historical && Array.isArray(historical.totals) ? historical.totals : [];
|
|
2971
|
+
|
|
2972
|
+
const byKey = new Map();
|
|
2973
|
+
const keyOf = (r) => `${textOf(r.providerKey)}|${textOf(r.model || "")}`;
|
|
2974
|
+
for (const r of sessionRows) {
|
|
2975
|
+
if (!r || !r.providerKey) continue;
|
|
2976
|
+
byKey.set(keyOf(r), { providerKey: textOf(r.providerKey), model: textOf(r.model || ""), session: r, historical: null });
|
|
2977
|
+
}
|
|
2978
|
+
for (const r of histRows) {
|
|
2979
|
+
if (!r || !r.providerKey) continue;
|
|
2980
|
+
const k = keyOf(r);
|
|
2981
|
+
const existing = byKey.get(k) || { providerKey: textOf(r.providerKey), model: textOf(r.model || ""), session: null, historical: null };
|
|
2982
|
+
existing.historical = r;
|
|
2983
|
+
byKey.set(k, existing);
|
|
2984
|
+
}
|
|
2985
|
+
|
|
2986
|
+
const rows = Array.from(byKey.values()).sort((a, b) => {
|
|
2987
|
+
const ak = `${a.providerKey}.${a.model}`;
|
|
2988
|
+
const bk = `${b.providerKey}.${b.model}`;
|
|
2989
|
+
return ak.localeCompare(bk);
|
|
2990
|
+
});
|
|
2991
|
+
|
|
2992
|
+
for (const row of rows) {
|
|
2993
|
+
const tr = document.createElement("tr");
|
|
2994
|
+
tr.appendChild(createCell("td", row.providerKey, "mono truncate", { title: true }));
|
|
2995
|
+
tr.appendChild(createCell("td", row.model || "—", "mono truncate", { title: true }));
|
|
2996
|
+
|
|
2997
|
+
const s = row.session;
|
|
2998
|
+
const sReqErr = s ? `${formatInt(s.requestCount)} / ${formatInt(s.errorCount)}` : "—";
|
|
2999
|
+
const sTok = s ? `${formatInt(s.totalPromptTokens)}/${formatInt(s.totalCompletionTokens)}/${formatInt(s.totalOutputTokens)}` : "—";
|
|
3000
|
+
tr.appendChild(createCell("td", sReqErr, "mono"));
|
|
3001
|
+
tr.appendChild(createCell("td", sTok, "mono"));
|
|
3002
|
+
|
|
3003
|
+
const h = row.historical;
|
|
3004
|
+
const hReqErr = h ? `${formatInt(h.requestCount)} / ${formatInt(h.errorCount)}` : "—";
|
|
3005
|
+
const hTok = h ? `${formatInt(h.totalPromptTokens)}/${formatInt(h.totalCompletionTokens)}/${formatInt(h.totalOutputTokens)}` : "—";
|
|
3006
|
+
tr.appendChild(createCell("td", hReqErr, "mono"));
|
|
3007
|
+
tr.appendChild(createCell("td", hTok, "mono"));
|
|
3008
|
+
|
|
3009
|
+
body.appendChild(tr);
|
|
3010
|
+
}
|
|
3011
|
+
} catch (e) {
|
|
3012
|
+
body.appendChild(createErrorRow(6, e && e.message ? e.message : e));
|
|
3013
|
+
}
|
|
3014
|
+
}
|
|
3015
|
+
|
|
3016
|
+
async function testProviderFromPool(providerId) {
|
|
3017
|
+
setLog("providerOpLog", "");
|
|
3018
|
+
try {
|
|
3019
|
+
const detail = await apiFetch(`/config/providers/${encodeURIComponent(providerId)}`);
|
|
3020
|
+
const provider = detail && detail.provider ? detail.provider : null;
|
|
3021
|
+
const models = provider && provider.models && typeof provider.models === "object" ? Object.keys(provider.models) : [];
|
|
3022
|
+
if (!models.length) {
|
|
3023
|
+
throw new Error("No models configured for this provider");
|
|
3024
|
+
}
|
|
3025
|
+
const modelId = models[0];
|
|
3026
|
+
const directModel = `${providerId}.${modelId}`;
|
|
3027
|
+
const payload = { model: directModel, input: [{ role: "user", content: "ping" }], stream: false };
|
|
3028
|
+
const headers = new Headers({ "content-type": "application/json" });
|
|
3029
|
+
const apiKey = getApiKey();
|
|
3030
|
+
if (apiKey) headers.set("x-api-key", apiKey);
|
|
3031
|
+
const started = Date.now();
|
|
3032
|
+
const resp = await fetch("/v1/responses", { method: "POST", headers, body: JSON.stringify(payload) });
|
|
3033
|
+
const text = await resp.text();
|
|
3034
|
+
const ms = Date.now() - started;
|
|
3035
|
+
if (!resp.ok) {
|
|
3036
|
+
throw new Error(`HTTP ${resp.status} (${ms}ms): ${text}`);
|
|
3037
|
+
}
|
|
3038
|
+
let json = null;
|
|
3039
|
+
try { json = text ? JSON.parse(text) : null; } catch { json = null; }
|
|
3040
|
+
const summary =
|
|
3041
|
+
json && typeof json.output_text === "string"
|
|
3042
|
+
? json.output_text.slice(0, 200)
|
|
3043
|
+
: json && Array.isArray(json.output)
|
|
3044
|
+
? "(output items=" + json.output.length + ")"
|
|
3045
|
+
: "(ok)";
|
|
3046
|
+
setLog("providerOpLog", `Test OK (${ms}ms) model=${directModel}\n${summary}`);
|
|
3047
|
+
} catch (e) {
|
|
3048
|
+
setLog("providerOpLog", `Test failed: ${e && e.message ? e.message : e}`);
|
|
3049
|
+
}
|
|
3050
|
+
}
|
|
3051
|
+
|
|
3052
|
+
async function quotaAction(kind, providerKey) {
|
|
3053
|
+
setLog("quotaOpLog", "");
|
|
3054
|
+
if (!providerKey) {
|
|
3055
|
+
setLog("quotaOpLog", "providerKey required");
|
|
3056
|
+
toast("providerKey required");
|
|
3057
|
+
return;
|
|
3058
|
+
}
|
|
3059
|
+
try {
|
|
3060
|
+
if (kind === "recover") {
|
|
3061
|
+
await apiFetch(`/quota/providers/${encodeURIComponent(providerKey)}/recover`, { method: "POST" });
|
|
3062
|
+
await refreshQuota();
|
|
3063
|
+
toast("Recovered.", "ok");
|
|
3064
|
+
return;
|
|
3065
|
+
}
|
|
3066
|
+
if (kind === "reset") {
|
|
3067
|
+
await apiFetch(`/quota/providers/${encodeURIComponent(providerKey)}/reset`, { method: "POST" });
|
|
3068
|
+
await refreshQuota();
|
|
3069
|
+
toast("Reset.", "ok");
|
|
3070
|
+
return;
|
|
3071
|
+
}
|
|
3072
|
+
if (kind === "disable") {
|
|
3073
|
+
const minutes = Number.parseFloat(textOf($("quotaDurationSelect").value || "60"));
|
|
3074
|
+
if (!Number.isFinite(minutes) || minutes <= 0) {
|
|
3075
|
+
throw new Error("Invalid minutes");
|
|
3076
|
+
}
|
|
3077
|
+
const modeRaw = (textOf($("quotaModeSelect").value) || "cooldown").trim().toLowerCase();
|
|
3078
|
+
const mode = modeRaw === "blacklist" ? "blacklist" : "cooldown";
|
|
3079
|
+
await apiFetch(`/quota/providers/${encodeURIComponent(providerKey)}/disable`, {
|
|
3080
|
+
method: "POST",
|
|
3081
|
+
body: JSON.stringify({ mode, durationMinutes: minutes })
|
|
3082
|
+
});
|
|
3083
|
+
await refreshQuota();
|
|
3084
|
+
toast("Applied.", "ok");
|
|
3085
|
+
return;
|
|
3086
|
+
}
|
|
3087
|
+
} catch (e) {
|
|
3088
|
+
if (e && e.status === 401) notifyUnauthorizedOnce("quota action");
|
|
3089
|
+
setLog("quotaOpLog", `Action failed: ${e && e.message ? e.message : e}`);
|
|
3090
|
+
toast(`Action failed: ${e && e.message ? e.message : String(e)}`);
|
|
3091
|
+
}
|
|
3092
|
+
}
|
|
3093
|
+
|
|
3094
|
+
async function resetQuotaModule() {
|
|
3095
|
+
setLog("quotaOpLog", "");
|
|
3096
|
+
if (!confirm("Reset provider-quota module now? This clears cooldown/blacklist state.")) return;
|
|
3097
|
+
try {
|
|
3098
|
+
const out = await apiFetch("/daemon/modules/provider-quota/reset", { method: "POST" });
|
|
3099
|
+
setLog("quotaOpLog", `OK. resetAt=${out.resetAt || "—"}`);
|
|
3100
|
+
await refreshQuota();
|
|
3101
|
+
toast("Quota module reset.", "ok");
|
|
3102
|
+
} catch (e) {
|
|
3103
|
+
if (e && e.status === 401) notifyUnauthorizedOnce("quota reset");
|
|
3104
|
+
setLog("quotaOpLog", `Reset failed: ${e.message}`);
|
|
3105
|
+
toast(`Reset failed: ${e.message}`);
|
|
3106
|
+
}
|
|
3107
|
+
}
|
|
3108
|
+
|
|
3109
|
+
// Bind events
|
|
3110
|
+
document.querySelectorAll(".tab").forEach((btn) => {
|
|
3111
|
+
btn.addEventListener("click", () => selectTab(btn.getAttribute("data-tab")));
|
|
3112
|
+
});
|
|
3113
|
+
|
|
3114
|
+
async function refreshAdminAuthStatus() {
|
|
3115
|
+
try {
|
|
3116
|
+
const status = await apiFetch("/daemon/auth/status", { method: "GET" });
|
|
3117
|
+
if (status && status.ok) {
|
|
3118
|
+
const hasPassword = Boolean(status.hasPassword);
|
|
3119
|
+
const authed = Boolean(status.authenticated);
|
|
3120
|
+
UI.adminAuth = { hasPassword, authenticated: authed };
|
|
3121
|
+
$("adminAuthHint").textContent = authed ? "authenticated" : hasPassword ? "login required" : "setup required (localhost only)";
|
|
3122
|
+
$("setupPasswordBtn").style.display = hasPassword ? "none" : "inline-block";
|
|
3123
|
+
$("loginPasswordBtn").style.display = hasPassword ? "inline-block" : "none";
|
|
3124
|
+
$("changePasswordDetails").style.display = hasPassword && authed ? "block" : "none";
|
|
3125
|
+
return { hasPassword, authenticated: authed };
|
|
3126
|
+
}
|
|
3127
|
+
} catch (e) {
|
|
3128
|
+
const msg = e && e.message ? e.message : String(e);
|
|
3129
|
+
$("adminAuthHint").textContent = msg;
|
|
3130
|
+
toast(`Admin auth status failed: ${msg}`);
|
|
3131
|
+
}
|
|
3132
|
+
UI.adminAuth = { hasPassword: false, authenticated: false };
|
|
3133
|
+
$("changePasswordDetails").style.display = "none";
|
|
3134
|
+
return { hasPassword: false, authenticated: false };
|
|
3135
|
+
}
|
|
3136
|
+
|
|
3137
|
+
$("adminPasswordInput").addEventListener("keydown", async (ev) => {
|
|
3138
|
+
if (ev.key !== "Enter") return;
|
|
3139
|
+
ev.preventDefault();
|
|
3140
|
+
const status = UI.adminAuth ? UI.adminAuth : await refreshAdminAuthStatus();
|
|
3141
|
+
if (status.hasPassword) $("loginPasswordBtn").click();
|
|
3142
|
+
else $("setupPasswordBtn").click();
|
|
3143
|
+
});
|
|
3144
|
+
|
|
3145
|
+
$("setupPasswordBtn").addEventListener("click", async () => {
|
|
3146
|
+
const pw = ($("adminPasswordInput").value || "");
|
|
3147
|
+
try {
|
|
3148
|
+
$("adminAuthHint").textContent = "setting password…";
|
|
3149
|
+
await apiFetch("/daemon/auth/setup", { method: "POST", body: JSON.stringify({ password: pw }) });
|
|
3150
|
+
$("adminPasswordInput").value = "";
|
|
3151
|
+
$("oldAdminPasswordInput").value = "";
|
|
3152
|
+
$("newAdminPasswordInput").value = "";
|
|
3153
|
+
toast("Password set. You're now logged in.", "ok");
|
|
3154
|
+
await refreshAdminAuthStatus();
|
|
3155
|
+
await refreshStatus();
|
|
3156
|
+
await refreshProviders();
|
|
3157
|
+
await refreshCredentials();
|
|
3158
|
+
await refreshQuota();
|
|
3159
|
+
await refreshQuotaSnapshot();
|
|
3160
|
+
await refreshRuntimes();
|
|
3161
|
+
await refreshRoutingSources();
|
|
3162
|
+
await loadRouting();
|
|
3163
|
+
} catch (e) {
|
|
3164
|
+
const msg = e && e.message ? e.message : String(e);
|
|
3165
|
+
$("adminAuthHint").textContent = msg;
|
|
3166
|
+
toast(`Set password failed: ${msg}`);
|
|
3167
|
+
}
|
|
3168
|
+
});
|
|
3169
|
+
|
|
3170
|
+
$("loginPasswordBtn").addEventListener("click", async () => {
|
|
3171
|
+
const pw = ($("adminPasswordInput").value || "");
|
|
3172
|
+
try {
|
|
3173
|
+
$("adminAuthHint").textContent = "logging in…";
|
|
3174
|
+
await apiFetch("/daemon/auth/login", { method: "POST", body: JSON.stringify({ password: pw }) });
|
|
3175
|
+
$("adminPasswordInput").value = "";
|
|
3176
|
+
$("oldAdminPasswordInput").value = "";
|
|
3177
|
+
$("newAdminPasswordInput").value = "";
|
|
3178
|
+
toast("Logged in.", "ok");
|
|
3179
|
+
await refreshAdminAuthStatus();
|
|
3180
|
+
await refreshStatus();
|
|
3181
|
+
await refreshProviders();
|
|
3182
|
+
await refreshCredentials();
|
|
3183
|
+
await refreshQuota();
|
|
3184
|
+
await refreshRuntimes();
|
|
3185
|
+
await refreshRoutingSources();
|
|
3186
|
+
await loadRouting();
|
|
3187
|
+
} catch (e) {
|
|
3188
|
+
const msg = e && e.message ? e.message : String(e);
|
|
3189
|
+
$("adminAuthHint").textContent = msg;
|
|
3190
|
+
toast(`Login failed: ${msg}`);
|
|
3191
|
+
}
|
|
3192
|
+
});
|
|
3193
|
+
|
|
3194
|
+
$("logoutPasswordBtn").addEventListener("click", async () => {
|
|
3195
|
+
try {
|
|
3196
|
+
$("adminAuthHint").textContent = "logging out…";
|
|
3197
|
+
await apiFetch("/daemon/auth/logout", { method: "POST" });
|
|
3198
|
+
toast("Logged out.", "ok");
|
|
3199
|
+
await refreshAdminAuthStatus();
|
|
3200
|
+
} catch (e) {
|
|
3201
|
+
const msg = e && e.message ? e.message : String(e);
|
|
3202
|
+
$("adminAuthHint").textContent = msg;
|
|
3203
|
+
toast(`Logout failed: ${msg}`);
|
|
3204
|
+
}
|
|
3205
|
+
});
|
|
3206
|
+
|
|
3207
|
+
$("changePasswordBtn").addEventListener("click", async () => {
|
|
3208
|
+
const oldPassword = ($("oldAdminPasswordInput").value || "");
|
|
3209
|
+
const newPassword = ($("newAdminPasswordInput").value || "");
|
|
3210
|
+
try {
|
|
3211
|
+
$("adminAuthHint").textContent = "changing password…";
|
|
3212
|
+
await apiFetch("/daemon/auth/change", {
|
|
3213
|
+
method: "POST",
|
|
3214
|
+
body: JSON.stringify({ oldPassword, newPassword })
|
|
3215
|
+
});
|
|
3216
|
+
$("oldAdminPasswordInput").value = "";
|
|
3217
|
+
$("newAdminPasswordInput").value = "";
|
|
3218
|
+
toast("Password changed.", "ok");
|
|
3219
|
+
await refreshAdminAuthStatus();
|
|
3220
|
+
} catch (e) {
|
|
3221
|
+
const msg = e && e.message ? e.message : String(e);
|
|
3222
|
+
$("adminAuthHint").textContent = msg;
|
|
3223
|
+
toast(`Change password failed: ${msg}`);
|
|
3224
|
+
}
|
|
3225
|
+
});
|
|
3226
|
+
|
|
3227
|
+
$("saveApiKeyBtn").addEventListener("click", () => {
|
|
3228
|
+
const value = ($("apiKeyInput").value || "").trim();
|
|
3229
|
+
setApiKey(value);
|
|
3230
|
+
$("apiKeyHint").textContent = value ? "saved (session only)" : "";
|
|
3231
|
+
Promise.resolve()
|
|
3232
|
+
.then(refreshStatus)
|
|
3233
|
+
.then(() => selectTab(getActiveTab()))
|
|
3234
|
+
.catch(() => {});
|
|
3235
|
+
});
|
|
3236
|
+
$("clearApiKeyBtn").addEventListener("click", () => {
|
|
3237
|
+
setApiKey("");
|
|
3238
|
+
$("apiKeyInput").value = "";
|
|
3239
|
+
$("apiKeyHint").textContent = "";
|
|
3240
|
+
Promise.resolve()
|
|
3241
|
+
.then(refreshStatus)
|
|
3242
|
+
.then(() => selectTab(getActiveTab()))
|
|
3243
|
+
.catch(() => {});
|
|
3244
|
+
});
|
|
3245
|
+
|
|
3246
|
+
$("restartRuntimeBtn").addEventListener("click", async () => {
|
|
3247
|
+
setLog("providerOpLog", "");
|
|
3248
|
+
if (!confirm("Reload config from disk and rebuild runtime now?")) return;
|
|
3249
|
+
try {
|
|
3250
|
+
const out = await apiFetch("/daemon/restart", { method: "POST" });
|
|
3251
|
+
const warnings = Array.isArray(out.warnings) && out.warnings.length ? `\nWarnings:\n- ${out.warnings.join("\n- ")}` : "";
|
|
3252
|
+
setLog("providerOpLog", `Restarted.\nconfigPath: ${out.configPath || "—"}\nreloadedAt: ${out.reloadedAt || "—"}${warnings}`);
|
|
3253
|
+
await refreshStatus();
|
|
3254
|
+
await refreshProviders();
|
|
3255
|
+
await refreshCredentials();
|
|
3256
|
+
await refreshQuota();
|
|
3257
|
+
await refreshQuotaSnapshot();
|
|
3258
|
+
await refreshRuntimes();
|
|
3259
|
+
} catch (e) {
|
|
3260
|
+
setLog("providerOpLog", `Restart failed: ${e.message}`);
|
|
3261
|
+
}
|
|
3262
|
+
});
|
|
3263
|
+
|
|
3264
|
+
$("refreshProvidersBtn").addEventListener("click", refreshProviders);
|
|
3265
|
+
$("refreshTokensBtn").addEventListener("click", refreshTokens);
|
|
3266
|
+
$("newProviderBtn").addEventListener("click", () => {
|
|
3267
|
+
$("providerEditorTitle").textContent = "Provider editor (new)";
|
|
3268
|
+
$("providerIdInput").value = "";
|
|
3269
|
+
setSelectedProviderId("");
|
|
3270
|
+
providerEditorSetValue(presetFor($("providerPreset").value));
|
|
3271
|
+
setLog("providerOpLog", "");
|
|
3272
|
+
});
|
|
3273
|
+
$("providersTbody").addEventListener("click", async (ev) => {
|
|
3274
|
+
const btn = ev.target.closest("button");
|
|
3275
|
+
if (btn) {
|
|
3276
|
+
const id = btn.getAttribute("data-id");
|
|
3277
|
+
const action = btn.getAttribute("data-action");
|
|
3278
|
+
if (action === "test" && id) {
|
|
3279
|
+
setSelectedProviderId(id);
|
|
3280
|
+
await testProviderFromPool(id);
|
|
3281
|
+
return;
|
|
3282
|
+
}
|
|
3283
|
+
if (action === "edit" && id) {
|
|
3284
|
+
await loadProvider(id);
|
|
3285
|
+
return;
|
|
3286
|
+
}
|
|
3287
|
+
if (action === "delete" && id) {
|
|
3288
|
+
await deleteProvider(id);
|
|
3289
|
+
return;
|
|
3290
|
+
}
|
|
3291
|
+
return;
|
|
3292
|
+
}
|
|
3293
|
+
|
|
3294
|
+
const row = ev.target.closest("tr.provider-row");
|
|
3295
|
+
if (!row) return;
|
|
3296
|
+
const id = row.getAttribute("data-provider-id");
|
|
3297
|
+
if (!id) return;
|
|
3298
|
+
await loadProvider(id);
|
|
3299
|
+
});
|
|
3300
|
+
$("loadProviderBtn").addEventListener("click", async () => {
|
|
3301
|
+
const id = ($("providerIdInput").value || "").trim();
|
|
3302
|
+
if (id) await loadProvider(id);
|
|
3303
|
+
});
|
|
3304
|
+
$("applyPresetBtn").addEventListener("click", applyPresetToEditor);
|
|
3305
|
+
$("saveProviderBtn").addEventListener("click", saveProvider);
|
|
3306
|
+
$("deleteProviderBtn").addEventListener("click", async () => {
|
|
3307
|
+
const id = ($("providerIdInput").value || "").trim();
|
|
3308
|
+
await deleteProvider(id);
|
|
3309
|
+
});
|
|
3310
|
+
$("createApiKeyCredentialBtn").addEventListener("click", async () => {
|
|
3311
|
+
const secretRef = await createApiKeyCredential();
|
|
3312
|
+
if (secretRef) {
|
|
3313
|
+
providerEditorSetAuthApiKey(secretRef);
|
|
3314
|
+
toast("Authfile created and applied to provider editor.", "ok");
|
|
3315
|
+
}
|
|
3316
|
+
});
|
|
3317
|
+
|
|
3318
|
+
$("providerEditorExpandBtn").addEventListener("click", () => setAllDetailsOpen($("providerKvEditor"), true, true));
|
|
3319
|
+
$("providerEditorCollapseBtn").addEventListener("click", () => setAllDetailsOpen($("providerKvEditor"), false, true));
|
|
3320
|
+
|
|
3321
|
+
$("authMode").addEventListener("change", updateAuthModeUi);
|
|
3322
|
+
updateAuthModeUi();
|
|
3323
|
+
|
|
3324
|
+
$("refreshCredentialsBtn").addEventListener("click", refreshCredentials);
|
|
3325
|
+
$("saveOauthBrowserBtn").addEventListener("click", saveSettings);
|
|
3326
|
+
$("oauthAuthorizeBtn").addEventListener("click", authorizeOauth);
|
|
3327
|
+
|
|
3328
|
+
$("refreshQuotaBtn").addEventListener("click", refreshQuota);
|
|
3329
|
+
$("refreshQuotaSnapshotBtn").addEventListener("click", refreshQuotaSnapshotNow);
|
|
3330
|
+
$("quotaFilterInput").addEventListener("input", renderQuotaProviders);
|
|
3331
|
+
$("quotaHideOkToggle").addEventListener("change", renderQuotaProviders);
|
|
3332
|
+
$("quotaOnlyRoutedTargetsToggle").addEventListener("change", renderQuotaProviders);
|
|
3333
|
+
$("quotaApplyDisableBtn").addEventListener("click", () => void quotaAction("disable", $("quotaKeyInput").value));
|
|
3334
|
+
$("quotaApplyRecoverBtn").addEventListener("click", () => void quotaAction("recover", $("quotaKeyInput").value));
|
|
3335
|
+
$("quotaApplyResetBtn").addEventListener("click", () => void quotaAction("reset", $("quotaKeyInput").value));
|
|
3336
|
+
$("quotaTbody").addEventListener("click", (ev) => {
|
|
3337
|
+
const tr = ev.target.closest("tr");
|
|
3338
|
+
if (tr && tr.getAttribute) {
|
|
3339
|
+
const pk = tr.getAttribute("data-provider-key");
|
|
3340
|
+
if (pk) $("quotaKeyInput").value = pk;
|
|
3341
|
+
}
|
|
3342
|
+
const el = ev.target.closest("button");
|
|
3343
|
+
if (!el) return;
|
|
3344
|
+
const action = el.getAttribute("data-action");
|
|
3345
|
+
const key = el.getAttribute("data-key");
|
|
3346
|
+
if (key) $("quotaKeyInput").value = key;
|
|
3347
|
+
if (action === "quota-recover") void quotaAction("recover", key);
|
|
3348
|
+
else if (action === "quota-reset") void quotaAction("reset", key);
|
|
3349
|
+
else if (action === "quota-disable") void quotaAction("disable", key);
|
|
3350
|
+
});
|
|
3351
|
+
$("resetQuotaBtn").addEventListener("click", resetQuotaModule);
|
|
3352
|
+
|
|
3353
|
+
$("loadRoutingBtn").addEventListener("click", loadRouting);
|
|
3354
|
+
$("saveRoutingBtn").addEventListener("click", saveRouting);
|
|
3355
|
+
$("refreshRoutingSourcesBtn").addEventListener("click", refreshRoutingSources);
|
|
3356
|
+
$("routingSourceSelect").addEventListener("change", loadRouting);
|
|
3357
|
+
$("routingRegExpandBtn").addEventListener("click", () => setAllDetailsOpen($("routingKvEditor"), true));
|
|
3358
|
+
$("routingRegCollapseBtn").addEventListener("click", () => setAllDetailsOpen($("routingKvEditor"), false));
|
|
3359
|
+
$("refreshRoutingPoolBtn").addEventListener("click", async () => {
|
|
3360
|
+
try {
|
|
3361
|
+
await refreshRuntimes();
|
|
3362
|
+
toast("Pool status refreshed.", "ok");
|
|
3363
|
+
} catch (e) {
|
|
3364
|
+
toast(`Refresh failed: ${e && e.message ? e.message : String(e)}`);
|
|
3365
|
+
}
|
|
3366
|
+
});
|
|
3367
|
+
$("routingShowPathsToggle").addEventListener("change", () => {
|
|
3368
|
+
try {
|
|
3369
|
+
document.body.classList.toggle("show-routing-paths", Boolean($("routingShowPathsToggle").checked));
|
|
3370
|
+
} catch {}
|
|
3371
|
+
});
|
|
3372
|
+
|
|
3373
|
+
// Init
|
|
3374
|
+
(async () => {
|
|
3375
|
+
const auth = await refreshAdminAuthStatus();
|
|
3376
|
+
const savedKey = getApiKey();
|
|
3377
|
+
if (savedKey) {
|
|
3378
|
+
$("apiKeyHint").textContent = "saved (session only)";
|
|
3379
|
+
$("apiKeyInput").value = savedKey;
|
|
3380
|
+
}
|
|
3381
|
+
await refreshStatus();
|
|
3382
|
+
await refreshProviders();
|
|
3383
|
+
await refreshCredentials();
|
|
3384
|
+
await refreshQuota();
|
|
3385
|
+
await refreshRuntimes();
|
|
3386
|
+
if (auth && auth.authenticated) {
|
|
3387
|
+
await refreshRoutingSources();
|
|
3388
|
+
await loadRouting();
|
|
3389
|
+
}
|
|
3390
|
+
await loadSettings();
|
|
3391
|
+
})();
|
|
3392
|
+
</script>
|
|
3393
|
+
</body>
|
|
3394
|
+
</html>
|