coding-tool-x 3.8.8 → 3.8.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -388
- package/dist/web/assets/AgentsDrawer-Bam7kBjG.js +1 -0
- package/dist/web/assets/Analytics-D5dUWXEa.js +1 -0
- package/dist/web/assets/AssetPathField-CLqalYv-.js +1 -0
- package/dist/web/assets/{ChatHistoryDrawer-CrfS0KYV.js → ChatHistoryDrawer-C1At65O3.js} +1 -1
- package/dist/web/assets/{CollapseItem-ClMsJuUu.js → CollapseItem-CVWojShK.js} +1 -1
- package/dist/web/assets/CommandsDrawer-BEoypGBf.js +1 -0
- package/dist/web/assets/ConfigExportDrawer-DBvFDDWK.js +1 -0
- package/dist/web/assets/ConfigTemplates-BpaWG-iC.js +1 -0
- package/dist/web/assets/{ConfigTemplatesDrawer-BqM32Im7.js → ConfigTemplatesDrawer-C2lvyKMf.js} +1 -1
- package/dist/web/assets/ConfigTemplatesPanel-CorOY7kY.js +1 -0
- package/dist/web/assets/{Divider-DsdbkHrG.js → Divider-BLLhMTSt.js} +1 -1
- package/dist/web/assets/FavoritesDrawer-uW88N6te.js +1 -0
- package/dist/web/assets/GatewayConvertDrawer-i5Wt9Dd9.js +1 -0
- package/dist/web/assets/Home-BIxWvm29.js +1 -0
- package/dist/web/assets/{MarkdownEditor-BMMUufSc.js → MarkdownEditor-Ul0bSGj-.js} +1 -1
- package/dist/web/assets/McpDrawer-Cc2xq77I.js +1 -0
- package/dist/web/assets/McpFormDrawer-DLHX_nrI.js +1 -0
- package/dist/web/assets/{PluginManager-C4kJlpVX.js → PluginManager-BGQ0xF2F.js} +1 -1
- package/dist/web/assets/{PluginsDrawer-05R4k15x.js → PluginsDrawer-STwHEqiF.js} +1 -1
- package/dist/web/assets/PluginsPanel-1zuNP7da.js +1 -0
- package/dist/web/assets/ProjectList-VbIYSHbH.css +1 -0
- package/dist/web/assets/ProjectList-_pe2nk9c.js +1 -0
- package/dist/web/assets/PromptsDrawer-CJN1xZk7.js +1 -0
- package/dist/web/assets/RecentSessionsDrawer-DX1HI8Nm.js +1 -0
- package/dist/web/assets/SessionCard-Op6PHvi0.js +1 -0
- package/dist/web/assets/SessionList-Da_TAEYr.js +1 -0
- package/dist/web/assets/SettingsDrawer-DKLd4Sjy.js +1 -0
- package/dist/web/assets/{SkillManager-D9jkNuRM.js → SkillManager-PcAa5EbF.js} +1 -1
- package/dist/web/assets/{SkillsDrawer-0Z4IefxC.js → SkillsDrawer-DPdU4p6C.js} +1 -1
- package/dist/web/assets/SkillsPanel-B9qzRqjd.js +1 -0
- package/dist/web/assets/Upload-e1M9Yp5z.js +1 -0
- package/dist/web/assets/{WorkspaceDrawer-eujtWpoq.js → WorkspaceDrawer-oS6mB1VZ.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-CQ-8Ej68.js → WorkspaceManager-6VvRWe3r.js} +1 -1
- package/dist/web/assets/WorkspacePanel-CYq0Z8iY.js +1 -0
- package/dist/web/assets/client-BgcNg4Bt.js +1 -0
- package/dist/web/assets/headers-DbowkmZJ.js +1 -0
- package/dist/web/assets/{icons-DmFe4b5j.js → icons-B-AzZoeE.js} +1 -1
- package/dist/web/assets/index-BlUXw84U.js +2 -0
- package/dist/web/assets/index-Bn8BUnwP.css +1 -0
- package/dist/web/assets/{light-0eavxNvt.js → light-C2X7hegj.js} +1 -1
- package/dist/web/assets/platforms-DrzAtnjY.js +1 -0
- package/dist/web/assets/platforms-mqqe_82d.js +1 -0
- package/dist/web/assets/project-config-BVt5s-pH.js +1 -0
- package/dist/web/assets/sessions-53-Fr68H.js +1 -0
- package/dist/web/assets/{sessions-D4yQtfKb.js → sessions-CaWUaysM.js} +1 -1
- package/dist/web/assets/use-message-DBsCm7FJ.js +1 -0
- package/dist/web/assets/{useResponsiveDrawer-DZd5rNk5.js → useResponsiveDrawer-ChHZrTfl.js} +1 -1
- package/dist/web/assets/{workspaces-mjyPVGS8.js → workspaces-DQuZd4qw.js} +1 -1
- package/dist/web/index.html +8 -8
- package/package.json +4 -2
- package/src/commands/daemon.js +27 -4
- package/src/commands/export-config.js +17 -211
- package/src/config/model-metadata.json +180 -16
- package/src/platforms/channel-auth-registry.js +36 -6
- package/src/platforms/channel-auth-service.js +13 -3
- package/src/platforms/commands-service.js +28 -0
- package/src/platforms/driver-registry.js +1 -0
- package/src/platforms/drivers/claude/channels-implementation.js +79 -90
- package/src/platforms/drivers/claude/native-config-implementation.js +3 -4
- package/src/platforms/drivers/codex/channels-implementation.js +34 -49
- package/src/platforms/drivers/codex/native-config-implementation.js +3 -51
- package/src/platforms/drivers/dsh/api-operations.js +1 -1
- package/src/platforms/drivers/dsh/channels-implementation.js +28 -8
- package/src/platforms/drivers/dsh/channels.js +0 -18
- package/src/platforms/drivers/dsh/common.js +7 -0
- package/src/platforms/drivers/dsh/native-config.js +7 -2
- package/src/platforms/drivers/gemini/channels-implementation.js +47 -55
- package/src/platforms/drivers/gemini/native-config-implementation.js +8 -20
- package/src/platforms/drivers/generic-native-config.js +20 -0
- package/src/platforms/drivers/native-config-snapshot.js +30 -4
- package/src/platforms/drivers/omp/channels-implementation.js +51 -17
- package/src/platforms/drivers/omp/native-config-implementation.js +10 -17
- package/src/platforms/drivers/omp/prompts.js +58 -2
- package/src/platforms/drivers/omp/proxy-implementation.js +25 -1
- package/src/platforms/drivers/opencode/channels-implementation.js +31 -22
- package/src/platforms/drivers/opencode/channels.js +6 -1
- package/src/platforms/drivers/opencode/native-config-implementation.js +3 -67
- package/src/platforms/manifest-schema.js +35 -2
- package/src/platforms/manifests/claude.json +0 -8
- package/src/platforms/manifests/codex.json +0 -8
- package/src/platforms/manifests/dsh.json +6 -9
- package/src/platforms/manifests/gemini.json +0 -8
- package/src/platforms/manifests/omp.json +2 -9
- package/src/platforms/manifests/opencode.json +28 -8
- package/src/platforms/native-oauth-adapters.js +351 -51
- package/src/platforms/oauth-credentials-service.js +344 -13
- package/src/server/index.js +37 -1
- package/src/server/services/bounded-content.js +60 -0
- package/src/server/services/channel-balance.js +10 -4
- package/src/server/services/config-export-service.js +2 -2
- package/src/server/services/config-templates-service.js +5 -1
- package/src/server/services/plugins-service.js +162 -85
- package/src/server/services/session-history-maintenance-worker.js +71 -0
- package/src/server/services/session-history-maintenance.js +166 -0
- package/src/server/services/skill-service.js +23 -3
- package/src/server/services/speed-test.js +0 -72
- package/src/shared/channel-driver.js +16 -0
- package/src/shared/driver-factories/api.js +1 -0
- package/src/utils/native-config-patcher.js +474 -0
- package/dist/web/assets/AgentsDrawer-DRQuwi0m.js +0 -1
- package/dist/web/assets/Analytics-Cf5KwEdj.js +0 -1
- package/dist/web/assets/AssetPathField-DyniM0pF.js +0 -1
- package/dist/web/assets/CommandsDrawer-CzHelf56.js +0 -1
- package/dist/web/assets/ConfigExportDrawer-BEZ8wTby.js +0 -1
- package/dist/web/assets/ConfigTemplates-Crg3xWe5.js +0 -1
- package/dist/web/assets/ConfigTemplatesPanel-lTdutC8H.js +0 -1
- package/dist/web/assets/FavoritesDrawer-DDyWUBJR.js +0 -1
- package/dist/web/assets/GatewayConvertDrawer-Rigtt_5J.js +0 -1
- package/dist/web/assets/Home-tCSKmSHV.js +0 -1
- package/dist/web/assets/McpDrawer-CmE-d7VW.js +0 -1
- package/dist/web/assets/McpFormDrawer-ByvS18od.js +0 -1
- package/dist/web/assets/PluginsPanel-BCoEs7Og.js +0 -1
- package/dist/web/assets/ProjectList-BJt1OIaI.js +0 -1
- package/dist/web/assets/ProjectList-DG3yu8xZ.css +0 -1
- package/dist/web/assets/PromptsDrawer-DnwBf_oW.js +0 -1
- package/dist/web/assets/RecentSessionsDrawer-B7d-38wH.js +0 -1
- package/dist/web/assets/SessionCard-BUgjHQwX.js +0 -1
- package/dist/web/assets/SessionList-B-bXZRFk.js +0 -1
- package/dist/web/assets/SettingsDrawer-BITzjQLl.js +0 -1
- package/dist/web/assets/SkillsPanel-BKIVOj7n.js +0 -1
- package/dist/web/assets/SpeedTestDrawer-Bx1bYf5i.js +0 -1
- package/dist/web/assets/SpeedTestDrawer-Dc0WToJ_.css +0 -1
- package/dist/web/assets/Upload-CzC4rGI4.js +0 -1
- package/dist/web/assets/WorkspacePanel-Cql5qE2R.js +0 -1
- package/dist/web/assets/client-UDwXBP9i.js +0 -1
- package/dist/web/assets/headers-BqDmkMjy.js +0 -1
- package/dist/web/assets/index-Cmmwj3Fx.js +0 -2
- package/dist/web/assets/index-Cnbe696C.css +0 -1
- package/dist/web/assets/platforms-BmzzwnSw.js +0 -1
- package/dist/web/assets/platforms-DWXgmy2w.js +0 -1
- package/dist/web/assets/project-config-B2baPzYL.js +0 -1
- package/dist/web/assets/sessions-DqlbocLJ.js +0 -1
- package/dist/web/assets/use-message-DqCwefG6.js +0 -1
|
@@ -6,7 +6,9 @@ const {
|
|
|
6
6
|
inspectTool,
|
|
7
7
|
SUPPORTED_TOOLS,
|
|
8
8
|
fingerprintFor,
|
|
9
|
-
readAllNativeOAuth
|
|
9
|
+
readAllNativeOAuth,
|
|
10
|
+
updateCodexOAuthTokens,
|
|
11
|
+
applyOAuthCredential
|
|
10
12
|
} = require('./native-oauth-adapters');
|
|
11
13
|
const { maskToken } = require('../server/services/oauth-utils');
|
|
12
14
|
|
|
@@ -110,6 +112,7 @@ function sanitizeCredential(entry, defaultCredentialId) {
|
|
|
110
112
|
providerId: entry.providerId || '',
|
|
111
113
|
accountId: entry.accountId || '',
|
|
112
114
|
accountEmail: entry.accountEmail || '',
|
|
115
|
+
identityKey: entry.identityKey || '',
|
|
113
116
|
expiresAt: entry.expiresAt || null,
|
|
114
117
|
lastRefresh: entry.lastRefresh || null,
|
|
115
118
|
lastUsedAt: entry.lastUsedAt || null,
|
|
@@ -209,12 +212,34 @@ function extractSecrets(tool, metadata) {
|
|
|
209
212
|
importPayload: metadata.importPayload || null,
|
|
210
213
|
primaryToken: metadata.primaryToken || metadata.accessToken || ''
|
|
211
214
|
};
|
|
215
|
+
case 'opencode':
|
|
216
|
+
return {
|
|
217
|
+
providerId: metadata.providerId || '',
|
|
218
|
+
accessToken: metadata.accessToken || '',
|
|
219
|
+
idToken: metadata.idToken || '',
|
|
220
|
+
refreshToken: metadata.refreshToken || '',
|
|
221
|
+
expiresAt: metadata.expiresAt || null,
|
|
222
|
+
accountId: metadata.accountId || '',
|
|
223
|
+
accountEmail: metadata.accountEmail || '',
|
|
224
|
+
identityKey: metadata.identityKey || '',
|
|
225
|
+
primaryToken: metadata.primaryToken || metadata.accessToken || ''
|
|
226
|
+
};
|
|
212
227
|
default:
|
|
213
228
|
return { primaryToken: metadata.primaryToken || metadata.accessToken || '' };
|
|
214
229
|
}
|
|
215
230
|
}
|
|
216
231
|
|
|
217
232
|
function stableFingerprintValue(tool, metadata) {
|
|
233
|
+
// Codex account IDs remain stable while access tokens rotate.
|
|
234
|
+
if (tool === 'codex') {
|
|
235
|
+
return metadata.accountId
|
|
236
|
+
|| metadata.accountEmail
|
|
237
|
+
|| metadata.refreshToken
|
|
238
|
+
|| metadata.primaryToken
|
|
239
|
+
|| metadata.accessToken
|
|
240
|
+
|| '';
|
|
241
|
+
}
|
|
242
|
+
|
|
218
243
|
// 优先使用稳定标识符,避免 access token 轮换导致重复记录
|
|
219
244
|
const stableId = metadata.accountEmail
|
|
220
245
|
|| metadata.accountId
|
|
@@ -227,6 +252,9 @@ function stableFingerprintValue(tool, metadata) {
|
|
|
227
252
|
}
|
|
228
253
|
|
|
229
254
|
function resolveFingerprintValue(tool, metadata, options = {}) {
|
|
255
|
+
if (tool === 'codex') {
|
|
256
|
+
return stableFingerprintValue(tool, metadata);
|
|
257
|
+
}
|
|
230
258
|
if (options.fingerprintMode === 'primary-token') {
|
|
231
259
|
return metadata.primaryToken
|
|
232
260
|
|| metadata.accessToken
|
|
@@ -235,13 +263,28 @@ function resolveFingerprintValue(tool, metadata, options = {}) {
|
|
|
235
263
|
return stableFingerprintValue(tool, metadata);
|
|
236
264
|
}
|
|
237
265
|
|
|
266
|
+
function matchesCodexIdentity(entry = {}, metadata = {}) {
|
|
267
|
+
const entryAccountId = safeString(entry.accountId || entry.secrets?.accountId);
|
|
268
|
+
const metadataAccountId = safeString(metadata.accountId);
|
|
269
|
+
if (entryAccountId && metadataAccountId) {
|
|
270
|
+
return entryAccountId === metadataAccountId;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const entryEmail = safeString(entry.accountEmail);
|
|
274
|
+
const metadataEmail = safeString(metadata.accountEmail);
|
|
275
|
+
return Boolean(entryEmail && metadataEmail && entryEmail === metadataEmail);
|
|
276
|
+
}
|
|
277
|
+
|
|
238
278
|
function upsertCredential(tool, metadata, options = {}) {
|
|
239
279
|
const store = readStore();
|
|
240
280
|
const toolStore = getToolStore(store, tool);
|
|
241
281
|
const now = Date.now();
|
|
242
282
|
const primaryToken = metadata.primaryToken || metadata.accessToken || '';
|
|
243
283
|
const fingerprint = fingerprintFor(tool, resolveFingerprintValue(tool, metadata, options));
|
|
244
|
-
|
|
284
|
+
let existingIndex = toolStore.credentials.findIndex((item) => item.fingerprint === fingerprint);
|
|
285
|
+
if (existingIndex < 0 && tool === 'codex') {
|
|
286
|
+
existingIndex = toolStore.credentials.findIndex((item) => matchesCodexIdentity(item, metadata));
|
|
287
|
+
}
|
|
245
288
|
const existing = existingIndex >= 0 ? toolStore.credentials[existingIndex] : null;
|
|
246
289
|
|
|
247
290
|
const entry = {
|
|
@@ -267,7 +310,21 @@ function upsertCredential(tool, metadata, options = {}) {
|
|
|
267
310
|
toolStore.credentials.unshift(entry);
|
|
268
311
|
}
|
|
269
312
|
|
|
270
|
-
if (
|
|
313
|
+
if (tool === 'codex') {
|
|
314
|
+
const duplicateIds = new Set();
|
|
315
|
+
for (let index = toolStore.credentials.length - 1; index >= 0; index -= 1) {
|
|
316
|
+
const candidate = toolStore.credentials[index];
|
|
317
|
+
if (candidate.id === entry.id || !matchesCodexIdentity(candidate, metadata)) continue;
|
|
318
|
+
duplicateIds.add(candidate.id);
|
|
319
|
+
toolStore.credentials.splice(index, 1);
|
|
320
|
+
}
|
|
321
|
+
if (duplicateIds.has(toolStore.defaultCredentialId)) {
|
|
322
|
+
toolStore.defaultCredentialId = entry.id;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (!toolStore.defaultCredentialId
|
|
327
|
+
|| !toolStore.credentials.some((item) => item.id === toolStore.defaultCredentialId)) {
|
|
271
328
|
toolStore.defaultCredentialId = entry.id;
|
|
272
329
|
}
|
|
273
330
|
|
|
@@ -290,10 +347,13 @@ function syncLocalCredential(tool) {
|
|
|
290
347
|
throw new Error('当前本地未检测到可同步的 OAuth 凭证。');
|
|
291
348
|
}
|
|
292
349
|
|
|
293
|
-
const credentials = nativeCredentials.map((metadata) =>
|
|
294
|
-
source: 'synced-local'
|
|
295
|
-
|
|
296
|
-
|
|
350
|
+
const credentials = nativeCredentials.map((metadata) => {
|
|
351
|
+
const options = { source: 'synced-local' };
|
|
352
|
+
if (tool !== 'codex') {
|
|
353
|
+
options.fingerprintMode = 'primary-token';
|
|
354
|
+
}
|
|
355
|
+
return upsertCredential(tool, metadata, options);
|
|
356
|
+
});
|
|
297
357
|
|
|
298
358
|
return {
|
|
299
359
|
credential: credentials[0] || null,
|
|
@@ -302,6 +362,70 @@ function syncLocalCredential(tool) {
|
|
|
302
362
|
};
|
|
303
363
|
}
|
|
304
364
|
|
|
365
|
+
function restoreStoredOmpOAuthCredentials(channels = []) {
|
|
366
|
+
const oauthChannels = (Array.isArray(channels) ? channels : [])
|
|
367
|
+
.filter(channel => channel?.enabled !== false && channel?.authMode === 'oauth');
|
|
368
|
+
if (oauthChannels.length === 0) return { restored: [], warnings: [] };
|
|
369
|
+
|
|
370
|
+
const store = readStore();
|
|
371
|
+
const toolStore = getToolStore(store, 'omp');
|
|
372
|
+
const nativeCredentials = readAllNativeOAuth('omp');
|
|
373
|
+
const restored = [];
|
|
374
|
+
const warnings = [];
|
|
375
|
+
const seenCredentialIds = new Set();
|
|
376
|
+
|
|
377
|
+
for (const channel of oauthChannels) {
|
|
378
|
+
const ref = channel.authRef || {};
|
|
379
|
+
const stored = toolStore.credentials.find((entry) => {
|
|
380
|
+
if (ref.credentialId && entry.id === ref.credentialId) return true;
|
|
381
|
+
if (ref.providerId && entry.providerId !== ref.providerId) return false;
|
|
382
|
+
if (ref.accountId && entry.accountId === ref.accountId) return true;
|
|
383
|
+
if (ref.accountEmail && entry.accountEmail === ref.accountEmail) return true;
|
|
384
|
+
return !ref.credentialId && !ref.accountId && !ref.accountEmail && !ref.providerId;
|
|
385
|
+
});
|
|
386
|
+
if (!stored || seenCredentialIds.has(stored.id)) continue;
|
|
387
|
+
seenCredentialIds.add(stored.id);
|
|
388
|
+
|
|
389
|
+
const accessToken = stored.secrets?.accessToken || stored.secrets?.primaryToken || '';
|
|
390
|
+
if (!accessToken) {
|
|
391
|
+
warnings.push(`OMP OAuth credential ${stored.id} has no access token.`);
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
const expiresAt = Number(stored.expiresAt || stored.secrets?.expiresAt || 0);
|
|
395
|
+
if (expiresAt > 0 && expiresAt <= Date.now()) {
|
|
396
|
+
warnings.push(`OMP OAuth credential ${stored.id} is expired and was not restored.`);
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const alreadyNative = nativeCredentials.some((native) => (
|
|
401
|
+
native.providerId === stored.providerId
|
|
402
|
+
&& ((!stored.accountId && !stored.accountEmail)
|
|
403
|
+
|| (stored.accountId && native.accountId === stored.accountId)
|
|
404
|
+
|| (stored.accountEmail && native.accountEmail === stored.accountEmail))
|
|
405
|
+
));
|
|
406
|
+
if (alreadyNative) continue;
|
|
407
|
+
|
|
408
|
+
try {
|
|
409
|
+
applyOAuthCredential('omp', {
|
|
410
|
+
providerId: stored.providerId || stored.secrets?.providerId,
|
|
411
|
+
credentialType: stored.secrets?.credentialType || 'oauth',
|
|
412
|
+
accessToken,
|
|
413
|
+
refreshToken: stored.secrets?.refreshToken || '',
|
|
414
|
+
expiresAt: expiresAt || null,
|
|
415
|
+
accountId: stored.accountId || stored.secrets?.accountId || '',
|
|
416
|
+
accountEmail: stored.accountEmail || stored.secrets?.accountEmail || '',
|
|
417
|
+
identityKey: stored.identityKey || stored.secrets?.identityKey || stored.accountId || '',
|
|
418
|
+
primaryToken: accessToken
|
|
419
|
+
});
|
|
420
|
+
restored.push(stored.id);
|
|
421
|
+
} catch (error) {
|
|
422
|
+
warnings.push(`OMP OAuth credential ${stored.id} restore failed: ${error.message}`);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return { restored, warnings };
|
|
427
|
+
}
|
|
428
|
+
|
|
305
429
|
|
|
306
430
|
function findStoredCredential(tool, credentialId) {
|
|
307
431
|
const store = readStore();
|
|
@@ -359,6 +483,11 @@ function httpPost(url, body, headers = {}) {
|
|
|
359
483
|
});
|
|
360
484
|
}
|
|
361
485
|
|
|
486
|
+
const CODEX_OAUTH_CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann';
|
|
487
|
+
const CODEX_OAUTH_TOKEN_URL = 'https://auth.openai.com/oauth/token';
|
|
488
|
+
const CODEX_REFRESH_SKEW_MS = 60 * 1000;
|
|
489
|
+
const codexRefreshPromises = new Map();
|
|
490
|
+
|
|
362
491
|
function parseJsonBody(body) {
|
|
363
492
|
try {
|
|
364
493
|
return JSON.parse(body);
|
|
@@ -400,11 +529,21 @@ function extractCodexAccountId(...tokens) {
|
|
|
400
529
|
return '';
|
|
401
530
|
}
|
|
402
531
|
|
|
532
|
+
function isCodexOAuthProvider(providerId = '') {
|
|
533
|
+
const value = String(providerId || '').trim().toLowerCase();
|
|
534
|
+
return value === 'codex'
|
|
535
|
+
|| value === 'openai'
|
|
536
|
+
|| value.includes('openai-codex')
|
|
537
|
+
|| value.includes('codex');
|
|
538
|
+
}
|
|
539
|
+
|
|
403
540
|
async function fetchCodexUsage(accessToken, accountId = '') {
|
|
404
541
|
try {
|
|
405
542
|
const headers = {
|
|
406
543
|
'Authorization': `Bearer ${accessToken}`,
|
|
407
|
-
'User-Agent': 'codex-cli/0.1'
|
|
544
|
+
'User-Agent': 'codex-cli/0.1',
|
|
545
|
+
'Accept': 'application/json',
|
|
546
|
+
'originator': 'codex_cli_rs'
|
|
408
547
|
};
|
|
409
548
|
if (accountId) headers['ChatGPT-Account-Id'] = accountId;
|
|
410
549
|
const result = await httpGet('https://chatgpt.com/backend-api/wham/usage', headers);
|
|
@@ -415,6 +554,166 @@ async function fetchCodexUsage(accessToken, accountId = '') {
|
|
|
415
554
|
}
|
|
416
555
|
}
|
|
417
556
|
|
|
557
|
+
function isCodexCredential(tool, entry = {}) {
|
|
558
|
+
if (tool === 'codex') return true;
|
|
559
|
+
if (tool !== 'omp' && tool !== 'opencode') return false;
|
|
560
|
+
return isCodexOAuthProvider(entry.providerId || entry.secrets?.providerId);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function credentialExpiresAt(entry = {}) {
|
|
564
|
+
const explicit = Number(entry.expiresAt || entry.secrets?.expiresAt || 0);
|
|
565
|
+
if (Number.isFinite(explicit) && explicit > 0) return explicit;
|
|
566
|
+
const { decodeJwtPayload } = require('../server/services/oauth-utils');
|
|
567
|
+
for (const token of [entry.secrets?.accessToken, entry.secrets?.idToken]) {
|
|
568
|
+
const exp = Number(decodeJwtPayload(token)?.exp || 0);
|
|
569
|
+
if (Number.isFinite(exp) && exp > 0) return exp * 1000;
|
|
570
|
+
}
|
|
571
|
+
return null;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
async function refreshCodexCredential(entry) {
|
|
575
|
+
const refreshToken = String(entry?.secrets?.refreshToken || '').trim();
|
|
576
|
+
if (!refreshToken) return { entry, refreshed: false, attempted: false };
|
|
577
|
+
const existing = codexRefreshPromises.get(entry.id);
|
|
578
|
+
if (existing) return existing;
|
|
579
|
+
|
|
580
|
+
const promise = (async () => {
|
|
581
|
+
const form = new URLSearchParams({
|
|
582
|
+
grant_type: 'refresh_token',
|
|
583
|
+
refresh_token: refreshToken,
|
|
584
|
+
client_id: CODEX_OAUTH_CLIENT_ID
|
|
585
|
+
}).toString();
|
|
586
|
+
const result = await httpPost(CODEX_OAUTH_TOKEN_URL, form, {
|
|
587
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
588
|
+
'Accept': 'application/json'
|
|
589
|
+
});
|
|
590
|
+
const payload = parseJsonBody(result.body) || {};
|
|
591
|
+
if (result.statusCode < 200 || result.statusCode >= 300 || !payload.access_token) {
|
|
592
|
+
return { entry, refreshed: false, attempted: true };
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
const accessToken = String(payload.access_token).trim();
|
|
596
|
+
const nextRefreshToken = String(payload.refresh_token || refreshToken).trim();
|
|
597
|
+
const idToken = String(payload.id_token || entry.secrets.idToken || '').trim();
|
|
598
|
+
const accountId = extractCodexAccountId(idToken, accessToken)
|
|
599
|
+
|| entry.accountId
|
|
600
|
+
|| entry.secrets.accountId
|
|
601
|
+
|| '';
|
|
602
|
+
const expiresIn = Number(payload.expires_in || 0);
|
|
603
|
+
const expiresAt = expiresIn > 0
|
|
604
|
+
? Date.now() + expiresIn * 1000
|
|
605
|
+
: credentialExpiresAt({ secrets: { accessToken, idToken } });
|
|
606
|
+
const lastRefresh = new Date().toISOString();
|
|
607
|
+
|
|
608
|
+
const store = readStore();
|
|
609
|
+
const toolStore = getToolStore(store, entry.tool);
|
|
610
|
+
const stored = toolStore.credentials.find(item => item.id === entry.id);
|
|
611
|
+
if (!stored) return { entry, refreshed: false, attempted: true };
|
|
612
|
+
stored.accountId = accountId || stored.accountId || '';
|
|
613
|
+
stored.expiresAt = expiresAt || stored.expiresAt || null;
|
|
614
|
+
stored.updatedAt = Date.now();
|
|
615
|
+
stored.secrets = {
|
|
616
|
+
...stored.secrets,
|
|
617
|
+
accessToken,
|
|
618
|
+
refreshToken: nextRefreshToken,
|
|
619
|
+
idToken,
|
|
620
|
+
accountId: accountId || stored.secrets.accountId || '',
|
|
621
|
+
expiresAt: expiresAt || stored.secrets.expiresAt || null,
|
|
622
|
+
lastRefresh,
|
|
623
|
+
primaryToken: accessToken
|
|
624
|
+
};
|
|
625
|
+
writeStore(store);
|
|
626
|
+
|
|
627
|
+
if (entry.tool === 'codex' && typeof updateCodexOAuthTokens === 'function') {
|
|
628
|
+
try {
|
|
629
|
+
updateCodexOAuthTokens({
|
|
630
|
+
authMode: stored.secrets.authMode,
|
|
631
|
+
accessToken,
|
|
632
|
+
refreshToken: nextRefreshToken,
|
|
633
|
+
idToken,
|
|
634
|
+
accountId,
|
|
635
|
+
lastRefresh
|
|
636
|
+
});
|
|
637
|
+
} catch {
|
|
638
|
+
// The credential store is sufficient for quota lookup; native sync is best-effort.
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
if (entry.tool === 'omp' && typeof applyOAuthCredential === 'function') {
|
|
643
|
+
try {
|
|
644
|
+
applyOAuthCredential('omp', {
|
|
645
|
+
providerId: stored.providerId || stored.secrets.providerId,
|
|
646
|
+
credentialType: stored.secrets.credentialType || 'oauth',
|
|
647
|
+
accessToken,
|
|
648
|
+
refreshToken: nextRefreshToken,
|
|
649
|
+
expiresAt,
|
|
650
|
+
accountId,
|
|
651
|
+
accountEmail: stored.accountEmail || stored.secrets.accountEmail || '',
|
|
652
|
+
identityKey: stored.identityKey || stored.secrets.identityKey || accountId,
|
|
653
|
+
primaryToken: accessToken
|
|
654
|
+
});
|
|
655
|
+
} catch {
|
|
656
|
+
// Quota lookup can still use the refreshed app credential if native sync is unavailable.
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
return { entry: stored, refreshed: true, attempted: true };
|
|
661
|
+
})().catch(() => ({ entry, refreshed: false, attempted: true }));
|
|
662
|
+
codexRefreshPromises.set(entry.id, promise);
|
|
663
|
+
try {
|
|
664
|
+
return await promise;
|
|
665
|
+
} finally {
|
|
666
|
+
codexRefreshPromises.delete(entry.id);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
async function ensureFreshCodexCredential(entry) {
|
|
671
|
+
const expiresAt = credentialExpiresAt(entry);
|
|
672
|
+
if (expiresAt && expiresAt - Date.now() > CODEX_REFRESH_SKEW_MS) {
|
|
673
|
+
return { entry, refreshed: false, attempted: false };
|
|
674
|
+
}
|
|
675
|
+
return refreshCodexCredential(entry);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
async function refreshNativeCodexOAuth() {
|
|
679
|
+
const native = readAllNativeOAuth('codex')[0];
|
|
680
|
+
if (!native) {
|
|
681
|
+
return { available: false, refreshed: false, synchronized: false };
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
const synced = syncLocalCredential('codex');
|
|
685
|
+
const credentialId = synced.credential?.id;
|
|
686
|
+
if (!credentialId) {
|
|
687
|
+
return { available: true, refreshed: false, synchronized: false };
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
let entry = findStoredCredential('codex', credentialId);
|
|
691
|
+
const result = await ensureFreshCodexCredential(entry);
|
|
692
|
+
entry = result.entry || entry;
|
|
693
|
+
|
|
694
|
+
let synchronized = Boolean(result.refreshed);
|
|
695
|
+
if (!result.refreshed && !result.attempted && entry.secrets?.refreshToken) {
|
|
696
|
+
updateCodexOAuthTokens({
|
|
697
|
+
authMode: native.authMode,
|
|
698
|
+
accessToken: native.accessToken,
|
|
699
|
+
refreshToken: native.refreshToken,
|
|
700
|
+
idToken: native.idToken,
|
|
701
|
+
accountId: native.accountId,
|
|
702
|
+
lastRefresh: native.lastRefresh
|
|
703
|
+
});
|
|
704
|
+
synchronized = true;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
return {
|
|
708
|
+
available: true,
|
|
709
|
+
refreshed: Boolean(result.refreshed),
|
|
710
|
+
refreshAttempted: Boolean(result.attempted),
|
|
711
|
+
refreshFailed: Boolean(result.attempted && !result.refreshed),
|
|
712
|
+
synchronized,
|
|
713
|
+
credentialId: entry.id
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
|
|
418
717
|
async function fetchGeminiUsage(accessToken) {
|
|
419
718
|
try {
|
|
420
719
|
const body = JSON.stringify({
|
|
@@ -586,9 +885,16 @@ function normalizeOAuthQuota(tool, raw) {
|
|
|
586
885
|
}
|
|
587
886
|
|
|
588
887
|
async function fetchCredentialUsage(tool, credentialId) {
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
888
|
+
let entry = findStoredCredential(tool, credentialId);
|
|
889
|
+
let secrets = entry.secrets || {};
|
|
890
|
+
let codexRefreshed = false;
|
|
891
|
+
if (isCodexCredential(tool, entry)) {
|
|
892
|
+
const refreshed = await ensureFreshCodexCredential(entry);
|
|
893
|
+
entry = refreshed.entry;
|
|
894
|
+
secrets = entry.secrets || {};
|
|
895
|
+
codexRefreshed = refreshed.refreshed;
|
|
896
|
+
}
|
|
897
|
+
let accessToken = secrets.accessToken || secrets.primaryToken || '';
|
|
592
898
|
if (!accessToken) return { error: '无有效 token' };
|
|
593
899
|
|
|
594
900
|
let response;
|
|
@@ -606,7 +912,7 @@ async function fetchCredentialUsage(tool, credentialId) {
|
|
|
606
912
|
response = await fetchGeminiUsage(accessToken);
|
|
607
913
|
break;
|
|
608
914
|
case 'omp': {
|
|
609
|
-
const providerId = entry.providerId || secrets.providerId || '';
|
|
915
|
+
const providerId = String(entry.providerId || secrets.providerId || '').toLowerCase();
|
|
610
916
|
response = providerId.includes('claude') || providerId.includes('anthropic')
|
|
611
917
|
? await fetchClaudeUsage(accessToken)
|
|
612
918
|
: providerId.includes('gemini') || providerId.includes('google')
|
|
@@ -614,11 +920,34 @@ async function fetchCredentialUsage(tool, credentialId) {
|
|
|
614
920
|
: await fetchCodexUsage(accessToken, extractCodexAccountId(accessToken) || entry.accountId || secrets.accountId);
|
|
615
921
|
break;
|
|
616
922
|
}
|
|
923
|
+
case 'opencode': {
|
|
924
|
+
const providerId = entry.providerId || secrets.providerId || '';
|
|
925
|
+
response = isCodexOAuthProvider(providerId)
|
|
926
|
+
? await fetchCodexUsage(accessToken, extractCodexAccountId(secrets.idToken, accessToken) || entry.accountId || secrets.accountId)
|
|
927
|
+
: { raw: null, provider: 'opencode', statusCode: 200 };
|
|
928
|
+
break;
|
|
929
|
+
}
|
|
617
930
|
default:
|
|
618
931
|
return { quota: null, status: 'unsupported', error: `不支持的工具: ${tool}` };
|
|
619
932
|
}
|
|
620
933
|
if (response?.error) return { quota: null, status: 'unavailable', error: response.error };
|
|
621
934
|
if (response?.statusCode === 401 || response?.statusCode === 403) {
|
|
935
|
+
if (isCodexCredential(tool, entry) && !codexRefreshed) {
|
|
936
|
+
const refreshed = await refreshCodexCredential(entry);
|
|
937
|
+
if (refreshed.refreshed) {
|
|
938
|
+
entry = refreshed.entry;
|
|
939
|
+
secrets = entry.secrets || {};
|
|
940
|
+
accessToken = secrets.accessToken || secrets.primaryToken || '';
|
|
941
|
+
response = await fetchCodexUsage(
|
|
942
|
+
accessToken,
|
|
943
|
+
extractCodexAccountId(secrets.idToken, accessToken) || entry.accountId || secrets.accountId
|
|
944
|
+
);
|
|
945
|
+
if (response?.error) return { quota: null, status: 'unavailable', error: response.error };
|
|
946
|
+
if (response?.statusCode !== 401 && response?.statusCode !== 403) {
|
|
947
|
+
return normalizeOAuthQuota(tool, response?.raw);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
}
|
|
622
951
|
return { quota: null, status: 'unauthorized', error: '上游 OAuth 授权已失效' };
|
|
623
952
|
}
|
|
624
953
|
return normalizeOAuthQuota(tool, response?.raw);
|
|
@@ -627,9 +956,11 @@ async function fetchCredentialUsage(tool, credentialId) {
|
|
|
627
956
|
module.exports = {
|
|
628
957
|
SUPPORTED_TOOLS,
|
|
629
958
|
syncLocalCredential,
|
|
959
|
+
restoreStoredOmpOAuthCredentials,
|
|
630
960
|
normalizeOAuthQuota,
|
|
631
961
|
fetchCredentialUsage,
|
|
632
962
|
extractCodexAccountId,
|
|
633
963
|
normalizeResetAt,
|
|
634
|
-
normalizeWindow
|
|
964
|
+
normalizeWindow,
|
|
965
|
+
refreshNativeCodexOAuth
|
|
635
966
|
};
|
package/src/server/index.js
CHANGED
|
@@ -26,6 +26,7 @@ const {
|
|
|
26
26
|
const { createApiRequestLogger } = require('./services/request-logger');
|
|
27
27
|
const { inspectWebBuildState, ensureWebDistReady } = require('./services/web-build');
|
|
28
28
|
const { ensureHttpsCredentials } = require('./services/https-cert');
|
|
29
|
+
const { scheduleSessionHistoryMaintenance } = require('./services/session-history-maintenance');
|
|
29
30
|
const notificationHooks = require('../platforms/notification-hooks');
|
|
30
31
|
const {
|
|
31
32
|
prepareNativeCliLogObserver,
|
|
@@ -58,6 +59,30 @@ function traceStartupMemory(stage) {
|
|
|
58
59
|
})}`);
|
|
59
60
|
}
|
|
60
61
|
|
|
62
|
+
async function refreshNativeCodexOAuthOnStartup({ oauthService } = {}) {
|
|
63
|
+
try {
|
|
64
|
+
const service = oauthService || require('../platforms/oauth-credentials-service');
|
|
65
|
+
if (typeof service.refreshNativeCodexOAuth !== 'function') {
|
|
66
|
+
return { available: false, refreshed: false, synchronized: false };
|
|
67
|
+
}
|
|
68
|
+
const result = await service.refreshNativeCodexOAuth();
|
|
69
|
+
if (result?.refreshed) {
|
|
70
|
+
console.log('[Codex OAuth] Native OAuth token refreshed during startup');
|
|
71
|
+
} else if (result?.refreshFailed) {
|
|
72
|
+
console.warn('[Codex OAuth] 启动刷新失败,请运行 codex login 重新登录');
|
|
73
|
+
}
|
|
74
|
+
return result;
|
|
75
|
+
} catch (error) {
|
|
76
|
+
console.warn(`[Codex OAuth] 启动同步失败,保留本地凭证: ${error.message}`);
|
|
77
|
+
return {
|
|
78
|
+
available: false,
|
|
79
|
+
refreshed: false,
|
|
80
|
+
synchronized: false,
|
|
81
|
+
error: error.message
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
61
86
|
function printPortConflictHelp(port) {
|
|
62
87
|
console.log(chalk.yellow('\n[TIP] 解决方案:'));
|
|
63
88
|
console.log(chalk.gray(' 1. 运行 ctx 命令,选择"配置端口"修改端口'));
|
|
@@ -299,6 +324,7 @@ async function startServer(port, host = '127.0.0.1', options = {}) {
|
|
|
299
324
|
server.once('listening', onListening);
|
|
300
325
|
server.once('error', onError);
|
|
301
326
|
});
|
|
327
|
+
scheduleSessionHistoryMaintenance(PATHS.sessionHistoryIndex);
|
|
302
328
|
|
|
303
329
|
console.log(`\n[START] Coding-Tool Web UI running at:`);
|
|
304
330
|
const protocol = useHttps ? 'https' : 'http';
|
|
@@ -335,6 +361,7 @@ async function startServer(port, host = '127.0.0.1', options = {}) {
|
|
|
335
361
|
console.log(chalk.yellow(' [LOCK] 已启用 LAN 安全保护:远程写操作默认关闭'));
|
|
336
362
|
}
|
|
337
363
|
}
|
|
364
|
+
await refreshNativeCodexOAuthOnStartup();
|
|
338
365
|
// 自动恢复代理状态
|
|
339
366
|
try {
|
|
340
367
|
await autoRestoreProxies({ ...platformContext, config });
|
|
@@ -382,6 +409,10 @@ async function autoRestoreProxies({ registry, runtime, config, fsImpl = require(
|
|
|
382
409
|
try {
|
|
383
410
|
const result = await driver.restoreOnBoot({ config: resolvedConfig });
|
|
384
411
|
if (!result || result.status === 'ok') {
|
|
412
|
+
const warnings = result?.warnings || result?.data?.warnings || result?.result?.warnings || [];
|
|
413
|
+
warnings.forEach((warning) => {
|
|
414
|
+
console.log(chalk.yellow(`[WARN] ${platform.label || key}: ${warning}`));
|
|
415
|
+
});
|
|
385
416
|
const port = result?.port;
|
|
386
417
|
const suffix = port
|
|
387
418
|
? platform.proxyMode === 'managed'
|
|
@@ -464,5 +495,10 @@ async function performStartupHealthCheck({ config = loadConfig() } = {}) {
|
|
|
464
495
|
|
|
465
496
|
module.exports = {
|
|
466
497
|
startServer,
|
|
467
|
-
_test: {
|
|
498
|
+
_test: {
|
|
499
|
+
autoRestoreProxies,
|
|
500
|
+
performStartupHealthCheck,
|
|
501
|
+
traceStartupMemory,
|
|
502
|
+
refreshNativeCodexOAuthOnStartup
|
|
503
|
+
}
|
|
468
504
|
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
|
|
5
|
+
function normalizeLimit(maxBytes) {
|
|
6
|
+
const limit = Number(maxBytes);
|
|
7
|
+
if (!Number.isSafeInteger(limit) || limit <= 0) {
|
|
8
|
+
throw new Error('A positive byte limit is required');
|
|
9
|
+
}
|
|
10
|
+
return limit;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function truncateUtf8(value, maxBytes) {
|
|
14
|
+
const limit = normalizeLimit(maxBytes);
|
|
15
|
+
const text = String(value ?? '');
|
|
16
|
+
const bytes = Buffer.byteLength(text, 'utf8');
|
|
17
|
+
if (bytes <= limit) {
|
|
18
|
+
return { text, bytes, truncated: false };
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
text: Buffer.from(text, 'utf8').subarray(0, limit).toString('utf8'),
|
|
22
|
+
bytes,
|
|
23
|
+
truncated: true
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function readTextFileLimited(filePath, maxBytes, fsImpl = fs) {
|
|
28
|
+
const limit = normalizeLimit(maxBytes);
|
|
29
|
+
const totalBytes = fsImpl.statSync(filePath).size;
|
|
30
|
+
if (totalBytes <= limit) {
|
|
31
|
+
return {
|
|
32
|
+
text: fsImpl.readFileSync(filePath, 'utf8'),
|
|
33
|
+
bytes: totalBytes,
|
|
34
|
+
truncated: false
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const fd = fsImpl.openSync(filePath, 'r');
|
|
39
|
+
try {
|
|
40
|
+
const buffer = Buffer.allocUnsafe(limit);
|
|
41
|
+
let offset = 0;
|
|
42
|
+
while (offset < limit) {
|
|
43
|
+
const bytesRead = fsImpl.readSync(fd, buffer, offset, limit - offset, offset);
|
|
44
|
+
if (!bytesRead) break;
|
|
45
|
+
offset += bytesRead;
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
text: buffer.subarray(0, offset).toString('utf8'),
|
|
49
|
+
bytes: totalBytes,
|
|
50
|
+
truncated: true
|
|
51
|
+
};
|
|
52
|
+
} finally {
|
|
53
|
+
fsImpl.closeSync(fd);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = {
|
|
58
|
+
readTextFileLimited,
|
|
59
|
+
truncateUtf8
|
|
60
|
+
};
|
|
@@ -14,6 +14,7 @@ const {
|
|
|
14
14
|
} = require('./snapshot-cache');
|
|
15
15
|
|
|
16
16
|
const CACHE_TTL_MS = 5 * 60 * 1000;
|
|
17
|
+
const OAUTH_QUOTA_CACHE_TTL_MS = 60 * 1000;
|
|
17
18
|
const REQUEST_TIMEOUT_MS = 8000;
|
|
18
19
|
const MAX_RESPONSE_BYTES = 1024 * 1024;
|
|
19
20
|
const QUOTA_UNIT = 500000;
|
|
@@ -124,6 +125,10 @@ function isOAuthChannel(channel = {}) {
|
|
|
124
125
|
return channel?.authMode === 'oauth';
|
|
125
126
|
}
|
|
126
127
|
|
|
128
|
+
function getBalanceCacheTtlMs(channel = {}) {
|
|
129
|
+
return isOAuthChannel(channel) ? OAUTH_QUOTA_CACHE_TTL_MS : CACHE_TTL_MS;
|
|
130
|
+
}
|
|
131
|
+
|
|
127
132
|
function resolveOAuthCacheIdentity(channel = {}) {
|
|
128
133
|
const ref = channel.authRef && typeof channel.authRef === 'object' ? channel.authRef : {};
|
|
129
134
|
return [
|
|
@@ -1581,6 +1586,7 @@ function makeFallbackBalanceSnapshot(currentTime = Date.now()) {
|
|
|
1581
1586
|
async function refreshChannelBalanceSnapshot(source, channel, options = {}) {
|
|
1582
1587
|
const force = options.force === true;
|
|
1583
1588
|
const currentTime = options.now || Date.now();
|
|
1589
|
+
const cacheTtlMs = getBalanceCacheTtlMs(channel);
|
|
1584
1590
|
const key = buildCacheKey(source, channel);
|
|
1585
1591
|
const cached = balanceCache.get(key);
|
|
1586
1592
|
|
|
@@ -1598,7 +1604,7 @@ async function refreshChannelBalanceSnapshot(source, channel, options = {}) {
|
|
|
1598
1604
|
if (snapshot.visible) {
|
|
1599
1605
|
balanceCache.set(key, {
|
|
1600
1606
|
snapshot,
|
|
1601
|
-
expiresAt: currentTime +
|
|
1607
|
+
expiresAt: currentTime + cacheTtlMs
|
|
1602
1608
|
});
|
|
1603
1609
|
return snapshot;
|
|
1604
1610
|
}
|
|
@@ -1613,7 +1619,7 @@ async function refreshChannelBalanceSnapshot(source, channel, options = {}) {
|
|
|
1613
1619
|
};
|
|
1614
1620
|
balanceCache.set(key, {
|
|
1615
1621
|
snapshot: staleSnapshot,
|
|
1616
|
-
expiresAt: currentTime +
|
|
1622
|
+
expiresAt: currentTime + cacheTtlMs
|
|
1617
1623
|
});
|
|
1618
1624
|
return staleSnapshot;
|
|
1619
1625
|
}
|
|
@@ -1621,7 +1627,7 @@ async function refreshChannelBalanceSnapshot(source, channel, options = {}) {
|
|
|
1621
1627
|
const hiddenSnapshot = makeHiddenSnapshot(source, { updatedAt: nowIso(currentTime) });
|
|
1622
1628
|
balanceCache.set(key, {
|
|
1623
1629
|
snapshot: hiddenSnapshot,
|
|
1624
|
-
expiresAt: currentTime +
|
|
1630
|
+
expiresAt: currentTime + cacheTtlMs
|
|
1625
1631
|
});
|
|
1626
1632
|
return hiddenSnapshot;
|
|
1627
1633
|
}
|
|
@@ -1799,7 +1805,7 @@ async function getChannelBalances(source, options = {}) {
|
|
|
1799
1805
|
const currentTime = options.now || Date.now();
|
|
1800
1806
|
const entries = await mapWithConcurrency(channels, 4, async (channel) => {
|
|
1801
1807
|
const snapshot = await getSnapshot(buildBalanceSnapshotKey(normalizedSource, channel), {
|
|
1802
|
-
ttlMs:
|
|
1808
|
+
ttlMs: getBalanceCacheTtlMs(channel),
|
|
1803
1809
|
fallbackValue: makeFallbackBalanceSnapshot(currentTime),
|
|
1804
1810
|
refresh: () => refreshChannelBalanceSnapshot(normalizedSource, channel, { ...options, force: true, now: Date.now() })
|
|
1805
1811
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 配置导出/导入服务
|
|
3
|
-
*
|
|
3
|
+
* 支持配置模板、频道配置及按平台 Manifest 声明的原生配置快照导出与导入
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
const fs = require('fs');
|
|
@@ -517,7 +517,7 @@ function buildExportReadme(exportData) {
|
|
|
517
517
|
- 插件 (Plugins)
|
|
518
518
|
- MCP 服务器配置
|
|
519
519
|
- OAuth 凭证管理池
|
|
520
|
-
-
|
|
520
|
+
- 各平台原生配置(由平台 Manifest 的 nativeConfigSnapshot 声明决定)
|
|
521
521
|
- UI 配置(主题、面板显示、排序等)
|
|
522
522
|
- Prompts 预设
|
|
523
523
|
- 安全配置
|