coding-tool-x 3.3.7 → 3.3.9

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 (89) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +253 -326
  3. package/dist/web/assets/{Analytics-IW6eAy9u.js → Analytics-D6LzK9hk.js} +1 -1
  4. package/dist/web/assets/{ConfigTemplates-BPtkTMSc.js → ConfigTemplates-BUDYuxRi.js} +1 -1
  5. package/dist/web/assets/Home-BQxQ1LhR.css +1 -0
  6. package/dist/web/assets/Home-D7KX7iF8.js +1 -0
  7. package/dist/web/assets/{PluginManager-BGx9MSDV.js → PluginManager-DTgQ--vB.js} +1 -1
  8. package/dist/web/assets/{ProjectList-BCn-mrCx.js → ProjectList-DMCiGmCT.js} +1 -1
  9. package/dist/web/assets/{SessionList-CzLfebJQ.js → SessionList-CRBsdVRe.js} +1 -1
  10. package/dist/web/assets/{SkillManager-CXz2vBQx.js → SkillManager-DMwx2Q4k.js} +1 -1
  11. package/dist/web/assets/{WorkspaceManager-CHtgMfKc.js → WorkspaceManager-DapB4ljL.js} +1 -1
  12. package/dist/web/assets/{icons-B29onFfZ.js → icons-B5Pl4lrD.js} +1 -1
  13. package/dist/web/assets/index-CL-qpoJ_.js +2 -0
  14. package/dist/web/assets/index-D_5dRFOL.css +1 -0
  15. package/dist/web/assets/{markdown-C9MYpaSi.js → markdown-DyTJGI4N.js} +1 -1
  16. package/dist/web/assets/{naive-ui-CxpuzdjU.js → naive-ui-Bdxp09n2.js} +1 -1
  17. package/dist/web/assets/{vendors-DMjSfzlv.js → vendors-CKPV1OAU.js} +2 -2
  18. package/dist/web/assets/{vue-vendor-DET08QYg.js → vue-vendor-3bf-fPGP.js} +1 -1
  19. package/dist/web/index.html +7 -7
  20. package/docs/home.png +0 -0
  21. package/package.json +14 -5
  22. package/src/commands/daemon.js +3 -2
  23. package/src/commands/security.js +1 -2
  24. package/src/commands/toggle-proxy.js +100 -5
  25. package/src/config/paths.js +718 -90
  26. package/src/server/api/agents.js +1 -1
  27. package/src/server/api/channels.js +9 -0
  28. package/src/server/api/claude-hooks.js +13 -8
  29. package/src/server/api/codex-channels.js +9 -0
  30. package/src/server/api/codex-proxy.js +27 -15
  31. package/src/server/api/gemini-proxy.js +22 -11
  32. package/src/server/api/hooks.js +45 -0
  33. package/src/server/api/oauth-credentials.js +163 -0
  34. package/src/server/api/opencode-proxy.js +22 -10
  35. package/src/server/api/plugins.js +2 -1
  36. package/src/server/api/proxy.js +39 -44
  37. package/src/server/api/skills.js +91 -13
  38. package/src/server/api/ui-config.js +5 -0
  39. package/src/server/codex-proxy-server.js +90 -70
  40. package/src/server/gemini-proxy-server.js +107 -88
  41. package/src/server/index.js +2 -0
  42. package/src/server/opencode-proxy-server.js +381 -225
  43. package/src/server/proxy-server.js +86 -60
  44. package/src/server/services/alias.js +3 -3
  45. package/src/server/services/channels.js +21 -24
  46. package/src/server/services/codex-channels.js +158 -255
  47. package/src/server/services/codex-config.js +2 -5
  48. package/src/server/services/codex-env-manager.js +423 -0
  49. package/src/server/services/codex-settings-manager.js +21 -357
  50. package/src/server/services/codex-statistics-service.js +3 -27
  51. package/src/server/services/config-export-service.js +43 -9
  52. package/src/server/services/config-registry-service.js +3 -2
  53. package/src/server/services/config-sync-manager.js +1 -1
  54. package/src/server/services/favorites.js +4 -3
  55. package/src/server/services/gemini-channels.js +14 -12
  56. package/src/server/services/gemini-statistics-service.js +3 -25
  57. package/src/server/services/mcp-service.js +35 -19
  58. package/src/server/services/model-detector.js +4 -3
  59. package/src/server/services/native-keychain.js +243 -0
  60. package/src/server/services/native-oauth-adapters.js +891 -0
  61. package/src/server/services/network-access.js +39 -1
  62. package/src/server/services/notification-hooks.js +951 -0
  63. package/src/server/services/oauth-credentials-service.js +786 -0
  64. package/src/server/services/oauth-utils.js +49 -0
  65. package/src/server/services/opencode-channels.js +19 -15
  66. package/src/server/services/opencode-sessions.js +2 -2
  67. package/src/server/services/opencode-settings-manager.js +169 -16
  68. package/src/server/services/opencode-statistics-service.js +3 -27
  69. package/src/server/services/plugins-service.js +115 -15
  70. package/src/server/services/prompts-service.js +2 -3
  71. package/src/server/services/proxy-log-helper.js +242 -0
  72. package/src/server/services/proxy-runtime.js +6 -4
  73. package/src/server/services/repo-scanner-base.js +12 -4
  74. package/src/server/services/request-logger.js +7 -7
  75. package/src/server/services/security-config.js +4 -4
  76. package/src/server/services/session-cache.js +2 -2
  77. package/src/server/services/sessions.js +2 -2
  78. package/src/server/services/settings-manager.js +13 -0
  79. package/src/server/services/skill-service.js +867 -368
  80. package/src/server/services/statistics-service.js +5 -5
  81. package/src/server/services/ui-config.js +4 -3
  82. package/src/server/services/workspace-service.js +1 -1
  83. package/src/server/websocket-server.js +5 -4
  84. package/dist/web/assets/Home-BsSioaaB.css +0 -1
  85. package/dist/web/assets/Home-obifg_9E.js +0 -1
  86. package/dist/web/assets/index-C7LPdVsN.js +0 -2
  87. package/dist/web/assets/index-eEmjZKWP.css +0 -1
  88. package/docs/bannel.png +0 -0
  89. package/docs/model-redirection.md +0 -251
