codex-multi-auth 2.23.0 → 2.24.0
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/.codex-plugin/plugin.json +9 -9
- package/LICENSE +21 -21
- package/README.md +159 -152
- package/assets/codex-multi-auth-icon.svg +39 -39
- package/assets/readme-hero.svg +31 -31
- package/config/README.md +31 -31
- package/config/codex-legacy.json +1021 -1021
- package/config/codex-modern.json +347 -347
- package/config/minimal-codex.json +15 -15
- package/dist/index.js +33 -10
- package/dist/lib/account-policy.d.ts +2 -0
- package/dist/lib/account-policy.js +3 -1
- package/dist/lib/accounts/rate-limits.d.ts +6 -0
- package/dist/lib/accounts/rate-limits.js +14 -1
- package/dist/lib/accounts.d.ts +6 -1
- package/dist/lib/accounts.js +33 -8
- package/dist/lib/auth/token-utils.d.ts +5 -0
- package/dist/lib/auth/token-utils.js +10 -0
- package/dist/lib/codex-manager/commands/account.js +15 -0
- package/dist/lib/codex-manager/commands/resets.js +18 -5
- package/dist/lib/codex-manager/commands/workspace.d.ts +4 -2
- package/dist/lib/codex-manager/commands/workspace.js +33 -2
- package/dist/lib/codex-manager/help.js +2 -2
- package/dist/lib/codex-manager.js +14 -1
- package/dist/lib/constants.d.ts +2 -0
- package/dist/lib/constants.js +2 -0
- package/dist/lib/context-budget-response.js +8 -8
- package/dist/lib/context-overflow.js +10 -10
- package/dist/lib/oauth-success.html +125 -125
- package/dist/lib/policy/runtime-policy-cache.js +1 -1
- package/dist/lib/policy/runtime-policy.d.ts +1 -0
- package/dist/lib/policy/runtime-policy.js +4 -0
- package/dist/lib/preemptive-quota-scheduler.js +12 -3
- package/dist/lib/prompts/codex-host-bridge.js +138 -138
- package/dist/lib/prompts/codex.js +79 -79
- package/dist/lib/quota-cache.d.ts +4 -0
- package/dist/lib/quota-readiness.d.ts +1 -1
- package/dist/lib/quota-readiness.js +9 -2
- package/dist/lib/request/rate-limit-decision.js +1 -1
- package/dist/lib/runtime/account-reset-credits.d.ts +11 -2
- package/dist/lib/runtime/account-reset-credits.js +35 -12
- package/dist/lib/runtime/automatic-account-checks.js +6 -5
- package/dist/lib/runtime/automatic-subscription-checks.js +4 -3
- package/dist/lib/runtime/model-discovery-status.js +2 -2
- package/dist/lib/runtime/reset-credit-routing.d.ts +11 -3
- package/dist/lib/runtime/reset-credit-routing.js +66 -21
- package/dist/lib/runtime/reset-credits.d.ts +31 -7
- package/dist/lib/runtime/reset-credits.js +135 -28
- package/dist/lib/runtime/rotation-proxy-state.d.ts +3 -0
- package/dist/lib/runtime/subscription-quota-order.d.ts +1 -0
- package/dist/lib/runtime/subscription-quota-order.js +7 -3
- package/dist/lib/runtime/workspace-model-scopes.d.ts +2 -0
- package/dist/lib/runtime/workspace-model-scopes.js +14 -4
- package/dist/lib/runtime-rotation-proxy.js +108 -29
- package/dist/lib/storage/snapshot-merge.js +15 -4
- package/dist/lib/web-dashboard-html.js +53 -53
- package/node_modules/@codex-ai/plugin/dist/index.d.ts +2 -2
- package/node_modules/@codex-ai/plugin/dist/index.js +2 -2
- package/node_modules/@codex-ai/plugin/dist/tool.d.ts +42 -42
- package/node_modules/@codex-ai/plugin/dist/tool.js +29 -29
- package/node_modules/@codex-ai/plugin/package.json +9 -9
- package/node_modules/@codex-ai/sdk/dist/index.d.ts +4 -4
- package/node_modules/@codex-ai/sdk/dist/index.js +2 -2
- package/node_modules/@codex-ai/sdk/package.json +8 -8
- package/package.json +209 -209
- package/scripts/codex-app-launcher.js +653 -653
- package/scripts/codex-app-router.js +446 -446
- package/scripts/codex-bin-resolver.js +332 -332
- package/scripts/codex-multi-auth.js +65 -65
- package/scripts/codex-routing.js +63 -63
- package/scripts/codex.js +7143 -7143
- package/scripts/install-codex-auth-utils.js +213 -213
- package/scripts/mcodex.js +367 -367
- package/scripts/postinstall.js +114 -114
- package/scripts/preuninstall.js +265 -265
- package/skills/codex-auth-setup/SKILL.md +73 -73
- package/vendor/codex-ai-plugin/dist/index.d.ts +2 -2
- package/vendor/codex-ai-plugin/dist/index.js +2 -2
- package/vendor/codex-ai-plugin/dist/tool.d.ts +42 -42
- package/vendor/codex-ai-plugin/dist/tool.js +29 -29
- package/vendor/codex-ai-plugin/package.json +9 -9
- package/vendor/codex-ai-sdk/dist/index.d.ts +4 -4
- package/vendor/codex-ai-sdk/dist/index.js +2 -2
- package/vendor/codex-ai-sdk/package.json +8 -8
|
@@ -6,11 +6,11 @@ import { fetchWithProxy } from "./request/proxy-fetch.js";
|
|
|
6
6
|
import { createNativeAccountStorageReader } from "./runtime/native-account-storage.js";
|
|
7
7
|
import { resetSnapshotQuota } from "./runtime/reset-credits.js";
|
|
8
8
|
import { createResetCreditService, loadResetCreditState } from "./runtime/account-reset-credits.js";
|
|
9
|
-
import { recoverResetQuota, applyConfirmedReset } from "./runtime/reset-credit-routing.js";
|
|
9
|
+
import { recoverResetQuota, applyConfirmedReset, primaryPoolFullyExhausted } from "./runtime/reset-credit-routing.js";
|
|
10
10
|
import { readSubscriptionQuotaEvent } from "./runtime/subscription-quota-event.js";
|
|
11
11
|
import { loadQuotaCache } from "./quota-cache.js";
|
|
12
12
|
import { findQuotaCacheEntryForAccount } from "./quota-readiness.js";
|
|
13
|
-
import { subscriptionQuotaPreference, compareSubscriptionQuota } from "./runtime/subscription-quota-order.js";
|
|
13
|
+
import { hasFreshOrdinaryUsagePermission, subscriptionQuotaPreference, compareSubscriptionQuota } from "./runtime/subscription-quota-order.js";
|
|
14
14
|
import { inferenceAccountKey } from "./runtime/inference-activity.js";
|
|
15
15
|
import { modelScopeId, workspaceModelScopes } from "./runtime/workspace-model-scopes.js";
|
|
16
16
|
import { RuntimeCapabilityFailures, classifyCapabilityFailure } from "./runtime/runtime-capability-failures.js";
|
|
@@ -33,7 +33,7 @@ import { AccountManager, AUTH_INVALIDATION_MARKER, extractAccountId, } from "./a
|
|
|
33
33
|
import { withRoutingMutex } from "./routing-mutex.js";
|
|
34
34
|
import { getQuotaKey } from "./accounts/rate-limits.js";
|
|
35
35
|
import { getFetchTimeoutMs, getNetworkErrorCooldownMs, getRetryAllAccountsMaxRetries, getServerErrorCooldownMs, getSessionAffinity, getSessionAffinityMaxEntries, getSessionAffinityTtlMs, getStreamStallTimeoutMs, getMinRotationIntervalMs, getTokenInvalidationCooldownMs, getTokenRefreshSkewMs, getPidOffsetEnabled, getPreemptiveQuotaEnabled, getPreemptiveQuotaMaxDeferralMs, getPreemptiveQuotaRemainingPercent5h, getPreemptiveQuotaRemainingPercent7d, getContextBudgetGuardEnabled, getContextBudgetGuardSoftPercent, getContextBudgetGuardHardPercent, getContextBudgetGuardModelWindowOverrides, getRoutingMutexMode, getSchedulingStrategy, loadPluginConfig, } from "./config.js";
|
|
36
|
-
import { CODEX_BASE_URL, HTTP_STATUS, LUNA_RESERVE_MODEL, MAX_RATE_LIMIT_DELAY_MS, OPENAI_HEADERS, OPENAI_HEADER_VALUES, URL_PATHS, } from "./constants.js";
|
|
36
|
+
import { CODEX_BASE_URL, HTTP_STATUS, LUNA_RESERVE_MODEL, MAX_RATE_LIMIT_DELAY_MS, RATE_LIMIT_CACHE_TTL_MS, OPENAI_HEADERS, OPENAI_HEADER_VALUES, URL_PATHS, } from "./constants.js";
|
|
37
37
|
import { getModelFamily } from "./prompts/codex.js";
|
|
38
38
|
import { CURRENT_CODEX_MODEL } from "./request/helpers/model-map.js";
|
|
39
39
|
import { recordRuntimeInferenceRequest, flushRuntimeInferenceActivity, mutateRuntimeObservabilitySnapshot, recordRuntimeAccountRecovery, recordRuntimePoolExhaustion, } from "./runtime/runtime-observability.js";
|
|
@@ -830,7 +830,8 @@ function writeUnauthorized(res) {
|
|
|
830
830
|
}
|
|
831
831
|
function writePoolExhausted(params) {
|
|
832
832
|
const { res, accountManager, family, model, reason } = params;
|
|
833
|
-
const waitMs = params.retryAfterMs ??
|
|
833
|
+
const waitMs = params.retryAfterMs ??
|
|
834
|
+
accountManager.getMinWaitTimeForFamily(family, model, params.ineligibleAccountIndexes);
|
|
834
835
|
const accountCount = accountManager.getAccountCount();
|
|
835
836
|
const accountSkipReasons = params.accountSkipReasons ?? {};
|
|
836
837
|
recordRuntimePoolExhaustion({
|
|
@@ -1046,6 +1047,7 @@ export async function startRuntimeRotationProxy(options) {
|
|
|
1046
1047
|
await Promise.all([automaticChecks?.stop(), automaticApiChecks?.stop()]);
|
|
1047
1048
|
websocketGateway.close();
|
|
1048
1049
|
await closeServer(server, sockets);
|
|
1050
|
+
state.subscriptionQuotaRejections?.clear();
|
|
1049
1051
|
await flushRuntimeInferenceActivity();
|
|
1050
1052
|
await state.activeAccountManager.flushPendingSave();
|
|
1051
1053
|
},
|
|
@@ -1607,7 +1609,7 @@ async function handleRequestInner(state, req, res, traceId) {
|
|
|
1607
1609
|
}
|
|
1608
1610
|
}
|
|
1609
1611
|
};
|
|
1610
|
-
|
|
1612
|
+
let resetCreditState = state.nativeOpenai && isResponsesRequest ? await cachedRead(state, "reset-credits", loadResetCreditState).catch(() => null) : null;
|
|
1611
1613
|
const quotaForScope = (account, scope) => {
|
|
1612
1614
|
// Scoped observations belong to this credential and workspace; legacy entries only describe the binding.
|
|
1613
1615
|
const cached = findQuotaCacheEntryForAccount(subscriptionQuotaCache, account, accountManager.getAccountsSnapshot(), undefined, scope.accountId);
|
|
@@ -1727,7 +1729,7 @@ async function handleRequestInner(state, req, res, traceId) {
|
|
|
1727
1729
|
if (retryAt > state.now())
|
|
1728
1730
|
throw new CatalogRetryError(retryAt - state.now());
|
|
1729
1731
|
backoff.delete(key);
|
|
1730
|
-
const scope = state.activeAccountManager.getAccountsSnapshot().flatMap(workspaceModelScopes).find(s => s.id === key && s.
|
|
1732
|
+
const scope = state.activeAccountManager.getAccountsSnapshot().flatMap(workspaceModelScopes).find(s => s.id === key && s.routable);
|
|
1731
1733
|
const account = scope ? state.activeAccountManager.getAccountByIndex(scope.accountIndex) : null;
|
|
1732
1734
|
if (!account || account.enabled === false || account.authInvalidatedAt ||
|
|
1733
1735
|
(account.cooldownReason === "auth-failure" && (account.coolingDownUntil ?? 0) > state.now()))
|
|
@@ -1800,7 +1802,7 @@ async function handleRequestInner(state, req, res, traceId) {
|
|
|
1800
1802
|
a.email === reference.email &&
|
|
1801
1803
|
a.accountId === reference.accountId)));
|
|
1802
1804
|
const scopes = discovery.flatMap(workspaceModelScopes);
|
|
1803
|
-
const enabledKeys = scopes.filter(scope => scope.
|
|
1805
|
+
const enabledKeys = scopes.filter(scope => scope.routable).map(scope => scope.id);
|
|
1804
1806
|
const routableKeys = scopes.filter(scope => scope.routable).map(scope => scope.id);
|
|
1805
1807
|
const oauthRefresh = modelCatalog.list(enabledKeys);
|
|
1806
1808
|
// Explicit check waits for all workspaces; picker discovery runs API and OAuth in parallel.
|
|
@@ -1968,20 +1970,45 @@ async function handleRequestInner(state, req, res, traceId) {
|
|
|
1968
1970
|
observations: state.subscriptionQuotaObservations ??= new Map(), now: state.now(), clearQuotaScheduler: a => state.preemptiveQuotaScheduler.clear(buildQuotaScheduleKey(a, context.family, context.model)) });
|
|
1969
1971
|
}
|
|
1970
1972
|
}
|
|
1971
|
-
|
|
1973
|
+
const primaryWorkspaceCandidates = () => new Map([...workspaceCandidates].filter(([index]) => {
|
|
1974
|
+
const account = accountManager.getAccountByIndex(index);
|
|
1975
|
+
// Catalog candidates can outlive credential invalidation while preparing a request.
|
|
1976
|
+
return account && account.enabled !== false && !account.authInvalidatedAt &&
|
|
1977
|
+
!policyDecision?.emergencyAccountIndexes?.has(index) && !policyDecision?.blockedAccountIndexes.has(index);
|
|
1978
|
+
}));
|
|
1979
|
+
const emergencyBackupsAllowed = () => state.nativeOpenai && isResponsesRequest &&
|
|
1980
|
+
context.model !== LUNA_RESERVE_MODEL && primaryPoolFullyExhausted(primaryWorkspaceCandidates(), resetCreditState, state.now(), (index, scope) => {
|
|
1981
|
+
const account = accountManager.getAccountByIndex(index);
|
|
1982
|
+
return account ? quotaForScope(account, scope) : undefined;
|
|
1983
|
+
});
|
|
1984
|
+
const recoverPrimaryQuota = async () => {
|
|
1985
|
+
if (!state.nativeOpenai || !isResponsesRequest || !context.model || context.model === LUNA_RESERVE_MODEL || isPinned)
|
|
1986
|
+
return false;
|
|
1972
1987
|
try {
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1988
|
+
const observations = state.subscriptionQuotaObservations ??= new Map();
|
|
1989
|
+
const recovered = await recoverResetQuota({ model: context.model, family: context.family, native: true, pinned: false, manager: accountManager,
|
|
1990
|
+
scopes: primaryWorkspaceCandidates(), quotaForScope, priorityByAccount: policyDecision?.priorityByAccount, preferredIndex: storageMeta.pinnedAccountIndex, service: createResetCreditService(accountManager),
|
|
1991
|
+
observations, quotaRejections: state.subscriptionQuotaRejections, now: state.now,
|
|
1976
1992
|
clearQuotaScheduler: account => state.preemptiveQuotaScheduler.clear(buildQuotaScheduleKey(account, context.family, context.model)),
|
|
1977
1993
|
});
|
|
1994
|
+
try {
|
|
1995
|
+
resetCreditState = await loadResetCreditState();
|
|
1996
|
+
}
|
|
1997
|
+
catch {
|
|
1998
|
+
resetCreditState = null;
|
|
1999
|
+
}
|
|
2000
|
+
return recovered;
|
|
1978
2001
|
}
|
|
1979
2002
|
catch {
|
|
2003
|
+
resetCreditState = null;
|
|
1980
2004
|
state.status.lastError = "Subscription reset recovery could not be confirmed; no further automatic redemption attempted.";
|
|
2005
|
+
return false;
|
|
1981
2006
|
}
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
2007
|
+
finally {
|
|
2008
|
+
state.readCache?.delete("reset-credits");
|
|
2009
|
+
}
|
|
2010
|
+
};
|
|
2011
|
+
await recoverPrimaryQuota();
|
|
1985
2012
|
// A pinned Luna request can arrive after an earlier turn already proved
|
|
1986
2013
|
// ordinary subscription quota exhausted. That persisted family marker
|
|
1987
2014
|
// would make chooseAccount reject the hard pin before any upstream call,
|
|
@@ -2032,8 +2059,10 @@ async function handleRequestInner(state, req, res, traceId) {
|
|
|
2032
2059
|
// alternative account, so exhausting that local heuristic can only reject a
|
|
2033
2060
|
// request that the pinned account could serve. Transcription is not inference
|
|
2034
2061
|
// and must not debit the Codex model pool or claim its inference circuit probe.
|
|
2062
|
+
// Native subscriptions use upstream quota for admission; local scoring must
|
|
2063
|
+
// not synthesize a 429 while actual subscription capacity remains.
|
|
2035
2064
|
// Transcription uses credential-only admission below.
|
|
2036
|
-
const bypassPoolTokenBucket = isPinned || isTranscribeRequest;
|
|
2065
|
+
const bypassPoolTokenBucket = isPinned || isTranscribeRequest || (state.nativeOpenai && isResponsesRequest);
|
|
2037
2066
|
const refundConsumedPoolToken = (account) => {
|
|
2038
2067
|
if (!bypassPoolTokenBucket) {
|
|
2039
2068
|
accountManager.refundToken(account, context.family, context.model);
|
|
@@ -2125,8 +2154,10 @@ async function handleRequestInner(state, req, res, traceId) {
|
|
|
2125
2154
|
return "retry";
|
|
2126
2155
|
};
|
|
2127
2156
|
let runtimeSelectionIterations = 0;
|
|
2157
|
+
let recoveryRetriedAfterExhaustion = false;
|
|
2128
2158
|
while ((isPinned ||
|
|
2129
2159
|
attemptedIndexes.size < accountCount ||
|
|
2160
|
+
(!isPinned && !recoveryRetriedAfterExhaustion) ||
|
|
2130
2161
|
(isTranscribeRequest && !state.nativeOpenai && accountCount === 0 && !reloadedAfterNoAccount)) &&
|
|
2131
2162
|
transientAttempts < transientAttemptLimit &&
|
|
2132
2163
|
(!isPinned ||
|
|
@@ -2177,6 +2208,14 @@ async function handleRequestInner(state, req, res, traceId) {
|
|
|
2177
2208
|
}
|
|
2178
2209
|
}
|
|
2179
2210
|
const attemptedForSelection = new Set([...attemptedIndexes, ...catalogExcludedIndexes()]);
|
|
2211
|
+
if (!isPinned && policyDecision?.emergencyAccountIndexes?.size) {
|
|
2212
|
+
const backupsAllowed = emergencyBackupsAllowed();
|
|
2213
|
+
if (!backupsAllowed)
|
|
2214
|
+
for (const index of policyDecision.emergencyAccountIndexes) {
|
|
2215
|
+
attemptedForSelection.add(index);
|
|
2216
|
+
accountSkipReasons.set(index, "policy: emergency backup reserved");
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2180
2219
|
if (isTranscribeRequest) {
|
|
2181
2220
|
const candidate = chooseTranscriptionAccount({
|
|
2182
2221
|
accountManager,
|
|
@@ -2248,6 +2287,20 @@ async function handleRequestInner(state, req, res, traceId) {
|
|
|
2248
2287
|
for (const index of catalogExcludedIndexes())
|
|
2249
2288
|
accountSkipReasons.set(index, "model-not-supported");
|
|
2250
2289
|
if (!selected) {
|
|
2290
|
+
// A 429 can be the first evidence that cached ordinary capacity is gone.
|
|
2291
|
+
// Recheck once before failing; only an explicit pre-inference rejection
|
|
2292
|
+
// reaches this path, never an ambiguous or already-streaming generation.
|
|
2293
|
+
if (!isPinned && !recoveryRetriedAfterExhaustion) {
|
|
2294
|
+
recoveryRetriedAfterExhaustion = true;
|
|
2295
|
+
const backupsWereAllowed = emergencyBackupsAllowed();
|
|
2296
|
+
if (await recoverPrimaryQuota()) {
|
|
2297
|
+
attemptedIndexes.clear();
|
|
2298
|
+
accountSkipReasons.clear();
|
|
2299
|
+
continue;
|
|
2300
|
+
}
|
|
2301
|
+
if (!backupsWereAllowed && emergencyBackupsAllowed())
|
|
2302
|
+
continue;
|
|
2303
|
+
}
|
|
2251
2304
|
if (isTranscribeRequest &&
|
|
2252
2305
|
!state.nativeOpenai &&
|
|
2253
2306
|
!reloadedAfterNoAccount) {
|
|
@@ -2347,10 +2400,12 @@ async function handleRequestInner(state, req, res, traceId) {
|
|
|
2347
2400
|
}
|
|
2348
2401
|
attemptedIndexes.add(selected.index);
|
|
2349
2402
|
const quotaScheduleKey = buildQuotaScheduleKey(selected, context.family, context.model);
|
|
2403
|
+
const selectedScope = workspaceCandidates.get(selected.index)?.[0];
|
|
2404
|
+
const nativePermission = selectedScope && hasFreshOrdinaryUsagePermission(quotaForScope(selected, selectedScope), state.now());
|
|
2350
2405
|
const preemptiveDeferral = state.preemptiveQuotaScheduler.getDeferral(quotaScheduleKey, state.now());
|
|
2351
2406
|
if (!isTranscribeRequest &&
|
|
2352
2407
|
context.model !== LUNA_RESERVE_MODEL &&
|
|
2353
|
-
preemptiveDeferral.defer &&
|
|
2408
|
+
!nativePermission && preemptiveDeferral.defer &&
|
|
2354
2409
|
preemptiveDeferral.waitMs > 0) {
|
|
2355
2410
|
accountSkipReasons.set(selected.index, preemptiveDeferral.reason ?? "quota-near-exhaustion");
|
|
2356
2411
|
exhaustionReason = "rate-limit";
|
|
@@ -2495,7 +2550,8 @@ async function handleRequestInner(state, req, res, traceId) {
|
|
|
2495
2550
|
continue;
|
|
2496
2551
|
}
|
|
2497
2552
|
let pendingScopes = workspaceCandidates.get(refreshed.account.index);
|
|
2498
|
-
|
|
2553
|
+
// Routes without model-catalog preparation still use the selected enabled workspace.
|
|
2554
|
+
if (isImageRequest || isTranscribeRequest || isThreadGoalRequest) {
|
|
2499
2555
|
pendingScopes = workspaceModelScopes(refreshed.account)
|
|
2500
2556
|
.filter((scope) => scope.routable)
|
|
2501
2557
|
.sort((a, b) => Number(b.selected) - Number(a.selected) || Number(b.bound) - Number(a.bound));
|
|
@@ -2552,14 +2608,10 @@ async function handleRequestInner(state, req, res, traceId) {
|
|
|
2552
2608
|
: undefined;
|
|
2553
2609
|
const disk = latest?.accounts.find(item => item.recordId && item.recordId === refreshed.account.recordId)
|
|
2554
2610
|
?? latest?.accounts.find(item => item.accountId === refreshed.account.accountId && sanitizeEmail(item.email) === sanitizeEmail(refreshed.account.email)) ?? retained;
|
|
2555
|
-
const workspace = disk?.workspaces?.find(item => item.id.trim() === accountId);
|
|
2556
|
-
// Derive the stored binding exactly as workspaceModelScopes does: trimmed,
|
|
2557
|
-
// falling back to the token's workspace claim when no id was stored.
|
|
2558
|
-
// A stored record carries accessToken; the transient-grace fallback is a live account (access).
|
|
2559
2611
|
const diskToken = disk?.accessToken ?? disk?.access;
|
|
2560
|
-
const diskBoundId = disk ? disk.accountId?.trim() || extractAccountId(diskToken)?.trim() : undefined;
|
|
2561
2612
|
const stillEligible = disk && disk.enabled !== false && !disk.authInvalidatedAt &&
|
|
2562
|
-
(
|
|
2613
|
+
workspaceModelScopes({ ...disk, index: refreshed.account.index, access: diskToken })
|
|
2614
|
+
.some(scope => scope.routable && scope.accountId === accountId);
|
|
2563
2615
|
if (!stillEligible) {
|
|
2564
2616
|
writeJson(res, 503, { error: { code: "routing_configuration_changed", message: "Routing eligibility changed while preparing the request. Retry with the current configuration." } });
|
|
2565
2617
|
return;
|
|
@@ -2732,7 +2784,9 @@ async function handleRequestInner(state, req, res, traceId) {
|
|
|
2732
2784
|
const key = JSON.stringify([requestScope.id, context.model]);
|
|
2733
2785
|
if (observations.size >= 1000 && !observations.has(key))
|
|
2734
2786
|
observations.delete(observations.keys().next().value ?? "");
|
|
2735
|
-
observations.set(key, { ...merged, model: context.model ?? "unknown", planType: planType ?? previous?.planType
|
|
2787
|
+
observations.set(key, { ...merged, model: context.model ?? "unknown", planType: planType ?? previous?.planType,
|
|
2788
|
+
...(snapshot.status === 200 && hasFreshOrdinaryUsagePermission(previous, state.now()) ? { ordinaryUsageAllowed: true, ordinaryUsageCheckedAt: previous?.ordinaryUsageCheckedAt ?? previous?.updatedAt } : {}),
|
|
2789
|
+
});
|
|
2736
2790
|
}
|
|
2737
2791
|
};
|
|
2738
2792
|
const quotaSnapshot = readQuotaSchedulerSnapshot(upstream.headers, upstream.status, state.now());
|
|
@@ -2833,6 +2887,17 @@ async function handleRequestInner(state, req, res, traceId) {
|
|
|
2833
2887
|
if (outcome === "retry")
|
|
2834
2888
|
continue;
|
|
2835
2889
|
}
|
|
2890
|
+
if (state.nativeOpenai && isResponsesRequest && requestScope && context.model !== LUNA_RESERVE_MODEL && subscriptionQuotaExceeded && !modelAtCapacity) {
|
|
2891
|
+
// A body-only quota rejection is newer evidence than an earlier
|
|
2892
|
+
// native permission. Record no invented quota percentage or permission.
|
|
2893
|
+
// An older in-flight success cannot clear this hint; newer native
|
|
2894
|
+
// permission checks or its bounded lifetime invalidate it during recovery.
|
|
2895
|
+
const rejections = state.subscriptionQuotaRejections ??= new Map();
|
|
2896
|
+
const key = JSON.stringify([requestScope.id, context.model]);
|
|
2897
|
+
if (rejections.size >= 1000 && !rejections.has(key))
|
|
2898
|
+
rejections.delete(rejections.keys().next().value ?? "");
|
|
2899
|
+
rejections.set(key, { key: requestScope.id, accountId: requestScope.accountId, observedAt: state.now() });
|
|
2900
|
+
}
|
|
2836
2901
|
if (context.model !== LUNA_RESERVE_MODEL && subscriptionQuotaExceeded) {
|
|
2837
2902
|
state.preemptiveQuotaScheduler.markRateLimited(quotaScheduleKey, retryAfterMs, state.now());
|
|
2838
2903
|
}
|
|
@@ -2845,7 +2910,7 @@ async function handleRequestInner(state, req, res, traceId) {
|
|
|
2845
2910
|
(context.model === "gpt-6-luna" || context.model === "gpt-5.6-luna")) {
|
|
2846
2911
|
const confirmedKey = confirmedQuotaExhaustionKey(context.family, context.model);
|
|
2847
2912
|
const confirmedResetAt = state.now() +
|
|
2848
|
-
Math.min(Math.max(0, Math.floor(retryAfterMs)),
|
|
2913
|
+
Math.min(Math.max(0, Math.floor(retryAfterMs)), RATE_LIMIT_CACHE_TTL_MS);
|
|
2849
2914
|
rateLimitState.account.rateLimitResetTimes[confirmedKey] = Math.max(rateLimitState.account.rateLimitResetTimes[confirmedKey] ?? 0, confirmedResetAt);
|
|
2850
2915
|
}
|
|
2851
2916
|
rateLimitState.accountManager.saveToDiskDebounced();
|
|
@@ -2885,7 +2950,11 @@ async function handleRequestInner(state, req, res, traceId) {
|
|
|
2885
2950
|
}
|
|
2886
2951
|
exhaustionReason = "rate-limit";
|
|
2887
2952
|
capabilityRejectionIsLatest = false;
|
|
2888
|
-
|
|
2953
|
+
// Account-local throttles are bounded by distinct accounts/workspaces.
|
|
2954
|
+
// They must not spend the network/server budget before a usable sibling.
|
|
2955
|
+
// Pins and model-wide capacity failures retain the transient bound.
|
|
2956
|
+
if (isPinned || modelAtCapacity)
|
|
2957
|
+
transientAttempts += 1;
|
|
2889
2958
|
transientExhaustionReason = "rate-limit";
|
|
2890
2959
|
state.status.retries += 1;
|
|
2891
2960
|
noteRotation();
|
|
@@ -2895,9 +2964,9 @@ async function handleRequestInner(state, req, res, traceId) {
|
|
|
2895
2964
|
const bodyText = await readErrorBody(upstream, state.streamStallTimeoutMs);
|
|
2896
2965
|
const errorCode = extractErrorCodeFromBody(bodyText);
|
|
2897
2966
|
if (isWorkspaceDisabledError(upstream.status, errorCode, bodyText)) {
|
|
2898
|
-
if (requestScope
|
|
2967
|
+
if (requestScope?.storedWorkspaceId !== undefined) {
|
|
2899
2968
|
const workspaceState = responseStateTarget();
|
|
2900
|
-
if (workspaceState.accountManager.disableWorkspace(workspaceState.account, requestScope.
|
|
2969
|
+
if (workspaceState.accountManager.disableWorkspace(workspaceState.account, requestScope.storedWorkspaceId))
|
|
2901
2970
|
workspaceState.accountManager.saveToDiskDebounced();
|
|
2902
2971
|
pendingScopes?.shift();
|
|
2903
2972
|
refundConsumedPoolToken(refreshed.account);
|
|
@@ -3132,7 +3201,7 @@ async function handleRequestInner(state, req, res, traceId) {
|
|
|
3132
3201
|
// No-op when no reason is recorded, so the hot path stays write-free.
|
|
3133
3202
|
if (!isTranscribeRequest) {
|
|
3134
3203
|
const quotaDeferral = state.preemptiveQuotaScheduler.getDeferral(quotaScheduleKey, state.now());
|
|
3135
|
-
const nearExhaustionWaitMs = context.model === LUNA_RESERVE_MODEL
|
|
3204
|
+
const nearExhaustionWaitMs = (context.model === LUNA_RESERVE_MODEL || (requestScope && hasFreshOrdinaryUsagePermission(quotaForScope(refreshed.account, requestScope), state.now())))
|
|
3136
3205
|
? 0
|
|
3137
3206
|
: quotaDeferral.defer
|
|
3138
3207
|
? quotaDeferral.waitMs
|
|
@@ -3403,9 +3472,13 @@ async function handleRequestInner(state, req, res, traceId) {
|
|
|
3403
3472
|
// Read live account state, not only selection skip reasons. The final
|
|
3404
3473
|
// refresh attempt can install an auth cooldown after the last selection
|
|
3405
3474
|
// pass, so that blocker may never have been written to accountSkipReasons.
|
|
3475
|
+
const backupsReserved = !isPinned && !emergencyBackupsAllowed();
|
|
3406
3476
|
for (const account of accountManager.getAccountsSnapshot()) {
|
|
3407
3477
|
if (policyDecision?.blockedAccountIndexes.has(account.index))
|
|
3408
3478
|
continue;
|
|
3479
|
+
// Selection never offers a reserved backup, so it is not ready capacity.
|
|
3480
|
+
if (backupsReserved && policyDecision?.emergencyAccountIndexes?.has(account.index))
|
|
3481
|
+
continue;
|
|
3409
3482
|
if (account.enabled === false)
|
|
3410
3483
|
continue;
|
|
3411
3484
|
if (!accountManager.hasEnabledWorkspaces(account))
|
|
@@ -3430,6 +3503,11 @@ async function handleRequestInner(state, req, res, traceId) {
|
|
|
3430
3503
|
return minWaitMs ?? 0;
|
|
3431
3504
|
})()
|
|
3432
3505
|
: undefined;
|
|
3506
|
+
const ineligibleAccountIndexes = new Set(policyDecision?.blockedAccountIndexes ?? []);
|
|
3507
|
+
if (!isPinned && !emergencyBackupsAllowed()) {
|
|
3508
|
+
for (const index of policyDecision?.emergencyAccountIndexes ?? [])
|
|
3509
|
+
ineligibleAccountIndexes.add(index);
|
|
3510
|
+
}
|
|
3433
3511
|
writePoolExhausted({
|
|
3434
3512
|
res,
|
|
3435
3513
|
accountManager,
|
|
@@ -3437,6 +3515,7 @@ async function handleRequestInner(state, req, res, traceId) {
|
|
|
3437
3515
|
model: context.model,
|
|
3438
3516
|
reason: exhaustionReason,
|
|
3439
3517
|
retryAfterMs: transcriptionRetryAfterMs,
|
|
3518
|
+
ineligibleAccountIndexes,
|
|
3440
3519
|
accountSkipReasons: Object.fromEntries([...accountSkipReasons.entries()].map(([index, reason]) => [
|
|
3441
3520
|
String(index),
|
|
3442
3521
|
reason,
|
|
@@ -26,7 +26,9 @@ const byId = (rows) => new Map((rows ?? []).map(row => [row.id, row]));
|
|
|
26
26
|
/**
|
|
27
27
|
* Merge workspaces by id, never by array position. Fields changed on one side
|
|
28
28
|
* win; enabled/disabledAt move as one unit, and when both sides changed it an
|
|
29
|
-
* intentional re-enable wins over a disable.
|
|
29
|
+
* intentional re-enable wins over a disable. Concurrent disables preserve an
|
|
30
|
+
* operator exclusion (no disabledAt) over a recoverable runtime health marker.
|
|
31
|
+
* Other two-sided edits conflict.
|
|
30
32
|
* Removal follows the account rule: a record removed on either side stays
|
|
31
33
|
* removed.
|
|
32
34
|
*/
|
|
@@ -44,7 +46,13 @@ function mergeWorkspaces(base, disk, local) {
|
|
|
44
46
|
continue;
|
|
45
47
|
}
|
|
46
48
|
const state = (row) => ({ enabled: row?.enabled, disabledAt: row?.disabledAt });
|
|
47
|
-
const stateSource = equal(state(b), state(l)) ? d
|
|
49
|
+
const stateSource = equal(state(b), state(l)) ? d
|
|
50
|
+
: equal(state(b), state(d)) ? l
|
|
51
|
+
: d.enabled !== false ? d
|
|
52
|
+
: l.enabled !== false ? l
|
|
53
|
+
: d.disabledAt === undefined ? d
|
|
54
|
+
: l.disabledAt === undefined ? l
|
|
55
|
+
: d.disabledAt >= l.disabledAt ? d : l;
|
|
48
56
|
const pick = (row) => ({ ...row, enabled: stateSource.enabled, disabledAt: stateSource.disabledAt });
|
|
49
57
|
const merged = mergeValue(b ? pick(b) : undefined, pick(d), pick(l));
|
|
50
58
|
if (merged.disabledAt === undefined)
|
|
@@ -54,7 +62,7 @@ function mergeWorkspaces(base, disk, local) {
|
|
|
54
62
|
const currentId = (row) => row.workspaces?.[row.currentWorkspaceIndex ?? 0]?.id;
|
|
55
63
|
const chosen = currentId(local) !== currentId(base) ? currentId(local) : currentId(disk);
|
|
56
64
|
let index = workspaces.findIndex(row => row.id === chosen);
|
|
57
|
-
if (index < 0
|
|
65
|
+
if (index < 0) {
|
|
58
66
|
const enabled = workspaces.findIndex(row => row.enabled !== false);
|
|
59
67
|
index = enabled >= 0 ? enabled : Math.max(index, 0);
|
|
60
68
|
}
|
|
@@ -86,7 +94,10 @@ function mergeRuntimeAccount(base, disk, local) {
|
|
|
86
94
|
const before = base.rateLimitResetTimes?.[key], a = disk.rateLimitResetTimes?.[key], b = local.rateLimitResetTimes?.[key];
|
|
87
95
|
// An explicit clear is an operation, not a new observation.
|
|
88
96
|
const clear = before !== undefined && ((a === undefined && b === before) || (b === undefined && a === before) || (a === undefined && b === undefined));
|
|
89
|
-
|
|
97
|
+
// A side that kept the baseline made no observation, so the other side's
|
|
98
|
+
// value wins even when lower (e.g. a load-time revalidation bound).
|
|
99
|
+
// Only two concurrent observations keep the later deadline.
|
|
100
|
+
const value = clear ? undefined : a === before ? b : b === before ? a : Math.max(a ?? 0, b ?? 0);
|
|
90
101
|
if (value !== undefined)
|
|
91
102
|
limits[key] = value;
|
|
92
103
|
}
|
|
@@ -1,56 +1,56 @@
|
|
|
1
|
-
const DASHBOARD_HTML = `
|
|
2
|
-
<!doctype html>
|
|
3
|
-
<html lang="zh-CN">
|
|
4
|
-
<head>
|
|
5
|
-
<meta charset="utf-8">
|
|
6
|
-
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
7
|
-
<title>Codex Multi-Auth · 号池监控</title>
|
|
8
|
-
<style>
|
|
9
|
-
:root{color-scheme:dark;--bg:#09111f;--panel:#101c2f;--panel2:#14243b;--line:#223653;--text:#edf5ff;--muted:#8ea5c2;--green:#36d399;--amber:#f7c95c;--red:#fb7185;--blue:#65a8ff;--shadow:0 14px 40px rgba(0,0,0,.25)}
|
|
10
|
-
*{box-sizing:border-box}body{margin:0;background:radial-gradient(circle at 20% 0,#17345a 0,transparent 38%),var(--bg);color:var(--text);font:14px/1.5 ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}
|
|
11
|
-
.shell{max-width:1440px;margin:0 auto;padding:28px 26px 48px}.top{display:flex;justify-content:space-between;align-items:flex-start;gap:18px;margin-bottom:26px}.brand{display:flex;gap:14px;align-items:center}.mark{width:44px;height:44px;border-radius:13px;display:grid;place-items:center;background:linear-gradient(135deg,#4b8dff,#55d6ae);color:#07111f;font-weight:900;font-size:18px;box-shadow:0 8px 24px #3b82f655}.eyebrow{color:var(--blue);font-size:12px;letter-spacing:.12em;text-transform:uppercase}.title{font-size:28px;font-weight:800;letter-spacing:-.03em}.subtitle{color:var(--muted);margin-top:2px}.actions{display:flex;gap:10px;align-items:center}.button{border:1px solid var(--line);background:var(--panel);color:var(--text);border-radius:9px;padding:9px 13px;cursor:pointer}.button:hover{border-color:var(--blue);background:var(--panel2)}.live{display:flex;gap:7px;align-items:center;color:var(--muted);font-size:12px}.dot{width:8px;height:8px;border-radius:50%;background:var(--green);box-shadow:0 0 10px var(--green)}.dot.off{background:var(--red);box-shadow:0 0 10px var(--red)}
|
|
12
|
-
.summary{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:12px;margin-bottom:22px}.card,.panel{background:linear-gradient(145deg,#122139dd,#0e192add);border:1px solid var(--line);border-radius:14px;box-shadow:var(--shadow)}.card{padding:17px}.label{color:var(--muted);font-size:12px}.value{font-size:26px;font-weight:800;margin-top:7px}.value.green{color:var(--green)}.value.amber{color:var(--amber)}.value.red{color:var(--red)}.value.blue{color:var(--blue)}
|
|
13
|
-
.grid{display:grid;grid-template-columns:minmax(0,1.7fr) minmax(280px,.7fr);gap:18px;align-items:start}.panel{padding:18px}.panel-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px}.panel-title{font-size:16px;font-weight:750}.panel-meta{color:var(--muted);font-size:12px}.accounts{display:grid;gap:10px}.account{padding:15px;border:1px solid var(--line);border-radius:11px;background:#0e1a2caa}.account-head{display:flex;justify-content:space-between;gap:12px;align-items:flex-start}.account-name{font-weight:700;overflow-wrap:anywhere}.account-meta{color:var(--muted);font-size:12px;margin-top:3px}.badge{border-radius:99px;padding:3px 9px;font-size:11px;white-space:nowrap;border:1px solid #ffffff18}.badge.ready{color:var(--green);background:#36d39918}.badge.login{color:var(--red);background:#fb718518}.badge.disabled{color:var(--muted);background:#8ea5c218}.badge.current{color:var(--blue);background:#65a8ff18}.account-stats{display:grid;grid-template-columns:1fr 1fr 120px;gap:15px;margin-top:14px}.quota{min-width:0}.quota-row{display:flex;justify-content:space-between;gap:7px;color:var(--muted);font-size:11px;margin-top:8px}.bar{height:8px;border-radius:99px;background:#243752;overflow:hidden;margin-top:5px}.fill{height:100%;border-radius:99px;background:linear-gradient(90deg,var(--green),#7de2ab)}.fill.warn{background:linear-gradient(90deg,var(--amber),#ff9e5c)}.fill.danger{background:linear-gradient(90deg,var(--red),#ffadad)}.risk{text-align:right}.risk-num{font-size:23px;font-weight:800}.risk-low{color:var(--green)}.risk-medium{color:var(--amber)}.risk-high{color:var(--red)}.reason{color:var(--muted);font-size:12px;margin-top:8px;overflow-wrap:anywhere}.empty{padding:35px;text-align:center;color:var(--muted)}.subhead{margin:14px 0 4px;color:var(--muted);font-size:11px;letter-spacing:.1em;text-transform:uppercase}.side{display:grid;gap:18px}.kv{display:flex;justify-content:space-between;gap:12px;padding:9px 0;border-bottom:1px solid #ffffff0d}.kv:last-child{border-bottom:0}.kv b{font-weight:650;text-align:right;overflow-wrap:anywhere}.notice{padding:12px;border-radius:10px;background:#f7c95c13;border:1px solid #f7c95c3d;color:#fbe6a2;font-size:12px}.error{padding:12px;border-radius:10px;background:#fb718513;border:1px solid #fb71853d;color:#ffc2cb;font-size:12px}.foot{color:var(--muted);font-size:12px;margin-top:20px;text-align:center}
|
|
14
|
-
@media(max-width:1050px){.summary{grid-template-columns:repeat(3,minmax(0,1fr))}.grid{grid-template-columns:1fr}.side{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:680px){.shell{padding:18px 13px 36px}.top{display:block}.actions{margin-top:15px;justify-content:space-between}.summary{grid-template-columns:repeat(2,minmax(0,1fr))}.account-stats{grid-template-columns:1fr 1fr}.risk{text-align:left}.side{grid-template-columns:1fr}.title{font-size:23px}}
|
|
15
|
-
</style>
|
|
16
|
-
</head>
|
|
17
|
-
<body>
|
|
18
|
-
<main class="shell">
|
|
19
|
-
<header class="top"><div class="brand"><div class="mark">MA</div><div><div class="eyebrow">Codex Multi-Auth</div><div class="title" data-i18n="title">号池实时监控</div><div class="subtitle" data-i18n="subtitle">账号、配额与轮换状态</div></div></div><div class="actions"><div class="live"><span id="dot" class="dot"></span><span id="connection" data-i18n="connecting">连接中…</span></div><button id="lang" class="button">EN</button><button id="refresh" class="button" data-i18n="refresh">刷新</button></div></header>
|
|
20
|
-
<section id="summary" class="summary"></section>
|
|
21
|
-
<div id="error" class="error" hidden></div>
|
|
22
|
-
<section class="grid"><section class="panel"><div class="panel-head"><div class="panel-title" data-i18n="accounts">账号池</div><div id="updated" class="panel-meta">—</div></div><div id="accounts" class="accounts"></div></section><aside class="side"><section class="panel"><div class="panel-head"><div class="panel-title" data-i18n="routing">路由</div></div><div id="routing"></div></section><section class="panel"><div class="panel-head"><div class="panel-title" data-i18n="runtime">运行时</div></div><div id="runtime"></div></section></aside></section>
|
|
23
|
-
<div class="foot" data-i18n="foot">数据来自本机号池;页面默认仅监听回环地址,按 Ctrl-C 停止服务。</div>
|
|
24
|
-
</main>
|
|
25
|
-
<script>
|
|
26
|
-
(function(){
|
|
27
|
-
var token="__CMA_TOKEN__";
|
|
28
|
-
var lang="__CMA_LANG__";
|
|
29
|
-
var text={zh:{title:"号池实时监控",subtitle:"账号、配额与轮换状态",connecting:"连接中…",connected:"实时连接",offline:"连接中断",refresh:"刷新",accounts:"账号池",apiAccounts:"API/ZDR 账号",routing:"路由",runtime:"运行时",total:"总账号",ready:"可用",login:"需重新登录",disabled:"已禁用",enabled:"已启用",recommend:"推荐账号",active:"当前账号",requests:"请求",success:"成功",failed:"失败",last:"最近请求",quota:"配额",risk:"风险",unknown:"未知",reason:"原因",primary:"主窗口",secondary:"次窗口",priority:"优先级",models:"可见模型",foot:"数据来自本机号池;页面默认仅监听回环地址,按 Ctrl-C 停止服务。"},en:{title:"Pool live monitor",subtitle:"Accounts, quota and routing",connecting:"Connecting…",connected:"Live",offline:"Disconnected",refresh:"Refresh",accounts:"Account pool",apiAccounts:"API/ZDR accounts",routing:"Routing",runtime:"Runtime",total:"Accounts",ready:"Ready",login:"Re-login needed",disabled:"Disabled",enabled:"Enabled",recommend:"Recommended",active:"Current",requests:"Requests",success:"Success",failed:"Failed",last:"Last request",quota:"Quota",risk:"Risk",unknown:"Unknown",reason:"Reason",primary:"primary",secondary:"secondary",priority:"priority",models:"visible models",foot:"Data comes from the local pool; the server listens on loopback by default. Ctrl-C stops it."}};
|
|
30
|
-
function t(k){return text[lang][k]||k}
|
|
31
|
-
function label(v){var s=String(v==null?"":v);return lang==="zh"?s.replace(/^Account /,"账号 ").replace(/Token account/g,"令牌账号").replace(/, id:/g,",ID:").replace(/, /g,","):s}
|
|
32
|
-
function reasonText(v){var s=String(v==null?"—":v);if(lang!=="zh")return s;return s.replace("token-invalid — re-login needed","令牌无效——需要重新登录").replace("primary quota ","主窗口配额 ").replace(" secondary quota "," 次窗口配额 ").replace(" used","已使用").replace("account is disabled","账号已禁用").replace("quota cache exhausted","配额缓存已用尽")}
|
|
33
|
-
function esc(v){return String(v==null?"":v).replace(/[&<>"']/g,function(c){return({"&":"&","<":"<",">":">",'"':""","'":"'"})[c]})}
|
|
34
|
-
function api(path){return path+(path.indexOf("?")>=0?"&":"?")+"token="+encodeURIComponent(token)}
|
|
35
|
-
function ago(ms){if(typeof ms!=="number"||!isFinite(ms))return "—";var s=Math.max(0,Math.round(ms/1000));if(s<60)return s+"s";if(s<3600)return Math.floor(s/60)+"m";if(s<86400)return Math.floor(s/3600)+"h";return Math.floor(s/86400)+"d"}
|
|
36
|
-
function statusClass(a){var m=(a.markers||[]).join(" ");if(a.enabled===false)return "disabled";if(/invalid|sign-in|re-login/i.test(m))return "login";return "ready"}
|
|
37
|
-
function winLabel(w,key){var m=w&&w.windowMinutes;if(typeof m==="number"&&isFinite(m)&&m>0){var zh=lang==="zh";if(m%1440===0)return (m/1440)+(zh?"天":"d");if(m%60===0)return (m/60)+(zh?"小时":"h");return m+(zh?"分钟":"m")}return t(key)}
|
|
38
|
-
function bar(window,key){if(!window||typeof window.usedPercent!=="number")return '<div class="label">'+t("unknown")+"</div>";var p=Math.max(0,Math.min(100,Math.round(window.usedPercent)));var cls=p>=90?" danger":p>=75?" warn":"";return '<div class="quota-row"><span>'+esc(winLabel(window,key))+" "+t("quota")+'</span><span>'+p+"% / "+(100-p)+"%</span></div><div class="bar"><div class="fill'+cls+'" style="width:'+p+'%"></div></div>'}
|
|
39
|
-
function draw(data){window.__data=data;var all=data.accounts||[];var ready=all.filter(function(a){return a.enabled!==false&&statusClass(a)==="ready"}).length;var login=all.filter(function(a){return statusClass(a)==="login"}).length;var disabled=all.filter(function(a){return a.enabled===false}).length;var rec=data.recommendedIndex==null?"—":"#"+(data.recommendedIndex+1);var rt=data.runtime||{};document.getElementById("summary").innerHTML='<div class="card"><div class="label">'+t("total")+'</div><div class="value blue">'+(data.totalAccountCount||all.length)+'</div></div><div class="card"><div class="label">'+t("ready")+'</div><div class="value green">'+ready+'</div></div><div class="card"><div class="label">'+t("login")+'</div><div class="value red">'+login+'</div></div><div class="card"><div class="label">'+t("disabled")+'</div><div class="value">'+disabled+'</div></div><div class="card"><div class="label">'+t("recommend")+'</div><div class="value amber">'+rec+'</div></div>';
|
|
40
|
-
document.getElementById("accounts").innerHTML=all.length?all.map(function(a){var sub=a.subscription||{};var q=a.quota||{};var risk=a.forecastRiskScore==null?"—":a.forecastRiskScore+"/100";var rl=a.forecastRiskLevel||"low";var badges=(a.markers||[]).map(function(m){return '<span class="badge '+(/current/.test(m)?"current":statusClass(a))+'">'+esc(label(m))+'</span>'}).join(" ");return '<article class="account"><div class="account-head"><div><div class="account-name">'+esc(label(a.label||("Account "+(a.index+1))))+'</div><div class="account-meta">'+esc(sub.planLabel||t("unknown"))+" · "+esc(label(sub.selectedWorkspace||""))+'</div></div><div>'+badges+' <span class="badge '+statusClass(a)+'">'+(statusClass(a)==="ready"?t("ready"):statusClass(a)==="login"?t("login"):t("disabled"))+'</span></div></div><div class="account-stats"><div class="quota">'+bar(q.primary,"primary")+bar(q.secondary,"secondary")+'</div><div><div class="label">'+t("last")+'</div><div class="value" style="font-size:18px">'+ago(a.lastInferenceRequestAt?Date.now()-a.lastInferenceRequestAt:null)+'</div><div class="reason">'+t("reason")+": "+esc(reasonText(a.reason||"—"))+'</div></div><div class="risk"><div class="label">'+t("risk")+'</div><div class="risk-num risk-'+esc(rl)+'">'+esc(risk)+"</div></div></div></article>"}).join(""):'<div class="empty">'+t("unknown")+"</div>";
|
|
41
|
-
var apis=(data.apiAccounts||[]).map(function(a){return '<article class="account"><div class="account-head"><div><div class="account-name">'+esc(label(a.label||"API"))+'</div><div class="account-meta">'+esc(a.kind||"api")+" · "+t("priority")+" "+esc(a.priority==null?"—":a.priority)+'</div></div><div><span class="badge '+(a.enabled===false?"disabled":"ready")+'">'+(a.enabled===false?t("disabled"):t("enabled"))+'</span></div></div><div class="account-meta">'+t("models")+": "+esc((a.visibleModels||[]).length||"—")+" · "+t("last")+": "+ago(a.lastInferenceRequestAt?Date.now()-a.lastInferenceRequestAt:null)+'</div></article>'}).join("");
|
|
42
|
-
if(apis)document.getElementById("accounts").innerHTML+='<div class="subhead">'+t("apiAccounts")+"</div>"+apis;
|
|
43
|
-
var recReason=data.recommendationReason||"—";document.getElementById("routing").innerHTML='<div class="kv"><span>'+t("active")+'</span><b>'+esc(data.activeIndex==null?"—":"#"+(data.activeIndex+1))+'</b></div><div class="kv"><span>'+t("recommend")+'</span><b>'+esc(rec)+"</b></div><div class="kv"><span>'+t("reason")+'</span><b>'+esc(recReason)+'</b></div>';
|
|
44
|
-
document.getElementById("runtime").innerHTML='<div class="kv"><span>'+t("requests")+'</span><b>'+esc(rt.responsesRequests==null?"—":rt.responsesRequests)+'</b></div><div class="kv"><span>'+t("success")+'</span><b>'+esc(rt.successfulRequests==null?"—":rt.successfulRequests)+'</b></div><div class="kv"><span>'+t("failed")+'</span><b>'+esc(rt.failedRequests==null?"—":rt.failedRequests)+'</b></div><div class="kv"><span>'+t("last")+'</span><b>'+esc(data.runtimeInUseIndex==null?"—":"#"+(data.runtimeInUseIndex+1))+'</b></div>';
|
|
45
|
-
document.getElementById("updated").textContent=new Date().toLocaleTimeString();document.getElementById("error").hidden=true;document.getElementById("dot").className="dot";document.getElementById("connection").textContent=t("connected")
|
|
46
|
-
}
|
|
47
|
-
function setLang(){document.documentElement.lang=lang==="zh"?"zh-CN":"en";document.querySelectorAll("[data-i18n]").forEach(function(e){e.textContent=t(e.getAttribute("data-i18n"))});document.getElementById("lang").textContent=lang==="zh"?"EN":"中文";if(window.__data)draw(window.__data)}
|
|
48
|
-
function failed(e){document.getElementById("error").hidden=false;document.getElementById("error").textContent=e&&e.message?e.message:"Request failed";document.getElementById("dot").className="dot off";document.getElementById("connection").textContent=t("offline")}
|
|
49
|
-
function load(){fetch(api("/api/status"),{cache:"no-store"}).then(function(r){if(!r.ok)throw Error("HTTP "+r.status);return r.json()}).then(draw).catch(failed)}
|
|
50
|
-
document.getElementById("refresh").onclick=load;document.getElementById("lang").onclick=function(){lang=lang==="zh"?"en":"zh";setLang()};setLang();load();try{var es=new EventSource(api("/events"));es.onmessage=function(e){try{draw(JSON.parse(e.data))}catch(_){}};es.onerror=function(){document.getElementById("dot").className="dot off";document.getElementById("connection").textContent=t("offline")}}catch(_){setInterval(load,5000)}
|
|
51
|
-
})();
|
|
52
|
-
</script>
|
|
53
|
-
</body>
|
|
1
|
+
const DASHBOARD_HTML = `
|
|
2
|
+
<!doctype html>
|
|
3
|
+
<html lang="zh-CN">
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="utf-8">
|
|
6
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
7
|
+
<title>Codex Multi-Auth · 号池监控</title>
|
|
8
|
+
<style>
|
|
9
|
+
:root{color-scheme:dark;--bg:#09111f;--panel:#101c2f;--panel2:#14243b;--line:#223653;--text:#edf5ff;--muted:#8ea5c2;--green:#36d399;--amber:#f7c95c;--red:#fb7185;--blue:#65a8ff;--shadow:0 14px 40px rgba(0,0,0,.25)}
|
|
10
|
+
*{box-sizing:border-box}body{margin:0;background:radial-gradient(circle at 20% 0,#17345a 0,transparent 38%),var(--bg);color:var(--text);font:14px/1.5 ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}
|
|
11
|
+
.shell{max-width:1440px;margin:0 auto;padding:28px 26px 48px}.top{display:flex;justify-content:space-between;align-items:flex-start;gap:18px;margin-bottom:26px}.brand{display:flex;gap:14px;align-items:center}.mark{width:44px;height:44px;border-radius:13px;display:grid;place-items:center;background:linear-gradient(135deg,#4b8dff,#55d6ae);color:#07111f;font-weight:900;font-size:18px;box-shadow:0 8px 24px #3b82f655}.eyebrow{color:var(--blue);font-size:12px;letter-spacing:.12em;text-transform:uppercase}.title{font-size:28px;font-weight:800;letter-spacing:-.03em}.subtitle{color:var(--muted);margin-top:2px}.actions{display:flex;gap:10px;align-items:center}.button{border:1px solid var(--line);background:var(--panel);color:var(--text);border-radius:9px;padding:9px 13px;cursor:pointer}.button:hover{border-color:var(--blue);background:var(--panel2)}.live{display:flex;gap:7px;align-items:center;color:var(--muted);font-size:12px}.dot{width:8px;height:8px;border-radius:50%;background:var(--green);box-shadow:0 0 10px var(--green)}.dot.off{background:var(--red);box-shadow:0 0 10px var(--red)}
|
|
12
|
+
.summary{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:12px;margin-bottom:22px}.card,.panel{background:linear-gradient(145deg,#122139dd,#0e192add);border:1px solid var(--line);border-radius:14px;box-shadow:var(--shadow)}.card{padding:17px}.label{color:var(--muted);font-size:12px}.value{font-size:26px;font-weight:800;margin-top:7px}.value.green{color:var(--green)}.value.amber{color:var(--amber)}.value.red{color:var(--red)}.value.blue{color:var(--blue)}
|
|
13
|
+
.grid{display:grid;grid-template-columns:minmax(0,1.7fr) minmax(280px,.7fr);gap:18px;align-items:start}.panel{padding:18px}.panel-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px}.panel-title{font-size:16px;font-weight:750}.panel-meta{color:var(--muted);font-size:12px}.accounts{display:grid;gap:10px}.account{padding:15px;border:1px solid var(--line);border-radius:11px;background:#0e1a2caa}.account-head{display:flex;justify-content:space-between;gap:12px;align-items:flex-start}.account-name{font-weight:700;overflow-wrap:anywhere}.account-meta{color:var(--muted);font-size:12px;margin-top:3px}.badge{border-radius:99px;padding:3px 9px;font-size:11px;white-space:nowrap;border:1px solid #ffffff18}.badge.ready{color:var(--green);background:#36d39918}.badge.login{color:var(--red);background:#fb718518}.badge.disabled{color:var(--muted);background:#8ea5c218}.badge.current{color:var(--blue);background:#65a8ff18}.account-stats{display:grid;grid-template-columns:1fr 1fr 120px;gap:15px;margin-top:14px}.quota{min-width:0}.quota-row{display:flex;justify-content:space-between;gap:7px;color:var(--muted);font-size:11px;margin-top:8px}.bar{height:8px;border-radius:99px;background:#243752;overflow:hidden;margin-top:5px}.fill{height:100%;border-radius:99px;background:linear-gradient(90deg,var(--green),#7de2ab)}.fill.warn{background:linear-gradient(90deg,var(--amber),#ff9e5c)}.fill.danger{background:linear-gradient(90deg,var(--red),#ffadad)}.risk{text-align:right}.risk-num{font-size:23px;font-weight:800}.risk-low{color:var(--green)}.risk-medium{color:var(--amber)}.risk-high{color:var(--red)}.reason{color:var(--muted);font-size:12px;margin-top:8px;overflow-wrap:anywhere}.empty{padding:35px;text-align:center;color:var(--muted)}.subhead{margin:14px 0 4px;color:var(--muted);font-size:11px;letter-spacing:.1em;text-transform:uppercase}.side{display:grid;gap:18px}.kv{display:flex;justify-content:space-between;gap:12px;padding:9px 0;border-bottom:1px solid #ffffff0d}.kv:last-child{border-bottom:0}.kv b{font-weight:650;text-align:right;overflow-wrap:anywhere}.notice{padding:12px;border-radius:10px;background:#f7c95c13;border:1px solid #f7c95c3d;color:#fbe6a2;font-size:12px}.error{padding:12px;border-radius:10px;background:#fb718513;border:1px solid #fb71853d;color:#ffc2cb;font-size:12px}.foot{color:var(--muted);font-size:12px;margin-top:20px;text-align:center}
|
|
14
|
+
@media(max-width:1050px){.summary{grid-template-columns:repeat(3,minmax(0,1fr))}.grid{grid-template-columns:1fr}.side{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:680px){.shell{padding:18px 13px 36px}.top{display:block}.actions{margin-top:15px;justify-content:space-between}.summary{grid-template-columns:repeat(2,minmax(0,1fr))}.account-stats{grid-template-columns:1fr 1fr}.risk{text-align:left}.side{grid-template-columns:1fr}.title{font-size:23px}}
|
|
15
|
+
</style>
|
|
16
|
+
</head>
|
|
17
|
+
<body>
|
|
18
|
+
<main class="shell">
|
|
19
|
+
<header class="top"><div class="brand"><div class="mark">MA</div><div><div class="eyebrow">Codex Multi-Auth</div><div class="title" data-i18n="title">号池实时监控</div><div class="subtitle" data-i18n="subtitle">账号、配额与轮换状态</div></div></div><div class="actions"><div class="live"><span id="dot" class="dot"></span><span id="connection" data-i18n="connecting">连接中…</span></div><button id="lang" class="button">EN</button><button id="refresh" class="button" data-i18n="refresh">刷新</button></div></header>
|
|
20
|
+
<section id="summary" class="summary"></section>
|
|
21
|
+
<div id="error" class="error" hidden></div>
|
|
22
|
+
<section class="grid"><section class="panel"><div class="panel-head"><div class="panel-title" data-i18n="accounts">账号池</div><div id="updated" class="panel-meta">—</div></div><div id="accounts" class="accounts"></div></section><aside class="side"><section class="panel"><div class="panel-head"><div class="panel-title" data-i18n="routing">路由</div></div><div id="routing"></div></section><section class="panel"><div class="panel-head"><div class="panel-title" data-i18n="runtime">运行时</div></div><div id="runtime"></div></section></aside></section>
|
|
23
|
+
<div class="foot" data-i18n="foot">数据来自本机号池;页面默认仅监听回环地址,按 Ctrl-C 停止服务。</div>
|
|
24
|
+
</main>
|
|
25
|
+
<script>
|
|
26
|
+
(function(){
|
|
27
|
+
var token="__CMA_TOKEN__";
|
|
28
|
+
var lang="__CMA_LANG__";
|
|
29
|
+
var text={zh:{title:"号池实时监控",subtitle:"账号、配额与轮换状态",connecting:"连接中…",connected:"实时连接",offline:"连接中断",refresh:"刷新",accounts:"账号池",apiAccounts:"API/ZDR 账号",routing:"路由",runtime:"运行时",total:"总账号",ready:"可用",login:"需重新登录",disabled:"已禁用",enabled:"已启用",recommend:"推荐账号",active:"当前账号",requests:"请求",success:"成功",failed:"失败",last:"最近请求",quota:"配额",risk:"风险",unknown:"未知",reason:"原因",primary:"主窗口",secondary:"次窗口",priority:"优先级",models:"可见模型",foot:"数据来自本机号池;页面默认仅监听回环地址,按 Ctrl-C 停止服务。"},en:{title:"Pool live monitor",subtitle:"Accounts, quota and routing",connecting:"Connecting…",connected:"Live",offline:"Disconnected",refresh:"Refresh",accounts:"Account pool",apiAccounts:"API/ZDR accounts",routing:"Routing",runtime:"Runtime",total:"Accounts",ready:"Ready",login:"Re-login needed",disabled:"Disabled",enabled:"Enabled",recommend:"Recommended",active:"Current",requests:"Requests",success:"Success",failed:"Failed",last:"Last request",quota:"Quota",risk:"Risk",unknown:"Unknown",reason:"Reason",primary:"primary",secondary:"secondary",priority:"priority",models:"visible models",foot:"Data comes from the local pool; the server listens on loopback by default. Ctrl-C stops it."}};
|
|
30
|
+
function t(k){return text[lang][k]||k}
|
|
31
|
+
function label(v){var s=String(v==null?"":v);return lang==="zh"?s.replace(/^Account /,"账号 ").replace(/Token account/g,"令牌账号").replace(/, id:/g,",ID:").replace(/, /g,","):s}
|
|
32
|
+
function reasonText(v){var s=String(v==null?"—":v);if(lang!=="zh")return s;return s.replace("token-invalid — re-login needed","令牌无效——需要重新登录").replace("primary quota ","主窗口配额 ").replace(" secondary quota "," 次窗口配额 ").replace(" used","已使用").replace("account is disabled","账号已禁用").replace("quota cache exhausted","配额缓存已用尽")}
|
|
33
|
+
function esc(v){return String(v==null?"":v).replace(/[&<>"']/g,function(c){return({"&":"&","<":"<",">":">",'"':""","'":"'"})[c]})}
|
|
34
|
+
function api(path){return path+(path.indexOf("?")>=0?"&":"?")+"token="+encodeURIComponent(token)}
|
|
35
|
+
function ago(ms){if(typeof ms!=="number"||!isFinite(ms))return "—";var s=Math.max(0,Math.round(ms/1000));if(s<60)return s+"s";if(s<3600)return Math.floor(s/60)+"m";if(s<86400)return Math.floor(s/3600)+"h";return Math.floor(s/86400)+"d"}
|
|
36
|
+
function statusClass(a){var m=(a.markers||[]).join(" ");if(a.enabled===false)return "disabled";if(/invalid|sign-in|re-login/i.test(m))return "login";return "ready"}
|
|
37
|
+
function winLabel(w,key){var m=w&&w.windowMinutes;if(typeof m==="number"&&isFinite(m)&&m>0){var zh=lang==="zh";if(m%1440===0)return (m/1440)+(zh?"天":"d");if(m%60===0)return (m/60)+(zh?"小时":"h");return m+(zh?"分钟":"m")}return t(key)}
|
|
38
|
+
function bar(window,key){if(!window||typeof window.usedPercent!=="number")return '<div class="label">'+t("unknown")+"</div>";var p=Math.max(0,Math.min(100,Math.round(window.usedPercent)));var cls=p>=90?" danger":p>=75?" warn":"";return '<div class="quota-row"><span>'+esc(winLabel(window,key))+" "+t("quota")+'</span><span>'+p+"% / "+(100-p)+"%</span></div><div class="bar"><div class="fill'+cls+'" style="width:'+p+'%"></div></div>'}
|
|
39
|
+
function draw(data){window.__data=data;var all=data.accounts||[];var ready=all.filter(function(a){return a.enabled!==false&&statusClass(a)==="ready"}).length;var login=all.filter(function(a){return statusClass(a)==="login"}).length;var disabled=all.filter(function(a){return a.enabled===false}).length;var rec=data.recommendedIndex==null?"—":"#"+(data.recommendedIndex+1);var rt=data.runtime||{};document.getElementById("summary").innerHTML='<div class="card"><div class="label">'+t("total")+'</div><div class="value blue">'+(data.totalAccountCount||all.length)+'</div></div><div class="card"><div class="label">'+t("ready")+'</div><div class="value green">'+ready+'</div></div><div class="card"><div class="label">'+t("login")+'</div><div class="value red">'+login+'</div></div><div class="card"><div class="label">'+t("disabled")+'</div><div class="value">'+disabled+'</div></div><div class="card"><div class="label">'+t("recommend")+'</div><div class="value amber">'+rec+'</div></div>';
|
|
40
|
+
document.getElementById("accounts").innerHTML=all.length?all.map(function(a){var sub=a.subscription||{};var q=a.quota||{};var risk=a.forecastRiskScore==null?"—":a.forecastRiskScore+"/100";var rl=a.forecastRiskLevel||"low";var badges=(a.markers||[]).map(function(m){return '<span class="badge '+(/current/.test(m)?"current":statusClass(a))+'">'+esc(label(m))+'</span>'}).join(" ");return '<article class="account"><div class="account-head"><div><div class="account-name">'+esc(label(a.label||("Account "+(a.index+1))))+'</div><div class="account-meta">'+esc(sub.planLabel||t("unknown"))+" · "+esc(label(sub.selectedWorkspace||""))+'</div></div><div>'+badges+' <span class="badge '+statusClass(a)+'">'+(statusClass(a)==="ready"?t("ready"):statusClass(a)==="login"?t("login"):t("disabled"))+'</span></div></div><div class="account-stats"><div class="quota">'+bar(q.primary,"primary")+bar(q.secondary,"secondary")+'</div><div><div class="label">'+t("last")+'</div><div class="value" style="font-size:18px">'+ago(a.lastInferenceRequestAt?Date.now()-a.lastInferenceRequestAt:null)+'</div><div class="reason">'+t("reason")+": "+esc(reasonText(a.reason||"—"))+'</div></div><div class="risk"><div class="label">'+t("risk")+'</div><div class="risk-num risk-'+esc(rl)+'">'+esc(risk)+"</div></div></div></article>"}).join(""):'<div class="empty">'+t("unknown")+"</div>";
|
|
41
|
+
var apis=(data.apiAccounts||[]).map(function(a){return '<article class="account"><div class="account-head"><div><div class="account-name">'+esc(label(a.label||"API"))+'</div><div class="account-meta">'+esc(a.kind||"api")+" · "+t("priority")+" "+esc(a.priority==null?"—":a.priority)+'</div></div><div><span class="badge '+(a.enabled===false?"disabled":"ready")+'">'+(a.enabled===false?t("disabled"):t("enabled"))+'</span></div></div><div class="account-meta">'+t("models")+": "+esc((a.visibleModels||[]).length||"—")+" · "+t("last")+": "+ago(a.lastInferenceRequestAt?Date.now()-a.lastInferenceRequestAt:null)+'</div></article>'}).join("");
|
|
42
|
+
if(apis)document.getElementById("accounts").innerHTML+='<div class="subhead">'+t("apiAccounts")+"</div>"+apis;
|
|
43
|
+
var recReason=data.recommendationReason||"—";document.getElementById("routing").innerHTML='<div class="kv"><span>'+t("active")+'</span><b>'+esc(data.activeIndex==null?"—":"#"+(data.activeIndex+1))+'</b></div><div class="kv"><span>'+t("recommend")+'</span><b>'+esc(rec)+"</b></div><div class="kv"><span>'+t("reason")+'</span><b>'+esc(recReason)+'</b></div>';
|
|
44
|
+
document.getElementById("runtime").innerHTML='<div class="kv"><span>'+t("requests")+'</span><b>'+esc(rt.responsesRequests==null?"—":rt.responsesRequests)+'</b></div><div class="kv"><span>'+t("success")+'</span><b>'+esc(rt.successfulRequests==null?"—":rt.successfulRequests)+'</b></div><div class="kv"><span>'+t("failed")+'</span><b>'+esc(rt.failedRequests==null?"—":rt.failedRequests)+'</b></div><div class="kv"><span>'+t("last")+'</span><b>'+esc(data.runtimeInUseIndex==null?"—":"#"+(data.runtimeInUseIndex+1))+'</b></div>';
|
|
45
|
+
document.getElementById("updated").textContent=new Date().toLocaleTimeString();document.getElementById("error").hidden=true;document.getElementById("dot").className="dot";document.getElementById("connection").textContent=t("connected")
|
|
46
|
+
}
|
|
47
|
+
function setLang(){document.documentElement.lang=lang==="zh"?"zh-CN":"en";document.querySelectorAll("[data-i18n]").forEach(function(e){e.textContent=t(e.getAttribute("data-i18n"))});document.getElementById("lang").textContent=lang==="zh"?"EN":"中文";if(window.__data)draw(window.__data)}
|
|
48
|
+
function failed(e){document.getElementById("error").hidden=false;document.getElementById("error").textContent=e&&e.message?e.message:"Request failed";document.getElementById("dot").className="dot off";document.getElementById("connection").textContent=t("offline")}
|
|
49
|
+
function load(){fetch(api("/api/status"),{cache:"no-store"}).then(function(r){if(!r.ok)throw Error("HTTP "+r.status);return r.json()}).then(draw).catch(failed)}
|
|
50
|
+
document.getElementById("refresh").onclick=load;document.getElementById("lang").onclick=function(){lang=lang==="zh"?"en":"zh";setLang()};setLang();load();try{var es=new EventSource(api("/events"));es.onmessage=function(e){try{draw(JSON.parse(e.data))}catch(_){}};es.onerror=function(){document.getElementById("dot").className="dot off";document.getElementById("connection").textContent=t("offline")}}catch(_){setInterval(load,5000)}
|
|
51
|
+
})();
|
|
52
|
+
</script>
|
|
53
|
+
</body>
|
|
54
54
|
</html>`;
|
|
55
55
|
export function renderDashboardHtml(token, locale = "zh") {
|
|
56
56
|
const safeToken = JSON.stringify(token ?? "")
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export type PluginInput = any;
|
|
2
|
-
export type Plugin = any;
|
|
1
|
+
export type PluginInput = any;
|
|
2
|
+
export type Plugin = any;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export {};
|
|
2
|
-
|
|
1
|
+
export {};
|
|
2
|
+
|