@jingyi0605/codingns4dsh 0.2.1-rc.2 → 0.2.1-rc.3
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.en.md +6 -4
- package/README.md +6 -6
- package/data/build/dist/bootstrap/dsh-connection-adapter.d.ts +1 -1
- package/data/build/dist/bootstrap/index.d.ts +1 -1
- package/data/build/dist/client/bundle.js +5306 -493
- package/data/build/dist/client/bundle.js.map +1 -1
- package/data/build/dist/client/client.magpie-model-tag.js +155 -5
- package/data/build/dist/client/client.magpie-model-tag.js.map +1 -1
- package/data/build/dist/client/client.native-workspace-store.js +30 -4
- package/data/build/dist/client/client.native-workspace-store.js.map +1 -1
- package/data/build/dist/client/client.transport.js +725 -48
- package/data/build/dist/client/client.transport.js.map +1 -1
- package/data/build/dist/client/desktop-assistant.js +931 -185
- package/data/build/dist/client/feature-requirement.d.ts +43 -0
- package/data/build/dist/client/feature-requirement.d.ts.map +1 -0
- package/data/build/dist/client/feature-requirement.js +105 -0
- package/data/build/dist/client/feature-requirement.js.map +1 -0
- package/data/build/dist/client/features/index.d.ts +1 -0
- package/data/build/dist/client/features/index.d.ts.map +1 -1
- package/data/build/dist/client/features/index.js +3 -0
- package/data/build/dist/client/features/index.js.map +1 -1
- package/data/build/dist/client/features/magpie-management-controls.d.ts +46 -0
- package/data/build/dist/client/features/magpie-management-controls.d.ts.map +1 -0
- package/data/build/dist/client/features/magpie-management-controls.js +759 -0
- package/data/build/dist/client/features/magpie-management-controls.js.map +1 -0
- package/data/build/dist/client/features/magpie-management-page.d.ts +124 -0
- package/data/build/dist/client/features/magpie-management-page.d.ts.map +1 -0
- package/data/build/dist/client/features/magpie-management-page.js +1256 -0
- package/data/build/dist/client/features/magpie-management-page.js.map +1 -0
- package/data/build/dist/client/features/magpie-management.d.ts +7 -0
- package/data/build/dist/client/features/magpie-management.d.ts.map +1 -0
- package/data/build/dist/client/features/magpie-management.js +154 -0
- package/data/build/dist/client/features/magpie-management.js.map +1 -0
- package/data/build/dist/client/features/magpie-mark.d.ts +10 -0
- package/data/build/dist/client/features/magpie-mark.d.ts.map +1 -0
- package/data/build/dist/client/features/magpie-mark.js +21 -0
- package/data/build/dist/client/features/magpie-mark.js.map +1 -0
- package/data/build/dist/client/features/magpie-provider-logo.d.ts +11 -0
- package/data/build/dist/client/features/magpie-provider-logo.d.ts.map +1 -0
- package/data/build/dist/client/features/magpie-provider-logo.js +35 -0
- package/data/build/dist/client/features/magpie-provider-logo.js.map +1 -0
- package/data/build/dist/client/features/subscription-quota-groups.d.ts +193 -0
- package/data/build/dist/client/features/subscription-quota-groups.d.ts.map +1 -0
- package/data/build/dist/client/features/subscription-quota-groups.js +548 -0
- package/data/build/dist/client/features/subscription-quota-groups.js.map +1 -0
- package/data/build/dist/client/git-management.d.ts.map +1 -1
- package/data/build/dist/client/git-management.js +11 -9
- package/data/build/dist/client/git-management.js.map +1 -1
- package/data/build/dist/client/git-panel-styles.js +1 -0
- package/data/build/dist/client/git-panel-styles.js.map +1 -1
- package/data/build/dist/client/index.d.ts +1 -1
- package/data/build/dist/client/index.d.ts.map +1 -1
- package/data/build/dist/client/index.js +1 -1
- package/data/build/dist/client/index.js.map +1 -1
- package/data/build/dist/client/locale.d.ts.map +1 -1
- package/data/build/dist/client/locale.js +7 -0
- package/data/build/dist/client/locale.js.map +1 -1
- package/data/build/dist/client/locales/magpie.d.ts +4 -0
- package/data/build/dist/client/locales/magpie.d.ts.map +1 -0
- package/data/build/dist/client/locales/magpie.js +622 -0
- package/data/build/dist/client/locales/magpie.js.map +1 -0
- package/data/build/dist/client/locales/usage.d.ts.map +1 -1
- package/data/build/dist/client/locales/usage.js +52 -0
- package/data/build/dist/client/locales/usage.js.map +1 -1
- package/data/build/dist/client/magpie-model-tag.d.ts +9 -1
- package/data/build/dist/client/magpie-model-tag.d.ts.map +1 -1
- package/data/build/dist/client/magpie-model-tag.js +15 -3
- package/data/build/dist/client/magpie-model-tag.js.map +1 -1
- package/data/build/dist/client/provider-icons.d.ts +8 -0
- package/data/build/dist/client/provider-icons.d.ts.map +1 -1
- package/data/build/dist/client/provider-icons.js +122 -2
- package/data/build/dist/client/provider-icons.js.map +1 -1
- package/data/build/dist/client/settings-bridge.d.ts +1 -0
- package/data/build/dist/client/settings-bridge.d.ts.map +1 -1
- package/data/build/dist/client/settings-bridge.js +29 -10
- package/data/build/dist/client/settings-bridge.js.map +1 -1
- package/data/build/dist/client/settings-section.d.ts.map +1 -1
- package/data/build/dist/client/settings-section.js +21 -3
- package/data/build/dist/client/settings-section.js.map +1 -1
- package/data/build/dist/client/style.css +224 -221
- package/data/build/dist/client/subscription-slot.d.ts +20 -0
- package/data/build/dist/client/subscription-slot.d.ts.map +1 -1
- package/data/build/dist/client/subscription-slot.js +321 -18
- package/data/build/dist/client/subscription-slot.js.map +1 -1
- package/data/build/dist/dsh-capabilities/client/global-panel-adapter.d.ts +27 -0
- package/data/build/dist/dsh-capabilities/client/global-panel-adapter.d.ts.map +1 -0
- package/data/build/dist/dsh-capabilities/client/global-panel-adapter.js +109 -0
- package/data/build/dist/dsh-capabilities/client/global-panel-adapter.js.map +1 -0
- package/data/build/dist/dsh-capabilities/host/session-title-adapter.d.ts +2 -1
- package/data/build/dist/dsh-capabilities/host/session-title-adapter.d.ts.map +1 -1
- package/data/build/dist/dsh-capabilities/host/session-title-adapter.js +32 -2
- package/data/build/dist/dsh-capabilities/host/session-title-adapter.js.map +1 -1
- package/data/build/dist/dsh-capabilities/matrix.d.ts.map +1 -1
- package/data/build/dist/dsh-capabilities/matrix.js +12 -11
- package/data/build/dist/dsh-capabilities/matrix.js.map +1 -1
- package/data/build/dist/dsh-capabilities/routes.d.ts.map +1 -1
- package/data/build/dist/dsh-capabilities/routes.js +12 -9
- package/data/build/dist/dsh-capabilities/routes.js.map +1 -1
- package/data/build/dist/dsh-capabilities/types.d.ts +1 -1
- package/data/build/dist/dsh-capabilities/types.d.ts.map +1 -1
- package/data/build/dist/dsh-capabilities/types.js.map +1 -1
- package/data/build/dist/host/cli-adapters/acp-cli-driver.d.ts +27 -0
- package/data/build/dist/host/cli-adapters/acp-cli-driver.d.ts.map +1 -1
- package/data/build/dist/host/cli-adapters/acp-cli-driver.js +88 -13
- package/data/build/dist/host/cli-adapters/acp-cli-driver.js.map +1 -1
- package/data/build/dist/host/cli-adapters/codex-driver.d.ts +10 -0
- package/data/build/dist/host/cli-adapters/codex-driver.d.ts.map +1 -1
- package/data/build/dist/host/cli-adapters/codex-driver.js +32 -1
- package/data/build/dist/host/cli-adapters/codex-driver.js.map +1 -1
- package/data/build/dist/host/cli-adapters/command-code-acp-loader.d.ts +6 -5
- package/data/build/dist/host/cli-adapters/command-code-acp-loader.d.ts.map +1 -1
- package/data/build/dist/host/cli-adapters/command-code-acp-loader.js +72 -12
- package/data/build/dist/host/cli-adapters/command-code-acp-loader.js.map +1 -1
- package/data/build/dist/host/cli-adapters/command-code-driver.d.ts.map +1 -1
- package/data/build/dist/host/cli-adapters/command-code-driver.js +26 -1
- package/data/build/dist/host/cli-adapters/command-code-driver.js.map +1 -1
- package/data/build/dist/host/cli-adapters/dsh-message-projector.d.ts.map +1 -1
- package/data/build/dist/host/cli-adapters/dsh-message-projector.js +15 -2
- package/data/build/dist/host/cli-adapters/dsh-message-projector.js.map +1 -1
- package/data/build/dist/host/cli-adapters/feature.d.ts +13 -0
- package/data/build/dist/host/cli-adapters/feature.d.ts.map +1 -1
- package/data/build/dist/host/cli-adapters/feature.js +390 -6
- package/data/build/dist/host/cli-adapters/feature.js.map +1 -1
- package/data/build/dist/host/cli-adapters/grok-driver.js +5 -0
- package/data/build/dist/host/cli-adapters/grok-driver.js.map +1 -1
- package/data/build/dist/host/cli-adapters/magpie-detection.d.ts +72 -0
- package/data/build/dist/host/cli-adapters/magpie-detection.d.ts.map +1 -0
- package/data/build/dist/host/cli-adapters/magpie-detection.js +298 -0
- package/data/build/dist/host/cli-adapters/magpie-detection.js.map +1 -0
- package/data/build/dist/host/cli-adapters/magpie-management.d.ts +50 -0
- package/data/build/dist/host/cli-adapters/magpie-management.d.ts.map +1 -0
- package/data/build/dist/host/cli-adapters/magpie-management.js +1268 -0
- package/data/build/dist/host/cli-adapters/magpie-management.js.map +1 -0
- package/data/build/dist/host/cli-adapters/magpie-update.d.ts +59 -0
- package/data/build/dist/host/cli-adapters/magpie-update.d.ts.map +1 -0
- package/data/build/dist/host/cli-adapters/magpie-update.js +249 -0
- package/data/build/dist/host/cli-adapters/magpie-update.js.map +1 -0
- package/data/build/dist/host/cli-adapters/magpie-usage.d.ts +66 -8
- package/data/build/dist/host/cli-adapters/magpie-usage.d.ts.map +1 -1
- package/data/build/dist/host/cli-adapters/magpie-usage.js +846 -122
- package/data/build/dist/host/cli-adapters/magpie-usage.js.map +1 -1
- package/data/build/dist/host/cli-adapters/magpie-web.d.ts +54 -0
- package/data/build/dist/host/cli-adapters/magpie-web.d.ts.map +1 -0
- package/data/build/dist/host/cli-adapters/magpie-web.js +407 -0
- package/data/build/dist/host/cli-adapters/magpie-web.js.map +1 -0
- package/data/build/dist/host/cli-adapters/process-utils.d.ts +2 -0
- package/data/build/dist/host/cli-adapters/process-utils.d.ts.map +1 -1
- package/data/build/dist/host/cli-adapters/process-utils.js +4 -0
- package/data/build/dist/host/cli-adapters/process-utils.js.map +1 -1
- package/data/build/dist/host/dsh-gateway-feature.d.ts.map +1 -1
- package/data/build/dist/host/dsh-gateway-feature.js +26 -3
- package/data/build/dist/host/dsh-gateway-feature.js.map +1 -1
- package/data/build/dist/host/features/host-status.d.ts +25 -0
- package/data/build/dist/host/features/host-status.d.ts.map +1 -1
- package/data/build/dist/host/features/host-status.js +60 -6
- package/data/build/dist/host/features/host-status.js.map +1 -1
- package/data/build/dist/host/features/lan-access-dsh.d.ts.map +1 -1
- package/data/build/dist/host/features/lan-access-dsh.js +61 -10
- package/data/build/dist/host/features/lan-access-dsh.js.map +1 -1
- package/data/build/dist/host/features/session-translation-protection.d.ts.map +1 -1
- package/data/build/dist/host/features/session-translation-protection.js +2 -1
- package/data/build/dist/host/features/session-translation-protection.js.map +1 -1
- package/data/build/dist/host/lan-access-dsh.d.ts.map +1 -1
- package/data/build/dist/host/lan-access-dsh.js +6 -2
- package/data/build/dist/host/lan-access-dsh.js.map +1 -1
- package/data/build/dist/host/modules/pwa/pwa-session-notifications.js +2 -1
- package/data/build/dist/host/modules/pwa/pwa-session-notifications.js.map +1 -1
- package/data/build/dist/host/rpc.d.ts +15 -2
- package/data/build/dist/host/rpc.d.ts.map +1 -1
- package/data/build/dist/host/rpc.js +47 -6
- package/data/build/dist/host/rpc.js.map +1 -1
- package/data/build/dist/host/settings.d.ts.map +1 -1
- package/data/build/dist/host/settings.js +5 -0
- package/data/build/dist/host/settings.js.map +1 -1
- package/data/build/dist/shared/contracts/config.d.ts +18 -0
- package/data/build/dist/shared/contracts/config.d.ts.map +1 -1
- package/data/build/dist/shared/contracts/config.js +8 -0
- package/data/build/dist/shared/contracts/config.js.map +1 -1
- package/data/build/dist/shared/contracts/errors.d.ts +2 -2
- package/data/build/dist/shared/contracts/feature.d.ts +7 -0
- package/data/build/dist/shared/contracts/feature.d.ts.map +1 -1
- package/data/build/dist/shared/contracts/magpie-management.d.ts +395 -0
- package/data/build/dist/shared/contracts/magpie-management.d.ts.map +1 -0
- package/data/build/dist/shared/contracts/magpie-management.js +2 -0
- package/data/build/dist/shared/contracts/magpie-management.js.map +1 -0
- package/data/build/dist/shared/contracts/subscription.d.ts +75 -1
- package/data/build/dist/shared/contracts/subscription.d.ts.map +1 -1
- package/data/build/dist/shared/contracts/subscription.js.map +1 -1
- package/data/build/dist/shared/contracts/version.d.ts +4 -4
- package/data/build/dist/shared/contracts/version.js +3 -3
- package/data/build/dist/shared/index.d.ts +2 -1
- package/data/build/dist/shared/index.d.ts.map +1 -1
- package/data/build/dist/shared/index.js +1 -1
- package/data/build/dist/shared/index.js.map +1 -1
- package/data/build/dist/shared/session-title.d.ts.map +1 -1
- package/data/build/dist/shared/session-title.js +10 -4
- package/data/build/dist/shared/session-title.js.map +1 -1
- package/package.json +29 -29
- package/profile/README.md +2 -2
- package/profile/package.json +3 -3
- package/profile/version.json +3 -3
- package/version.json +3 -3
|
@@ -7,8 +7,10 @@ const MAX_MAGPIE_MODEL_METADATA_CACHE_ENTRIES = 4;
|
|
|
7
7
|
const MAGPIE_MINIMUM_CLI_VERSION = [0, 1, 1295];
|
|
8
8
|
const MAGPIE_CLI_TIMEOUT_MS = 10_000;
|
|
9
9
|
const MAGPIE_HTTP_TIMEOUT_MS = 3_000;
|
|
10
|
+
const MAGPIE_DASHBOARD_CACHE_TTL_MS = 15_000;
|
|
10
11
|
const MAX_MAGPIE_CLI_OUTPUT_BYTES = 512 * 1024;
|
|
11
12
|
const MAX_MAGPIE_RESPONSE_BYTES = 2 * 1024 * 1024;
|
|
13
|
+
const MAX_MAGPIE_ICON_FETCHES = 64;
|
|
12
14
|
const MAX_MAGPIE_MODELS = 10_000;
|
|
13
15
|
const MAX_MODEL_TOKEN_LIMIT = 1_000_000_000;
|
|
14
16
|
const MAGPIE_MODEL_ID_PATTERN = '[A-Za-z0-9][A-Za-z0-9._-]*(?:/[A-Za-z0-9][A-Za-z0-9._:/-]*)+';
|
|
@@ -39,11 +41,7 @@ const MAGPIE_USAGE_AGENT_ALIASES = {
|
|
|
39
41
|
// usage ledger, while its verified model catalog uses `commandcode`.
|
|
40
42
|
commandcode: ['commandcode', 'codingns4dsh'],
|
|
41
43
|
};
|
|
42
|
-
/**
|
|
43
|
-
* Reads Magpie's public local Gateway API only after its CLI confirms that the
|
|
44
|
-
* current Agent model is in that Agent's catalog. No Agent or Magpie config
|
|
45
|
-
* files are opened here; the provider session ID is supplied by the Host.
|
|
46
|
-
*/
|
|
44
|
+
/** Reads usage from the authenticated local Magpie Web API and exact-matches session identity. */
|
|
47
45
|
export class MagpieUsageService {
|
|
48
46
|
readModels;
|
|
49
47
|
readVersion;
|
|
@@ -51,6 +49,8 @@ export class MagpieUsageService {
|
|
|
51
49
|
now;
|
|
52
50
|
timeoutMs;
|
|
53
51
|
lifetimeSignal;
|
|
52
|
+
webApi;
|
|
53
|
+
onDashboardTiming;
|
|
54
54
|
/** Agent CLI verification remains isolated; model metadata below is Gateway-wide and shared. */
|
|
55
55
|
catalogs = new Map();
|
|
56
56
|
catalogLoads = new Map();
|
|
@@ -59,6 +59,9 @@ export class MagpieUsageService {
|
|
|
59
59
|
modelMetadataLoads = new Map();
|
|
60
60
|
versionCheck;
|
|
61
61
|
versionLoad;
|
|
62
|
+
dashboardCache = new Map();
|
|
63
|
+
dashboardRefreshes = new Map();
|
|
64
|
+
quotaLogoCache = new Map();
|
|
62
65
|
constructor(options = {}) {
|
|
63
66
|
this.readModels = options.readModels ?? readMagpieModels;
|
|
64
67
|
// An injected catalog reader represents the whole fake CLI boundary in
|
|
@@ -70,6 +73,8 @@ export class MagpieUsageService {
|
|
|
70
73
|
this.now = options.now ?? Date.now;
|
|
71
74
|
this.timeoutMs = options.timeoutMs ?? MAGPIE_HTTP_TIMEOUT_MS;
|
|
72
75
|
this.lifetimeSignal = options.lifetimeSignal;
|
|
76
|
+
this.webApi = options.webApi;
|
|
77
|
+
this.onDashboardTiming = options.onDashboardTiming;
|
|
73
78
|
}
|
|
74
79
|
async read(session, signal, includeRoute = false, dshSessionId) {
|
|
75
80
|
signal?.throwIfAborted();
|
|
@@ -77,10 +82,6 @@ export class MagpieUsageService {
|
|
|
77
82
|
const requestedModelId = cleanText(session.modelId);
|
|
78
83
|
if (agent === null || requestedModelId === null)
|
|
79
84
|
return null;
|
|
80
|
-
const catalog = await this.loadCatalog(agent, signal);
|
|
81
|
-
const modelId = resolveCatalogModelId(agent, requestedModelId, catalog, requestedModelId);
|
|
82
|
-
if (catalog === null || modelId === null)
|
|
83
|
-
return null;
|
|
84
85
|
const providerSessionId = cleanText(session.providerSessionId);
|
|
85
86
|
const nativeSessionId = cleanText(dshSessionId);
|
|
86
87
|
// Native DSH requests use the DSH session ID as their Magpie session
|
|
@@ -88,7 +89,19 @@ export class MagpieUsageService {
|
|
|
88
89
|
const sessionId = providerSessionId !== null && providerSessionId !== nativeSessionId
|
|
89
90
|
? providerSessionId
|
|
90
91
|
: agent === 'dsh' ? nativeSessionId : null;
|
|
91
|
-
|
|
92
|
+
// Command Code's ACP child tags loopback Magpie requests with the DSH
|
|
93
|
+
// session ID injected by the Host, so the ledger key differs from the ACP
|
|
94
|
+
// session ID shown in the panel. Older Hosts without that ID keep the
|
|
95
|
+
// provider ID so the panel still reports the same "no usage" state.
|
|
96
|
+
const magpieSessionId = agent === 'commandcode' ? nativeSessionId ?? sessionId : sessionId;
|
|
97
|
+
// During a rolling update the ACP child may still tag requests with its ACP
|
|
98
|
+
// session ID; fall back to it when the DSH-ID ledger row is absent.
|
|
99
|
+
const fallbackSessionId = agent === 'commandcode' && nativeSessionId !== null && sessionId !== null && nativeSessionId !== sessionId
|
|
100
|
+
? sessionId
|
|
101
|
+
: null;
|
|
102
|
+
if (magpieSessionId === null)
|
|
103
|
+
return null;
|
|
104
|
+
return this.readFromWeb(agent, requestedModelId, sessionId, magpieSessionId, fallbackSessionId, signal, includeRoute);
|
|
92
105
|
}
|
|
93
106
|
/**
|
|
94
107
|
* Mark Magpie models and enrich their public metadata. The marker is
|
|
@@ -174,47 +187,201 @@ export class MagpieUsageService {
|
|
|
174
187
|
const metadata = await this.loadModelMetadata(catalog, signal);
|
|
175
188
|
return metadata?.get(id) ?? null;
|
|
176
189
|
}
|
|
177
|
-
|
|
190
|
+
/** Read bounded, graph-ready usage and quota data through authenticated Magpie Web. */
|
|
191
|
+
async readDashboard(period, signal) {
|
|
192
|
+
signal?.throwIfAborted();
|
|
193
|
+
const cached = this.dashboardCache.get(period);
|
|
194
|
+
if (cached !== undefined) {
|
|
195
|
+
if (this.now() - cached.loadedAt >= MAGPIE_DASHBOARD_CACHE_TTL_MS) {
|
|
196
|
+
void this.refreshDashboard(period).catch(() => undefined);
|
|
197
|
+
}
|
|
198
|
+
return cached.dashboard;
|
|
199
|
+
}
|
|
200
|
+
return this.refreshDashboard(period, signal);
|
|
201
|
+
}
|
|
202
|
+
/** Stream cumulative dashboard snapshots as each independent Web API part is ready. */
|
|
203
|
+
async *readDashboardStream(period, signal, force = false) {
|
|
204
|
+
signal?.throwIfAborted();
|
|
205
|
+
const cached = this.dashboardCache.get(period);
|
|
206
|
+
if (!force && cached !== undefined) {
|
|
207
|
+
const stale = this.now() - cached.loadedAt >= MAGPIE_DASHBOARD_CACHE_TTL_MS;
|
|
208
|
+
yield { part: 'cache', dashboard: cached.dashboard, stale };
|
|
209
|
+
if (!stale)
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
let latest;
|
|
213
|
+
for await (const chunk of this.readDashboardNetwork(period, signal)) {
|
|
214
|
+
latest = chunk.dashboard;
|
|
215
|
+
yield chunk;
|
|
216
|
+
}
|
|
217
|
+
if (latest !== undefined)
|
|
218
|
+
this.dashboardCache.set(period, { dashboard: latest, loadedAt: this.now() });
|
|
219
|
+
}
|
|
220
|
+
async refreshDashboard(period, signal) {
|
|
221
|
+
const pending = this.dashboardRefreshes.get(period);
|
|
222
|
+
if (pending !== undefined)
|
|
223
|
+
return awaitWithSignal(pending, signal);
|
|
224
|
+
const request = (async () => {
|
|
225
|
+
let latest;
|
|
226
|
+
for await (const chunk of this.readDashboardNetwork(period, signal))
|
|
227
|
+
latest = chunk.dashboard;
|
|
228
|
+
if (latest === undefined)
|
|
229
|
+
throw new Error('Magpie dashboard returned no snapshot');
|
|
230
|
+
this.dashboardCache.set(period, { dashboard: latest, loadedAt: this.now() });
|
|
231
|
+
return latest;
|
|
232
|
+
})().finally(() => {
|
|
233
|
+
if (this.dashboardRefreshes.get(period) === request)
|
|
234
|
+
this.dashboardRefreshes.delete(period);
|
|
235
|
+
});
|
|
236
|
+
this.dashboardRefreshes.set(period, request);
|
|
237
|
+
return awaitWithSignal(request, signal);
|
|
238
|
+
}
|
|
239
|
+
async *readDashboardNetwork(period, signal) {
|
|
240
|
+
const capturedAt = new Date(this.now()).toISOString();
|
|
241
|
+
let dashboard = emptyMagpieDashboard(period, capturedAt);
|
|
242
|
+
let quotas = null;
|
|
243
|
+
let logoLoad;
|
|
244
|
+
const results = new Map([
|
|
245
|
+
['usage', this.timedDashboardRead('usage', () => this.readWebJson(`/api/usage?period=${magpieWebPeriod(period)}`, signal))],
|
|
246
|
+
['quotas', this.timedDashboardRead('quotas', () => this.readWebJson('/api/usage/quotas', signal))],
|
|
247
|
+
['history', this.timedDashboardRead('history', () => this.readWebJson('/api/usage/quotas/history?days=30', signal))],
|
|
248
|
+
]);
|
|
249
|
+
const completed = new Map();
|
|
250
|
+
while (results.size > 0) {
|
|
251
|
+
const result = await Promise.race(results.values());
|
|
252
|
+
results.delete(result.part);
|
|
253
|
+
const response = asRecord(result.value);
|
|
254
|
+
const partOk = result.ok && response?.ok === true;
|
|
255
|
+
const partValue = partOk ? response.value : undefined;
|
|
256
|
+
completed.set(result.part, partOk);
|
|
257
|
+
if (result.part === 'usage') {
|
|
258
|
+
const parsed = partOk ? parseMagpieDashboardUsage(partValue) : null;
|
|
259
|
+
dashboard = {
|
|
260
|
+
...dashboard,
|
|
261
|
+
usageStatus: parsed === null ? 'unavailable' : 'available',
|
|
262
|
+
aggregate: parsed?.aggregate ?? null,
|
|
263
|
+
series: parsed?.series ?? [],
|
|
264
|
+
agents: parsed?.agents ?? [],
|
|
265
|
+
providers: parsed?.providers ?? [],
|
|
266
|
+
models: parsed?.models ?? [],
|
|
267
|
+
accounts: parsed?.accounts ?? [],
|
|
268
|
+
sessions: parsed?.sessions ?? [],
|
|
269
|
+
sessionCount: parsed?.sessionCount ?? null,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
else if (result.part === 'quotas') {
|
|
273
|
+
quotas = partOk ? parseMagpieQuotaCards(partValue) : null;
|
|
274
|
+
dashboard = { ...dashboard, quotaStatus: quotas === null ? 'unavailable' : 'available', quotas: quotas ?? [] };
|
|
275
|
+
if (quotas !== null)
|
|
276
|
+
logoLoad = this.timedDashboardRead('quota-logos', () => this.attachQuotaLogos(quotas, signal)).then((result) => result.value);
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
const parsed = partOk ? parseMagpieQuotaHistory(partValue) : null;
|
|
280
|
+
dashboard = { ...dashboard, quotaHistoryStatus: parsed === null ? 'unavailable' : 'available', quotaHistory: parsed ?? [] };
|
|
281
|
+
}
|
|
282
|
+
yield { part: result.part, dashboard, stale: false };
|
|
283
|
+
}
|
|
284
|
+
dashboard = { ...dashboard, availabilityIssue: [...completed.values()].every((ok) => !ok) ? 'web-api-unavailable' : null };
|
|
285
|
+
yield { part: 'complete', dashboard, stale: false };
|
|
286
|
+
if (logoLoad !== undefined) {
|
|
287
|
+
dashboard = { ...dashboard, quotas: await logoLoad };
|
|
288
|
+
yield { part: 'logos', dashboard, stale: false };
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
async timedDashboardRead(part, read) {
|
|
292
|
+
const started = this.now();
|
|
293
|
+
try {
|
|
294
|
+
return { part: part === 'quota-logos' ? 'logos' : part, value: await read(), ok: true };
|
|
295
|
+
}
|
|
296
|
+
finally {
|
|
297
|
+
this.onDashboardTiming?.({ part, elapsedMs: Math.max(0, this.now() - started) });
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
/** Read one bounded Web API ledger page and exact-match session identities on Host. */
|
|
301
|
+
async readRequestPage(options, signal) {
|
|
302
|
+
signal?.throwIfAborted();
|
|
303
|
+
if (!isMagpieDashboardPeriod(options.period)
|
|
304
|
+
|| !Number.isSafeInteger(options.offset) || options.offset < 0 || options.offset > 1_000_000
|
|
305
|
+
|| !Number.isSafeInteger(options.limit) || options.limit < 1 || options.limit > 100)
|
|
306
|
+
return null;
|
|
307
|
+
const sessionId = options.sessionId === undefined ? null : cleanText(options.sessionId);
|
|
308
|
+
const sessionAgent = options.sessionAgent === undefined ? null : cleanText(options.sessionAgent);
|
|
309
|
+
if ((options.sessionId !== undefined && sessionId === null)
|
|
310
|
+
|| (options.sessionAgent !== undefined && (sessionAgent === null || !isMagpieAgentId(sessionAgent)))
|
|
311
|
+
|| ((sessionId === null) !== (sessionAgent === null)))
|
|
312
|
+
return null;
|
|
313
|
+
const query = new URLSearchParams({ period: magpieWebPeriod(options.period), limit: String(options.limit), offset: String(options.offset) });
|
|
314
|
+
if (sessionId !== null)
|
|
315
|
+
query.set('q', sessionId);
|
|
316
|
+
const response = await this.readWebJson(`/api/usage/requests?${query.toString()}`, signal);
|
|
317
|
+
if (!response.ok)
|
|
318
|
+
return null;
|
|
319
|
+
const root = asRecord(response.value);
|
|
320
|
+
const rawRows = root?.rows;
|
|
321
|
+
if (!Array.isArray(rawRows) || rawRows.length > 500)
|
|
322
|
+
return null;
|
|
323
|
+
const candidates = rawRows.flatMap((row) => {
|
|
324
|
+
const record = asRecord(row);
|
|
325
|
+
if (record === null)
|
|
326
|
+
return [];
|
|
327
|
+
const candidateSession = cleanText(record.session);
|
|
328
|
+
const nativeSession = cleanText(record.native_session);
|
|
329
|
+
const agent = cleanText(record.agent);
|
|
330
|
+
const acceptedAgents = sessionAgent === null ? [] : MAGPIE_USAGE_AGENT_ALIASES[sessionAgent] ?? [sessionAgent];
|
|
331
|
+
if (sessionId !== null
|
|
332
|
+
&& (candidateSession !== sessionId && nativeSession !== sessionId || agent === null || !acceptedAgents.includes(agent)))
|
|
333
|
+
return [];
|
|
334
|
+
const item = parseMagpieRequest(record);
|
|
335
|
+
return item === null ? [] : [sessionId === null ? item : { ...item, sessionId }];
|
|
336
|
+
});
|
|
337
|
+
const total = sessionId === null ? safeCount(root?.total) : null;
|
|
338
|
+
return {
|
|
339
|
+
period: options.period,
|
|
340
|
+
offset: options.offset,
|
|
341
|
+
limit: options.limit,
|
|
342
|
+
total,
|
|
343
|
+
hasMore: total === null ? rawRows.length >= options.limit : options.offset + rawRows.length < total,
|
|
344
|
+
sessionId,
|
|
345
|
+
sessionAgent,
|
|
346
|
+
items: candidates.slice(0, options.limit),
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
async readFromWeb(agent, modelId, sessionId, magpieSessionId, fallbackSessionId, signal, includeRoute) {
|
|
178
350
|
const capturedAt = new Date(this.now()).toISOString();
|
|
179
|
-
if (gatewayOrigin === null)
|
|
180
|
-
return unavailableMagpieSnapshot(agent, modelId, sessionId, this.now(), capturedAt);
|
|
181
351
|
const [usageResult, quotaResult] = await Promise.all([
|
|
182
|
-
this.
|
|
183
|
-
this.
|
|
352
|
+
this.readWebJson('/api/usage?period=today', signal),
|
|
353
|
+
this.readWebJson('/api/usage/quotas', signal),
|
|
184
354
|
]);
|
|
185
355
|
const aggregate = usageResult.ok ? readTotals(usageResult.value) : null;
|
|
186
|
-
const
|
|
187
|
-
? readSession(usageResult.value, agent,
|
|
356
|
+
const primarySession = usageResult.ok && magpieSessionId !== null
|
|
357
|
+
? readSession(usageResult.value, agent, magpieSessionId)
|
|
358
|
+
: null;
|
|
359
|
+
const fallbackSession = primarySession === null && usageResult.ok && fallbackSessionId !== null
|
|
360
|
+
? readSession(usageResult.value, agent, fallbackSessionId)
|
|
188
361
|
: null;
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
? { status: 'session-unavailable', route: null }
|
|
202
|
-
: !includeRoute
|
|
203
|
-
? { status: 'not-requested', route: null }
|
|
204
|
-
: !canReadRoute
|
|
205
|
-
? { status: 'unavailable', route: null }
|
|
206
|
-
: !routeResult.ok
|
|
207
|
-
? { status: 'unavailable', route: null }
|
|
208
|
-
: readRoute(routeResult.value, sessionId);
|
|
209
|
-
const routeHistory = routeHistoryResult.ok && sessionId !== null && sessionUsage !== null
|
|
210
|
-
? readSessionRouteHistory(routeHistoryResult.value, agent, sessionId)
|
|
362
|
+
const sessionUsage = primarySession ?? fallbackSession;
|
|
363
|
+
const resolvedSessionId = primarySession !== null ? magpieSessionId : fallbackSession !== null ? fallbackSessionId : magpieSessionId;
|
|
364
|
+
if (sessionUsage === null)
|
|
365
|
+
return null;
|
|
366
|
+
const canReadRoute = includeRoute && resolvedSessionId !== null && resolvedSessionId.length <= 128;
|
|
367
|
+
const routeHistoryResult = canReadRoute
|
|
368
|
+
? await this.readWebJson(`/api/usage/requests?period=today&offset=0&limit=100&q=${encodeURIComponent(resolvedSessionId)}`, signal)
|
|
369
|
+
: { ok: false };
|
|
370
|
+
const routeRows = routeHistoryResult.ok ? asRecord(routeHistoryResult.value)?.rows : null;
|
|
371
|
+
const routeHistoryValid = Array.isArray(routeRows) && routeRows.length <= 500;
|
|
372
|
+
const routeHistory = routeHistoryResult.ok && routeHistoryValid && resolvedSessionId !== null
|
|
373
|
+
? readSessionRouteHistory(routeHistoryResult.value, agent, resolvedSessionId)
|
|
211
374
|
: [];
|
|
212
|
-
const route =
|
|
213
|
-
? routeHistory[0]?.route ?? null
|
|
214
|
-
: enrichRouteWithHistory(routeRead.route, routeHistory);
|
|
375
|
+
const route = routeHistory[0]?.route ?? null;
|
|
215
376
|
const routeStatus = route !== null
|
|
216
377
|
? 'available'
|
|
217
|
-
:
|
|
378
|
+
: resolvedSessionId === null
|
|
379
|
+
? 'session-unavailable'
|
|
380
|
+
: !includeRoute
|
|
381
|
+
? 'not-requested'
|
|
382
|
+
: !canReadRoute || !routeHistoryValid
|
|
383
|
+
? 'unavailable'
|
|
384
|
+
: 'no-route';
|
|
218
385
|
const allQuotas = quotaResult.ok ? parseMagpieQuotas(quotaResult.value) : null;
|
|
219
386
|
const routeProviders = [...new Set([
|
|
220
387
|
providerFromRoute(route),
|
|
@@ -224,9 +391,15 @@ export class MagpieUsageService {
|
|
|
224
391
|
? routeProviders
|
|
225
392
|
: [providerFromModelId(modelId, agent)].filter((provider) => provider !== null);
|
|
226
393
|
const upstreamProvider = upstreamProviders[0] ?? null;
|
|
227
|
-
const
|
|
394
|
+
const sessionQuotas = allQuotas === null
|
|
228
395
|
? []
|
|
229
396
|
: filterSessionQuotas(allQuotas, upstreamProviders, routeHistory);
|
|
397
|
+
const matchingQuotas = markSessionAccounts(sessionQuotas, routeHistory, route);
|
|
398
|
+
const quotas = allQuotas === null ? [] : await this.attachQuotaLogos(matchingQuotas, signal);
|
|
399
|
+
// 同厂商的其它账号单独下发:客户端分组展示,但绝不参与本会话余量。
|
|
400
|
+
const siblingQuotas = allQuotas === null
|
|
401
|
+
? []
|
|
402
|
+
: await this.attachQuotaLogos(otherProviderAccounts(allQuotas, upstreamProviders, sessionQuotas), signal);
|
|
230
403
|
return {
|
|
231
404
|
agent,
|
|
232
405
|
modelId,
|
|
@@ -241,8 +414,47 @@ export class MagpieUsageService {
|
|
|
241
414
|
upstreamProviders,
|
|
242
415
|
quotaStatus: allQuotas === null ? 'unavailable' : upstreamProviders.length === 0 ? 'provider-unavailable' : 'available',
|
|
243
416
|
quotas,
|
|
417
|
+
...(siblingQuotas.length === 0 ? {} : { providerAccounts: siblingQuotas }),
|
|
244
418
|
};
|
|
245
419
|
}
|
|
420
|
+
async attachQuotaLogos(quotas, signal) {
|
|
421
|
+
const requestAssetDataUrl = this.webApi?.requestAssetDataUrl;
|
|
422
|
+
if (typeof requestAssetDataUrl !== 'function')
|
|
423
|
+
return quotas;
|
|
424
|
+
const icons = [...new Set(quotas
|
|
425
|
+
.map((quota) => quota.icon)
|
|
426
|
+
.filter((icon) => icon !== undefined))].slice(0, MAX_MAGPIE_ICON_FETCHES);
|
|
427
|
+
if (icons.length === 0)
|
|
428
|
+
return quotas;
|
|
429
|
+
const logos = new Map();
|
|
430
|
+
await Promise.all(icons.map(async (icon) => {
|
|
431
|
+
const cached = this.quotaLogoCache.get(icon);
|
|
432
|
+
const logo = cached === undefined ? await this.readMagpieLogoDataUrl(icon, signal) : cached;
|
|
433
|
+
if (cached === undefined)
|
|
434
|
+
this.quotaLogoCache.set(icon, logo);
|
|
435
|
+
logos.set(icon, logo);
|
|
436
|
+
}));
|
|
437
|
+
return quotas.map((quota) => {
|
|
438
|
+
const logoDataUrl = quota.icon === undefined ? null : logos.get(quota.icon) ?? null;
|
|
439
|
+
return logoDataUrl === null ? quota : { ...quota, logoDataUrl };
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
async readMagpieLogoDataUrl(icon, signal) {
|
|
443
|
+
const requestAssetDataUrl = this.webApi?.requestAssetDataUrl;
|
|
444
|
+
if (typeof requestAssetDataUrl !== 'function')
|
|
445
|
+
return null;
|
|
446
|
+
for (const path of magpieIconAssetPaths(icon)) {
|
|
447
|
+
try {
|
|
448
|
+
const value = await requestAssetDataUrl.call(this.webApi, path, signal === undefined ? {} : { signal });
|
|
449
|
+
if (value !== null && value !== '')
|
|
450
|
+
return value;
|
|
451
|
+
}
|
|
452
|
+
catch {
|
|
453
|
+
signal?.throwIfAborted();
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
return null;
|
|
457
|
+
}
|
|
246
458
|
async loadCatalog(agent, signal) {
|
|
247
459
|
const cached = this.catalogs.get(agent);
|
|
248
460
|
const cacheTtl = cached?.catalog === null ? MAGPIE_FAILURE_CACHE_TTL_MS : MAGPIE_CATALOG_TTL_MS;
|
|
@@ -264,7 +476,12 @@ export class MagpieUsageService {
|
|
|
264
476
|
.catch(() => null);
|
|
265
477
|
const [supported, catalog] = await Promise.all([supportedRequest, catalogRequest]);
|
|
266
478
|
const result = supported ? catalog : null;
|
|
267
|
-
|
|
479
|
+
const unavailableIssue = result !== null
|
|
480
|
+
? null
|
|
481
|
+
: !supported
|
|
482
|
+
? this.versionCheck?.executableAvailable === true ? 'cli-version-unsupported' : 'cli-unavailable'
|
|
483
|
+
: 'model-catalog-unavailable';
|
|
484
|
+
this.catalogs.set(agent, { catalog: result, unavailableIssue, loadedAt: this.now() });
|
|
268
485
|
return result;
|
|
269
486
|
})
|
|
270
487
|
.catch(() => null)
|
|
@@ -320,13 +537,13 @@ export class MagpieUsageService {
|
|
|
320
537
|
if (this.versionLoad !== undefined)
|
|
321
538
|
return awaitWithSignal(this.versionLoad, signal);
|
|
322
539
|
const load = this.readVersion(this.lifetimeSignal)
|
|
323
|
-
.then((value) =>
|
|
324
|
-
|
|
325
|
-
this.versionCheck = { supported, checkedAt: this.now() };
|
|
540
|
+
.then((value) => {
|
|
541
|
+
const supported = parseMagpieCliVersion(value);
|
|
542
|
+
this.versionCheck = { supported, executableAvailable: value !== null, checkedAt: this.now() };
|
|
326
543
|
return supported;
|
|
327
544
|
})
|
|
328
545
|
.catch(() => {
|
|
329
|
-
this.versionCheck = { supported: false, checkedAt: this.now() };
|
|
546
|
+
this.versionCheck = { supported: false, executableAvailable: false, checkedAt: this.now() };
|
|
330
547
|
return false;
|
|
331
548
|
})
|
|
332
549
|
.finally(() => { if (this.versionLoad === load)
|
|
@@ -363,22 +580,17 @@ export class MagpieUsageService {
|
|
|
363
580
|
return { ok: false };
|
|
364
581
|
}
|
|
365
582
|
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
route: null,
|
|
378
|
-
upstreamProvider: providerFromModelId(modelId, agent),
|
|
379
|
-
quotaStatus: 'unavailable',
|
|
380
|
-
quotas: [],
|
|
381
|
-
};
|
|
583
|
+
async readWebJson(path, signal) {
|
|
584
|
+
if (this.webApi === undefined)
|
|
585
|
+
return { ok: false };
|
|
586
|
+
try {
|
|
587
|
+
return { ok: true, value: await this.webApi.requestJson(path, signal === undefined ? {} : { signal }) };
|
|
588
|
+
}
|
|
589
|
+
catch {
|
|
590
|
+
signal?.throwIfAborted();
|
|
591
|
+
return { ok: false };
|
|
592
|
+
}
|
|
593
|
+
}
|
|
382
594
|
}
|
|
383
595
|
function awaitWithSignal(task, signal) {
|
|
384
596
|
if (signal === undefined)
|
|
@@ -635,13 +847,71 @@ async function runMagpieCommand(args, signal) {
|
|
|
635
847
|
}
|
|
636
848
|
/**
|
|
637
849
|
* Magpie IDs normally match adapter IDs. Keep naming differences explicit;
|
|
638
|
-
*
|
|
639
|
-
* any Gateway request is made.
|
|
850
|
+
* it is used for exact Agent identity matching in Magpie usage records.
|
|
640
851
|
*/
|
|
641
852
|
export function magpieAgentForAdapter(adapterId) {
|
|
642
853
|
const agent = MAGPIE_AGENT_BY_ADAPTER[adapterId];
|
|
643
854
|
return agent !== undefined && isMagpieAgentId(agent) ? agent : null;
|
|
644
855
|
}
|
|
856
|
+
/** Return the exact Magpie Agent identities accepted for one adapter. */
|
|
857
|
+
export function magpieUsageAgentAliases(adapterId) {
|
|
858
|
+
const agent = magpieAgentForAdapter(adapterId);
|
|
859
|
+
return agent === null ? [] : MAGPIE_USAGE_AGENT_ALIASES[agent] ?? [agent];
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* Enrich dashboard session rows only after an exact `(Agent, session ID)` match.
|
|
863
|
+
* Provider session IDs and DSH IDs are both candidates because Command Code and
|
|
864
|
+
* native DSH use different identities across Magpie versions.
|
|
865
|
+
*/
|
|
866
|
+
export function attachMagpieSessionNames(dashboard, bindings) {
|
|
867
|
+
if (!Array.isArray(dashboard.sessions) || bindings.length === 0)
|
|
868
|
+
return dashboard;
|
|
869
|
+
const names = new Map();
|
|
870
|
+
const ambiguous = new Set();
|
|
871
|
+
for (const binding of bindings) {
|
|
872
|
+
const name = cleanSessionName(binding.name);
|
|
873
|
+
const dshSessionId = cleanText(binding.dshSessionId);
|
|
874
|
+
if (name === null || dshSessionId === null)
|
|
875
|
+
continue;
|
|
876
|
+
// Match the identity strategy used by compact usage: native DSH uses its
|
|
877
|
+
// DSH ID, Command Code may use either injected DSH or ACP ID, and other
|
|
878
|
+
// adapters use only their Provider session ID.
|
|
879
|
+
const providerSessionId = cleanText(binding.providerSessionId);
|
|
880
|
+
const ids = new Set((binding.adapterId === 'dsh'
|
|
881
|
+
? [dshSessionId]
|
|
882
|
+
: binding.adapterId === 'command-code'
|
|
883
|
+
? [dshSessionId, providerSessionId]
|
|
884
|
+
: [providerSessionId]).filter((value) => value !== null));
|
|
885
|
+
for (const agent of magpieUsageAgentAliases(binding.adapterId)) {
|
|
886
|
+
for (const id of ids) {
|
|
887
|
+
const key = `${agent}\u0000${id}`;
|
|
888
|
+
if (ambiguous.has(key))
|
|
889
|
+
continue;
|
|
890
|
+
const previous = names.get(key);
|
|
891
|
+
if (previous !== undefined && previous !== name) {
|
|
892
|
+
names.delete(key);
|
|
893
|
+
ambiguous.add(key);
|
|
894
|
+
}
|
|
895
|
+
else {
|
|
896
|
+
names.set(key, name);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
if (names.size === 0)
|
|
902
|
+
return dashboard;
|
|
903
|
+
let changed = false;
|
|
904
|
+
const sessions = dashboard.sessions.map((session) => {
|
|
905
|
+
if (session.dshSessionName !== undefined)
|
|
906
|
+
return session;
|
|
907
|
+
const name = names.get(`${session.agent}\u0000${session.id}`);
|
|
908
|
+
if (name === undefined)
|
|
909
|
+
return session;
|
|
910
|
+
changed = true;
|
|
911
|
+
return { ...session, dshSessionName: name };
|
|
912
|
+
});
|
|
913
|
+
return changed ? { ...dashboard, sessions } : dashboard;
|
|
914
|
+
}
|
|
645
915
|
function isMagpieAgentId(value) {
|
|
646
916
|
return value.length <= 64 && /^[a-z0-9][a-z0-9-]*$/u.test(value);
|
|
647
917
|
}
|
|
@@ -728,46 +998,6 @@ function readSession(value, agent, sessionId) {
|
|
|
728
998
|
const usageAgent = cleanText(record?.agent);
|
|
729
999
|
return record !== null && usageAgent !== null && totals !== null ? { id: sessionId, agent: usageAgent, ...totals } : null;
|
|
730
1000
|
}
|
|
731
|
-
function readRoute(value, expectedSessionId) {
|
|
732
|
-
const envelope = asRecord(value);
|
|
733
|
-
if (envelope === null || cleanText(envelope.session) !== expectedSessionId || !('route' in envelope))
|
|
734
|
-
return { status: 'unavailable', route: null };
|
|
735
|
-
if (envelope.route === null)
|
|
736
|
-
return { status: 'no-route', route: null };
|
|
737
|
-
const record = asRecord(envelope.route);
|
|
738
|
-
if (record === null)
|
|
739
|
-
return { status: 'unavailable', route: null };
|
|
740
|
-
const tries = Array.isArray(record.tries)
|
|
741
|
-
? record.tries.slice(0, 20).flatMap((item) => {
|
|
742
|
-
const attempt = asRecord(item);
|
|
743
|
-
if (attempt === null)
|
|
744
|
-
return [];
|
|
745
|
-
const model = routeText(attempt.model);
|
|
746
|
-
const provider = routeText(attempt.provider);
|
|
747
|
-
return [{
|
|
748
|
-
model: model !== null && provider !== null && !model.includes('/') ? `${provider}/${model}` : model ?? provider,
|
|
749
|
-
failed: routeText(attempt.fail) !== null,
|
|
750
|
-
}];
|
|
751
|
-
})
|
|
752
|
-
: [];
|
|
753
|
-
return {
|
|
754
|
-
status: 'available',
|
|
755
|
-
route: {
|
|
756
|
-
asked: routeText(record.asked),
|
|
757
|
-
group: routeText(record.group),
|
|
758
|
-
rule: readRule(record.rule),
|
|
759
|
-
model: routeText(record.model),
|
|
760
|
-
effort: routeText(record.effort),
|
|
761
|
-
tries,
|
|
762
|
-
done: typeof record.done === 'boolean' ? record.done : null,
|
|
763
|
-
status: routeStatusText(record.status),
|
|
764
|
-
served: routeText(record.served),
|
|
765
|
-
routeId: safeRouteId(record.id),
|
|
766
|
-
time: routeText(record.time),
|
|
767
|
-
providerAccount: routeText(record.providerAccount),
|
|
768
|
-
},
|
|
769
|
-
};
|
|
770
|
-
}
|
|
771
1001
|
/** Read the durable request ledger when Magpie's short in-memory trace has expired. */
|
|
772
1002
|
function readSessionRouteHistory(value, agent, sessionId) {
|
|
773
1003
|
const rows = asRecord(value)?.rows;
|
|
@@ -814,18 +1044,19 @@ function routeTime(value) {
|
|
|
814
1044
|
const time = Date.parse(value);
|
|
815
1045
|
return Number.isFinite(time) ? time : 0;
|
|
816
1046
|
}
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
1047
|
+
/**
|
|
1048
|
+
* 选出本会话确实使用过的上游账号配额。
|
|
1049
|
+
*
|
|
1050
|
+
* 归属必须严格:请求历史里没有出现的账号不能算作本会话的配额。
|
|
1051
|
+
*
|
|
1052
|
+
* - 账号维度能对上:只留命中的账号。
|
|
1053
|
+
* - 上游不提供账号名(Codex 这类量贩账号没有 `user`),无从按账号筛选:保留该厂商
|
|
1054
|
+
* 全部候选。过去这里返回空数组,`quotaStatus` 仍是 `available`,界面就只剩下
|
|
1055
|
+
* "上游没有报告配额窗口"的空态 —— 明明识别出了 `glorinfo-api-platform`,却看不到
|
|
1056
|
+
* 它的额度。
|
|
1057
|
+
* - 有账号记录但一条都不匹配:返回空,宁可显示"没有可用配额"也不能把别的账号的额度
|
|
1058
|
+
* 说成本会话的。
|
|
1059
|
+
*/
|
|
829
1060
|
function filterSessionQuotas(quotas, providers, history) {
|
|
830
1061
|
return providers.flatMap((provider) => {
|
|
831
1062
|
const candidates = quotas.filter((quota) => quota.provider === provider);
|
|
@@ -834,7 +1065,53 @@ function filterSessionQuotas(quotas, providers, history) {
|
|
|
834
1065
|
.map((record) => record.account));
|
|
835
1066
|
if (accounts.size === 0)
|
|
836
1067
|
return candidates;
|
|
837
|
-
|
|
1068
|
+
const matched = candidates.filter((quota) => quota.user !== null && accounts.has(quota.user));
|
|
1069
|
+
return matched.length > 0 ? matched : candidates.filter((quota) => quota.user === null);
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1072
|
+
/**
|
|
1073
|
+
* 与本会话同一厂商、但本会话没用过的其它账号。
|
|
1074
|
+
*
|
|
1075
|
+
* 单独返回而不是并进 `quotas`:客户端用它们做"同厂商账号分组"的补充展示,
|
|
1076
|
+
* 但这些账号的额度绝不参与本会话余量判断。顺序沿用上游。
|
|
1077
|
+
*/
|
|
1078
|
+
function otherProviderAccounts(quotas, providers, sessionQuotas) {
|
|
1079
|
+
const claimed = new Set(sessionQuotas.map((quota) => `${quota.provider}\u0000${quota.user ?? ''}`));
|
|
1080
|
+
return providers.flatMap((provider) => quotas
|
|
1081
|
+
.filter((quota) => quota.provider === provider)
|
|
1082
|
+
.filter((quota) => quota.user !== null && !claimed.has(`${quota.provider}\u0000${quota.user}`)));
|
|
1083
|
+
}
|
|
1084
|
+
/**
|
|
1085
|
+
* 标记本会话实际使用过的账号。
|
|
1086
|
+
*
|
|
1087
|
+
* 只有能追溯到会话请求记录的账号才标 `active`;route 的账号字段优先于请求历史,
|
|
1088
|
+
* 因为它来自 Magpie 对这一次请求的精确归属。
|
|
1089
|
+
*
|
|
1090
|
+
* 账号名比对必须同时限定厂商:不同厂商的账号名会重名(WorkBuddy 与 WorkBuddy AI 都有
|
|
1091
|
+
* `zj59122704501@gmail.com`),只比名字会把别的厂商的账号标成本会话正在使用。
|
|
1092
|
+
* 无法确认时保持未标记,客户端据此显示"未知活动账号",而不是随便挑一个账号当"正在使用"。
|
|
1093
|
+
*/
|
|
1094
|
+
function markSessionAccounts(quotas, history, route) {
|
|
1095
|
+
const used = new Set(history
|
|
1096
|
+
.filter((record) => record.account !== null)
|
|
1097
|
+
.map((record) => `${record.provider}\u0000${record.account}`));
|
|
1098
|
+
const routedProvider = providerFromRoute(route);
|
|
1099
|
+
const routedAccount = route?.providerAccount ?? null;
|
|
1100
|
+
const routedKey = routedAccount === null || routedProvider === null ? null : `${routedProvider}\u0000${routedAccount}`;
|
|
1101
|
+
if (used.size === 0 && routedKey === null)
|
|
1102
|
+
return quotas;
|
|
1103
|
+
// 每个厂商只承认一个活动账号:精确路由优先,其次是首个命中的历史账号。
|
|
1104
|
+
const claimed = new Set();
|
|
1105
|
+
return quotas.map((quota) => {
|
|
1106
|
+
if (quota.user === null)
|
|
1107
|
+
return quota;
|
|
1108
|
+
const key = `${quota.provider}\u0000${quota.user}`;
|
|
1109
|
+
if (claimed.has(key))
|
|
1110
|
+
return quota;
|
|
1111
|
+
if (key !== routedKey && !used.has(key))
|
|
1112
|
+
return quota;
|
|
1113
|
+
claimed.add(key);
|
|
1114
|
+
return { ...quota, active: true };
|
|
838
1115
|
});
|
|
839
1116
|
}
|
|
840
1117
|
function safeRouteId(value) {
|
|
@@ -866,6 +1143,100 @@ export function parseMagpieQuotas(value) {
|
|
|
866
1143
|
quotas.push({
|
|
867
1144
|
provider,
|
|
868
1145
|
name,
|
|
1146
|
+
...optionalQuotaIcon(record.icon),
|
|
1147
|
+
kind: routeText(record.kind),
|
|
1148
|
+
user: routeText(record.user),
|
|
1149
|
+
plan: routeText(record.plan),
|
|
1150
|
+
alias: routeText(record.alias),
|
|
1151
|
+
balance: routeText(record.balance),
|
|
1152
|
+
readAt: routeText(record.readAt),
|
|
1153
|
+
windows,
|
|
1154
|
+
...sessionQuotaDaily(record),
|
|
1155
|
+
...readQuotaAccountMeta(record),
|
|
1156
|
+
});
|
|
1157
|
+
}
|
|
1158
|
+
return quotas;
|
|
1159
|
+
}
|
|
1160
|
+
/** Normalize the live aggregate and per-dimension usage response. */
|
|
1161
|
+
export function parseMagpieDashboardUsage(value) {
|
|
1162
|
+
const root = asRecord(value);
|
|
1163
|
+
if (root === null)
|
|
1164
|
+
return null;
|
|
1165
|
+
const hasMetrics = ['calls', 'errors', 'input', 'output', 'cache_read', 'cache_write', 'cost'].some((key) => key in root);
|
|
1166
|
+
if (!hasMetrics && !Array.isArray(root.series) && !Array.isArray(root.sessions))
|
|
1167
|
+
return null;
|
|
1168
|
+
const series = readMagpieSeries(root.series);
|
|
1169
|
+
const agents = readMagpieUsageGroups(root.agents, 'agent');
|
|
1170
|
+
const providers = readMagpieUsageGroups(root.providerKeys ?? root.providers, 'provider');
|
|
1171
|
+
const models = readMagpieUsageGroups(root.models, 'model');
|
|
1172
|
+
const accounts = readMagpieUsageGroups(root.accounts, 'account');
|
|
1173
|
+
const sessions = readMagpieUsageSessions(root.sessions);
|
|
1174
|
+
if (series === null || agents === null || providers === null || models === null || accounts === null || sessions === null)
|
|
1175
|
+
return null;
|
|
1176
|
+
return {
|
|
1177
|
+
aggregate: readMagpieUsageMetrics(root),
|
|
1178
|
+
series,
|
|
1179
|
+
agents,
|
|
1180
|
+
providers,
|
|
1181
|
+
models,
|
|
1182
|
+
accounts,
|
|
1183
|
+
sessions,
|
|
1184
|
+
sessionCount: safeCount(root.sessionCount),
|
|
1185
|
+
};
|
|
1186
|
+
}
|
|
1187
|
+
/** Normalize quota cards while preserving nullable values and bounded history. */
|
|
1188
|
+
export function parseMagpieQuotaCards(value) {
|
|
1189
|
+
const root = asRecord(value);
|
|
1190
|
+
const rows = Array.isArray(value) ? value : Array.isArray(root?.data) ? root.data : null;
|
|
1191
|
+
if (rows === null || rows.length > 512)
|
|
1192
|
+
return null;
|
|
1193
|
+
const quotas = [];
|
|
1194
|
+
for (const row of rows) {
|
|
1195
|
+
const record = asRecord(row);
|
|
1196
|
+
const provider = routeText(record?.provider);
|
|
1197
|
+
const name = routeText(record?.name);
|
|
1198
|
+
if (record === null || provider === null || name === null
|
|
1199
|
+
|| !Array.isArray(record.windows) || record.windows.length > 64)
|
|
1200
|
+
return null;
|
|
1201
|
+
const windows = record.windows.flatMap((item) => {
|
|
1202
|
+
const window = asRecord(item);
|
|
1203
|
+
const windowName = routeText(window?.name);
|
|
1204
|
+
if (window === null || windowName === null)
|
|
1205
|
+
return [];
|
|
1206
|
+
return [{
|
|
1207
|
+
name: windowName,
|
|
1208
|
+
usedPercent: quotaUsedPercent(window),
|
|
1209
|
+
remainingPercent: quotaRemainingPercent(window),
|
|
1210
|
+
resetsAt: routeText(window.resetsAt),
|
|
1211
|
+
display: routeText(window.display),
|
|
1212
|
+
amount: safeDecimal(window.amount),
|
|
1213
|
+
limit: safeDecimal(window.limit),
|
|
1214
|
+
unit: routeText(window.unit),
|
|
1215
|
+
holdsWhy: routeText(window.holdsWhy),
|
|
1216
|
+
runsOutAt: routeText(window.runsOutAt),
|
|
1217
|
+
runsOut: safeDecimal(window.runsOut),
|
|
1218
|
+
}];
|
|
1219
|
+
});
|
|
1220
|
+
if (windows.length !== record.windows.length)
|
|
1221
|
+
return null;
|
|
1222
|
+
const dailyRecord = record.daily === undefined ? null : asRecord(record.daily);
|
|
1223
|
+
const dailyPoints = dailyRecord === null ? [] : dailyRecord.days;
|
|
1224
|
+
if (dailyRecord !== null && (!Array.isArray(dailyPoints) || dailyPoints.length > 366))
|
|
1225
|
+
return null;
|
|
1226
|
+
const days = (Array.isArray(dailyPoints) ? dailyPoints : []).flatMap((item) => {
|
|
1227
|
+
const point = asRecord(item);
|
|
1228
|
+
const day = routeText(point?.day);
|
|
1229
|
+
if (point === null || day === null)
|
|
1230
|
+
return [];
|
|
1231
|
+
const used = safeDecimal(point.used);
|
|
1232
|
+
return [{ day, used }];
|
|
1233
|
+
});
|
|
1234
|
+
if (days.length !== (Array.isArray(dailyPoints) ? dailyPoints.length : 0))
|
|
1235
|
+
return null;
|
|
1236
|
+
quotas.push({
|
|
1237
|
+
provider,
|
|
1238
|
+
name,
|
|
1239
|
+
...optionalQuotaIcon(record.icon),
|
|
869
1240
|
kind: routeText(record.kind),
|
|
870
1241
|
user: routeText(record.user),
|
|
871
1242
|
plan: routeText(record.plan),
|
|
@@ -873,22 +1244,369 @@ export function parseMagpieQuotas(value) {
|
|
|
873
1244
|
balance: routeText(record.balance),
|
|
874
1245
|
readAt: routeText(record.readAt),
|
|
875
1246
|
windows,
|
|
1247
|
+
daily: { since: routeText(dailyRecord?.since), days },
|
|
1248
|
+
...readQuotaAccountMeta(record),
|
|
876
1249
|
});
|
|
877
1250
|
}
|
|
878
1251
|
return quotas;
|
|
879
1252
|
}
|
|
1253
|
+
export function parseMagpieQuotaHistory(value) {
|
|
1254
|
+
const root = asRecord(value);
|
|
1255
|
+
const rows = Array.isArray(value) ? value : Array.isArray(root?.data) ? root.data : null;
|
|
1256
|
+
if (rows === null || rows.length > 512)
|
|
1257
|
+
return null;
|
|
1258
|
+
const history = [];
|
|
1259
|
+
for (const row of rows) {
|
|
1260
|
+
const record = asRecord(row);
|
|
1261
|
+
const provider = routeText(record?.provider);
|
|
1262
|
+
if (record === null || provider === null || !Array.isArray(record.lines) || record.lines.length > 64)
|
|
1263
|
+
return null;
|
|
1264
|
+
const lines = record.lines.flatMap((item) => {
|
|
1265
|
+
const line = asRecord(item);
|
|
1266
|
+
const name = routeText(line?.name);
|
|
1267
|
+
if (line === null || name === null || !Array.isArray(line.points) || line.points.length > 2_000)
|
|
1268
|
+
return [];
|
|
1269
|
+
const points = line.points.flatMap((pointValue) => {
|
|
1270
|
+
const point = asRecord(pointValue);
|
|
1271
|
+
const at = routeText(point?.at);
|
|
1272
|
+
return point === null || at === null ? [] : [{
|
|
1273
|
+
at,
|
|
1274
|
+
left: safeDecimal(point.left),
|
|
1275
|
+
start: routeText(point.start),
|
|
1276
|
+
resetsAt: routeText(point.resetsAt),
|
|
1277
|
+
}];
|
|
1278
|
+
});
|
|
1279
|
+
if (points.length !== line.points.length)
|
|
1280
|
+
return [];
|
|
1281
|
+
return [{ name, points }];
|
|
1282
|
+
});
|
|
1283
|
+
if (lines.length !== record.lines.length)
|
|
1284
|
+
return null;
|
|
1285
|
+
history.push({ provider, user: routeText(record.user), lines });
|
|
1286
|
+
}
|
|
1287
|
+
return history;
|
|
1288
|
+
}
|
|
1289
|
+
function readMagpieUsageMetrics(record) {
|
|
1290
|
+
return {
|
|
1291
|
+
calls: safeCount(record.calls),
|
|
1292
|
+
errors: safeCount(record.errors),
|
|
1293
|
+
retried: safeCount(record.retried),
|
|
1294
|
+
inputTokens: safeCount(record.input),
|
|
1295
|
+
outputTokens: safeCount(record.output),
|
|
1296
|
+
cacheReadTokens: safeCount(record.cache_read),
|
|
1297
|
+
cacheWriteTokens: safeCount(record.cache_write),
|
|
1298
|
+
reasoningTokens: safeCount(record.reasoning),
|
|
1299
|
+
cost: safeDecimal(record.cost),
|
|
1300
|
+
unpriced: safeDecimal(record.unpriced),
|
|
1301
|
+
credits: safeDecimal(record.credits),
|
|
1302
|
+
credited: safeDecimal(record.credited),
|
|
1303
|
+
timed: safeCount(record.timed),
|
|
1304
|
+
ttftMs: safeDecimal(record.ttft_ms),
|
|
1305
|
+
decodeMs: safeDecimal(record.decode_ms),
|
|
1306
|
+
decodeTokens: safeCount(record.decode_out),
|
|
1307
|
+
decodeCount: safeCount(record.decode_n),
|
|
1308
|
+
};
|
|
1309
|
+
}
|
|
1310
|
+
function readMagpieSeries(value) {
|
|
1311
|
+
if (value === undefined)
|
|
1312
|
+
return [];
|
|
1313
|
+
if (!Array.isArray(value) || value.length > 2_000)
|
|
1314
|
+
return null;
|
|
1315
|
+
return value.flatMap((item) => {
|
|
1316
|
+
const record = asRecord(item);
|
|
1317
|
+
return record === null ? [] : [{
|
|
1318
|
+
time: routeText(record.time),
|
|
1319
|
+
label: routeText(record.label),
|
|
1320
|
+
...readMagpieUsageMetrics(record),
|
|
1321
|
+
}];
|
|
1322
|
+
});
|
|
1323
|
+
}
|
|
1324
|
+
function readMagpieUsageGroups(value, dimension) {
|
|
1325
|
+
if (value === undefined)
|
|
1326
|
+
return [];
|
|
1327
|
+
if (!Array.isArray(value) || value.length > 2_000)
|
|
1328
|
+
return null;
|
|
1329
|
+
const idKeys = {
|
|
1330
|
+
agent: ['id', 'agent'],
|
|
1331
|
+
provider: ['provider', 'id'],
|
|
1332
|
+
model: ['model', 'id'],
|
|
1333
|
+
account: ['account', 'id'],
|
|
1334
|
+
};
|
|
1335
|
+
const groups = value.flatMap((item) => {
|
|
1336
|
+
const record = asRecord(item);
|
|
1337
|
+
if (record === null)
|
|
1338
|
+
return [];
|
|
1339
|
+
const id = firstRouteText(record, idKeys[dimension]);
|
|
1340
|
+
if (id === null)
|
|
1341
|
+
return [];
|
|
1342
|
+
return [{
|
|
1343
|
+
id,
|
|
1344
|
+
name: routeText(record.name),
|
|
1345
|
+
provider: routeText(record.provider),
|
|
1346
|
+
model: routeText(record.model),
|
|
1347
|
+
account: routeText(record.account),
|
|
1348
|
+
agent: routeText(record.agent ?? record.id),
|
|
1349
|
+
...readMagpieUsageMetrics(record),
|
|
1350
|
+
}];
|
|
1351
|
+
});
|
|
1352
|
+
if (dimension !== 'provider')
|
|
1353
|
+
return groups;
|
|
1354
|
+
const byProvider = new Map();
|
|
1355
|
+
for (const group of groups) {
|
|
1356
|
+
const current = byProvider.get(group.id);
|
|
1357
|
+
if (current === undefined) {
|
|
1358
|
+
byProvider.set(group.id, group);
|
|
1359
|
+
continue;
|
|
1360
|
+
}
|
|
1361
|
+
byProvider.set(group.id, {
|
|
1362
|
+
...current,
|
|
1363
|
+
calls: sumNullable(current.calls, group.calls),
|
|
1364
|
+
errors: sumNullable(current.errors, group.errors),
|
|
1365
|
+
retried: sumNullable(current.retried, group.retried),
|
|
1366
|
+
inputTokens: sumNullable(current.inputTokens, group.inputTokens),
|
|
1367
|
+
outputTokens: sumNullable(current.outputTokens, group.outputTokens),
|
|
1368
|
+
cacheReadTokens: sumNullable(current.cacheReadTokens, group.cacheReadTokens),
|
|
1369
|
+
cacheWriteTokens: sumNullable(current.cacheWriteTokens, group.cacheWriteTokens),
|
|
1370
|
+
reasoningTokens: sumNullable(current.reasoningTokens, group.reasoningTokens),
|
|
1371
|
+
cost: sumNullable(current.cost, group.cost),
|
|
1372
|
+
unpriced: sumNullable(current.unpriced, group.unpriced),
|
|
1373
|
+
credits: sumNullable(current.credits, group.credits),
|
|
1374
|
+
credited: sumNullable(current.credited, group.credited),
|
|
1375
|
+
timed: sumNullable(current.timed, group.timed),
|
|
1376
|
+
ttftMs: null,
|
|
1377
|
+
decodeMs: null,
|
|
1378
|
+
decodeTokens: sumNullable(current.decodeTokens, group.decodeTokens),
|
|
1379
|
+
decodeCount: sumNullable(current.decodeCount, group.decodeCount),
|
|
1380
|
+
});
|
|
1381
|
+
}
|
|
1382
|
+
return [...byProvider.values()];
|
|
1383
|
+
}
|
|
1384
|
+
function readMagpieUsageSessions(value) {
|
|
1385
|
+
if (value === undefined)
|
|
1386
|
+
return [];
|
|
1387
|
+
if (!Array.isArray(value) || value.length > 2_000)
|
|
1388
|
+
return null;
|
|
1389
|
+
return value.flatMap((item) => {
|
|
1390
|
+
const record = asRecord(item);
|
|
1391
|
+
const id = cleanText(record?.id);
|
|
1392
|
+
const agent = cleanText(record?.agent);
|
|
1393
|
+
return record === null || id === null || agent === null ? [] : [{ id, agent, ...readMagpieUsageMetrics(record) }];
|
|
1394
|
+
});
|
|
1395
|
+
}
|
|
1396
|
+
function parseMagpieRequest(record) {
|
|
1397
|
+
const agent = routeText(record.agent);
|
|
1398
|
+
const provider = routeText(record.provider);
|
|
1399
|
+
const requestedModel = routeText(record.req);
|
|
1400
|
+
const servedModel = routeText(record.served);
|
|
1401
|
+
const group = requestedModel?.startsWith('group/') === true ? requestedModel.slice('group/'.length) : null;
|
|
1402
|
+
return {
|
|
1403
|
+
routeId: safeRouteId(record.route_id),
|
|
1404
|
+
time: routeText(record.t),
|
|
1405
|
+
agent,
|
|
1406
|
+
provider,
|
|
1407
|
+
account: routeText(record.providerAccount),
|
|
1408
|
+
requestedModel,
|
|
1409
|
+
servedModel,
|
|
1410
|
+
group,
|
|
1411
|
+
effort: routeText(record.effort),
|
|
1412
|
+
inputTokens: safeCount(record.in),
|
|
1413
|
+
outputTokens: safeCount(record.out),
|
|
1414
|
+
cacheReadTokens: safeCount(record.cache_read),
|
|
1415
|
+
cacheWriteTokens: safeCount(record.cache_write),
|
|
1416
|
+
reasoningTokens: safeCount(record.reasoning),
|
|
1417
|
+
cost: safeDecimal(record.cost),
|
|
1418
|
+
priced: typeof record.priced === 'boolean' ? record.priced : null,
|
|
1419
|
+
status: routeStatusText(record.status),
|
|
1420
|
+
durationMs: safeDecimal(record.ms),
|
|
1421
|
+
ttftMs: safeDecimal(record.ttft_ms),
|
|
1422
|
+
firstTextMs: safeDecimal(record.first_text_ms),
|
|
1423
|
+
firstReasoningMs: safeDecimal(record.first_reasoning_ms),
|
|
1424
|
+
flowMs: safeDecimal(record.flow_ms),
|
|
1425
|
+
sessionId: cleanText(record.session) ?? cleanText(record.native_session),
|
|
1426
|
+
};
|
|
1427
|
+
}
|
|
1428
|
+
function firstRouteText(record, keys) {
|
|
1429
|
+
for (const key of keys) {
|
|
1430
|
+
const value = routeText(record[key]);
|
|
1431
|
+
if (value !== null)
|
|
1432
|
+
return value;
|
|
1433
|
+
}
|
|
1434
|
+
return null;
|
|
1435
|
+
}
|
|
1436
|
+
function sumNullable(left, right) {
|
|
1437
|
+
return left === null && right === null ? null : (left ?? 0) + (right ?? 0);
|
|
1438
|
+
}
|
|
1439
|
+
function isMagpieDashboardPeriod(value) {
|
|
1440
|
+
return value === 'today' || value === 'week' || value === 'month' || value === 'all';
|
|
1441
|
+
}
|
|
1442
|
+
function magpieWebPeriod(period) {
|
|
1443
|
+
switch (period) {
|
|
1444
|
+
case 'today': return 'today';
|
|
1445
|
+
case 'week': return '7d';
|
|
1446
|
+
case 'month': return '30d';
|
|
1447
|
+
case 'all': return 'all';
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
function emptyMagpieDashboard(period, capturedAt) {
|
|
1451
|
+
return {
|
|
1452
|
+
period,
|
|
1453
|
+
capturedAt,
|
|
1454
|
+
availabilityIssue: null,
|
|
1455
|
+
usageStatus: 'unavailable',
|
|
1456
|
+
aggregate: null,
|
|
1457
|
+
series: [],
|
|
1458
|
+
agents: [],
|
|
1459
|
+
providers: [],
|
|
1460
|
+
models: [],
|
|
1461
|
+
accounts: [],
|
|
1462
|
+
sessions: [],
|
|
1463
|
+
sessionCount: null,
|
|
1464
|
+
quotaStatus: 'unavailable',
|
|
1465
|
+
quotas: [],
|
|
1466
|
+
quotaHistoryStatus: 'unavailable',
|
|
1467
|
+
quotaHistory: [],
|
|
1468
|
+
};
|
|
1469
|
+
}
|
|
1470
|
+
/**
|
|
1471
|
+
* 窗口使用的百分比。
|
|
1472
|
+
*
|
|
1473
|
+
* Magpie 的 `used` 始终是 0–100 的百分比(见其 `auth.usage` 契约),但 `remaining`
|
|
1474
|
+
* 不一定:同一份数据里既有 `remaining: 96` 这种百分比,也有 `remaining: 86.57`
|
|
1475
|
+
* 配 `amount: 322.25 / limit: 2400` 这种"剩余量"。凡是带 `limit`(或 `amount`)的窗口,
|
|
1476
|
+
* `remaining` 一律按数量解释,不能直接当成 0–100 的百分比,否则 WorkBuddy 的
|
|
1477
|
+
* `322 / 2400` 会被读成"剩余 86.57%",和 `display` 里的数量对不上。
|
|
1478
|
+
*/
|
|
1479
|
+
function quotaUsedPercent(record) {
|
|
1480
|
+
return safePercent(record.used);
|
|
1481
|
+
}
|
|
1482
|
+
function quotaRemainingPercent(record) {
|
|
1483
|
+
const counted = safeDecimal(record.limit) !== null || safeDecimal(record.amount) !== null;
|
|
1484
|
+
if (counted) {
|
|
1485
|
+
const used = quotaUsedPercent(record);
|
|
1486
|
+
return used === null ? null : 100 - used;
|
|
1487
|
+
}
|
|
1488
|
+
return safePercent(record.remaining);
|
|
1489
|
+
}
|
|
880
1490
|
function readQuotaWindow(record, name) {
|
|
881
1491
|
return {
|
|
882
1492
|
name,
|
|
883
|
-
usedPercent:
|
|
884
|
-
remainingPercent:
|
|
1493
|
+
usedPercent: quotaUsedPercent(record),
|
|
1494
|
+
remainingPercent: quotaRemainingPercent(record),
|
|
885
1495
|
resetsAt: routeText(record.resetsAt),
|
|
886
1496
|
display: routeText(record.display),
|
|
887
1497
|
amount: safeDecimal(record.amount),
|
|
888
1498
|
limit: safeDecimal(record.limit),
|
|
889
1499
|
unit: routeText(record.unit),
|
|
890
1500
|
holdsWhy: routeText(record.holdsWhy),
|
|
1501
|
+
startsAt: routeText(record.startsAt),
|
|
1502
|
+
runsOut: safeDecimal(record.runsOut),
|
|
1503
|
+
runsOutAt: routeText(record.runsOutAt),
|
|
1504
|
+
};
|
|
1505
|
+
}
|
|
1506
|
+
/**
|
|
1507
|
+
* Read the daily check-in result of providers that have one (WorkBuddy, Trae, MiniMax).
|
|
1508
|
+
*
|
|
1509
|
+
* `outcome` stays an upstream string: the client only localizes the outcomes it knows, so a
|
|
1510
|
+
* new vendor state never gets presented as a successful check-in.
|
|
1511
|
+
*/
|
|
1512
|
+
function readQuotaCheckin(value) {
|
|
1513
|
+
const record = asRecord(value);
|
|
1514
|
+
if (record === null)
|
|
1515
|
+
return null;
|
|
1516
|
+
const checkin = {
|
|
1517
|
+
day: routeText(record.day),
|
|
1518
|
+
at: routeText(record.at),
|
|
1519
|
+
outcome: routeText(record.outcome),
|
|
1520
|
+
credit: safeDecimal(record.credit),
|
|
1521
|
+
streak: safeCount(record.streak),
|
|
891
1522
|
};
|
|
1523
|
+
return Object.values(checkin).every((item) => item === null) ? null : checkin;
|
|
1524
|
+
}
|
|
1525
|
+
function readQuotaDaily(value) {
|
|
1526
|
+
const record = asRecord(value);
|
|
1527
|
+
if (record === null)
|
|
1528
|
+
return undefined;
|
|
1529
|
+
const points = record.days;
|
|
1530
|
+
if (!Array.isArray(points) || points.length > 366)
|
|
1531
|
+
return undefined;
|
|
1532
|
+
const days = points.flatMap((item) => {
|
|
1533
|
+
const point = asRecord(item);
|
|
1534
|
+
const day = routeText(point?.day);
|
|
1535
|
+
return point === null || day === null ? [] : [{ day, used: safeDecimal(point.used) }];
|
|
1536
|
+
});
|
|
1537
|
+
if (days.length !== points.length)
|
|
1538
|
+
return undefined;
|
|
1539
|
+
return { since: routeText(record.since), days };
|
|
1540
|
+
}
|
|
1541
|
+
/**
|
|
1542
|
+
* Read a provider's remaining reset cards without exposing the vendor's raw record.
|
|
1543
|
+
*
|
|
1544
|
+
* `count` 缺失与 0 不同:上游在 0 张时会省略整个对象,因此这里只在对象存在时才返回,
|
|
1545
|
+
* 客户端把缺失显示为"未知"而不是"没有可用重置卡"。
|
|
1546
|
+
*/
|
|
1547
|
+
function readQuotaResets(value) {
|
|
1548
|
+
const record = asRecord(value);
|
|
1549
|
+
if (record === null)
|
|
1550
|
+
return null;
|
|
1551
|
+
const eachPoints = record.each;
|
|
1552
|
+
const each = Array.isArray(eachPoints)
|
|
1553
|
+
? eachPoints.flatMap((item) => {
|
|
1554
|
+
const point = asRecord(item);
|
|
1555
|
+
if (point === null)
|
|
1556
|
+
return [];
|
|
1557
|
+
return [{ window: routeText(point.window), until: routeText(point.until) }];
|
|
1558
|
+
}).slice(0, 64)
|
|
1559
|
+
: [];
|
|
1560
|
+
return {
|
|
1561
|
+
count: safeCount(record.count),
|
|
1562
|
+
until: routeText(record.until),
|
|
1563
|
+
each,
|
|
1564
|
+
byWindow: record.byWindow === true,
|
|
1565
|
+
fiveHour: safeCount(record.fiveHour),
|
|
1566
|
+
weekly: safeCount(record.weekly),
|
|
1567
|
+
};
|
|
1568
|
+
}
|
|
1569
|
+
/** 会话级配额卡只带上 `daily`;上游缺字段时整个属性不出现。 */
|
|
1570
|
+
function sessionQuotaDaily(record) {
|
|
1571
|
+
const daily = readQuotaDaily(record.daily);
|
|
1572
|
+
return daily === undefined ? {} : { daily };
|
|
1573
|
+
}
|
|
1574
|
+
/**
|
|
1575
|
+
* Read the account-level fields shared by both quota parsers.
|
|
1576
|
+
*
|
|
1577
|
+
* `/api/usage/quotas` (session popover) and the dashboard read the same rows; keeping these
|
|
1578
|
+
* fields in one place stops the two views drifting apart. Absent values stay `undefined` so
|
|
1579
|
+
* older Magpie builds keep working.
|
|
1580
|
+
*/
|
|
1581
|
+
function readQuotaAccountMeta(record) {
|
|
1582
|
+
const checkin = readQuotaCheckin(record.checkin);
|
|
1583
|
+
const resets = readQuotaResets(record.resets);
|
|
1584
|
+
return {
|
|
1585
|
+
...(typeof record.active === 'boolean' ? { active: record.active } : {}),
|
|
1586
|
+
...(checkin === null ? {} : { checkin }),
|
|
1587
|
+
...(record.checkins === true ? { checkins: true } : {}),
|
|
1588
|
+
error: routeText(record.error),
|
|
1589
|
+
until: routeText(record.until),
|
|
1590
|
+
renew: routeText(record.renew),
|
|
1591
|
+
...(resets === null ? {} : { resets }),
|
|
1592
|
+
credits: routeText(record.credits),
|
|
1593
|
+
};
|
|
1594
|
+
}
|
|
1595
|
+
function optionalQuotaIcon(value) {
|
|
1596
|
+
const icon = routeText(value);
|
|
1597
|
+
if (icon === null)
|
|
1598
|
+
return {};
|
|
1599
|
+
if (/^file:[A-Za-z0-9][A-Za-z0-9._~-]{0,255}$/u.test(icon))
|
|
1600
|
+
return { icon };
|
|
1601
|
+
return /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/u.test(icon) ? { icon } : {};
|
|
1602
|
+
}
|
|
1603
|
+
function magpieIconAssetPaths(icon) {
|
|
1604
|
+
if (/^file:[A-Za-z0-9][A-Za-z0-9._~-]{0,255}$/u.test(icon)) {
|
|
1605
|
+
return [`/api/icons/${encodeURIComponent(icon.slice('file:'.length))}`];
|
|
1606
|
+
}
|
|
1607
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/u.test(icon))
|
|
1608
|
+
return [];
|
|
1609
|
+
return [`/icons/${icon}.svg`, `/icons/${icon}.png`];
|
|
892
1610
|
}
|
|
893
1611
|
function providerFromRoute(route) {
|
|
894
1612
|
if (route === null)
|
|
@@ -947,6 +1665,12 @@ function cleanText(value) {
|
|
|
947
1665
|
const text = value.trim();
|
|
948
1666
|
return text === '' || text.length > 512 || /[\u0000-\u001f\u007f]/u.test(text) ? null : text;
|
|
949
1667
|
}
|
|
1668
|
+
function cleanSessionName(value) {
|
|
1669
|
+
if (typeof value !== 'string')
|
|
1670
|
+
return null;
|
|
1671
|
+
const text = value.trim().replace(/[\u0000-\u001f\u007f]/gu, '').slice(0, 240);
|
|
1672
|
+
return text === '' ? null : text;
|
|
1673
|
+
}
|
|
950
1674
|
/** Pi and Command Code store Magpie selections with a local `magpie/` provider prefix. */
|
|
951
1675
|
function resolveCatalogModelId(agent, requestedModelId, catalog, modelName) {
|
|
952
1676
|
if (catalog === null)
|