@@ -1,28 +1,26 @@
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, isWindowsLikePlatform } = require('../../utils/home-dir');
7
-
8
- const HOME_DIR = resolvePreferredHomeDir(process.platform, process.env, os.homedir());
5
+ const { NATIVE_PATHS } = require('../../config/paths');
6
+ const { syncCodexUserEnvironment } = require('./codex-env-manager');
9
7
 
10
8
  // Codex 配置文件路径
11
9
  function getConfigPath() {
12
- return path.join(HOME_DIR, '.codex', 'config.toml');
10
+ return NATIVE_PATHS.codex.config;
13
11
  }
14
12
 
15
13
  function getAuthPath() {
16
- return path.join(HOME_DIR, '.codex', 'auth.json');
14
+ return NATIVE_PATHS.codex.auth;
17
15
  }
18
16
 
19
17
  // 备份文件路径
20
18
  function getConfigBackupPath() {
21
- return path.join(HOME_DIR, '.codex', 'config.toml.cc-tool-backup');
19
+ return NATIVE_PATHS.codex.configBackup;
22
20
  }
23
21
 
24
22
  function getAuthBackupPath() {
25
- return path.join(HOME_DIR, '.codex', 'auth.json.cc-tool-backup');
23
+ return NATIVE_PATHS.codex.authBackup;
26
24
  }
27
25
 
28
26
  // 检查配置文件是否存在
@@ -39,172 +37,6 @@ function hasBackup() {
39
37
  return fs.existsSync(getConfigBackupPath()) || fs.existsSync(getAuthBackupPath());
40
38
  }
41
39
 
