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.
- package/dist/web/assets/{Analytics-D6LzK9hk.js → Analytics-CbGxotgz.js} +4 -4
- package/dist/web/assets/Analytics-RNn1BUbG.css +1 -0
- package/dist/web/assets/{ConfigTemplates-BUDYuxRi.js → ConfigTemplates-oP6nrFEb.js} +1 -1
- package/dist/web/assets/{Home-D7KX7iF8.js → Home-DMntmEvh.js} +1 -1
- package/dist/web/assets/{PluginManager-DTgQ--vB.js → PluginManager-BUC_c7nH.js} +1 -1
- package/dist/web/assets/{ProjectList-DMCiGmCT.js → ProjectList-CW8J49n7.js} +1 -1
- package/dist/web/assets/{SessionList-CRBsdVRe.js → SessionList-7lYnF92v.js} +1 -1
- package/dist/web/assets/{SkillManager-DMwx2Q4k.js → SkillManager-Cs08216i.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-DapB4ljL.js → WorkspaceManager-CY-oGtyB.js} +1 -1
- package/dist/web/assets/{index-D_5dRFOL.css → index-5qy5NMIP.css} +1 -1
- package/dist/web/assets/index-ClCqKpvX.js +2 -0
- package/dist/web/index.html +2 -2
- package/package.json +6 -2
- package/src/server/api/statistics.js +4 -4
- package/src/server/api/workspaces.js +1 -3
- package/src/server/codex-proxy-server.js +4 -92
- package/src/server/gemini-proxy-server.js +5 -28
- package/src/server/opencode-proxy-server.js +3 -93
- package/src/server/proxy-server.js +2 -57
- package/src/server/services/base/base-channel-service.js +247 -0
- package/src/server/services/base/proxy-utils.js +152 -0
- package/src/server/services/channel-health.js +30 -19
- package/src/server/services/channels.js +125 -293
- package/src/server/services/codex-channels.js +149 -517
- package/src/server/services/codex-env-manager.js +100 -67
- package/src/server/services/gemini-channels.js +2 -7
- package/src/server/services/oauth-credentials-service.js +12 -2
- package/src/server/services/opencode-channels.js +7 -9
- package/src/server/services/repo-scanner-base.js +1 -0
- package/src/server/services/statistics-service.js +5 -1
- package/src/server/services/workspace-service.js +100 -155
- package/dist/web/assets/Analytics-DuYvId7u.css +0 -1
- 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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
247
|
-
|
|
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:
|
|
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 ?
|
|
269
|
-
shellConfigPath:
|
|
270
|
-
shellConfigPaths:
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
14
|
-
|
|
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
|
};
|
|
@@ -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
|
-
|
|
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;
|