coding-tool-x 3.3.9 → 3.4.1

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 (33) hide show
  1. package/dist/web/assets/{Analytics-D6LzK9hk.js → Analytics-CbGxotgz.js} +4 -4
  2. package/dist/web/assets/Analytics-RNn1BUbG.css +1 -0
  3. package/dist/web/assets/{ConfigTemplates-BUDYuxRi.js → ConfigTemplates-oP6nrFEb.js} +1 -1
  4. package/dist/web/assets/{Home-D7KX7iF8.js → Home-DMntmEvh.js} +1 -1
  5. package/dist/web/assets/{PluginManager-DTgQ--vB.js → PluginManager-BUC_c7nH.js} +1 -1
  6. package/dist/web/assets/{ProjectList-DMCiGmCT.js → ProjectList-CW8J49n7.js} +1 -1
  7. package/dist/web/assets/{SessionList-CRBsdVRe.js → SessionList-7lYnF92v.js} +1 -1
  8. package/dist/web/assets/{SkillManager-DMwx2Q4k.js → SkillManager-Cs08216i.js} +1 -1
  9. package/dist/web/assets/{WorkspaceManager-DapB4ljL.js → WorkspaceManager-CY-oGtyB.js} +1 -1
  10. package/dist/web/assets/{index-D_5dRFOL.css → index-5qy5NMIP.css} +1 -1
  11. package/dist/web/assets/index-ClCqKpvX.js +2 -0
  12. package/dist/web/index.html +2 -2
  13. package/package.json +6 -2
  14. package/src/server/api/statistics.js +4 -4
  15. package/src/server/api/workspaces.js +1 -3
  16. package/src/server/codex-proxy-server.js +4 -92
  17. package/src/server/gemini-proxy-server.js +5 -28
  18. package/src/server/opencode-proxy-server.js +3 -93
  19. package/src/server/proxy-server.js +2 -57
  20. package/src/server/services/base/base-channel-service.js +247 -0
  21. package/src/server/services/base/proxy-utils.js +152 -0
  22. package/src/server/services/channel-health.js +30 -19
  23. package/src/server/services/channels.js +125 -293
  24. package/src/server/services/codex-channels.js +149 -517
  25. package/src/server/services/codex-env-manager.js +100 -67
  26. package/src/server/services/gemini-channels.js +2 -7
  27. package/src/server/services/oauth-credentials-service.js +12 -2
  28. package/src/server/services/opencode-channels.js +7 -9
  29. package/src/server/services/repo-scanner-base.js +1 -0
  30. package/src/server/services/statistics-service.js +5 -1
  31. package/src/server/services/workspace-service.js +100 -155
  32. package/dist/web/assets/Analytics-DuYvId7u.css +0 -1
  33. package/dist/web/assets/index-CL-qpoJ_.js +0 -2
@@ -77,6 +77,22 @@ function upsertManagedBlock(content, snippet) {
77
77
  return `${stripped.trimEnd()}\n\n${snippet}\n`;
78
78
  }
79
79
 