42
- const INVALID_ENV_NAME_PATTERN = /[\r\n]/;
43
- const SHELL_MARKER_PREFIX = '# Added by Coding-Tool for Codex';
44
-
45
- function normalizeEnvName(envName) {
46
- const normalized = String(envName || '').trim();
47
- if (!normalized || INVALID_ENV_NAME_PATTERN.test(normalized)) {
48
- return null;
49
- }
50
- return normalized;
51
- }
52
-
53
- function ensureParentDir(filePath) {
54
- const dirPath = path.dirname(filePath);
55
- if (!fs.existsSync(dirPath)) {
56
- fs.mkdirSync(dirPath, { recursive: true });
57
- }
58
- }
59
-
60
- function writeFileAtomic(filePath, content) {
61
- ensureParentDir(filePath);
62
- const tempPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
63
-
64
- try {
65
- fs.writeFileSync(tempPath, content, 'utf8');
66
- fs.renameSync(tempPath, filePath);
67
- } finally {
68
- if (fs.existsSync(tempPath)) {
69
- try {
70
- fs.unlinkSync(tempPath);
71
- } catch (cleanupErr) {
72
- // ignore cleanup errors
73
- }
74
- }
75
- }
76
- }
77
-
78
- function normalizeHomePath(filePath) {
79
- const normalizedPath = String(filePath || '').replace(/\\/g, '/');
80
- const normalizedHome = HOME_DIR.replace(/\\/g, '/');
81
- if (normalizedPath.startsWith(normalizedHome)) {
82
- return `~${normalizedPath.slice(normalizedHome.length)}`;
83
- }
84
- return filePath;
85
- }
86
-
87
- function compactBlankLines(lines) {
88
- const compacted = [];
89
- let previousIsBlank = false;
90
-
91
- for (const line of lines) {
92
- const isBlank = line.trim() === '';
93
- if (isBlank) {
94
- if (!previousIsBlank) {
95
- compacted.push('');
96
- }
97
- previousIsBlank = true;
98
- continue;
99
- }
100
-
101
- compacted.push(line);
102
- previousIsBlank = false;
103
- }
104
-
105
- while (compacted.length > 0 && compacted[compacted.length - 1].trim() === '') {
106
- compacted.pop();
107
- }
108
-
109
- return compacted;
110
- }
111
-
112
- function isPowerShellProfile(filePath) {
113
- return String(filePath || '').toLowerCase().endsWith('.ps1');
114
- }
115
-
116
- function getShellConfigCandidates() {
117
- const homeDir = HOME_DIR;
118
- const shell = String(process.env.SHELL || '').toLowerCase();
119
- const candidates = [];
120
-
121
- if (isWindowsLikePlatform(process.platform, process.env)) {
122
- const oneDriveDir = process.env.OneDrive || process.env.ONEDRIVE || '';
123
-
124
- if (shell.includes('zsh')) {
125
- candidates.push(path.join(homeDir, '.zshrc'));
126
- }
127
-
128
- if (shell.includes('bash')) {
129
- candidates.push(path.join(homeDir, '.bashrc'));
130
- candidates.push(path.join(homeDir, '.bash_profile'));
131
- }
132
-
133
- candidates.push(path.join(homeDir, 'Documents', 'PowerShell', 'Microsoft.PowerShell_profile.ps1'));
134
- candidates.push(path.join(homeDir, 'Documents', 'WindowsPowerShell', 'Microsoft.PowerShell_profile.ps1'));
135
- if (oneDriveDir) {
136
- candidates.push(path.join(oneDriveDir, 'Documents', 'PowerShell', 'Microsoft.PowerShell_profile.ps1'));
137
- candidates.push(path.join(oneDriveDir, 'Documents', 'WindowsPowerShell', 'Microsoft.PowerShell_profile.ps1'));
138
- }
139
- candidates.push(path.join(homeDir, '.bashrc'));
140
- candidates.push(path.join(homeDir, '.profile'));
141
- } else if (shell.includes('zsh')) {
142
- candidates.push(path.join(homeDir, '.zshrc'));
143
- candidates.push(path.join(homeDir, '.zprofile'));
144
- candidates.push(path.join(homeDir, '.profile'));
145
- } else if (shell.includes('bash')) {
146
- if (process.platform === 'darwin') {
147
- candidates.push(path.join(homeDir, '.bash_profile'));
148
- candidates.push(path.join(homeDir, '.bashrc'));
149
- } else {
150
- candidates.push(path.join(homeDir, '.bashrc'));
151
- candidates.push(path.join(homeDir, '.bash_profile'));
152
- }
153
- candidates.push(path.join(homeDir, '.profile'));
154
- } else {
155
- candidates.push(path.join(homeDir, '.zshrc'));
156
- candidates.push(path.join(homeDir, '.bashrc'));
157
- candidates.push(path.join(homeDir, '.bash_profile'));
158
- candidates.push(path.join(homeDir, '.profile'));
159
- }
160
-
161
- return [...new Set(candidates)];
162
- }
163
-
164
- function getShellReloadCommand(configPath) {
165
- if (!configPath) {
166
- return isWindowsLikePlatform(process.platform, process.env) ? '重启终端' : 'source ~/.zshrc';
167
- }
168
-
169
- const displayPath = normalizeHomePath(configPath);
170
- const normalized = String(displayPath || '').replace(/\\/g, '/').toLowerCase();
171
-
172
- if (normalized.endsWith('microsoft.powershell_profile.ps1')) {
173
- return '. $PROFILE';
174
- }
175
- if (normalized.endsWith('/.zshrc')) {
176
- return 'source ~/.zshrc';
177
- }
178
- if (normalized.endsWith('/.bash_profile')) {
179
- return 'source ~/.bash_profile';
180
- }
181
- if (normalized.endsWith('/.bashrc')) {
182
- return 'source ~/.bashrc';
183
- }
184
- if (normalized.endsWith('/.profile')) {
185
- return 'source ~/.profile';
186
- }
187
-
188
- if (isWindowsLikePlatform(process.platform, process.env)) {
189
- return '. $PROFILE';
190
- }
191
-
192
- return `source ${displayPath}`;
193
- }
194
-
195
- function escapeShellValue(value) {
196
- return String(value ?? '')
197
- .replace(/\\/g, '\\\\')
198
- .replace(/"/g, '\\"')
199
- .replace(/\$/g, '\\$')
200
- .replace(/`/g, '\\`');
201
- }
202
-
203
- function escapePowerShellValue(value) {
204
- return String(value ?? '')
205
- .replace(/`/g, '``')
206
- .replace(/"/g, '`"');
207
- }
208
40
 
209
41
  // 读取 config.toml
210
42
  function readConfig() {
@@ -308,7 +140,7 @@ function backupSettings() {
308
140
  const configContent = fs.readFileSync(getConfigPath(), 'utf8');
309
141
  fs.writeFileSync(getConfigBackupPath(), configContent, 'utf8');
310
142
 
311
- // 备份 auth.json (如果存在)
143
+ // 备份 auth.json (如果存在,主要用于 OAuth 状态回滚)
312
144
  if (authExists()) {
313
145
  const authContent = fs.readFileSync(getAuthPath(), 'utf8');
314
146
  fs.writeFileSync(getAuthBackupPath(), authContent, 'utf8');
@@ -359,11 +191,10 @@ function restoreSettings() {
359
191
  fs.unlinkSync(getAuthBackupPath());
360
192
  }
361
193
 
362
- // 清理 shell 配置文件中的环境变量(可选,不影响恢复结果)
363
- removeEnvFromShell('CC_PROXY_KEY');
364
-
365
- // 同步删除当前进程的环境变量,使恢复立即生效(无需新开终端)
366
- delete process.env.CC_PROXY_KEY;
194
+ syncCodexUserEnvironment({}, {
195
+ replace: false,
196
+ removeKeys: ['CC_PROXY_KEY']
197
+ });
367
198
 
368
199
  console.log('Codex settings restored from backup');
369
200
  return { success: true };
@@ -372,161 +203,6 @@ function restoreSettings() {
372
203
  }
373
204
  }
374
205
 
375
- // 获取用户的 shell 配置文件路径
376
- function getShellConfigPath() {
377
- const candidates = getShellConfigCandidates();
378
- const existing = candidates.find(filePath => fs.existsSync(filePath));
379
- return existing || candidates[0];
380
- }
381
-
382
- // 注入环境变量到 shell 配置文件
383
- function injectEnvToShell(envName, envValue) {
384
- const normalizedEnvName = normalizeEnvName(envName);
385
- if (!normalizedEnvName) {
386
- return {
387
- success: false,
388
- error: `Invalid environment variable name: ${envName}`,
389
- isFirstTime: false
390
- };
391
- }
392
-
393
- const configPath = getShellConfigPath();
394
- const marker = `${SHELL_MARKER_PREFIX} [${normalizedEnvName}]`;
395
- const usePowerShell = isPowerShellProfile(configPath);
396
- const exportLine = usePowerShell
397
- ? `$env:${normalizedEnvName} = "${escapePowerShellValue(envValue)}"`
398
- : `export ${normalizedEnvName}="${escapeShellValue(envValue)}"`;
399
-
400
- try {
401
- let content = '';
402
- if (fs.existsSync(configPath)) {
403
- content = fs.readFileSync(configPath, 'utf8');
404
- }
405
-
406
- const envKeyEscaped = String(normalizedEnvName).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
407
- const envLineRegex = usePowerShell
408
- ? new RegExp(`^\\s*\\$env:${envKeyEscaped}\\s*=`, 'i')
409
- : new RegExp(`^\\s*(?:export\\s+)?${envKeyEscaped}=`);
410
-
411
- const originalLines = content ? content.split(/\r?\n/) : [];
412
- const cleanedLines = [];
413
- let existed = false;
414
-
415
- for (let i = 0; i < originalLines.length; i++) {
416
- const currentLine = originalLines[i];
417
- const trimmedLine = currentLine.trim();
418
-
419
- if (trimmedLine === marker) {
420
- const nextLine = originalLines[i + 1] || '';
421
- if (envLineRegex.test(nextLine.trim())) {
422
- i += 1;
423
- }
424
- existed = true;
425
- continue;
426
- }
427
-
428
- if (envLineRegex.test(trimmedLine)) {
429
- existed = true;
430
- continue;
431
- }
432
-
433
- cleanedLines.push(currentLine);
434
- }
435
-
436
- while (cleanedLines.length > 0 && cleanedLines[cleanedLines.length - 1].trim() === '') {
437
- cleanedLines.pop();
438
- }
439
-
440
- if (cleanedLines.length > 0) {
441
- cleanedLines.push('');
442
- }
443
-
444
- cleanedLines.push(marker, exportLine);
445
-
446
- const nextContent = `${cleanedLines.join('\n')}\n`;
447
- if (nextContent !== content) {
448
- writeFileAtomic(configPath, nextContent);
449
- }
450
-
451
- // 同步更新当前进程的环境变量,使变更立即生效(无需新开终端)
452
- process.env[normalizedEnvName] = String(envValue ?? '');
453
-
454
- return { success: true, path: configPath, isFirstTime: !existed };
455
- } catch (err) {
456
- // 不抛出错误,只是警告,因为这不是致命问题
457
- console.warn(`[Codex] Failed to inject env to shell config: ${err.message}`);
458
- return { success: false, error: err.message, isFirstTime: false };
459
- }
460
- }
461
-
462
- // 从 shell 配置文件移除环境变量
463
- function removeEnvFromShell(envName) {
464
- const normalizedEnvName = normalizeEnvName(envName);
465
- if (!normalizedEnvName) {
466
- return {
467
- success: false,
468
- error: `Invalid environment variable name: ${envName}`
469
- };
470
- }
471
-
472
- const configPath = getShellConfigPath();
473
-
474
- try {
475
- if (!fs.existsSync(configPath)) {
476
- return { success: true };
477
- }
478
-
479
- const content = fs.readFileSync(configPath, 'utf8');
480
- const usePowerShell = isPowerShellProfile(configPath);
481
- const marker = `${SHELL_MARKER_PREFIX} [${normalizedEnvName}]`;
482
- const envKeyEscaped = String(normalizedEnvName).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
483
- const envLineRegex = usePowerShell
484
- ? new RegExp(`^\\s*\\$env:${envKeyEscaped}\\s*=`, 'i')
485
- : new RegExp(`^\\s*(?:export\\s+)?${envKeyEscaped}=`);
486
-
487
- const originalLines = content ? content.split(/\r?\n/) : [];
488
- const cleanedLines = [];
489
- let changed = false;
490
-
491
- for (let i = 0; i < originalLines.length; i++) {
492
- const currentLine = originalLines[i];
493
- const trimmedLine = currentLine.trim();
494
-
495
- if (trimmedLine === marker) {
496
- const nextLine = originalLines[i + 1] || '';
497
- if (envLineRegex.test(nextLine.trim())) {
498
- i += 1;
499
- }
500
- changed = true;
501
- continue;
502
- }
503
-
504
- if (envLineRegex.test(trimmedLine)) {
505
- changed = true;
506
- continue;
507
- }
508
-
509
- cleanedLines.push(currentLine);
510
- }
511
-
512
- if (!changed) {
513
- return { success: true };
514
- }
515
-
516
- const normalized = compactBlankLines(cleanedLines);
517
- const nextContent = normalized.length > 0 ? `${normalized.join('\n')}\n` : '';
518
- writeFileAtomic(configPath, nextContent);
519
-
520
- // 同步删除当前进程的环境变量,使变更立即生效(无需新开终端)
521
- delete process.env[normalizedEnvName];
522
-
523
- return { success: true };
524
- } catch (err) {
525
- console.warn(`[Codex] Failed to remove env from shell config: ${err.message}`);
526
- return { success: false, error: err.message };
527
- }
528
- }
529
-
530
206
  // 设置代理配置
531
207
  function setProxyConfig(proxyPort) {
532
208
  try {
@@ -555,29 +231,21 @@ function setProxyConfig(proxyPort) {
555
231
  // 写入配置
556
232
  writeConfig(config);
557
233
 
558
- // 写入 auth.json
559
- const auth = readAuth();
560
- auth.CC_PROXY_KEY = 'PROXY_KEY';
561
- writeAuth(auth);
562
-
563
- // 注入环境变量到 shell 配置文件(解决某些系统环境变量优先级问题)
564
- const shellInjectResult = injectEnvToShell('CC_PROXY_KEY', 'PROXY_KEY');
565
-
566
- // 同步更新当前进程的环境变量,使代理立即生效(无需新开终端)
567
- process.env.CC_PROXY_KEY = 'PROXY_KEY';
568
-
569
- // 获取 shell 配置文件路径用于提示信息
570
- const shellConfigPath = shellInjectResult.path || getShellConfigPath();
571
- const sourceCommand = getShellReloadCommand(shellConfigPath);
234
+ const envResult = syncCodexUserEnvironment({
235
+ CC_PROXY_KEY: 'PROXY_KEY'
236
+ }, {
237
+ replace: false
238
+ });
572
239
 
573
240
  console.log(`Codex settings updated to use proxy on port ${proxyPort}`);
574
241
  return {
575
242
  success: true,
576
243
  port: proxyPort,
577
- envInjected: shellInjectResult.success,
578
- isFirstTime: shellInjectResult.isFirstTime,
579
- shellConfigPath: shellConfigPath,
580
- sourceCommand: sourceCommand
244
+ envInjected: true,
245
+ isFirstTime: envResult.isFirstTime,
246
+ shellConfigPath: envResult.shellConfigPath,
247
+ sourceCommand: envResult.sourceCommand,
248
+ reloadRequired: envResult.reloadRequired
581
249
  };
582
250
  } catch (err) {
583
251
  throw new Error('Failed to set proxy config: ' + err.message);
@@ -652,8 +320,4 @@ module.exports = {
652
320
  setProxyConfig,
653
321
  isProxyConfig,
654
322
  getCurrentProxyPort,
655
- // 导出环境变量注入函数供其他模块使用
656
- getShellConfigPath,
657
- injectEnvToShell,
658
- removeEnvFromShell
659
323
  };
@@ -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,
@@ -16,7 +16,7 @@ const { CommandsService } = require('./commands-service');
16
16
  const { SkillService } = require('./skill-service');
17
17
  const { PATHS, NATIVE_PATHS } = require('../../config/paths');
18
18
 
19
- const CONFIG_VERSION = '1.3.0';
19
+ const CONFIG_VERSION = '1.4.0';
20
20
  const SKILL_FILE_ENCODING = 'base64';
21
21
  const SKILL_IGNORE_DIRS = new Set(['.git']);
22
22
  const SKILL_IGNORE_FILES = new Set(['.DS_Store']);
@@ -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
  }
@@ -206,12 +219,13 @@ function buildExportReadme(exportData) {
206
219
  - Agents / Skills / Commands
207
220
  - 插件 (Plugins)
208
221
  - MCP 服务器配置
222
+ - OAuth 凭证管理池
209
223
  - 各平台原生配置(Claude / Codex / Gemini / OpenCode)
210
224
  - UI 配置(主题、面板显示、排序等)
211
225
  - Prompts 预设
212
226
  - 安全配置
213
227
  - 高级配置(端口、日志、性能等)
214
- - Claude Hooks 配置(如通知脚本)
228
+ - 通知 Hook / 插件脚本(如 notify-hook.js)
215
229
 
216
230
  > 注意:配置包可能包含 API Key、Webhook 等敏感信息,请妥善保管。
217
231
  `;
@@ -857,9 +871,9 @@ function exportAllConfigs() {
857
871
  // 获取 Plugins 配置
858
872
  const plugins = exportPluginsSnapshot();
859
873
  const nativeConfigs = exportNativeConfigs();
874
+ const oauthCredentials = readJsonFileSafe(PATHS.oauthCredentials);
860
875
 
861
876
  // 读取 Markdown 配置文件
862
- const { PATHS } = require('../../config/paths');
863
877
  const markdownFiles = {};
864
878
  const mdFileNames = ['CLAUDE.md', 'AGENTS.md', 'GEMINI.md'];
865
879
 
@@ -914,6 +928,7 @@ function exportAllConfigs() {
914
928
  security: security,
915
929
  appConfig: appConfig,
916
930
  nativeConfigs,
931
+ oauthCredentials,
917
932
  claudeHooks: claudeHooks
918
933
  }
919
934
  };
@@ -979,6 +994,7 @@ async function importConfigs(importData, options = {}) {
979
994
  security: { success: 0, failed: 0, skipped: 0 },
980
995
  appConfig: { success: 0, failed: 0, skipped: 0 },
981
996
  nativeConfigs: { success: 0, failed: 0, skipped: 0 },
997
+ oauthCredentials: { success: 0, failed: 0, skipped: 0 },
982
998
  claudeHooks: { success: 0, failed: 0, skipped: 0 }
983
999
  };
984
1000
 
@@ -1007,6 +1023,7 @@ async function importConfigs(importData, options = {}) {
1007
1023
  security = null,
1008
1024
  appConfig = null,
1009
1025
  nativeConfigs = {},
1026
+ oauthCredentials = null,
1010
1027
  claudeHooks = null
1011
1028
  } = importData.data;
1012
1029
 
@@ -1550,6 +1567,22 @@ async function importConfigs(importData, options = {}) {
1550
1567
  }
1551
1568
  }
1552
1569
 
1570
+ if (oauthCredentials && typeof oauthCredentials === 'object') {
1571
+ try {
1572
+ const status = writeJsonFileAbsolute(PATHS.oauthCredentials, oauthCredentials, overwrite, { mode: 0o600 });
1573
+ if (status === 'success') {
1574
+ results.oauthCredentials.success++;
1575
+ } else if (status === 'skipped') {
1576
+ results.oauthCredentials.skipped++;
1577
+ } else {
1578
+ results.oauthCredentials.failed++;
1579
+ }
1580
+ } catch (err) {
1581
+ console.error('[ConfigImport] 导入 OAuth 凭证失败:', err);
1582
+ results.oauthCredentials.failed++;
1583
+ }
1584
+ }
1585
+
1553
1586
  // 导入 Claude Hooks 配置
1554
1587
  if (claudeHooks && typeof claudeHooks === 'object') {
1555
1588
  let didWrite = false;
@@ -1642,6 +1675,7 @@ function generateImportSummary(results) {
1642
1675
  { key: 'security', label: '安全配置' },
1643
1676
  { key: 'appConfig', label: '高级配置' },
1644
1677
  { key: 'nativeConfigs', label: '原生配置' },
1678
+ { key: 'oauthCredentials', label: 'OAuth凭证' },
1645
1679
  { key: 'claudeHooks', label: 'Claude Hooks' }
1646
1680
  ];
1647
1681
 
@@ -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