coding-tool-x 3.3.8 → 3.4.0

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 (79) hide show
  1. package/CHANGELOG.md +17 -2
  2. package/README.md +253 -326
  3. package/dist/web/assets/{Analytics-DLpoDZ2M.js → Analytics-DEjfL5Jx.js} +4 -4
  4. package/dist/web/assets/Analytics-RNn1BUbG.css +1 -0
  5. package/dist/web/assets/{ConfigTemplates-D_hRb55W.js → ConfigTemplates-DkRL_-tf.js} +1 -1
  6. package/dist/web/assets/Home-BQxQ1LhR.css +1 -0
  7. package/dist/web/assets/Home-CF-L640I.js +1 -0
  8. package/dist/web/assets/{PluginManager-JXsyym1s.js → PluginManager-BzNYTdNB.js} +1 -1
  9. package/dist/web/assets/{ProjectList-DZWSeb-q.js → ProjectList-C0-JgHMM.js} +1 -1
  10. package/dist/web/assets/{SessionList-Cs624DR3.js → SessionList-CkZUdX5N.js} +1 -1
  11. package/dist/web/assets/{SkillManager-bEliz7qz.js → SkillManager-Cak0-4d4.js} +1 -1
  12. package/dist/web/assets/{WorkspaceManager-J3RecFGn.js → WorkspaceManager-CGDJzwEr.js} +1 -1
  13. package/dist/web/assets/{icons-Cuc23WS7.js → icons-B5Pl4lrD.js} +1 -1
  14. package/dist/web/assets/index-D_WItvHE.js +2 -0
  15. package/dist/web/assets/index-Dz7v9OM0.css +1 -0
  16. package/dist/web/assets/{markdown-C9MYpaSi.js → markdown-DyTJGI4N.js} +1 -1
  17. package/dist/web/assets/{naive-ui-CxpuzdjU.js → naive-ui-Bdxp09n2.js} +1 -1
  18. package/dist/web/assets/{vendors-DMjSfzlv.js → vendors-CKPV1OAU.js} +2 -2
  19. package/dist/web/assets/{vue-vendor-DET08QYg.js → vue-vendor-3bf-fPGP.js} +1 -1
  20. package/dist/web/index.html +7 -7
  21. package/docs/home.png +0 -0
  22. package/package.json +13 -5
  23. package/src/commands/daemon.js +3 -2
  24. package/src/commands/security.js +1 -2
  25. package/src/config/paths.js +638 -93
  26. package/src/server/api/agents.js +1 -1
  27. package/src/server/api/claude-hooks.js +13 -8
  28. package/src/server/api/codex-proxy.js +5 -4
  29. package/src/server/api/hooks.js +45 -0
  30. package/src/server/api/plugins.js +0 -1
  31. package/src/server/api/statistics.js +4 -4
  32. package/src/server/api/ui-config.js +5 -0
  33. package/src/server/api/workspaces.js +1 -3
  34. package/src/server/codex-proxy-server.js +89 -59
  35. package/src/server/gemini-proxy-server.js +107 -88
  36. package/src/server/index.js +1 -0
  37. package/src/server/opencode-proxy-server.js +381 -225
  38. package/src/server/proxy-server.js +86 -60
  39. package/src/server/services/alias.js +3 -3
  40. package/src/server/services/channels.js +3 -2
  41. package/src/server/services/codex-channels.js +38 -87
  42. package/src/server/services/codex-env-manager.js +426 -0
  43. package/src/server/services/codex-settings-manager.js +15 -15
  44. package/src/server/services/codex-statistics-service.js +3 -27
  45. package/src/server/services/config-export-service.js +20 -7
  46. package/src/server/services/config-registry-service.js +3 -2
  47. package/src/server/services/config-sync-manager.js +1 -1
  48. package/src/server/services/favorites.js +4 -3
  49. package/src/server/services/gemini-channels.js +3 -3
  50. package/src/server/services/gemini-statistics-service.js +3 -25
  51. package/src/server/services/mcp-service.js +2 -3
  52. package/src/server/services/model-detector.js +4 -3
  53. package/src/server/services/native-oauth-adapters.js +2 -1
  54. package/src/server/services/network-access.js +39 -1
  55. package/src/server/services/notification-hooks.js +951 -0
  56. package/src/server/services/opencode-channels.js +6 -6
  57. package/src/server/services/opencode-sessions.js +2 -2
  58. package/src/server/services/opencode-statistics-service.js +3 -27
  59. package/src/server/services/plugins-service.js +110 -31
  60. package/src/server/services/prompts-service.js +2 -3
  61. package/src/server/services/proxy-log-helper.js +242 -0
  62. package/src/server/services/proxy-runtime.js +6 -4
  63. package/src/server/services/repo-scanner-base.js +12 -4
  64. package/src/server/services/request-logger.js +7 -7
  65. package/src/server/services/security-config.js +4 -4
  66. package/src/server/services/session-cache.js +2 -2
  67. package/src/server/services/sessions.js +2 -2
  68. package/src/server/services/skill-service.js +174 -55
  69. package/src/server/services/statistics-service.js +10 -6
  70. package/src/server/services/ui-config.js +4 -3
  71. package/src/server/services/workspace-service.js +101 -156
  72. package/src/server/websocket-server.js +5 -4
  73. package/dist/web/assets/Analytics-DuYvId7u.css +0 -1
  74. package/dist/web/assets/Home-BMoFdAwy.css +0 -1
  75. package/dist/web/assets/Home-DNwp-0J-.js +0 -1
  76. package/dist/web/assets/index-BXeSvAwU.js +0 -2
  77. package/dist/web/assets/index-DWAC3Tdv.css +0 -1
  78. package/docs/bannel.png +0 -0
  79. package/docs/model-redirection.md +0 -251