80
+ function buildExportCommand(envFilePath, homeDir) {
81
+ return `. "${buildHomeRelativeShellPath(envFilePath, homeDir)}"`;
82
+ }
83
+
84
+ function buildPosixEnvFileContent(nextValues) {
85
+ const keys = Object.keys(nextValues).sort();
86
+ if (!keys.length) {
87
+ return '';
88
+ }
89
+ return [
90
+ '# Managed by Coding-Tool',
91
+ ...keys.map((key) => `export ${key}=${shellQuote(nextValues[key])}`),
92
+ ''
93
+ ].join('\n');
94
+ }
95
+
80
96
  function readJsonFile(filePath, fallbackValue) {
81
97
  try {
82
98
  if (!fs.existsSync(filePath)) {
@@ -186,76 +202,60 @@ function syncPosixEnvironment(nextValues, previousState, options) {
186
202
  shellEnv,
187
203
  execSync
188
204
  } = options;
189
- const { preferred, candidates } = getPosixProfileCandidates(homeDir, shellEnv);
190
205
  const nextKeys = Object.keys(nextValues).sort();
191
- const previousProfiles = Array.isArray(previousState.profiles) ? previousState.profiles : [];
192
- const managedProfiles = new Set(previousProfiles);
193
- const sourceSnippet = buildSourceSnippet(envFilePath, homeDir);
194
206
  let changed = false;
207
+ const { preferred, candidates } = getPosixProfileCandidates(homeDir, shellEnv);
208
+ const sourceSnippet = buildSourceSnippet(envFilePath, homeDir);
209
+ const sourceCommand = buildExportCommand(envFilePath, homeDir);
195
210
 
196
- if (nextKeys.length > 0) {
197
- const envContent = [
198
- '# Managed by Coding-Tool for Codex env_key providers',
199
- ...nextKeys.map(key => `export ${key}=${shellQuote(nextValues[key])}`),
200
- ''
201
- ].join('\n');
202
- changed = writeTextFileIfChanged(envFilePath, envContent) || changed;
203
-
204
- const existingProfiles = candidates.filter(filePath => fs.existsSync(filePath));
205
- if (managedProfiles.size === 0) {
206
- if (existingProfiles.length > 0) {
207
- for (const profilePath of existingProfiles) {
208
- managedProfiles.add(profilePath);
209
- }
210
- } else {
211
- managedProfiles.add(preferred);
212
- }
213
- }
214
-
215
- for (const profilePath of managedProfiles) {
216
- const currentContent = fs.existsSync(profilePath) ? fs.readFileSync(profilePath, 'utf8') : '';
217
- const nextContent = upsertManagedBlock(currentContent, sourceSnippet);
218
- changed = writeTextFileIfChanged(profilePath, nextContent) || changed;
219
- }
220
- } else {
221
- if (fs.existsSync(envFilePath)) {
222
- fs.unlinkSync(envFilePath);
223
- changed = true;
224
- }
225
-
226
- const cleanupTargets = new Set([
227
- ...previousProfiles,
228
- ...candidates.filter(filePath => fs.existsSync(filePath))
229
- ]);
230
-
231
- for (const profilePath of cleanupTargets) {
232
- if (!fs.existsSync(profilePath)) continue;
233
- const currentContent = fs.readFileSync(profilePath, 'utf8');
234
- const nextContent = stripManagedBlock(currentContent);
235
- const finalContent = nextContent ? `${nextContent}\n` : '';
236
- changed = writeTextFileIfChanged(profilePath, finalContent) || changed;
237
- }
238
- managedProfiles.clear();
211
+ // 清理旧版本遗留的 shell profile 注入(迁移兼容)
212
+ const previousProfiles = Array.isArray(previousState.profiles) ? previousState.profiles : [];
213
+ const cleanupTargets = new Set([
214
+ ...previousProfiles,
215
+ ...candidates.filter((filePath) => fs.existsSync(filePath))
216
+ ]);
217
+ if (!nextKeys.length) {
218
+ cleanupTargets.add(preferred);
239
219
  }
240
-
241
- for (const key of Object.keys(previousState.values || {})) {
242
- if (!Object.prototype.hasOwnProperty.call(nextValues, key)) {
243
- delete process.env[key];
244
- }
220
+ for (const profilePath of cleanupTargets) {
221
+ if (!fs.existsSync(profilePath)) continue;
222
+ const currentContent = fs.readFileSync(profilePath, 'utf8');
223
+ if (!currentContent.includes(PROFILE_MARKER_START)) continue;
224
+ const nextContent = stripManagedBlock(currentContent);
225
+ const finalContent = nextContent ? `${nextContent}\n` : '';
226
+ changed = writeTextFileIfChanged(profilePath, finalContent) || changed;
245
227
  }
246
- for (const [key, value] of Object.entries(nextValues)) {
247
- process.env[key] = value;
228
+
229
+ if (nextKeys.length > 0) {
230
+ changed = writeTextFileIfChanged(envFilePath, buildPosixEnvFileContent(nextValues)) || changed;
231
+ const currentProfileContent = fs.existsSync(preferred) ? fs.readFileSync(preferred, 'utf8') : '';
232
+ const nextProfileContent = upsertManagedBlock(currentProfileContent, sourceSnippet);
233
+ changed = writeTextFileIfChanged(preferred, nextProfileContent) || changed;
234
+ } else if (fs.existsSync(envFilePath)) {
235
+ fs.unlinkSync(envFilePath);
236
+ changed = true;
248
237
  }
249
238
 
239
+ // macOS:用 launchctl 写入全局环境变量,新开终端/进程即生效
250
240
  if (runtime === 'darwin') {
251
241
  applyLaunchctlEnvironment(previousState.values || {}, nextValues, execSync);
242
+ const prevValues = previousState.values || {};
243
+ const keysChanged =
244
+ Object.keys(nextValues).length !== Object.keys(prevValues).length ||
245
+ Object.entries(nextValues).some(([k, v]) => prevValues[k] !== v);
246
+ changed = changed || keysChanged;
247
+ }
248
+
249
+ // Linux:写 ~/.config/environment.d/(桌面/systemd)+ ~/.profile(SSH/登录shell)
250
+ if (runtime === 'linux') {
251
+ changed = applyLinuxEnvironment(nextValues, homeDir) || changed;
252
252
  }
253
253
 
254
254
  if (nextKeys.length > 0) {
255
255
  writeJsonFile(stateFilePath, {
256
256
  version: 1,
257
257
  values: nextValues,
258
- profiles: Array.from(managedProfiles)
258
+ profiles: [preferred]
259
259
  });
260
260
  } else if (fs.existsSync(stateFilePath)) {
261
261
  fs.unlinkSync(stateFilePath);
@@ -265,14 +265,54 @@ function syncPosixEnvironment(nextValues, previousState, options) {
265
265
  changed,
266
266
  reloadRequired: changed,
267
267
  isFirstTime: Object.keys(previousState.values || {}).length === 0 && nextKeys.length > 0,
268
- sourceCommand: nextKeys.length > 0 ? `source "${buildHomeRelativeShellPath(envFilePath, homeDir)}"` : null,
269
- shellConfigPath: managedProfiles.size > 0 ? Array.from(managedProfiles)[0] : null,
270
- shellConfigPaths: Array.from(managedProfiles),
268
+ sourceCommand: nextKeys.length > 0 ? sourceCommand : null,
269
+ shellConfigPath: nextKeys.length > 0 ? preferred : null,
270
+ shellConfigPaths: nextKeys.length > 0 ? [preferred] : [],
271
271
  envFilePath: nextKeys.length > 0 ? envFilePath : null,
272
272
  managedKeys: nextKeys
273
273
  };
274
274
  }
275
275
 
276
+ function applyLinuxEnvironment(nextValues, homeDir) {
277
+ let changed = false;
278
+
279
+ // 1. ~/.config/environment.d/codex-env.conf(systemd 用户环境,桌面终端生效)
280
+ const envdDir = path.join(homeDir, '.config', 'environment.d');
281
+ const envdFile = path.join(envdDir, 'codex-env.conf');
282
+ const nextKeys = Object.keys(nextValues).sort();
283
+
284
+ if (nextKeys.length > 0) {
285
+ const envdContent = [
286
+ '# Managed by Coding-Tool',
287
+ ...nextKeys.map(key => `${key}=${nextValues[key]}`),
288
+ ''
289
+ ].join('\n');
290
+ changed = writeTextFileIfChanged(envdFile, envdContent) || changed;
291
+ } else if (fs.existsSync(envdFile)) {
292
+ fs.unlinkSync(envdFile);
293
+ changed = true;
294
+ }
295
+
296
+ // 2. ~/.profile(登录 shell,SSH 和新终端均生效)
297
+ const profilePath = path.join(homeDir, '.profile');
298
+ if (nextKeys.length > 0) {
299
+ const exportLines = nextKeys.map(key => `export ${key}=${shellQuote(nextValues[key])}`).join('\n');
300
+ const snippet = [PROFILE_MARKER_START, exportLines, PROFILE_MARKER_END].join('\n');
301
+ const currentContent = fs.existsSync(profilePath) ? fs.readFileSync(profilePath, 'utf8') : '';
302
+ const nextContent = upsertManagedBlock(currentContent, snippet);
303
+ changed = writeTextFileIfChanged(profilePath, nextContent) || changed;
304
+ } else if (fs.existsSync(profilePath)) {
305
+ const currentContent = fs.readFileSync(profilePath, 'utf8');
306
+ if (currentContent.includes(PROFILE_MARKER_START)) {
307
+ const nextContent = stripManagedBlock(currentContent);
308
+ const finalContent = nextContent ? `${nextContent}\n` : '';
309
+ changed = writeTextFileIfChanged(profilePath, finalContent) || changed;
310
+ }
311
+ }
312
+
313
+ return changed;
314
+ }
315
+
276
316
  function applyLaunchctlEnvironment(previousValues, nextValues, execSync) {
277
317
  const previousKeys = new Set(Object.keys(previousValues || {}));
278
318
  for (const [key, value] of Object.entries(nextValues || {})) {
@@ -306,21 +346,14 @@ function syncWindowsEnvironment(nextValues, previousState, options) {
306
346
  let changed = false;
307
347
 
308
348
  for (const [key, value] of Object.entries(nextValues)) {
309
- if (previousValues[key] === value) {
310
- process.env[key] = value;
311
- continue;
312
- }
349
+ if (previousValues[key] === value) continue;
313
350
  setWindowsUserEnv(key, value, execSync);
314
- process.env[key] = value;
315
351
  changed = true;
316
352
  }
317
353
 
318
354
  for (const key of Object.keys(previousValues)) {
319
- if (Object.prototype.hasOwnProperty.call(nextValues, key)) {
320
- continue;
321
- }
355
+ if (Object.prototype.hasOwnProperty.call(nextValues, key)) continue;
322
356
  removeWindowsUserEnv(key, execSync);
323
- delete process.env[key];
324
357
  changed = true;
325
358
  }
326
359
 
@@ -3,6 +3,7 @@ const path = require('path');
3
3
  const crypto = require('crypto');
4
4
  const { PATHS, NATIVE_PATHS } = require('../../config/paths');
5
5
  const { clearNativeOAuth } = require('./native-oauth-adapters');
6
+ const { normalizeGatewaySourceType } = require('./base/proxy-utils');
6
7
 
7
8
  /**
8
9
  * Gemini 渠道管理服务(多渠道架构)
@@ -17,13 +18,7 @@ const { clearNativeOAuth } = require('./native-oauth-adapters');
17
18
  * - 使用 weight 和 maxConcurrency 控制负载均衡
18
19
  */
19
20
 
20
- function normalizeGatewaySourceType(value, fallback = 'gemini') {
21
- const normalized = String(value || '').trim().toLowerCase();
22
- if (normalized === 'claude') return 'claude';
23
- if (normalized === 'codex') return 'codex';
24
- if (normalized === 'gemini') return 'gemini';
25
- return fallback;
26
- }
21
+ // normalizeGatewaySourceType imported from base/proxy-utils
27
22
 
28
23
  // 获取 Gemini 配置目录
29
24
  function getGeminiDir() {
@@ -380,12 +380,21 @@ function stableFingerprintValue(tool, metadata) {
380
380
  return stableId;
381
381
  }
382
382
 
383
+ function resolveFingerprintValue(tool, metadata, options = {}) {
384
+ if (options.fingerprintMode === 'primary-token') {
385
+ return metadata.primaryToken
386
+ || metadata.accessToken
387
+ || stableFingerprintValue(tool, metadata);
388
+ }
389
+ return stableFingerprintValue(tool, metadata);
390
+ }
391
+
383
392
  function upsertCredential(tool, metadata, options = {}) {
384
393
  const store = readStore();
385
394
  const toolStore = getToolStore(store, tool);
386
395
  const now = Date.now();
387
396
  const primaryToken = metadata.primaryToken || metadata.accessToken || '';
388
- const fingerprint = fingerprintFor(tool, stableFingerprintValue(tool, metadata));
397
+ const fingerprint = fingerprintFor(tool, resolveFingerprintValue(tool, metadata, options));
389
398
  const existingIndex = toolStore.credentials.findIndex((item) => item.fingerprint === fingerprint);
390
399
  const existing = existingIndex >= 0 ? toolStore.credentials[existingIndex] : null;
391
400
 
@@ -456,7 +465,8 @@ function syncLocalCredential(tool) {
456
465
  }
457
466
 
458
467
  const credentials = nativeCredentials.map((metadata) => upsertCredential(tool, metadata, {
459
- source: 'synced-local'
468
+ source: 'synced-local',
469
+ fingerprintMode: 'primary-token'
460
470
  }));
461
471
 
462
472
  return {
@@ -4,18 +4,15 @@ const crypto = require('crypto');
4
4
  const { PATHS } = require('../../config/paths');
5
5
  const { clearNativeOAuth } = require('./native-oauth-adapters');
6
6
  const { setChannelConfig } = require('./opencode-settings-manager');
7
+ const { normalizeGatewaySourceType } = require('./base/proxy-utils');
7
8
 
8
9
  /**
9
10
  * OpenCode 渠道管理服务
10
11
  * 存储位置: ~/.cc-tool/opencode-channels.json
11
12
  */
12
13
 
13
- function normalizeGatewaySourceType(value) {
14
- const normalized = String(value || '').trim().toLowerCase();
15
- if (normalized === 'claude') return 'claude';
16
- if (normalized === 'gemini') return 'gemini';
17
- return 'codex';
18
- }
14
+ // normalizeGatewaySourceType imported from base/proxy-utils
15
+ // OpenCode default fallback is 'codex'
19
16
 
20
17
  function normalizeApiKey(value) {
21
18
  if (typeof value !== 'string') return '';
@@ -80,7 +77,7 @@ function loadChannels() {
80
77
  modelRedirects: ch.modelRedirects || [],
81
78
  speedTestModel: ch.speedTestModel || null,
82
79
  wireApi: ch.wireApi || 'openai', // OpenCode 默认使用 OpenAI 兼容格式
83
- gatewaySourceType: normalizeGatewaySourceType(ch.gatewaySourceType),
80
+ gatewaySourceType: normalizeGatewaySourceType(ch.gatewaySourceType, 'codex'),
84
81
  allowedModels: ch.allowedModels || []
85
82
  };
86
83
  normalized.providerKey = deriveProviderKey(normalized);
@@ -132,7 +129,7 @@ function createChannel(name, baseUrl, apiKey, extraConfig = {}) {
132
129
  modelRedirects: extraConfig.modelRedirects || [],
133
130
  speedTestModel: extraConfig.speedTestModel || null,
134
131
  model: extraConfig.model || null,
135
- gatewaySourceType: normalizeGatewaySourceType(extraConfig.gatewaySourceType),
132
+ gatewaySourceType: normalizeGatewaySourceType(extraConfig.gatewaySourceType, 'codex'),
136
133
  providerKey: extraConfig.providerKey || null,
137
134
  presetId: extraConfig.presetId || null,
138
135
  websiteUrl: extraConfig.websiteUrl || '',
@@ -169,7 +166,8 @@ function updateChannel(channelId, updates) {
169
166
  gatewaySourceType: normalizeGatewaySourceType(
170
167
  updates.gatewaySourceType !== undefined
171
168
  ? updates.gatewaySourceType
172
- : oldChannel.gatewaySourceType
169
+ : oldChannel.gatewaySourceType,
170
+ 'codex'
173
171
  ),
174
172
  updatedAt: Date.now()
175
173
  };
@@ -7,6 +7,7 @@
7
7
 
8
8
  const fs = require('fs');
9
9
  const path = require('path');
10
+ const os = require('os');
10
11
  const https = require('https');
11
12
  const http = require('http');
12
13
  const { createWriteStream } = require('fs');
@@ -920,9 +920,13 @@ async function getTrendStatistics({ startDate, endDate, granularity = 'day', ste
920
920
 
921
921
  if (granularity === 'day') {
922
922
  labels.push(dateStr);
923
- const byDimension = activeFilters
923
+ let byDimension = activeFilters
924
924
  ? readJsonlForDay(year, month, day, groupBy, activeFilters)
925
925
  : mergeAllToolsDailyStats(dateStr, groupBy);
926
+ if (!activeFilters && Object.keys(byDimension).length === 0) {
927
+ // Fallback: if daily stats are missing, derive from JSONL logs
928
+ byDimension = readJsonlForDay(year, month, day, groupBy);
929
+ }
926
930
 
927
931
  // Accumulate dimensions seen so far with 0 for this label position
928
932
  const labelIdx = labels.length - 1;