coding-tool-x 3.8.5 → 3.8.6
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/dist/web/assets/{AgentsDrawer-BG6P3DX-.js → AgentsDrawer-Db9wAyaZ.js} +1 -1
- package/dist/web/assets/{Analytics--z_FmL5P.js → Analytics-C4gMzv7g.js} +1 -1
- package/dist/web/assets/{AssetPathField-CrJtRKHy.js → AssetPathField-Bd_nMFu3.js} +1 -1
- package/dist/web/assets/{ChatHistoryDrawer-BiuEryJj.js → ChatHistoryDrawer-CwzJHyNP.js} +1 -1
- package/dist/web/assets/{CollapseItem-BAYEsL_Q.js → CollapseItem-CTCfSiue.js} +1 -1
- package/dist/web/assets/{CommandsDrawer-YJ-U7BXN.js → CommandsDrawer-vDghB1U9.js} +1 -1
- package/dist/web/assets/{ConfigExportDrawer-Ce_hAknW.js → ConfigExportDrawer-C-XDvlHc.js} +1 -1
- package/dist/web/assets/{ConfigTemplates-Bvr1NN6K.js → ConfigTemplates-CYY8XRED.js} +1 -1
- package/dist/web/assets/{ConfigTemplatesDrawer-BjIeHfF-.js → ConfigTemplatesDrawer-CbsJp76X.js} +1 -1
- package/dist/web/assets/{ConfigTemplatesPanel-AV00tm_D.js → ConfigTemplatesPanel-liMSTh7s.js} +1 -1
- package/dist/web/assets/{FavoritesDrawer-Cz95o4CD.js → FavoritesDrawer-D6QSiYaL.js} +1 -1
- package/dist/web/assets/{GatewayConvertDrawer-DLlbMz27.js → GatewayConvertDrawer-RvET6iNG.js} +1 -1
- package/dist/web/assets/{Home-D7CCOFQC.js → Home-P8eseZcf.js} +1 -1
- package/dist/web/assets/{MarkdownEditor-rEZ-c72L.js → MarkdownEditor-CYmLGYju.js} +1 -1
- package/dist/web/assets/{McpDrawer-DfoCyxU2.js → McpDrawer-Ch3bkKIq.js} +1 -1
- package/dist/web/assets/{McpFormDrawer-SKu8HNUR.js → McpFormDrawer-BcTlgSpN.js} +1 -1
- package/dist/web/assets/{PluginManager-BbeK-3rV.js → PluginManager-Dh2o0xkZ.js} +1 -1
- package/dist/web/assets/{PluginsDrawer-CWVJ7yjS.js → PluginsDrawer-deKQ5u3I.js} +1 -1
- package/dist/web/assets/{PluginsPanel-Cm2N3StG.js → PluginsPanel-BofsgQgj.js} +1 -1
- package/dist/web/assets/{ProjectList-CsjnsNPv.js → ProjectList-DhSWJHTA.js} +1 -1
- package/dist/web/assets/{PromptsDrawer-EGd3djFa.js → PromptsDrawer-Cfb2FeeU.js} +1 -1
- package/dist/web/assets/{RecentSessionsDrawer-D1vtoLAs.js → RecentSessionsDrawer-3eXamJhr.js} +1 -1
- package/dist/web/assets/{SessionCard-BgLg49U3.js → SessionCard-CW0eHURI.js} +1 -1
- package/dist/web/assets/{SessionList-TRiTklWk.js → SessionList-DQfyDGWU.js} +1 -1
- package/dist/web/assets/SettingsDrawer-Dw5NUgHC.css +1 -0
- package/dist/web/assets/SettingsDrawer-u_vwUuPS.js +1 -0
- package/dist/web/assets/{SkillManager-BTjTUbHq.js → SkillManager-DHMprJLA.js} +1 -1
- package/dist/web/assets/{SkillsDrawer-pN0hbkK3.js → SkillsDrawer-CkF3BpyC.js} +1 -1
- package/dist/web/assets/{SkillsPanel-gmBWEoiF.js → SkillsPanel-DWE4pFGx.js} +1 -1
- package/dist/web/assets/{SpeedTestDrawer-DcNzuVJk.js → SpeedTestDrawer-BgfG1Ked.js} +1 -1
- package/dist/web/assets/{Upload-v5rV0qTT.js → Upload-CNwaGOws.js} +1 -1
- package/dist/web/assets/{WorkspaceDrawer-DxmicM4U.js → WorkspaceDrawer-Br5CeCzL.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-CJHCjARN.js → WorkspaceManager-B2O8-qIw.js} +1 -1
- package/dist/web/assets/{WorkspacePanel-D8_Zq-du.js → WorkspacePanel-5oDKit3c.js} +1 -1
- package/dist/web/assets/{index-DnEocV0M.js → index-BMAUPq5K.js} +2 -2
- package/dist/web/assets/{index-C0EHRym0.css → index-Bi1MQHYX.css} +1 -1
- package/dist/web/assets/{useResponsiveDrawer-B3uojyvV.js → useResponsiveDrawer-jbmqZuJs.js} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/src/commands/daemon.js +10 -0
- package/src/platforms/channel-auth-registry.js +3 -0
- package/src/platforms/channel-auth-service.js +33 -8
- package/src/platforms/native-oauth-adapters.js +11 -1
- package/src/platforms/notification-hooks.js +55 -5
- package/src/platforms/oauth-credentials-service.js +170 -41
- package/src/server/services/channel-balance.js +122 -4
- package/src/server/services/network-access.js +11 -1
- package/src/server/services/session-history-index.js +106 -11
- package/dist/web/assets/SettingsDrawer-d5liXjHq.css +0 -1
- package/dist/web/assets/SettingsDrawer-rz2WejGY.js +0 -1
|
@@ -359,6 +359,14 @@ function httpPost(url, body, headers = {}) {
|
|
|
359
359
|
});
|
|
360
360
|
}
|
|
361
361
|
|
|
362
|
+
function parseJsonBody(body) {
|
|
363
|
+
try {
|
|
364
|
+
return JSON.parse(body);
|
|
365
|
+
} catch {
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
362
370
|
async function fetchClaudeUsage(accessToken) {
|
|
363
371
|
try {
|
|
364
372
|
const result = await httpGet('https://api.anthropic.com/api/oauth/usage', {
|
|
@@ -367,38 +375,40 @@ async function fetchClaudeUsage(accessToken) {
|
|
|
367
375
|
'anthropic-version': '2023-06-01',
|
|
368
376
|
'User-Agent': 'claude-cli/1.0'
|
|
369
377
|
});
|
|
370
|
-
const data =
|
|
378
|
+
const data = parseJsonBody(result.body);
|
|
371
379
|
return { raw: data, provider: 'claude', statusCode: result.statusCode };
|
|
372
380
|
} catch (err) {
|
|
373
381
|
return { error: err.message, provider: 'claude' };
|
|
374
382
|
}
|
|
375
383
|
}
|
|
376
384
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
statusCode: 200
|
|
391
|
-
};
|
|
385
|
+
function extractCodexAccountId(...tokens) {
|
|
386
|
+
const { decodeJwtPayload } = require('../server/services/oauth-utils');
|
|
387
|
+
for (const token of tokens) {
|
|
388
|
+
if (!token) continue;
|
|
389
|
+
try {
|
|
390
|
+
const payload = decodeJwtPayload(token) || {};
|
|
391
|
+
const accountId = payload.chatgpt_account_id
|
|
392
|
+
|| payload['https://api.openai.com/auth.chatgpt_account_id']
|
|
393
|
+
|| payload['https://api.openai.com/auth']?.chatgpt_account_id
|
|
394
|
+
|| payload.organizations?.[0]?.id;
|
|
395
|
+
if (accountId) return String(accountId);
|
|
396
|
+
} catch (_) {
|
|
397
|
+
// Try the next token; malformed JWTs should not prevent the usage request.
|
|
392
398
|
}
|
|
393
|
-
} catch (_) {
|
|
394
|
-
// fall through to API call
|
|
395
399
|
}
|
|
400
|
+
return '';
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
async function fetchCodexUsage(accessToken, accountId = '') {
|
|
396
404
|
try {
|
|
397
|
-
const
|
|
405
|
+
const headers = {
|
|
398
406
|
'Authorization': `Bearer ${accessToken}`,
|
|
399
|
-
'User-Agent': '
|
|
400
|
-
}
|
|
401
|
-
|
|
407
|
+
'User-Agent': 'codex-cli/0.1'
|
|
408
|
+
};
|
|
409
|
+
if (accountId) headers['ChatGPT-Account-Id'] = accountId;
|
|
410
|
+
const result = await httpGet('https://chatgpt.com/backend-api/wham/usage', headers);
|
|
411
|
+
const data = parseJsonBody(result.body);
|
|
402
412
|
return { raw: data, provider: 'codex', statusCode: result.statusCode };
|
|
403
413
|
} catch (err) {
|
|
404
414
|
return { error: err.message, provider: 'codex' };
|
|
@@ -409,7 +419,7 @@ async function fetchGeminiUsage(accessToken) {
|
|
|
409
419
|
try {
|
|
410
420
|
const body = JSON.stringify({
|
|
411
421
|
metadata: {
|
|
412
|
-
ideType: '
|
|
422
|
+
ideType: 'IDE_UNSPECIFIED',
|
|
413
423
|
platform: 'PLATFORM_UNSPECIFIED',
|
|
414
424
|
pluginType: 'GEMINI'
|
|
415
425
|
}
|
|
@@ -421,8 +431,30 @@ async function fetchGeminiUsage(accessToken) {
|
|
|
421
431
|
'X-Goog-Api-Client': 'google-cloud-sdk vscode_cloudshelleditor/0.1',
|
|
422
432
|
'Client-Metadata': '{"ideType":"IDE_UNSPECIFIED","platform":"PLATFORM_UNSPECIFIED","pluginType":"GEMINI"}'
|
|
423
433
|
});
|
|
424
|
-
const
|
|
425
|
-
|
|
434
|
+
const loadData = parseJsonBody(result.body) || {};
|
|
435
|
+
if (result.statusCode < 200 || result.statusCode >= 300) {
|
|
436
|
+
return { raw: loadData, provider: 'gemini', statusCode: result.statusCode };
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const project = typeof loadData.cloudaicompanionProject === 'string'
|
|
440
|
+
? loadData.cloudaicompanionProject
|
|
441
|
+
: loadData.cloudaicompanionProject?.id || '';
|
|
442
|
+
const quotaResult = await httpPost(
|
|
443
|
+
'https://cloudcode-pa.googleapis.com/v1internal:retrieveUserQuota',
|
|
444
|
+
JSON.stringify(project ? { project } : {}),
|
|
445
|
+
{
|
|
446
|
+
'Authorization': `Bearer ${accessToken}`,
|
|
447
|
+
'Content-Type': 'application/json',
|
|
448
|
+
'User-Agent': 'google-api-nodejs-client/9.15.1',
|
|
449
|
+
'X-Goog-Api-Client': 'google-cloud-sdk vscode_cloudshelleditor/0.1',
|
|
450
|
+
'Client-Metadata': '{"ideType":"IDE_UNSPECIFIED","platform":"PLATFORM_UNSPECIFIED","pluginType":"GEMINI"}'
|
|
451
|
+
}
|
|
452
|
+
);
|
|
453
|
+
return {
|
|
454
|
+
raw: parseJsonBody(quotaResult.body),
|
|
455
|
+
provider: 'gemini',
|
|
456
|
+
statusCode: quotaResult.statusCode
|
|
457
|
+
};
|
|
426
458
|
} catch (err) {
|
|
427
459
|
return { error: err.message, provider: 'gemini' };
|
|
428
460
|
}
|
|
@@ -435,31 +467,122 @@ function clampPercent(value) {
|
|
|
435
467
|
return Math.max(0, Math.min(100, percent));
|
|
436
468
|
}
|
|
437
469
|
|
|
438
|
-
function
|
|
470
|
+
function normalizeResetAt(value, resetAfterSeconds) {
|
|
471
|
+
const numeric = Number(value);
|
|
472
|
+
if (Number.isFinite(numeric) && numeric > 0) {
|
|
473
|
+
const milliseconds = numeric < 1e12 ? numeric * 1000 : numeric;
|
|
474
|
+
const date = new Date(milliseconds);
|
|
475
|
+
if (!Number.isNaN(date.getTime())) return date.toISOString();
|
|
476
|
+
}
|
|
477
|
+
if (value) {
|
|
478
|
+
const date = new Date(value);
|
|
479
|
+
if (!Number.isNaN(date.getTime())) return date.toISOString();
|
|
480
|
+
}
|
|
481
|
+
const afterSeconds = Number(resetAfterSeconds);
|
|
482
|
+
if (Number.isFinite(afterSeconds) && afterSeconds >= 0) {
|
|
483
|
+
return new Date(Date.now() + afterSeconds * 1000).toISOString();
|
|
484
|
+
}
|
|
485
|
+
return null;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function normalizeWindow(window, label, id = '') {
|
|
439
489
|
if (!window || typeof window !== 'object') return null;
|
|
440
|
-
const usedPercent = clampPercent(window.used_percent ?? window.utilization);
|
|
441
|
-
|
|
490
|
+
const usedPercent = clampPercent(window.used_percent ?? window.usedPercent ?? window.utilization);
|
|
491
|
+
const remainingPercent = clampPercent(
|
|
492
|
+
window.remaining_percent
|
|
493
|
+
?? window.remainingPercent
|
|
494
|
+
?? window.remaining_fraction
|
|
495
|
+
?? window.remainingFraction
|
|
496
|
+
);
|
|
497
|
+
if (usedPercent === null && remainingPercent === null) return null;
|
|
498
|
+
const normalizedUsedPercent = usedPercent === null ? 100 - remainingPercent : usedPercent;
|
|
499
|
+
const normalizedRemainingPercent = remainingPercent === null ? 100 - usedPercent : remainingPercent;
|
|
442
500
|
return {
|
|
501
|
+
...(id ? { id } : {}),
|
|
443
502
|
label,
|
|
444
|
-
remainingPercent: Math.max(0, Math.min(100,
|
|
445
|
-
usedPercent,
|
|
446
|
-
resetsAt:
|
|
503
|
+
remainingPercent: Math.max(0, Math.min(100, normalizedRemainingPercent)),
|
|
504
|
+
usedPercent: Math.max(0, Math.min(100, normalizedUsedPercent)),
|
|
505
|
+
resetsAt: normalizeResetAt(
|
|
506
|
+
window.resets_at
|
|
507
|
+
?? window.resetsAt
|
|
508
|
+
?? window.reset_at
|
|
509
|
+
?? window.resetAt
|
|
510
|
+
?? window.reset_time
|
|
511
|
+
?? window.resetTime,
|
|
512
|
+
window.reset_after_seconds ?? window.resetAfterSeconds
|
|
513
|
+
)
|
|
447
514
|
};
|
|
448
515
|
}
|
|
449
516
|
|
|
517
|
+
function normalizeGeminiQuota(value) {
|
|
518
|
+
const buckets = Array.isArray(value?.buckets) ? value.buckets : [];
|
|
519
|
+
const windows = buckets.map((bucket, index) => {
|
|
520
|
+
const remainingPercent = clampPercent(bucket?.remainingFraction ?? bucket?.remaining_fraction);
|
|
521
|
+
if (remainingPercent === null) return null;
|
|
522
|
+
const modelId = String(bucket?.modelId || bucket?.model_id || '').trim();
|
|
523
|
+
return {
|
|
524
|
+
id: `gemini:${modelId || index}`,
|
|
525
|
+
label: modelId || 'Gemini',
|
|
526
|
+
remainingPercent,
|
|
527
|
+
usedPercent: Math.max(0, Math.min(100, 100 - remainingPercent)),
|
|
528
|
+
resetsAt: normalizeResetAt(bucket?.resetTime || bucket?.reset_time),
|
|
529
|
+
scope: modelId ? { modelId } : null
|
|
530
|
+
};
|
|
531
|
+
}).filter(Boolean);
|
|
532
|
+
return windows.length ? { status: 'available', windows } : null;
|
|
533
|
+
}
|
|
534
|
+
|
|
450
535
|
function normalizeOAuthQuota(tool, raw) {
|
|
451
536
|
const value = raw && typeof raw === 'object' ? raw : {};
|
|
452
|
-
|
|
453
|
-
|
|
537
|
+
if (tool === 'gemini') {
|
|
538
|
+
const geminiQuota = normalizeGeminiQuota(value);
|
|
539
|
+
if (geminiQuota) return { quota: geminiQuota, status: 'available' };
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
let primary = normalizeWindow(value.five_hour || value.fiveHour, '5h', 'primary');
|
|
543
|
+
let secondary = normalizeWindow(value.seven_day || value.sevenDay, '7d', 'secondary');
|
|
454
544
|
const rateLimit = value.rate_limit || value.rateLimit;
|
|
455
545
|
if (rateLimit) {
|
|
456
|
-
primary = primary || normalizeWindow(rateLimit.primary_window || rateLimit.primaryWindow, '5h');
|
|
457
|
-
secondary = secondary || normalizeWindow(rateLimit.secondary_window || rateLimit.secondaryWindow, '7d');
|
|
546
|
+
primary = primary || normalizeWindow(rateLimit.primary_window || rateLimit.primaryWindow, '5h', 'primary');
|
|
547
|
+
secondary = secondary || normalizeWindow(rateLimit.secondary_window || rateLimit.secondaryWindow, '7d', 'secondary');
|
|
458
548
|
}
|
|
459
|
-
|
|
460
|
-
|
|
549
|
+
|
|
550
|
+
const additional = [];
|
|
551
|
+
const additionalLimits = Array.isArray(value.additional_rate_limits)
|
|
552
|
+
? value.additional_rate_limits
|
|
553
|
+
: Array.isArray(value.additionalRateLimits) ? value.additionalRateLimits : [];
|
|
554
|
+
additionalLimits.forEach((limit, index) => {
|
|
555
|
+
const additionalRateLimit = limit?.rate_limit || limit?.rateLimit || limit;
|
|
556
|
+
const name = String(
|
|
557
|
+
limit?.limit_name || limit?.limitName || limit?.metered_feature || limit?.meteredFeature || '附加额度'
|
|
558
|
+
).trim();
|
|
559
|
+
const additionalPrimary = normalizeWindow(
|
|
560
|
+
additionalRateLimit?.primary_window || additionalRateLimit?.primaryWindow,
|
|
561
|
+
`${name} 5h`,
|
|
562
|
+
`additional:${index}:primary`
|
|
563
|
+
);
|
|
564
|
+
const additionalSecondary = normalizeWindow(
|
|
565
|
+
additionalRateLimit?.secondary_window || additionalRateLimit?.secondaryWindow,
|
|
566
|
+
`${name} 7d`,
|
|
567
|
+
`additional:${index}:secondary`
|
|
568
|
+
);
|
|
569
|
+
if (additionalPrimary) additional.push(additionalPrimary);
|
|
570
|
+
if (additionalSecondary) additional.push(additionalSecondary);
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
if (!primary && !secondary && !additional.length) {
|
|
574
|
+
return { quota: null, status: 'unsupported', warning: `${tool}: provider did not return usable quota windows` };
|
|
461
575
|
}
|
|
462
|
-
return {
|
|
576
|
+
return {
|
|
577
|
+
quota: {
|
|
578
|
+
status: 'available',
|
|
579
|
+
primary,
|
|
580
|
+
secondary,
|
|
581
|
+
additional,
|
|
582
|
+
windows: [primary, secondary, ...additional].filter(Boolean)
|
|
583
|
+
},
|
|
584
|
+
status: 'available'
|
|
585
|
+
};
|
|
463
586
|
}
|
|
464
587
|
|
|
465
588
|
async function fetchCredentialUsage(tool, credentialId) {
|
|
@@ -474,7 +597,10 @@ async function fetchCredentialUsage(tool, credentialId) {
|
|
|
474
597
|
response = await fetchClaudeUsage(accessToken);
|
|
475
598
|
break;
|
|
476
599
|
case 'codex':
|
|
477
|
-
response = await fetchCodexUsage(
|
|
600
|
+
response = await fetchCodexUsage(
|
|
601
|
+
accessToken,
|
|
602
|
+
extractCodexAccountId(secrets.idToken, accessToken) || entry.accountId || secrets.accountId
|
|
603
|
+
);
|
|
478
604
|
break;
|
|
479
605
|
case 'gemini':
|
|
480
606
|
response = await fetchGeminiUsage(accessToken);
|
|
@@ -485,7 +611,7 @@ async function fetchCredentialUsage(tool, credentialId) {
|
|
|
485
611
|
? await fetchClaudeUsage(accessToken)
|
|
486
612
|
: providerId.includes('gemini') || providerId.includes('google')
|
|
487
613
|
? await fetchGeminiUsage(accessToken)
|
|
488
|
-
: await fetchCodexUsage(accessToken);
|
|
614
|
+
: await fetchCodexUsage(accessToken, extractCodexAccountId(accessToken) || entry.accountId || secrets.accountId);
|
|
489
615
|
break;
|
|
490
616
|
}
|
|
491
617
|
default:
|
|
@@ -502,5 +628,8 @@ module.exports = {
|
|
|
502
628
|
SUPPORTED_TOOLS,
|
|
503
629
|
syncLocalCredential,
|
|
504
630
|
normalizeOAuthQuota,
|
|
505
|
-
fetchCredentialUsage
|
|
631
|
+
fetchCredentialUsage,
|
|
632
|
+
extractCodexAccountId,
|
|
633
|
+
normalizeResetAt,
|
|
634
|
+
normalizeWindow
|
|
506
635
|
};
|
|
@@ -120,6 +120,22 @@ function resolveBalanceToken(channel = {}) {
|
|
|
120
120
|
);
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
function isOAuthChannel(channel = {}) {
|
|
124
|
+
return channel?.authMode === 'oauth';
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function resolveOAuthCacheIdentity(channel = {}) {
|
|
128
|
+
const ref = channel.authRef && typeof channel.authRef === 'object' ? channel.authRef : {};
|
|
129
|
+
return [
|
|
130
|
+
channel.oauthProviderId,
|
|
131
|
+
ref.credentialId,
|
|
132
|
+
ref.providerId,
|
|
133
|
+
ref.accountId,
|
|
134
|
+
ref.identityKey,
|
|
135
|
+
ref.accountEmail
|
|
136
|
+
].map(value => String(value || '').trim()).filter(Boolean).join('|');
|
|
137
|
+
}
|
|
138
|
+
|
|
123
139
|
function resolveBalanceUserId(channel = {}) {
|
|
124
140
|
const raw = channel.balanceUserId
|
|
125
141
|
?? channel.platformUserId
|
|
@@ -250,6 +266,64 @@ function makeVisibleSnapshot(platform, input, { stale = false, updatedAt = nowIs
|
|
|
250
266
|
return snapshot;
|
|
251
267
|
}
|
|
252
268
|
|
|
269
|
+
function formatPercent(value) {
|
|
270
|
+
const number = parseFiniteNumber(value);
|
|
271
|
+
if (number == null) return '';
|
|
272
|
+
return Number.isInteger(number) ? String(number) : number.toFixed(1).replace(/\.0$/, '');
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function normalizeOAuthWindows(quota = {}) {
|
|
276
|
+
const source = quota && typeof quota === 'object' ? quota : {};
|
|
277
|
+
const candidates = [];
|
|
278
|
+
if (source.primary) candidates.push({ id: 'primary', ...source.primary });
|
|
279
|
+
if (source.secondary) candidates.push({ id: 'secondary', ...source.secondary });
|
|
280
|
+
if (Array.isArray(source.windows)) candidates.push(...source.windows);
|
|
281
|
+
if (Array.isArray(source.additional)) candidates.push(...source.additional);
|
|
282
|
+
|
|
283
|
+
const seen = new Set();
|
|
284
|
+
return candidates.map((window, index) => {
|
|
285
|
+
const remainingPercent = parseFiniteNumber(window?.remainingPercent);
|
|
286
|
+
const usedPercent = parseFiniteNumber(window?.usedPercent);
|
|
287
|
+
if (remainingPercent == null && usedPercent == null) return null;
|
|
288
|
+
const id = String(window?.id || `window-${index}`);
|
|
289
|
+
if (seen.has(id)) return null;
|
|
290
|
+
seen.add(id);
|
|
291
|
+
return {
|
|
292
|
+
id,
|
|
293
|
+
label: String(window?.label || id),
|
|
294
|
+
remainingPercent: remainingPercent != null
|
|
295
|
+
? Math.max(0, Math.min(100, remainingPercent))
|
|
296
|
+
: Math.max(0, Math.min(100, 100 - usedPercent)),
|
|
297
|
+
usedPercent: usedPercent != null
|
|
298
|
+
? Math.max(0, Math.min(100, usedPercent))
|
|
299
|
+
: Math.max(0, Math.min(100, 100 - remainingPercent)),
|
|
300
|
+
resetsAt: window?.resetsAt || null,
|
|
301
|
+
scope: window?.scope || null
|
|
302
|
+
};
|
|
303
|
+
}).filter(Boolean);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function buildOAuthBalanceSnapshot(platform, quota, { stale = false, updatedAt = nowIso() } = {}) {
|
|
307
|
+
const windows = normalizeOAuthWindows(quota);
|
|
308
|
+
if (!windows.length) return makeHiddenSnapshot(platform, { stale, updatedAt });
|
|
309
|
+
|
|
310
|
+
const primaryWindows = windows.filter(window => window.id === 'primary' || window.id === 'secondary');
|
|
311
|
+
const displayWindows = primaryWindows.length ? primaryWindows : windows;
|
|
312
|
+
const label = displayWindows
|
|
313
|
+
.map(window => `${window.label} 剩余 ${formatPercent(window.remainingPercent)}%`)
|
|
314
|
+
.join(' · ');
|
|
315
|
+
|
|
316
|
+
return {
|
|
317
|
+
visible: true,
|
|
318
|
+
kind: 'oauth-quota',
|
|
319
|
+
platform,
|
|
320
|
+
label,
|
|
321
|
+
windows,
|
|
322
|
+
updatedAt,
|
|
323
|
+
stale
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
253
327
|
function normalizeBaseUrl(value) {
|
|
254
328
|
const raw = String(value || '').trim();
|
|
255
329
|
if (!raw) return '';
|
|
@@ -1483,14 +1557,16 @@ async function probeBalanceFromBase(baseUrl, channel, token) {
|
|
|
1483
1557
|
|
|
1484
1558
|
function buildCacheKey(source, channel) {
|
|
1485
1559
|
const token = resolveBalanceToken(channel);
|
|
1486
|
-
const
|
|
1487
|
-
?
|
|
1488
|
-
:
|
|
1560
|
+
const identity = isOAuthChannel(channel)
|
|
1561
|
+
? `oauth:${resolveOAuthCacheIdentity(channel) || channel.id || channel.name || 'unknown'}`
|
|
1562
|
+
: token
|
|
1563
|
+
? crypto.createHash('sha256').update(token).digest('hex').slice(0, 16)
|
|
1564
|
+
: 'no-token';
|
|
1489
1565
|
return [
|
|
1490
1566
|
source,
|
|
1491
1567
|
channel.id || channel.name || 'unknown',
|
|
1492
1568
|
normalizeBaseUrl(channel.baseUrl || ''),
|
|
1493
|
-
|
|
1569
|
+
identity
|
|
1494
1570
|
].join(':');
|
|
1495
1571
|
}
|
|
1496
1572
|
|
|
@@ -1512,6 +1588,44 @@ async function refreshChannelBalanceSnapshot(source, channel, options = {}) {
|
|
|
1512
1588
|
return cached.snapshot;
|
|
1513
1589
|
}
|
|
1514
1590
|
|
|
1591
|
+
if (isOAuthChannel(channel)) {
|
|
1592
|
+
try {
|
|
1593
|
+
const { fetchChannelAuthQuota } = require('../../platforms/channel-auth-service');
|
|
1594
|
+
const result = await fetchChannelAuthQuota(source, channel.id, { refresh: force });
|
|
1595
|
+
const snapshot = buildOAuthBalanceSnapshot(source, result?.quota, {
|
|
1596
|
+
updatedAt: result?.checkedAt || nowIso(currentTime)
|
|
1597
|
+
});
|
|
1598
|
+
if (snapshot.visible) {
|
|
1599
|
+
balanceCache.set(key, {
|
|
1600
|
+
snapshot,
|
|
1601
|
+
expiresAt: currentTime + CACHE_TTL_MS
|
|
1602
|
+
});
|
|
1603
|
+
return snapshot;
|
|
1604
|
+
}
|
|
1605
|
+
} catch {
|
|
1606
|
+
// OAuth providers are best-effort; preserve a previous successful quota below.
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
if (cached?.snapshot?.visible) {
|
|
1610
|
+
const staleSnapshot = {
|
|
1611
|
+
...cached.snapshot,
|
|
1612
|
+
stale: true
|
|
1613
|
+
};
|
|
1614
|
+
balanceCache.set(key, {
|
|
1615
|
+
snapshot: staleSnapshot,
|
|
1616
|
+
expiresAt: currentTime + CACHE_TTL_MS
|
|
1617
|
+
});
|
|
1618
|
+
return staleSnapshot;
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
const hiddenSnapshot = makeHiddenSnapshot(source, { updatedAt: nowIso(currentTime) });
|
|
1622
|
+
balanceCache.set(key, {
|
|
1623
|
+
snapshot: hiddenSnapshot,
|
|
1624
|
+
expiresAt: currentTime + CACHE_TTL_MS
|
|
1625
|
+
});
|
|
1626
|
+
return hiddenSnapshot;
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1515
1629
|
const bases = buildBaseCandidates(channel.baseUrl);
|
|
1516
1630
|
if (!bases.length || !resolveBalanceToken(channel)) {
|
|
1517
1631
|
return makeHiddenSnapshot(null, { updatedAt: nowIso(currentTime) });
|
|
@@ -1766,6 +1880,10 @@ module.exports = {
|
|
|
1766
1880
|
buildSub2ApiUsageSnapshot,
|
|
1767
1881
|
resolveStatusPlatform,
|
|
1768
1882
|
resolveBalanceToken,
|
|
1883
|
+
isOAuthChannel,
|
|
1884
|
+
resolveOAuthCacheIdentity,
|
|
1885
|
+
normalizeOAuthWindows,
|
|
1886
|
+
buildOAuthBalanceSnapshot,
|
|
1769
1887
|
resolveBalanceUserId,
|
|
1770
1888
|
buildCookieCandidates,
|
|
1771
1889
|
build88CodeApiBaseCandidates,
|
|
@@ -44,7 +44,17 @@ function isSameOriginRequest(req) {
|
|
|
44
44
|
|
|
45
45
|
try {
|
|
46
46
|
const originUrl = new URL(origin);
|
|
47
|
-
|
|
47
|
+
if (originUrl.host === host) {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// The Vite dev UI runs on a separate local port and proxies API calls to
|
|
52
|
+
// the backend. Treat that as local-only traffic, but do not extend this
|
|
53
|
+
// exception to requests arriving from a LAN or public address.
|
|
54
|
+
const originHost = String(originUrl.hostname || '').replace(/^\[|\]$/g, '');
|
|
55
|
+
return (originUrl.protocol === 'http:' || originUrl.protocol === 'https:')
|
|
56
|
+
&& isLoopbackRequest(req)
|
|
57
|
+
&& isLoopbackAddress(originHost);
|
|
48
58
|
} catch (error) {
|
|
49
59
|
return false;
|
|
50
60
|
}
|
|
@@ -19,6 +19,8 @@ const INDEX_INVENTORY_TTL_MS = 30000;
|
|
|
19
19
|
const INDEX_COLD_WAIT_MS = 1500;
|
|
20
20
|
/** @type {number} Maximum age for a lock whose owner cannot be inspected */
|
|
21
21
|
const INVENTORY_LOCK_STALE_MS = 30 * 60 * 1000;
|
|
22
|
+
/** @type {number} Maximum time a competing inventory waits for the database writer */
|
|
23
|
+
const INVENTORY_LOCK_WAIT_MS = 30000;
|
|
22
24
|
/** @type {number} Number of files parsed before results are released */
|
|
23
25
|
const INVENTORY_PARSE_BATCH_SIZE = 8;
|
|
24
26
|
/** @type {number} Maximum number of files parsed concurrently */
|
|
@@ -34,9 +36,8 @@ function _isUsableRuntime(runtime) {
|
|
|
34
36
|
return !!runtime && typeof runtime === 'object' && typeof runtime.getDriver === 'function';
|
|
35
37
|
}
|
|
36
38
|
|
|
37
|
-
function _inventoryLockPath(dbPath
|
|
38
|
-
|
|
39
|
-
return `${dbPath}.${sourceKey}.inventory.lock`;
|
|
39
|
+
function _inventoryLockPath(dbPath) {
|
|
40
|
+
return `${path.resolve(String(dbPath))}.inventory.lock`;
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
function _isProcessAlive(pid) {
|
|
@@ -50,19 +51,19 @@ function _isProcessAlive(pid) {
|
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
/**
|
|
53
|
-
* Acquire a durable
|
|
54
|
+
* Acquire a durable database-wide inventory lock. The in-memory single-flight
|
|
54
55
|
* map only protects requests inside one Node process; production indexing is
|
|
55
56
|
* deliberately performed in child processes, so it cannot prevent two
|
|
56
|
-
* workers from
|
|
57
|
+
* workers from writing the same database after a parent restart.
|
|
57
58
|
*/
|
|
58
59
|
function _tryAcquireInventoryLock(dbPath, source) {
|
|
59
|
-
const lockPath = _inventoryLockPath(dbPath
|
|
60
|
+
const lockPath = _inventoryLockPath(dbPath);
|
|
60
61
|
|
|
61
62
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
62
63
|
let fd;
|
|
63
64
|
try {
|
|
64
65
|
fd = fs.openSync(lockPath, 'wx');
|
|
65
|
-
fs.writeFileSync(fd, JSON.stringify({ pid: process.pid, startedAt: Date.now() }), 'utf8');
|
|
66
|
+
fs.writeFileSync(fd, JSON.stringify({ pid: process.pid, source, startedAt: Date.now() }), 'utf8');
|
|
66
67
|
return { fd, lockPath };
|
|
67
68
|
} catch (error) {
|
|
68
69
|
if (fd !== undefined) {
|
|
@@ -81,7 +82,14 @@ function _tryAcquireInventoryLock(dbPath, source) {
|
|
|
81
82
|
}
|
|
82
83
|
|
|
83
84
|
const ownerPid = Number(owner?.pid);
|
|
84
|
-
if (_isProcessAlive(ownerPid)
|
|
85
|
+
if (_isProcessAlive(ownerPid)) {
|
|
86
|
+
return {
|
|
87
|
+
busy: true,
|
|
88
|
+
sameSource: owner?.source && String(owner.source) === String(source)
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if ((!Number.isInteger(ownerPid) || ownerPid <= 0) && ageMs < INVENTORY_LOCK_STALE_MS) {
|
|
85
93
|
return null;
|
|
86
94
|
}
|
|
87
95
|
|
|
@@ -95,12 +103,91 @@ function _tryAcquireInventoryLock(dbPath, source) {
|
|
|
95
103
|
return null;
|
|
96
104
|
}
|
|
97
105
|
|
|
106
|
+
async function _acquireInventoryLock(dbPath, source) {
|
|
107
|
+
const deadline = Date.now() + INVENTORY_LOCK_WAIT_MS;
|
|
108
|
+
while (true) {
|
|
109
|
+
const lock = _tryAcquireInventoryLock(dbPath, source);
|
|
110
|
+
if (lock?.busy) {
|
|
111
|
+
if (lock.sameSource) return null;
|
|
112
|
+
} else if (lock) {
|
|
113
|
+
return lock;
|
|
114
|
+
}
|
|
115
|
+
if (Date.now() >= deadline) return null;
|
|
116
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
98
120
|
function _releaseInventoryLock(lock) {
|
|
99
121
|
if (!lock) return;
|
|
100
122
|
try { fs.closeSync(lock.fd); } catch (_err) {}
|
|
101
123
|
try { fs.unlinkSync(lock.lockPath); } catch (_err) {}
|
|
102
124
|
}
|
|
103
125
|
|
|
126
|
+
/**
|
|
127
|
+
* Remove inventory locks left by workers that are no longer alive.
|
|
128
|
+
*
|
|
129
|
+
* A normal worker releases its lock in `_runInventory`'s finally block. This
|
|
130
|
+
* fallback is needed for `ctx stop` when a worker is terminated before that
|
|
131
|
+
* block can run. Locks owned by a live PID are intentionally retained.
|
|
132
|
+
*
|
|
133
|
+
* @param {string} [dbPath]
|
|
134
|
+
* @returns {{ removed: string[], retained: string[] }}
|
|
135
|
+
*/
|
|
136
|
+
function cleanupStaleInventoryLocks(dbPath = PATHS?.sessionHistoryIndex) {
|
|
137
|
+
const targetPath = dbPath ? path.resolve(String(dbPath)) : '';
|
|
138
|
+
if (!targetPath) return { removed: [], retained: [] };
|
|
139
|
+
|
|
140
|
+
const directory = path.dirname(targetPath);
|
|
141
|
+
const prefix = `${path.basename(targetPath)}.`;
|
|
142
|
+
let entries;
|
|
143
|
+
try {
|
|
144
|
+
entries = fs.readdirSync(directory);
|
|
145
|
+
} catch (_) {
|
|
146
|
+
return { removed: [], retained: [] };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const removed = [];
|
|
150
|
+
const retained = [];
|
|
151
|
+
for (const entry of entries) {
|
|
152
|
+
if (!entry.startsWith(prefix) || !entry.endsWith('.inventory.lock')) continue;
|
|
153
|
+
const lockPath = path.join(directory, entry);
|
|
154
|
+
let owner = null;
|
|
155
|
+
let ageMs = 0;
|
|
156
|
+
try {
|
|
157
|
+
ageMs = Math.max(0, Date.now() - fs.statSync(lockPath).mtimeMs);
|
|
158
|
+
} catch (_) {
|
|
159
|
+
retained.push(lockPath);
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
try {
|
|
163
|
+
owner = JSON.parse(fs.readFileSync(lockPath, 'utf8'));
|
|
164
|
+
} catch (_) {
|
|
165
|
+
// Keep malformed young locks; an old one is handled by the stale rule.
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const ownerPid = Number(owner?.pid);
|
|
169
|
+
if (_isProcessAlive(ownerPid)) {
|
|
170
|
+
retained.push(lockPath);
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// A valid but dead owner can be cleaned immediately. Unreadable lock
|
|
175
|
+
// files remain protected by the normal acquisition path's stale check.
|
|
176
|
+
if ((Number.isInteger(ownerPid) && ownerPid > 0) || ageMs >= INVENTORY_LOCK_STALE_MS) {
|
|
177
|
+
try {
|
|
178
|
+
fs.unlinkSync(lockPath);
|
|
179
|
+
removed.push(lockPath);
|
|
180
|
+
} catch (_) {
|
|
181
|
+
retained.push(lockPath);
|
|
182
|
+
}
|
|
183
|
+
} else {
|
|
184
|
+
retained.push(lockPath);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return { removed, retained };
|
|
189
|
+
}
|
|
190
|
+
|
|
104
191
|
|
|
105
192
|
// ---------------------------------------------------------------------------
|
|
106
193
|
// Schema
|
|
@@ -597,18 +684,23 @@ function createSessionHistoryIndex(opts = {}) {
|
|
|
597
684
|
}
|
|
598
685
|
|
|
599
686
|
async function _runInventory(source, { force = false, config = indexConfig } = {}) {
|
|
600
|
-
|
|
601
|
-
|
|
687
|
+
// SQLite's writer lock is database-wide, so source-specific lock files do
|
|
688
|
+
// not prevent two different sources from entering BEGIN IMMEDIATE at the
|
|
689
|
+
// same time. Acquire one lock for the whole index before opening/schema
|
|
690
|
+
// initializing the writer connection.
|
|
691
|
+
const inventoryLock = await _acquireInventoryLock(dbPath, source);
|
|
602
692
|
if (!inventoryLock) {
|
|
603
693
|
// Another process is already doing the expensive work. Existing rows
|
|
604
694
|
// remain readable, and the next scheduled inventory will observe the
|
|
605
695
|
// new source_state timestamp once that worker finishes.
|
|
606
696
|
return;
|
|
607
697
|
}
|
|
698
|
+
let db = null;
|
|
608
699
|
let errorMsg = null;
|
|
609
700
|
let stateToRecord = null;
|
|
610
701
|
|
|
611
702
|
try {
|
|
703
|
+
db = _getDb();
|
|
612
704
|
if (!force && _isSourceFresh(source)) {
|
|
613
705
|
return;
|
|
614
706
|
}
|
|
@@ -798,7 +890,9 @@ function createSessionHistoryIndex(opts = {}) {
|
|
|
798
890
|
} finally {
|
|
799
891
|
if (stateToRecord) {
|
|
800
892
|
try {
|
|
801
|
-
|
|
893
|
+
if (db) {
|
|
894
|
+
_recordSourceState(db, source, stateToRecord.lastInventoryMs, stateToRecord.lastError);
|
|
895
|
+
}
|
|
802
896
|
} catch (_err) {}
|
|
803
897
|
}
|
|
804
898
|
_releaseInventoryLock(inventoryLock);
|
|
@@ -1494,4 +1588,5 @@ module.exports = {
|
|
|
1494
1588
|
invalidateSource,
|
|
1495
1589
|
getSourceIndexMeta,
|
|
1496
1590
|
closeSessionHistoryIndex,
|
|
1591
|
+
cleanupStaleInventoryLocks,
|
|
1497
1592
|
};
|