@@ -0,0 +1,426 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { execFileSync } = require('child_process');
4
+ const { PATHS, HOME_DIR } = require('../../config/paths');
5
+
6
+ const PROFILE_MARKER_START = '# >>> coding-tool codex env >>>';
7
+ const PROFILE_MARKER_END = '# <<< coding-tool codex env <<<';
8
+
9
+ function defaultEnvFilePath(configDir) {
10
+ return path.join(configDir, 'codex-env.sh');
11
+ }
12
+
13
+ function defaultStateFilePath(configDir) {
14
+ return path.join(configDir, 'codex-env-state.json');
15
+ }
16
+
17
+ function ensureDir(dirPath) {
18
+ if (!fs.existsSync(dirPath)) {
19
+ fs.mkdirSync(dirPath, { recursive: true });
20
+ }
21
+ }
22
+
23
+ function hasValue(value) {
24
+ return String(value || '').trim() !== '';
25
+ }
26
+
27
+ function shellQuote(value) {
28
+ return `'${String(value).replace(/'/g, `'\"'\"'`)}'`;
29
+ }
30
+
31
+ function powershellQuote(value) {
32
+ return `'${String(value).replace(/'/g, "''")}'`;
33
+ }
34
+
35
+ function buildHomeRelativeShellPath(filePath, homeDir) {
36
+ const normalizedHome = path.resolve(homeDir);
37
+ const normalizedFilePath = path.resolve(filePath);
38
+ if (normalizedFilePath === normalizedHome) {
39
+ return '$HOME';
40
+ }
41
+ if (normalizedFilePath.startsWith(`${normalizedHome}${path.sep}`)) {
42
+ const relativePath = normalizedFilePath.slice(normalizedHome.length).replace(/\\/g, '/');
43
+ return `$HOME${relativePath}`;
44
+ }
45
+ return normalizedFilePath.replace(/\\/g, '/');
46
+ }
47
+
48
+ function buildSourceSnippet(envFilePath, homeDir) {
49
+ const shellPath = buildHomeRelativeShellPath(envFilePath, homeDir);
50
+ return [
51
+ PROFILE_MARKER_START,
52
+ `[ -f "${shellPath}" ] && . "${shellPath}"`,
53
+ PROFILE_MARKER_END
54
+ ].join('\n');
55
+ }
56
+
57
+ function stripManagedBlock(content = '') {
58
+ if (!content.includes(PROFILE_MARKER_START)) {
59
+ return content;
60
+ }
61
+ const pattern = new RegExp(
62
+ `\\n?${escapeRegex(PROFILE_MARKER_START)}[\\s\\S]*?${escapeRegex(PROFILE_MARKER_END)}\\n?`,
63
+ 'g'
64
+ );
65
+ return content.replace(pattern, '\n').replace(/\n{3,}/g, '\n\n').trimEnd();
66
+ }
67
+
68
+ function escapeRegex(value) {
69
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
70
+ }
71
+
72
+ function upsertManagedBlock(content, snippet) {
73
+ const stripped = stripManagedBlock(content);
74
+ if (!stripped.trim()) {
75
+ return `${snippet}\n`;
76
+ }
77
+ return `${stripped.trimEnd()}\n\n${snippet}\n`;
78
+ }
79
+
80
+ function readJsonFile(filePath, fallbackValue) {
81
+ try {
82
+ if (!fs.existsSync(filePath)) {
83
+ return fallbackValue;
84
+ }
85
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
86
+ } catch {
87
+ return fallbackValue;
88
+ }
89
+ }
90
+
91
+ function writeJsonFile(filePath, payload) {
92
+ ensureDir(path.dirname(filePath));
93
+ fs.writeFileSync(filePath, JSON.stringify(payload, null, 2), 'utf8');
94
+ }
95
+
96
+ function readState(stateFilePath) {
97
+ const state = readJsonFile(stateFilePath, {
98
+ version: 1,
99
+ values: {},
100
+ profiles: []
101
+ });
102
+ return {
103
+ version: 1,
104
+ values: state && typeof state.values === 'object' ? state.values : {},
105
+ profiles: Array.isArray(state?.profiles) ? state.profiles : []
106
+ };
107
+ }
108
+
109
+ function normalizeEnvMap(envMap = {}) {
110
+ const normalized = {};
111
+ for (const [key, value] of Object.entries(envMap || {})) {
112
+ if (!key || !hasValue(value)) continue;
113
+ normalized[String(key).trim()] = String(value);
114
+ }
115
+ return normalized;
116
+ }
117
+
118
+ function buildNextEnvValues(previousValues, envMap, { replace = true, removeKeys = [] } = {}) {
119
+ const nextValues = replace
120
+ ? {}
121
+ : { ...previousValues };
122
+
123
+ if (replace) {
124
+ Object.assign(nextValues, normalizeEnvMap(envMap));
125
+ } else {
126
+ const normalizedIncoming = normalizeEnvMap(envMap);
127
+ for (const [key, value] of Object.entries(normalizedIncoming)) {
128
+ nextValues[key] = value;
129
+ }
130
+ }
131
+
132
+ for (const key of removeKeys || []) {
133
+ delete nextValues[key];
134
+ }
135
+
136
+ for (const [key, value] of Object.entries(nextValues)) {
137
+ if (!hasValue(value)) {
138
+ delete nextValues[key];
139
+ }
140
+ }
141
+
142
+ return nextValues;
143
+ }
144
+
145
+ function getPosixProfileCandidates(homeDir, shellEnv = process.env) {
146
+ const candidates = [
147
+ '.bashrc',
148
+ '.bash_profile',
149
+ '.bash_login',
150
+ '.zshrc',
151
+ '.zshenv',
152
+ '.zprofile',
153
+ '.zlogin',
154
+ '.profile'
155
+ ].map(fileName => path.join(homeDir, fileName));
156
+
157
+ const shell = String(shellEnv.SHELL || '').toLowerCase();
158
+ const preferred = shell.includes('zsh')
159
+ ? path.join(homeDir, '.zshrc')
160
+ : shell.includes('bash')
161
+ ? path.join(homeDir, '.bashrc')
162
+ : path.join(homeDir, '.profile');
163
+
164
+ return {
165
+ preferred,
166
+ candidates
167
+ };
168
+ }
169
+
170
+ function writeTextFileIfChanged(filePath, content) {
171
+ const previous = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : null;
172
+ if (previous === content) {
173
+ return false;
174
+ }
175
+ ensureDir(path.dirname(filePath));
176
+ fs.writeFileSync(filePath, content, 'utf8');
177
+ return true;
178
+ }
179
+
180
+ function syncPosixEnvironment(nextValues, previousState, options) {
181
+ const {
182
+ runtime,
183
+ homeDir,
184
+ stateFilePath,
185
+ shellEnv,
186
+ execSync
187
+ } = options;
188
+ const nextKeys = Object.keys(nextValues).sort();
189
+ let changed = false;
190
+
191
+ // 清理旧版本遗留的 shell profile 注入(迁移兼容)
192
+ const previousProfiles = Array.isArray(previousState.profiles) ? previousState.profiles : [];
193
+ if (previousProfiles.length > 0) {
194
+ const { candidates } = getPosixProfileCandidates(homeDir, shellEnv);
195
+ const cleanupTargets = new Set([
196
+ ...previousProfiles,
197
+ ...candidates.filter(filePath => fs.existsSync(filePath))
198
+ ]);
199
+ for (const profilePath of cleanupTargets) {
200
+ if (!fs.existsSync(profilePath)) continue;
201
+ const currentContent = fs.readFileSync(profilePath, 'utf8');
202
+ if (!currentContent.includes(PROFILE_MARKER_START)) continue;
203
+ const nextContent = stripManagedBlock(currentContent);
204
+ const finalContent = nextContent ? `${nextContent}\n` : '';
205
+ changed = writeTextFileIfChanged(profilePath, finalContent) || changed;
206
+ }
207
+ }
208
+
209
+ // macOS:用 launchctl 写入全局环境变量,新开终端/进程即生效
210
+ if (runtime === 'darwin') {
211
+ applyLaunchctlEnvironment(previousState.values || {}, nextValues, execSync);
212
+ const prevValues = previousState.values || {};
213
+ const keysChanged =
214
+ Object.keys(nextValues).length !== Object.keys(prevValues).length ||
215
+ Object.entries(nextValues).some(([k, v]) => prevValues[k] !== v);
216
+ changed = changed || keysChanged;
217
+ }
218
+
219
+ // Linux:写 ~/.config/environment.d/(桌面/systemd)+ ~/.profile(SSH/登录shell)
220
+ if (runtime === 'linux') {
221
+ changed = applyLinuxEnvironment(nextValues, homeDir) || changed;
222
+ }
223
+
224
+ if (nextKeys.length > 0) {
225
+ writeJsonFile(stateFilePath, {
226
+ version: 1,
227
+ values: nextValues,
228
+ profiles: []
229
+ });
230
+ } else if (fs.existsSync(stateFilePath)) {
231
+ fs.unlinkSync(stateFilePath);
232
+ }
233
+
234
+ return {
235
+ changed,
236
+ reloadRequired: changed,
237
+ isFirstTime: Object.keys(previousState.values || {}).length === 0 && nextKeys.length > 0,
238
+ sourceCommand: null,
239
+ shellConfigPath: null,
240
+ shellConfigPaths: [],
241
+ envFilePath: null,
242
+ managedKeys: nextKeys
243
+ };
244
+ }
245
+
246
+ function applyLinuxEnvironment(nextValues, homeDir) {
247
+ let changed = false;
248
+
249
+ // 1. ~/.config/environment.d/codex-env.conf(systemd 用户环境,桌面终端生效)
250
+ const envdDir = path.join(homeDir, '.config', 'environment.d');
251
+ const envdFile = path.join(envdDir, 'codex-env.conf');
252
+ const nextKeys = Object.keys(nextValues).sort();
253
+
254
+ if (nextKeys.length > 0) {
255
+ const envdContent = [
256
+ '# Managed by Coding-Tool',
257
+ ...nextKeys.map(key => `${key}=${nextValues[key]}`),
258
+ ''
259
+ ].join('\n');
260
+ changed = writeTextFileIfChanged(envdFile, envdContent) || changed;
261
+ } else if (fs.existsSync(envdFile)) {
262
+ fs.unlinkSync(envdFile);
263
+ changed = true;
264
+ }
265
+
266
+ // 2. ~/.profile(登录 shell,SSH 和新终端均生效)
267
+ const profilePath = path.join(homeDir, '.profile');
268
+ if (nextKeys.length > 0) {
269
+ const exportLines = nextKeys.map(key => `export ${key}=${shellQuote(nextValues[key])}`).join('\n');
270
+ const snippet = [PROFILE_MARKER_START, exportLines, PROFILE_MARKER_END].join('\n');
271
+ const currentContent = fs.existsSync(profilePath) ? fs.readFileSync(profilePath, 'utf8') : '';
272
+ const nextContent = upsertManagedBlock(currentContent, snippet);
273
+ changed = writeTextFileIfChanged(profilePath, nextContent) || changed;
274
+ } else if (fs.existsSync(profilePath)) {
275
+ const currentContent = fs.readFileSync(profilePath, 'utf8');
276
+ if (currentContent.includes(PROFILE_MARKER_START)) {
277
+ const nextContent = stripManagedBlock(currentContent);
278
+ const finalContent = nextContent ? `${nextContent}\n` : '';
279
+ changed = writeTextFileIfChanged(profilePath, finalContent) || changed;
280
+ }
281
+ }
282
+
283
+ return changed;
284
+ }
285
+
286
+ function applyLaunchctlEnvironment(previousValues, nextValues, execSync) {
287
+ const previousKeys = new Set(Object.keys(previousValues || {}));
288
+ for (const [key, value] of Object.entries(nextValues || {})) {
289
+ if (previousValues[key] === value) {
290
+ previousKeys.delete(key);
291
+ continue;
292
+ }
293
+ runLaunchctlCommand(['setenv', key, value], execSync);
294
+ previousKeys.delete(key);
295
+ }
296
+ for (const key of previousKeys) {
297
+ runLaunchctlCommand(['unsetenv', key], execSync);
298
+ }
299
+ }
300
+
301
+ function runLaunchctlCommand(args, execSync) {
302
+ try {
303
+ execSync('launchctl', args, {
304
+ stdio: ['ignore', 'ignore', 'ignore'],
305
+ timeout: 3000
306
+ });
307
+ } catch {
308
+ // ignore launchctl failures; shell profile remains the durable source
309
+ }
310
+ }
311
+
312
+ function syncWindowsEnvironment(nextValues, previousState, options) {
313
+ const { stateFilePath, execSync } = options;
314
+ const nextKeys = Object.keys(nextValues).sort();
315
+ const previousValues = previousState.values || {};
316
+ let changed = false;
317
+
318
+ for (const [key, value] of Object.entries(nextValues)) {
319
+ if (previousValues[key] === value) continue;
320
+ setWindowsUserEnv(key, value, execSync);
321
+ changed = true;
322
+ }
323
+
324
+ for (const key of Object.keys(previousValues)) {
325
+ if (Object.prototype.hasOwnProperty.call(nextValues, key)) continue;
326
+ removeWindowsUserEnv(key, execSync);
327
+ changed = true;
328
+ }
329
+
330
+ if (nextKeys.length > 0) {
331
+ writeJsonFile(stateFilePath, {
332
+ version: 1,
333
+ values: nextValues,
334
+ profiles: []
335
+ });
336
+ } else if (fs.existsSync(stateFilePath)) {
337
+ fs.unlinkSync(stateFilePath);
338
+ }
339
+
340
+ return {
341
+ changed,
342
+ reloadRequired: changed,
343
+ isFirstTime: Object.keys(previousValues).length === 0 && nextKeys.length > 0,
344
+ sourceCommand: null,
345
+ shellConfigPath: null,
346
+ shellConfigPaths: [],
347
+ envFilePath: null,
348
+ managedKeys: nextKeys
349
+ };
350
+ }
351
+
352
+ function runWindowsEnvCommand(script, execSync) {
353
+ const candidates = ['powershell', 'pwsh'];
354
+ let lastError = null;
355
+ for (const command of candidates) {
356
+ try {
357
+ execSync(command, ['-NoProfile', '-NonInteractive', '-Command', script], {
358
+ stdio: ['ignore', 'ignore', 'ignore'],
359
+ timeout: 5000
360
+ });
361
+ return;
362
+ } catch (error) {
363
+ lastError = error;
364
+ }
365
+ }
366
+ throw lastError || new Error('No PowerShell executable available');
367
+ }
368
+
369
+ function setWindowsUserEnv(key, value, execSync) {
370
+ runWindowsEnvCommand(
371
+ `[Environment]::SetEnvironmentVariable(${powershellQuote(key)}, ${powershellQuote(value)}, 'User')`,
372
+ execSync
373
+ );
374
+ }
375
+
376
+ function removeWindowsUserEnv(key, execSync) {
377
+ runWindowsEnvCommand(
378
+ `[Environment]::SetEnvironmentVariable(${powershellQuote(key)}, $null, 'User')`,
379
+ execSync
380
+ );
381
+ }
382
+
383
+ function syncCodexUserEnvironment(envMap = {}, options = {}) {
384
+ const configDir = options.configDir || PATHS.config;
385
+ const homeDir = options.homeDir || HOME_DIR;
386
+ const envFilePath = options.envFilePath || defaultEnvFilePath(configDir);
387
+ const stateFilePath = options.stateFilePath || defaultStateFilePath(configDir);
388
+ const runtime = options.runtime || process.platform;
389
+ const shellEnv = options.shellEnv || process.env;
390
+ const execSync = options.execFileSync || execFileSync;
391
+ const previousState = readState(stateFilePath);
392
+ const nextValues = buildNextEnvValues(previousState.values, envMap, {
393
+ replace: options.replace !== false,
394
+ removeKeys: options.removeKeys || []
395
+ });
396
+
397
+ const syncOptions = {
398
+ runtime,
399
+ homeDir,
400
+ envFilePath,
401
+ stateFilePath,
402
+ shellEnv,
403
+ execSync
404
+ };
405
+
406
+ if (runtime === 'win32') {
407
+ return syncWindowsEnvironment(nextValues, previousState, syncOptions);
408
+ }
409
+
410
+ return syncPosixEnvironment(nextValues, previousState, syncOptions);
411
+ }
412
+
413
+ module.exports = {
414
+ syncCodexUserEnvironment,
415
+ _test: {
416
+ buildHomeRelativeShellPath,
417
+ buildNextEnvValues,
418
+ buildSourceSnippet,
419
+ getPosixProfileCandidates,
420
+ readState,
421
+ shellQuote,
422
+ stripManagedBlock,
423
+ syncCodexUserEnvironment,
424
+ upsertManagedBlock
425
+ }
426
+ };
@@ -1,12 +1,9 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
- const os = require('os');
4
3
  const toml = require('toml');
