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.
Files changed (134) hide show
  1. package/README.md +56 -388
  2. package/dist/web/assets/AgentsDrawer-Bam7kBjG.js +1 -0
  3. package/dist/web/assets/Analytics-D5dUWXEa.js +1 -0
  4. package/dist/web/assets/AssetPathField-CLqalYv-.js +1 -0
  5. package/dist/web/assets/{ChatHistoryDrawer-CrfS0KYV.js → ChatHistoryDrawer-C1At65O3.js} +1 -1
  6. package/dist/web/assets/{CollapseItem-ClMsJuUu.js → CollapseItem-CVWojShK.js} +1 -1
  7. package/dist/web/assets/CommandsDrawer-BEoypGBf.js +1 -0
  8. package/dist/web/assets/ConfigExportDrawer-DBvFDDWK.js +1 -0
  9. package/dist/web/assets/ConfigTemplates-BpaWG-iC.js +1 -0
  10. package/dist/web/assets/{ConfigTemplatesDrawer-BqM32Im7.js → ConfigTemplatesDrawer-C2lvyKMf.js} +1 -1
  11. package/dist/web/assets/ConfigTemplatesPanel-CorOY7kY.js +1 -0
  12. package/dist/web/assets/{Divider-DsdbkHrG.js → Divider-BLLhMTSt.js} +1 -1
  13. package/dist/web/assets/FavoritesDrawer-uW88N6te.js +1 -0
  14. package/dist/web/assets/GatewayConvertDrawer-i5Wt9Dd9.js +1 -0
  15. package/dist/web/assets/Home-BIxWvm29.js +1 -0
  16. package/dist/web/assets/{MarkdownEditor-BMMUufSc.js → MarkdownEditor-Ul0bSGj-.js} +1 -1
  17. package/dist/web/assets/McpDrawer-Cc2xq77I.js +1 -0
  18. package/dist/web/assets/McpFormDrawer-DLHX_nrI.js +1 -0
  19. package/dist/web/assets/{PluginManager-C4kJlpVX.js → PluginManager-BGQ0xF2F.js} +1 -1
  20. package/dist/web/assets/{PluginsDrawer-05R4k15x.js → PluginsDrawer-STwHEqiF.js} +1 -1
  21. package/dist/web/assets/PluginsPanel-1zuNP7da.js +1 -0
  22. package/dist/web/assets/ProjectList-VbIYSHbH.css +1 -0
  23. package/dist/web/assets/ProjectList-_pe2nk9c.js +1 -0
  24. package/dist/web/assets/PromptsDrawer-CJN1xZk7.js +1 -0
  25. package/dist/web/assets/RecentSessionsDrawer-DX1HI8Nm.js +1 -0
  26. package/dist/web/assets/SessionCard-Op6PHvi0.js +1 -0
  27. package/dist/web/assets/SessionList-Da_TAEYr.js +1 -0
  28. package/dist/web/assets/SettingsDrawer-DKLd4Sjy.js +1 -0
  29. package/dist/web/assets/{SkillManager-D9jkNuRM.js → SkillManager-PcAa5EbF.js} +1 -1
  30. package/dist/web/assets/{SkillsDrawer-0Z4IefxC.js → SkillsDrawer-DPdU4p6C.js} +1 -1
  31. package/dist/web/assets/SkillsPanel-B9qzRqjd.js +1 -0
  32. package/dist/web/assets/Upload-e1M9Yp5z.js +1 -0
  33. package/dist/web/assets/{WorkspaceDrawer-eujtWpoq.js → WorkspaceDrawer-oS6mB1VZ.js} +1 -1
  34. package/dist/web/assets/{WorkspaceManager-CQ-8Ej68.js → WorkspaceManager-6VvRWe3r.js} +1 -1
  35. package/dist/web/assets/WorkspacePanel-CYq0Z8iY.js +1 -0
  36. package/dist/web/assets/client-BgcNg4Bt.js +1 -0
  37. package/dist/web/assets/headers-DbowkmZJ.js +1 -0
  38. package/dist/web/assets/{icons-DmFe4b5j.js → icons-B-AzZoeE.js} +1 -1
  39. package/dist/web/assets/index-BlUXw84U.js +2 -0
  40. package/dist/web/assets/index-Bn8BUnwP.css +1 -0
  41. package/dist/web/assets/{light-0eavxNvt.js → light-C2X7hegj.js} +1 -1
  42. package/dist/web/assets/platforms-DrzAtnjY.js +1 -0
  43. package/dist/web/assets/platforms-mqqe_82d.js +1 -0
  44. package/dist/web/assets/project-config-BVt5s-pH.js +1 -0
  45. package/dist/web/assets/sessions-53-Fr68H.js +1 -0
  46. package/dist/web/assets/{sessions-D4yQtfKb.js → sessions-CaWUaysM.js} +1 -1
  47. package/dist/web/assets/use-message-DBsCm7FJ.js +1 -0
  48. package/dist/web/assets/{useResponsiveDrawer-DZd5rNk5.js → useResponsiveDrawer-ChHZrTfl.js} +1 -1
  49. package/dist/web/assets/{workspaces-mjyPVGS8.js → workspaces-DQuZd4qw.js} +1 -1
  50. package/dist/web/index.html +8 -8
  51. package/package.json +4 -2
  52. package/src/commands/daemon.js +27 -4
  53. package/src/commands/export-config.js +17 -211
  54. package/src/config/model-metadata.json +180 -16
  55. package/src/platforms/channel-auth-registry.js +36 -6
  56. package/src/platforms/channel-auth-service.js +13 -3
  57. package/src/platforms/commands-service.js +28 -0
  58. package/src/platforms/driver-registry.js +1 -0
  59. package/src/platforms/drivers/claude/channels-implementation.js +79 -90
  60. package/src/platforms/drivers/claude/native-config-implementation.js +3 -4
  61. package/src/platforms/drivers/codex/channels-implementation.js +34 -49
  62. package/src/platforms/drivers/codex/native-config-implementation.js +3 -51
  63. package/src/platforms/drivers/dsh/api-operations.js +1 -1
  64. package/src/platforms/drivers/dsh/channels-implementation.js +28 -8
  65. package/src/platforms/drivers/dsh/channels.js +0 -18
  66. package/src/platforms/drivers/dsh/common.js +7 -0
  67. package/src/platforms/drivers/dsh/native-config.js +7 -2
  68. package/src/platforms/drivers/gemini/channels-implementation.js +47 -55
  69. package/src/platforms/drivers/gemini/native-config-implementation.js +8 -20
  70. package/src/platforms/drivers/generic-native-config.js +20 -0
  71. package/src/platforms/drivers/native-config-snapshot.js +30 -4
  72. package/src/platforms/drivers/omp/channels-implementation.js +51 -17
  73. package/src/platforms/drivers/omp/native-config-implementation.js +10 -17
  74. package/src/platforms/drivers/omp/prompts.js +58 -2
  75. package/src/platforms/drivers/omp/proxy-implementation.js +25 -1
  76. package/src/platforms/drivers/opencode/channels-implementation.js +31 -22
  77. package/src/platforms/drivers/opencode/channels.js +6 -1
  78. package/src/platforms/drivers/opencode/native-config-implementation.js +3 -67
  79. package/src/platforms/manifest-schema.js +35 -2
  80. package/src/platforms/manifests/claude.json +0 -8
  81. package/src/platforms/manifests/codex.json +0 -8
  82. package/src/platforms/manifests/dsh.json +6 -9
  83. package/src/platforms/manifests/gemini.json +0 -8
  84. package/src/platforms/manifests/omp.json +2 -9
  85. package/src/platforms/manifests/opencode.json +28 -8
  86. package/src/platforms/native-oauth-adapters.js +351 -51
  87. package/src/platforms/oauth-credentials-service.js +344 -13
  88. package/src/server/index.js +37 -1
  89. package/src/server/services/bounded-content.js +60 -0
  90. package/src/server/services/channel-balance.js +10 -4
  91. package/src/server/services/config-export-service.js +2 -2
  92. package/src/server/services/config-templates-service.js +5 -1
  93. package/src/server/services/plugins-service.js +162 -85
  94. package/src/server/services/session-history-maintenance-worker.js +71 -0
  95. package/src/server/services/session-history-maintenance.js +166 -0
  96. package/src/server/services/skill-service.js +23 -3
  97. package/src/server/services/speed-test.js +0 -72
  98. package/src/shared/channel-driver.js +16 -0
  99. package/src/shared/driver-factories/api.js +1 -0
  100. package/src/utils/native-config-patcher.js +474 -0
  101. package/dist/web/assets/AgentsDrawer-DRQuwi0m.js +0 -1
  102. package/dist/web/assets/Analytics-Cf5KwEdj.js +0 -1
  103. package/dist/web/assets/AssetPathField-DyniM0pF.js +0 -1
  104. package/dist/web/assets/CommandsDrawer-CzHelf56.js +0 -1
  105. package/dist/web/assets/ConfigExportDrawer-BEZ8wTby.js +0 -1
  106. package/dist/web/assets/ConfigTemplates-Crg3xWe5.js +0 -1
  107. package/dist/web/assets/ConfigTemplatesPanel-lTdutC8H.js +0 -1
  108. package/dist/web/assets/FavoritesDrawer-DDyWUBJR.js +0 -1
  109. package/dist/web/assets/GatewayConvertDrawer-Rigtt_5J.js +0 -1
  110. package/dist/web/assets/Home-tCSKmSHV.js +0 -1
  111. package/dist/web/assets/McpDrawer-CmE-d7VW.js +0 -1
  112. package/dist/web/assets/McpFormDrawer-ByvS18od.js +0 -1
  113. package/dist/web/assets/PluginsPanel-BCoEs7Og.js +0 -1
  114. package/dist/web/assets/ProjectList-BJt1OIaI.js +0 -1
  115. package/dist/web/assets/ProjectList-DG3yu8xZ.css +0 -1
  116. package/dist/web/assets/PromptsDrawer-DnwBf_oW.js +0 -1
  117. package/dist/web/assets/RecentSessionsDrawer-B7d-38wH.js +0 -1
  118. package/dist/web/assets/SessionCard-BUgjHQwX.js +0 -1
  119. package/dist/web/assets/SessionList-B-bXZRFk.js +0 -1
  120. package/dist/web/assets/SettingsDrawer-BITzjQLl.js +0 -1
  121. package/dist/web/assets/SkillsPanel-BKIVOj7n.js +0 -1
  122. package/dist/web/assets/SpeedTestDrawer-Bx1bYf5i.js +0 -1
  123. package/dist/web/assets/SpeedTestDrawer-Dc0WToJ_.css +0 -1
  124. package/dist/web/assets/Upload-CzC4rGI4.js +0 -1
  125. package/dist/web/assets/WorkspacePanel-Cql5qE2R.js +0 -1
  126. package/dist/web/assets/client-UDwXBP9i.js +0 -1
  127. package/dist/web/assets/headers-BqDmkMjy.js +0 -1
  128. package/dist/web/assets/index-Cmmwj3Fx.js +0 -2
  129. package/dist/web/assets/index-Cnbe696C.css +0 -1
  130. package/dist/web/assets/platforms-BmzzwnSw.js +0 -1
  131. package/dist/web/assets/platforms-DWXgmy2w.js +0 -1
  132. package/dist/web/assets/project-config-B2baPzYL.js +0 -1
  133. package/dist/web/assets/sessions-DqlbocLJ.js +0 -1
  134. 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
- const existingIndex = toolStore.credentials.findIndex((item) => item.fingerprint === fingerprint);
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 (!toolStore.defaultCredentialId) {
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) => upsertCredential(tool, metadata, {
294
- source: 'synced-local',
295
- fingerprintMode: 'primary-token'
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
- const entry = findStoredCredential(tool, credentialId);
590
- const secrets = entry.secrets || {};
591
- const accessToken = secrets.accessToken || secrets.primaryToken || '';
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
  };
@@ -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: { autoRestoreProxies, performStartupHealthCheck, traceStartupMemory }
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 + CACHE_TTL_MS
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 + CACHE_TTL_MS
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 + CACHE_TTL_MS
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: CACHE_TTL_MS,
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
- - 各平台原生配置(Claude / Codex / Gemini / OpenCode / OMP)
520
+ - 各平台原生配置(由平台 Manifest 的 nativeConfigSnapshot 声明决定)
521
521
  - UI 配置(主题、面板显示、排序等)
522
522
  - Prompts 预设
523
523
  - 安全配置