5
4
  const tomlStringify = require('@iarna/toml').stringify;
6
- const { resolvePreferredHomeDir } = require('../../utils/home-dir');
7
5
  const { NATIVE_PATHS } = require('../../config/paths');
8
-
9
- const HOME_DIR = resolvePreferredHomeDir(process.platform, process.env, os.homedir());
6
+ const { syncCodexUserEnvironment } = require('./codex-env-manager');
10
7
 
11
8
  // Codex 配置文件路径
12
9
  function getConfigPath() {
@@ -143,7 +140,7 @@ function backupSettings() {
143
140
  const configContent = fs.readFileSync(getConfigPath(), 'utf8');
144
141
  fs.writeFileSync(getConfigBackupPath(), configContent, 'utf8');
145
142
 
146
- // 备份 auth.json (如果存在)
143
+ // 备份 auth.json (如果存在,主要用于 OAuth 状态回滚)
147
144
  if (authExists()) {
148
145
  const authContent = fs.readFileSync(getAuthPath(), 'utf8');
149
146
  fs.writeFileSync(getAuthBackupPath(), authContent, 'utf8');
@@ -194,8 +191,10 @@ function restoreSettings() {
194
191
  fs.unlinkSync(getAuthBackupPath());
195
192
  }
196
193
 
197
- // auth.json 已恢复,同步删除当前进程的环境变量
198
- delete process.env.CC_PROXY_KEY;
194
+ syncCodexUserEnvironment({}, {
195
+ replace: false,
196
+ removeKeys: ['CC_PROXY_KEY']
197
+ });
199
198
 
200
199
  console.log('Codex settings restored from backup');
201
200
  return { success: true };
@@ -232,20 +231,21 @@ function setProxyConfig(proxyPort) {
232
231
  // 写入配置
233
232
  writeConfig(config);
234
233
 
235
- // 写入 auth.json
236
- const auth = readAuth();
237
- auth.CC_PROXY_KEY = 'PROXY_KEY';
238
- writeAuth(auth);
234
+ const envResult = syncCodexUserEnvironment({
235
+ CC_PROXY_KEY: 'PROXY_KEY'
236
+ }, {
237
+ replace: false
238
+ });
239
239
 
240
- // auth.json 已写入 CC_PROXY_KEY,Codex 优先读取 auth.json,无需注入 shell 配置文件
241
240
  console.log(`Codex settings updated to use proxy on port ${proxyPort}`);
242
241
  return {
243
242
  success: true,
244
243
  port: proxyPort,
245
244
  envInjected: true,
246
- isFirstTime: false,
247
- shellConfigPath: null,
248
- sourceCommand: null
245
+ isFirstTime: envResult.isFirstTime,
246
+ shellConfigPath: envResult.shellConfigPath,
247
+ sourceCommand: envResult.sourceCommand,
248
+ reloadRequired: envResult.reloadRequired
249
249
  };
250
250
  } catch (err) {
251
251
  throw new Error('Failed to set proxy config: ' + err.message);
@@ -4,36 +4,12 @@ const {
4
4
  getDailyStatistics: getSharedDailyStatistics,
5
5
  getTodayStatistics: getSharedTodayStatistics
6
6
  } = require('./statistics-service');
7
+ const { normalizeUsageTokens, toNumber } = require('./proxy-log-helper');
7
8
 
8
9
  const TOOL_TYPE = 'codex';
9
10
 
10
- function toNumber(value) {
11
- const num = Number(value);
12
- return Number.isFinite(num) ? num : 0;
13
- }
14
-
15
- function normalizeToolTokens(tokens = {}) {
16
- const input = toNumber(tokens.input);
17
- const output = toNumber(tokens.output);
18
- const reasoning = toNumber(tokens.reasoning);
19
- const cached = toNumber(tokens.cached);
20
- const cacheCreation = toNumber(tokens.cacheCreation);
21
- const cacheRead = toNumber(tokens.cacheRead || cached);
22
- const total = toNumber(tokens.total) || (input + output + reasoning);
23
-
24
- return {
25
- input,
26
- output,
27
- reasoning,
28
- cached,
29
- cacheCreation,
30
- cacheRead,
31
- total
32
- };
33
- }
34
-
35
11
  function toLegacyEntryShape(entry = {}, includeName = false) {
36
- const normalized = normalizeToolTokens(entry.tokens || {});
12
+ const normalized = normalizeUsageTokens(TOOL_TYPE, entry.tokens || {});
37
13
  const result = {
38
14
  requests: toNumber(entry.requests),
39
15
  tokens: {
@@ -126,7 +102,7 @@ function buildDailyStatistics(sharedDaily = {}, fallbackDate) {
126
102
  }
127
103
 
128
104
  function recordRequest(requestData = {}) {
129
- const normalizedTokens = normalizeToolTokens(requestData.tokens || {});
105
+ const normalizedTokens = normalizeUsageTokens(TOOL_TYPE, requestData.tokens || {});
130
106
  return recordSharedRequest({
131
107
  ...requestData,
132
108
  toolType: TOOL_TYPE,
@@ -39,11 +39,11 @@ const PLUGIN_SENSITIVE_PATTERNS = [
39
39
  /\.p12$/i,
40
40
  /\.pfx$/i
41
41
  ];
42
- const CC_UI_CONFIG_PATH = path.join(CC_TOOL_DIR, 'ui-config.json');
43
- const CC_PROMPTS_PATH = path.join(CC_TOOL_DIR, 'prompts.json');
44
- const CC_SECURITY_PATH = path.join(CC_TOOL_DIR, 'security.json');
45
- const LEGACY_UI_CONFIG_PATH = path.join(LEGACY_CC_TOOL_DIR, 'ui-config.json');
46
- const LEGACY_NOTIFY_HOOK_PATH = path.join(LEGACY_CC_TOOL_DIR, 'notify-hook.js');
42
+ const CC_UI_CONFIG_PATH = PATHS.uiConfig;
43
+ const CC_PROMPTS_PATH = PATHS.prompts;
44
+ const CC_SECURITY_PATH = PATHS.security;
45
+ const LEGACY_UI_CONFIG_PATH = PATHS.uiConfig;
46
+ const LEGACY_NOTIFY_HOOK_PATH = PATHS.notifyHook;
47
47
  const GEMINI_SETTINGS_PATH = path.join(path.dirname(NATIVE_PATHS.gemini.env), 'settings.json');
48
48
  const AGENT_PLATFORMS = ['claude', 'codex', 'opencode'];
49
49
  const COMMAND_PLATFORMS = ['claude', 'opencode'];
@@ -58,8 +58,18 @@ function getOpenCodeConfigPaths() {
58
58
  }
59
59
  }
60
60
 
61
+ function getOpenCodeNotificationPluginPath() {
62
+ try {
63
+ const { getOpenCodeManagedPluginPath } = require('./notification-hooks');
64
+ return typeof getOpenCodeManagedPluginPath === 'function' ? getOpenCodeManagedPluginPath() : '';
65
+ } catch (err) {
66
+ return '';
67
+ }
68
+ }
69
+
61
70
  function getNativeConfigSpecs() {
62
71
  const openCodeConfigPaths = getOpenCodeConfigPaths();
72
+ const openCodeNotificationPluginPath = getOpenCodeNotificationPluginPath();
63
73
  return {
64
74
  claude: {
65
75
  settings: { path: NATIVE_PATHS.claude.settings, format: 'json' }
@@ -75,7 +85,10 @@ function getNativeConfigSpecs() {
75
85
  opencode: {
76
86
  opencodeJsonc: { path: openCodeConfigPaths.opencodec, format: 'text' },
77
87
  opencodeJson: { path: openCodeConfigPaths.opencode, format: 'text' },
78
- configJson: { path: openCodeConfigPaths.config, format: 'text' }
88
+ configJson: { path: openCodeConfigPaths.config, format: 'text' },
89
+ ...(openCodeNotificationPluginPath
90
+ ? { codingToolNotifyPlugin: { path: openCodeNotificationPluginPath, format: 'text' } }
91
+ : {})
79
92
  }
80
93
  };
81
94
  }
@@ -212,7 +225,7 @@ function buildExportReadme(exportData) {
212
225
  - Prompts 预设
213
226
  - 安全配置
214
227
  - 高级配置(端口、日志、性能等)
215
- - Claude Hooks 配置(如通知脚本)
228
+ - 通知 Hook / 插件脚本(如 notify-hook.js)
216
229
 
217
230
  > 注意:配置包可能包含 API Key、Webhook 等敏感信息,请妥善保管。
218
231
  `;
@@ -13,8 +13,8 @@ const { PATHS, NATIVE_PATHS } = require('../../config/paths');
13
13
 
14
14
  // Configuration paths
15
15
  const CC_TOOL_DIR = PATHS.base;
16
- const REGISTRY_FILE = path.join(CC_TOOL_DIR, 'config-registry.json');
17
- const CONFIGS_DIR = path.join(CC_TOOL_DIR, 'configs');
16
+ const REGISTRY_FILE = PATHS.configRegistry;
17
+ const CONFIGS_DIR = PATHS.configs;
18
18
 
19
19
  // Claude Code native directories
20
20
  const CLAUDE_HOME_DIR = path.dirname(NATIVE_PATHS.claude.settings);
@@ -113,6 +113,7 @@ class ConfigRegistryService {
113
113
  */
114
114
  _ensureDirs() {
115
115
  ensureDir(CC_TOOL_DIR);
116
+ ensureDir(path.dirname(this.registryPath));
116
117
  ensureDir(this.configsDir);
117
118
 
118
119
  for (const type of CONFIG_TYPES) {
@@ -24,7 +24,7 @@ const { PATHS, NATIVE_PATHS, HOME_DIR, ensureStorageDirMigrated } = require('../
24
24
 
25
25
  // Paths
26
26
  const HOME = HOME_DIR || os.homedir();
27
- const CC_TOOL_CONFIGS = path.join(PATHS.base, 'configs');
27
+ const CC_TOOL_CONFIGS = PATHS.configs;
28
28
  const CLAUDE_CODE_DIR = path.join(HOME, '.claude');
29
29
  const CODEX_DIR = path.join(HOME, '.codex');
30
30
  const GEMINI_DIR = path.join(HOME, '.gemini');
@@ -1,7 +1,7 @@
1
1
  const fs = require('fs');
2
+ const path = require('path');
2
3
  const { PATHS } = require('../../config/paths');
3
4
 
4
- const FAVORITES_DIR = PATHS.base;
5
5
  const FAVORITES_FILE = PATHS.favorites;
6
6
 
7
7
  // 内存缓存
@@ -17,8 +17,9 @@ const DEFAULT_FAVORITES = {
17
17
 
18
18
  // Ensure favorites directory exists
19
19
  function ensureFavoritesDir() {
20
- if (!fs.existsSync(FAVORITES_DIR)) {
21
- fs.mkdirSync(FAVORITES_DIR, { recursive: true });
20
+ const dir = path.dirname(FAVORITES_FILE);
21
+ if (!fs.existsSync(dir)) {
22
+ fs.mkdirSync(dir, { recursive: true });
22
23
  }
23
24
  }
24
25
 
@@ -32,9 +32,9 @@ function getGeminiDir() {
32
32
 
33
33
  // 获取渠道存储文件路径
34
34
  function getChannelsFilePath() {
35
- const ccToolDir = PATHS.base;
36
- if (!fs.existsSync(ccToolDir)) {
37
- fs.mkdirSync(ccToolDir, { recursive: true });
35
+ const channelsDir = path.dirname(PATHS.channels.gemini);
36
+ if (!fs.existsSync(channelsDir)) {
37
+ fs.mkdirSync(channelsDir, { recursive: true });
38
38
  }
39
39
  return PATHS.channels.gemini;
40
40
  }