coding-tool-x 3.5.9 → 3.5.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +4 -25
  3. package/bin/ctx.js +1 -6
  4. package/dist/web/assets/{Analytics-C5W3axXs.js → Analytics-DPb1zErn.js} +2 -2
  5. package/dist/web/assets/Analytics-RNn1BUbG.css +1 -0
  6. package/dist/web/assets/{ConfigTemplates-DzyVFDx9.js → ConfigTemplates-Bki2k4Lo.js} +1 -1
  7. package/dist/web/assets/{Home-C9TQNB6f.js → Home-BKYHKSax.js} +1 -1
  8. package/dist/web/assets/Home-BQxQ1LhR.css +1 -0
  9. package/dist/web/assets/{PluginManager-9B_brLWT.js → PluginManager-jgeQOE24.js} +1 -1
  10. package/dist/web/assets/ProjectList-CF67RzP_.js +1 -0
  11. package/dist/web/assets/ProjectList-DL4JK6ci.css +1 -0
  12. package/dist/web/assets/SessionList-DhHsY5q9.js +1 -0
  13. package/dist/web/assets/SessionList-lz23U6ZJ.css +1 -0
  14. package/dist/web/assets/{SkillManager-vST8DRRg.js → SkillManager-CtEz5gGp.js} +1 -1
  15. package/dist/web/assets/{WorkspaceManager-ov1KgRXR.js → WorkspaceManager-D3B0Y5XE.js} +1 -1
  16. package/dist/web/assets/icons-lKCTySl7.js +1 -0
  17. package/dist/web/assets/index-BS7aplYp.css +1 -0
  18. package/dist/web/assets/index-BnyNm74G.js +2 -0
  19. package/dist/web/assets/{markdown-DyTJGI4N.js → markdown-BNJfEgCA.js} +1 -1
  20. package/dist/web/assets/{naive-ui-Cg4_ZeoT.js → naive-ui-DM897mIP.js} +1 -1
  21. package/dist/web/assets/vendors-DQA4Hw9v.js +7 -0
  22. package/dist/web/assets/vue-vendor-B5RqjEjQ.js +45 -0
  23. package/dist/web/index.html +8 -8
  24. package/package.json +1 -2
  25. package/src/commands/daemon.js +5 -33
  26. package/src/commands/export-config.js +6 -6
  27. package/src/commands/stats.js +4 -41
  28. package/src/commands/ui.js +3 -12
  29. package/src/config/channel-preset-websites.js +114 -0
  30. package/src/config/default.js +9 -5
  31. package/src/config/loader.js +2 -2
  32. package/src/config/model-metadata.json +16 -20
  33. package/src/config/paths.js +33 -166
  34. package/src/index.js +34 -125
  35. package/src/server/api/agents.js +2 -52
  36. package/src/server/api/codex-proxy.js +4 -2
  37. package/src/server/api/codex-sessions.js +89 -12
  38. package/src/server/api/commands.js +2 -38
  39. package/src/server/api/dashboard.js +1 -25
  40. package/src/server/api/gemini-proxy.js +4 -2
  41. package/src/server/api/gemini-sessions.js +86 -9
  42. package/src/server/api/hooks.js +1 -17
  43. package/src/server/api/opencode-proxy.js +7 -2
  44. package/src/server/api/opencode-sessions.js +83 -10
  45. package/src/server/api/plugins.js +34 -128
  46. package/src/server/api/proxy.js +5 -3
  47. package/src/server/api/sessions.js +181 -27
  48. package/src/server/api/workspaces.js +0 -14
  49. package/src/server/index.js +9 -31
  50. package/src/server/services/agents-service.js +62 -269
  51. package/src/server/services/base/base-channel-service.js +3 -1
  52. package/src/server/services/channels.js +5 -0
  53. package/src/server/services/codex-channels.js +2 -0
  54. package/src/server/services/codex-sessions.js +78 -108
  55. package/src/server/services/commands-service.js +81 -281
  56. package/src/server/services/config-export-service.js +7 -7
  57. package/src/server/services/config-registry-service.js +9 -8
  58. package/src/server/services/config-sync-manager.js +41 -61
  59. package/src/server/services/config-sync-service.js +3 -3
  60. package/src/server/services/gemini-channels.js +17 -6
  61. package/src/server/services/gemini-config.js +4 -3
  62. package/src/server/services/gemini-sessions.js +51 -26
  63. package/src/server/services/gemini-settings-manager.js +3 -2
  64. package/src/server/services/mcp-service.js +14 -9
  65. package/src/server/services/native-oauth-adapters.js +3 -3
  66. package/src/server/services/network-access.js +2 -61
  67. package/src/server/services/notification-hooks.js +19 -184
  68. package/src/server/services/opencode-channels.js +9 -0
  69. package/src/server/services/opencode-sessions.js +6 -16
  70. package/src/server/services/opencode-settings-manager.js +3 -3
  71. package/src/server/services/plugins-service.js +62 -996
  72. package/src/server/services/prompts-service.js +9 -5
  73. package/src/server/services/sessions.js +161 -183
  74. package/src/server/services/skill-service.js +18 -155
  75. package/src/server/websocket-server.js +4 -35
  76. package/dist/web/assets/Analytics-vQS5IWvs.css +0 -1
  77. package/dist/web/assets/Home-qzk118Of.css +0 -1
  78. package/dist/web/assets/ProjectList-Bjt6mrsV.js +0 -1
  79. package/dist/web/assets/ProjectList-GCC2QOmq.css +0 -1
  80. package/dist/web/assets/SessionList-BsHPgmUR.css +0 -1
  81. package/dist/web/assets/SessionList-DcBH13uA.js +0 -1
  82. package/dist/web/assets/icons-CEq2hYB-.js +0 -1
  83. package/dist/web/assets/index-Dih_bOsv.css +0 -1
  84. package/dist/web/assets/index-Duc7QP4e.js +0 -2
  85. package/dist/web/assets/vendors-Bsp-dq2d.js +0 -7
  86. package/dist/web/assets/vue-vendor-BxIT0uQq.js +0 -45
  87. package/docs/Caddyfile.example +0 -19
  88. package/docs/reverse-proxy-https.md +0 -57
  89. package/src/server/services/https-cert.js +0 -171
  90. package/src/server/services/session-launch-command.js +0 -58
  91. package/src/server/services/web-ui-runtime.js +0 -54
@@ -16,43 +16,21 @@
16
16
 
17
17
  const fs = require('fs');
18
18
  const path = require('path');
19
+ const os = require('os');
19
20
  const toml = require('toml');
20
21
  const tomlStringify = require('@iarna/toml').stringify;
21
22
  const { convertSkillToCodex, convertCommandToCodex } = require('./format-converter');
22
- const {
23
- PATHS,
24
- NATIVE_PATHS,
25
- HOME_DIR,
26
- ensureStorageDirMigrated,
27
- getNativePlatformDir,
28
- isWindowsAbsolutePath,
29
- isNativeAbsolutePath,
30
- joinNativeBasePath,
31
- resolveNativeBasePath,
32
- getNativePathDir
33
- } = require('../../config/paths');
23
+ const { PATHS, NATIVE_PATHS, HOME_DIR, ensureStorageDirMigrated } = require('../../config/paths');
34
24
 
25
+ // Paths
26
+ const HOME = HOME_DIR || os.homedir();
35
27
  const CC_TOOL_CONFIGS = PATHS.configs;
36
- const CLAUDE_CODE_DIR = getNativePlatformDir('claude');
37
- const CODEX_DIR = getNativePlatformDir('codex');
38
- const GEMINI_DIR = getNativePlatformDir('gemini');
28
+ const CLAUDE_CODE_DIR = path.join(HOME, '.claude');
29
+ const CODEX_DIR = path.join(HOME, '.codex');
30
+ const GEMINI_DIR = path.join(HOME, '.gemini');
39
31
  const OPENCODE_DIR = NATIVE_PATHS.opencode.config;
40
32
  const CODEX_CONFIG_PATH = NATIVE_PATHS.codex.config;
41
33
 
42
- function isPathInsideNativeDir(targetPath, baseDir) {
43
- const shouldFoldCase = isWindowsAbsolutePath(baseDir);
44
- const normalize = (value) => {
45
- const normalized = String(value || '')
46
- .replace(/[\\/]+/g, '/')
47
- .replace(/\/+$/, '');
48
- return shouldFoldCase ? normalized.toLowerCase() : normalized;
49
- };
50
-
51
- const normalizedTarget = normalize(targetPath);
52
- const normalizedBase = normalize(baseDir);
53
- return normalizedTarget === normalizedBase || normalizedTarget.startsWith(`${normalizedBase}/`);
54
- }
55
-
56
34
  // Config type definitions
57
35
  const CONFIG_TYPES = {
58
36
  skills: {
@@ -130,7 +108,7 @@ class ConfigSyncManager {
130
108
  return { success: false, error: 'Invalid config item name' };
131
109
  }
132
110
  const sourcePath = path.join(this.ccToolConfigs, type, safeName);
133
- const targetPath = joinNativeBasePath(this.claudeDir, config.claudeTarget, safeName);
111
+ const targetPath = path.join(this.claudeDir, config.claudeTarget, safeName);
134
112
 
135
113
  // Check if source exists
136
114
  if (!fs.existsSync(sourcePath)) {
@@ -141,12 +119,12 @@ class ConfigSyncManager {
141
119
  try {
142
120
  if (config.isDirectory) {
143
121
  // Copy entire directory recursively
144
- this._ensureDir(getNativePathDir(targetPath));
122
+ this._ensureDir(path.dirname(targetPath));
145
123
  this._copyDirRecursive(sourcePath, targetPath);
146
124
  console.log(`[ConfigSyncManager] Synced ${type}/${name} to Claude Code (directory)`);
147
125
  } else {
148
126
  // Copy single file, preserving subdirectory structure
149
- this._ensureDir(getNativePathDir(targetPath));
127
+ this._ensureDir(path.dirname(targetPath));
150
128
  this._copyFile(sourcePath, targetPath);
151
129
  console.log(`[ConfigSyncManager] Synced ${type}/${name} to Claude Code (file)`);
152
130
  }
@@ -174,7 +152,7 @@ class ConfigSyncManager {
174
152
  if (!safeName) {
175
153
  return { success: false, error: 'Invalid config item name' };
176
154
  }
177
- const targetPath = joinNativeBasePath(this.claudeDir, config.claudeTarget, safeName);
155
+ const targetPath = path.join(this.claudeDir, config.claudeTarget, safeName);
178
156
 
179
157
  if (!fs.existsSync(targetPath)) {
180
158
  console.log(`[ConfigSyncManager] Target not found (already removed): ${targetPath}`);
@@ -192,7 +170,7 @@ class ConfigSyncManager {
192
170
  console.log(`[ConfigSyncManager] Removed ${type}/${name} from Claude Code (file)`);
193
171
 
194
172
  // Clean up empty parent directories for file-based configs
195
- this._cleanupEmptyParents(getNativePathDir(targetPath), joinNativeBasePath(this.claudeDir, config.claudeTarget));
173
+ this._cleanupEmptyParents(path.dirname(targetPath), path.join(this.claudeDir, config.claudeTarget));
196
174
  }
197
175
 
198
176
  return { success: true };
@@ -282,7 +260,7 @@ class ConfigSyncManager {
282
260
 
283
261
  if (type === 'skills') {
284
262
  // Skills: copy directory, convert SKILL.md content
285
- const targetPath = joinNativeBasePath(this.codexDir, config.codexTarget, safeName);
263
+ const targetPath = path.join(this.codexDir, config.codexTarget, safeName);
286
264
  this._ensureDir(targetPath);
287
265
 
288
266
  // Copy all files, converting SKILL.md
@@ -310,8 +288,8 @@ class ConfigSyncManager {
310
288
  }
311
289
 
312
290
  // Target path in codex prompts (same relative path structure)
313
- const targetPath = joinNativeBasePath(this.codexDir, config.codexTarget, safeName);
314
- this._ensureDir(getNativePathDir(targetPath));
291
+ const targetPath = path.join(this.codexDir, config.codexTarget, safeName);
292
+ this._ensureDir(path.dirname(targetPath));
315
293
  fs.writeFileSync(targetPath, result.content, 'utf-8');
316
294
 
317
295
  console.log(`[ConfigSyncManager] Synced ${type}/${name} to Codex (prompt)`);
@@ -376,7 +354,7 @@ class ConfigSyncManager {
376
354
  }
377
355
  }
378
356
 
379
- const targetPath = joinNativeBasePath(this.codexDir, config.codexTarget, safeName);
357
+ const targetPath = path.join(this.codexDir, config.codexTarget, safeName);
380
358
 
381
359
  if (!fs.existsSync(targetPath)) {
382
360
  console.log(`[ConfigSyncManager] Target not found (already removed): ${targetPath}`);
@@ -394,7 +372,7 @@ class ConfigSyncManager {
394
372
  console.log(`[ConfigSyncManager] Removed ${type}/${name} from Codex (prompt)`);
395
373
 
396
374
  // Clean up empty parent directories
397
- this._cleanupEmptyParents(getNativePathDir(targetPath), joinNativeBasePath(this.codexDir, config.codexTarget));
375
+ this._cleanupEmptyParents(path.dirname(targetPath), path.join(this.codexDir, config.codexTarget));
398
376
  }
399
377
 
400
378
  return { success: true };
@@ -433,8 +411,8 @@ class ConfigSyncManager {
433
411
  }
434
412
 
435
413
  try {
436
- const targetPath = joinNativeBasePath(this.geminiDir, config.geminiTarget, safeName);
437
- this._ensureDir(getNativePathDir(targetPath));
414
+ const targetPath = path.join(this.geminiDir, config.geminiTarget, safeName);
415
+ this._ensureDir(path.dirname(targetPath));
438
416
  this._copyDirRecursive(sourcePath, targetPath);
439
417
  console.log(`[ConfigSyncManager] Synced ${type}/${name} to Gemini (directory)`);
440
418
  return { success: true, target: targetPath };
@@ -465,7 +443,7 @@ class ConfigSyncManager {
465
443
  return { success: true, skipped: true, reason: 'Not supported by Gemini' };
466
444
  }
467
445
 
468
- const targetPath = joinNativeBasePath(this.geminiDir, config.geminiTarget, safeName);
446
+ const targetPath = path.join(this.geminiDir, config.geminiTarget, safeName);
469
447
  if (!fs.existsSync(targetPath)) {
470
448
  console.log(`[ConfigSyncManager] Target not found (already removed): ${targetPath}`);
471
449
  return { success: true, message: 'Already removed' };
@@ -511,14 +489,14 @@ class ConfigSyncManager {
511
489
 
512
490
  try {
513
491
  const targetBaseDir = this._getOpenCodeTypeBaseDir(config);
514
- const targetPath = joinNativeBasePath(targetBaseDir, safeName);
492
+ const targetPath = path.join(targetBaseDir, safeName);
515
493
 
516
494
  if (config.isDirectory) {
517
- this._ensureDir(getNativePathDir(targetPath));
495
+ this._ensureDir(path.dirname(targetPath));
518
496
  this._copyDirRecursive(sourcePath, targetPath);
519
497
  console.log(`[ConfigSyncManager] Synced ${type}/${name} to OpenCode (directory)`);
520
498
  } else {
521
- this._ensureDir(getNativePathDir(targetPath));
499
+ this._ensureDir(path.dirname(targetPath));
522
500
  this._copyFile(sourcePath, targetPath);
523
501
  console.log(`[ConfigSyncManager] Synced ${type}/${name} to OpenCode (file)`);
524
502
  }
@@ -566,7 +544,7 @@ class ConfigSyncManager {
566
544
  } else {
567
545
  fs.unlinkSync(targetPath);
568
546
  console.log(`[ConfigSyncManager] Removed ${type}/${name} from OpenCode (file)`);
569
- this._cleanupEmptyParents(getNativePathDir(targetPath), targetBaseDir);
547
+ this._cleanupEmptyParents(path.dirname(targetPath), targetBaseDir);
570
548
  }
571
549
 
572
550
  return { success: true };
@@ -766,7 +744,7 @@ class ConfigSyncManager {
766
744
  * OpenCode supports both plural (new) and singular (legacy) folder names.
767
745
  */
768
746
  _getOpenCodeTypeBaseDir(config) {
769
- const modernDir = joinNativeBasePath(this.opencodeDir, config.opencodeTarget);
747
+ const modernDir = path.join(this.opencodeDir, config.opencodeTarget);
770
748
  // 技能目录强制使用 modern/plural 形式,避免 legacy 目录带来的跨平台历史污染
771
749
  if (config === this.configTypes.skills) {
772
750
  return modernDir;
@@ -776,7 +754,7 @@ class ConfigSyncManager {
776
754
  return modernDir;
777
755
  }
778
756
 
779
- const legacyDir = joinNativeBasePath(this.opencodeDir, config.opencodeLegacyTarget);
757
+ const legacyDir = path.join(this.opencodeDir, config.opencodeLegacyTarget);
780
758
  if (fs.existsSync(legacyDir) && !fs.existsSync(modernDir)) {
781
759
  return legacyDir;
782
760
  }
@@ -799,11 +777,12 @@ class ConfigSyncManager {
799
777
  * Clean up empty parent directories up to the base directory
800
778
  */
801
779
  _cleanupEmptyParents(dir, baseDir) {
802
- const normalizedDir = resolveNativeBasePath(dir);
803
- const normalizedBase = resolveNativeBasePath(baseDir);
780
+ // Normalize paths for comparison
781
+ const normalizedDir = path.resolve(dir);
782
+ const normalizedBase = path.resolve(baseDir);
804
783
 
805
784
  // Don't go above base directory
806
- if (!isPathInsideNativeDir(normalizedDir, normalizedBase) || normalizedDir === normalizedBase) {
785
+ if (!normalizedDir.startsWith(normalizedBase) || normalizedDir === normalizedBase) {
807
786
  return;
808
787
  }
809
788
 
@@ -813,7 +792,7 @@ class ConfigSyncManager {
813
792
  fs.rmdirSync(dir);
814
793
  console.log(`[ConfigSyncManager] Removed empty directory: ${dir}`);
815
794
  // Recurse to parent
816
- this._cleanupEmptyParents(getNativePathDir(dir), baseDir);
795
+ this._cleanupEmptyParents(path.dirname(dir), baseDir);
817
796
  }
818
797
  } catch (err) {
819
798
  // Ignore errors (directory might not exist or permission issues)
@@ -831,7 +810,7 @@ class ConfigSyncManager {
831
810
  return null;
832
811
  }
833
812
 
834
- if (isNativeAbsolutePath(raw) || raw.startsWith('/')) {
813
+ if (path.isAbsolute(raw) || raw.startsWith('/')) {
835
814
  return null;
836
815
  }
837
816
 
@@ -881,34 +860,35 @@ class ConfigSyncManager {
881
860
  if (!normalized) return '';
882
861
 
883
862
  if (normalized.startsWith('~/')) {
884
- return joinNativeBasePath(HOME_DIR, normalized.slice(2));
863
+ return path.join(HOME, normalized.slice(2));
885
864
  }
886
865
 
887
- if (isNativeAbsolutePath(normalized)) {
866
+ if (path.isAbsolute(normalized)) {
888
867
  return normalized;
889
868
  }
890
869
 
891
- return resolveNativeBasePath(CODEX_DIR, normalized);
870
+ return path.resolve(path.dirname(CODEX_CONFIG_PATH), normalized);
892
871
  }
893
872
 
894
873
  _isManagedCodexAgentConfigPath(configPath) {
895
874
  const resolved = this._resolveCodexPath(configPath);
896
875
  if (!resolved) return false;
897
876
 
898
- return isPathInsideNativeDir(resolved, this._getCodexManagedAgentConfigDir());
877
+ const managedRoot = path.resolve(this._getCodexManagedAgentConfigDir()) + path.sep;
878
+ return resolved.startsWith(managedRoot) || resolved === path.resolve(this._getCodexManagedAgentConfigDir());
899
879
  }
900
880
 
901
881
  _getCodexManagedAgentConfigDir() {
902
- return NATIVE_PATHS.codex.agents;
882
+ return path.join(this.codexDir, 'agents');
903
883
  }
904
884
 
905
885
  _getCodexManagedAgentConfigPath(fileName) {
906
- return joinNativeBasePath(this._getCodexManagedAgentConfigDir(), `${fileName}.toml`);
886
+ return path.join(this._getCodexManagedAgentConfigDir(), `${fileName}.toml`);
907
887
  }
908
888
 
909
889
  _writeCodexAgentConfigFile(configPath, data) {
910
890
  const resolved = this._resolveCodexPath(configPath);
911
- this._ensureDir(getNativePathDir(resolved));
891
+ this._ensureDir(path.dirname(resolved));
912
892
  const tempPath = `${resolved}.tmp-${process.pid}-${Date.now()}`;
913
893
  fs.writeFileSync(tempPath, tomlStringify(data), 'utf-8');
914
894
  fs.renameSync(tempPath, resolved);
@@ -943,7 +923,7 @@ class ConfigSyncManager {
943
923
  }
944
924
 
945
925
  _writeCodexConfigToml(config) {
946
- this._ensureDir(getNativePathDir(CODEX_CONFIG_PATH));
926
+ this._ensureDir(path.dirname(CODEX_CONFIG_PATH));
947
927
  const tempPath = `${CODEX_CONFIG_PATH}.tmp-${process.pid}-${Date.now()}`;
948
928
  fs.writeFileSync(tempPath, tomlStringify(config), 'utf-8');
949
929
  fs.renameSync(tempPath, CODEX_CONFIG_PATH);
@@ -15,10 +15,10 @@
15
15
 
16
16
  const fs = require('fs');
17
17
  const path = require('path');
18
- const { NATIVE_PATHS, joinNativeBasePath } = require('../../config/paths');
18
+ const { HOME_DIR } = require('../../config/paths');
19
19
 
20
20
  // 全局配置目录
21
- const GLOBAL_CONFIG_DIR = NATIVE_PATHS.claude.dir;
21
+ const GLOBAL_CONFIG_DIR = path.join(HOME_DIR, '.claude');
22
22
 
23
23
  // 配置类型定义
24
24
  const CONFIG_TYPES = {
@@ -96,7 +96,7 @@ class ConfigSyncService {
96
96
  let dir;
97
97
 
98
98
  if (source === 'global') {
99
- dir = joinNativeBasePath(this.globalConfigDir, config.globalDir);
99
+ dir = path.join(this.globalConfigDir, config.globalDir);
100
100
  } else if (source === 'workspace' && projectPath) {
101
101
  if (!config.projectDir) {
102
102
  // skills 不支持项目级
@@ -2,6 +2,7 @@ const fs = require('fs');
2
2
  const path = require('path');
3
3
  const crypto = require('crypto');
4
4
  const { PATHS, NATIVE_PATHS } = require('../../config/paths');
5
+ const { resolveChannelWebsiteUrl } = require('../../config/channel-preset-websites');
5
6
  const { clearNativeOAuth } = require('./native-oauth-adapters');
6
7
  const { normalizeGatewaySourceType } = require('./base/proxy-utils');
7
8
 
@@ -22,7 +23,7 @@ const { normalizeGatewaySourceType } = require('./base/proxy-utils');
22
23
 
23
24
  // 获取 Gemini 配置目录
24
25
  function getGeminiDir() {
25
- return NATIVE_PATHS.gemini.dir;
26
+ return path.dirname(NATIVE_PATHS.gemini.env);
26
27
  }
27
28
 
28
29
  // 获取渠道存储文件路径
@@ -35,7 +36,7 @@ function getChannelsFilePath() {
35
36
  }
36
37
 
37
38
  function readExistingGeminiEnv() {
38
- const envPath = NATIVE_PATHS.gemini.env;
39
+ const envPath = path.join(getGeminiDir(), '.env');
39
40
  if (!fs.existsSync(envPath)) {
40
41
  return {};
41
42
  }
@@ -60,7 +61,7 @@ function readExistingGeminiEnv() {
60
61
  }
61
62
 
62
63
  function writeGeminiEnv(env = {}) {
63
- const envPath = NATIVE_PATHS.gemini.env;
64
+ const envPath = path.join(getGeminiDir(), '.env');
64
65
  const content = Object.entries(env)
65
66
  .map(([key, value]) => `${key}=${value}`)
66
67
  .join('\n');
@@ -73,7 +74,7 @@ function writeGeminiEnv(env = {}) {
73
74
 
74
75
  // 检查是否在代理模式
75
76
  function isProxyConfig() {
76
- const envPath = NATIVE_PATHS.gemini.env;
77
+ const envPath = path.join(getGeminiDir(), '.env');
77
78
  if (!fs.existsSync(envPath)) {
78
79
  return false;
79
80
  }
@@ -108,7 +109,7 @@ function loadChannels() {
108
109
  // 确保渠道有 enabled 字段(兼容旧数据)
109
110
  if (data.channels) {
110
111
  data.channels = data.channels.map(ch => {
111
- return {
112
+ const normalized = {
112
113
  ...ch,
113
114
  enabled: ch.enabled !== false, // 默认启用
114
115
  weight: ch.weight || 1,
@@ -117,6 +118,8 @@ function loadChannels() {
117
118
  speedTestModel: ch.speedTestModel || null,
118
119
  gatewaySourceType: normalizeGatewaySourceType(ch.gatewaySourceType, 'gemini')
119
120
  };
121
+ normalized.websiteUrl = resolveChannelWebsiteUrl('gemini', normalized);
122
+ return normalized;
120
123
  });
121
124
  }
122
125
  return data;
@@ -128,7 +131,7 @@ function loadChannels() {
128
131
 
129
132
  // 从现有 .env 初始化渠道
130
133
  function initializeFromEnv() {
131
- const envPath = NATIVE_PATHS.gemini.env;
134
+ const envPath = path.join(getGeminiDir(), '.env');
132
135
 
133
136
  const defaultData = { channels: [] };
134
137
 
@@ -166,6 +169,7 @@ function initializeFromEnv() {
166
169
  createdAt: Date.now(),
167
170
  updatedAt: Date.now()
168
171
  };
172
+ channel.websiteUrl = resolveChannelWebsiteUrl('gemini', channel);
169
173
 
170
174
  const data = {
171
175
  channels: [channel]
@@ -224,6 +228,7 @@ function createChannel(name, baseUrl, apiKey, model = 'gemini-2.5-pro', extraCon
224
228
  createdAt: Date.now(),
225
229
  updatedAt: Date.now()
226
230
  };
231
+ newChannel.websiteUrl = resolveChannelWebsiteUrl('gemini', newChannel);
227
232
 
228
233
  data.channels.push(newChannel);
229
234
  saveChannels(data);
@@ -267,6 +272,7 @@ function updateChannel(channelId, updates) {
267
272
  gatewaySourceType: normalizeGatewaySourceType(merged.gatewaySourceType, 'gemini'),
268
273
  updatedAt: Date.now()
269
274
  };
275
+ nextChannel.websiteUrl = resolveChannelWebsiteUrl('gemini', nextChannel);
270
276
  data.channels[index] = nextChannel;
271
277
 
272
278
  // Get proxy status
@@ -438,6 +444,10 @@ function getEnabledChannels() {
438
444
  return data.channels.filter(c => c.enabled !== false);
439
445
  }
440
446
 
447
+ function markChannelAsRecentlyUsed(channelId) {
448
+ return updateChannel(channelId, {});
449
+ }
450
+
441
451
  function getEffectiveApiKey(channel) {
442
452
  return channel.apiKey || null;
443
453
  }
@@ -476,6 +486,7 @@ module.exports = {
476
486
  getChannels,
477
487
  createChannel,
478
488
  updateChannel,
489
+ markChannelAsRecentlyUsed,
479
490
  deleteChannel,
480
491
  getEnabledChannels,
481
492
  getEffectiveApiKey,
@@ -1,18 +1,19 @@
1
1
  const fs = require('fs');
2
+ const path = require('path');
2
3
  const { NATIVE_PATHS } = require('../../config/paths');
3
4
 
4
5
  /**
5
6
  * 获取 Gemini 配置目录
6
7
  */
7
8
  function getGeminiDir() {
8
- return NATIVE_PATHS.gemini.dir;
9
+ return path.dirname(NATIVE_PATHS.gemini.env);
9
10
  }
10
11
 
11
12
  /**
12
13
  * 读取 .env 文件
13
14
  */
14
15
  function loadEnv() {
15
- const envPath = NATIVE_PATHS.gemini.env;
16
+ const envPath = path.join(getGeminiDir(), '.env');
16
17
 
17
18
  if (!fs.existsSync(envPath)) {
18
19
  return {};
@@ -43,7 +44,7 @@ function loadEnv() {
43
44
  * 读取 settings.json
44
45
  */
45
46
  function loadSettings() {
46
- const settingsPath = NATIVE_PATHS.gemini.settings;
47
+ const settingsPath = path.join(getGeminiDir(), 'settings.json');
47
48
 
48
49
  if (!fs.existsSync(settingsPath)) {
49
50
  return {};
@@ -1,7 +1,7 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
3
  const crypto = require('crypto');
4
- const { HOME_DIR, NATIVE_PATHS, joinNativeBasePath } = require('../../config/paths');
4
+ const { HOME_DIR } = require('../../config/paths');
5
5
  const { getGeminiDir } = require('./gemini-config');
6
6
 
7
7
  // 路径映射缓存
@@ -13,7 +13,7 @@ const PATH_MAPPING_CACHE_TTL = 60000; // 1分钟缓存
13
13
  * 获取 Gemini tmp 目录(包含所有项目)
14
14
  */
15
15
  function getTmpDir() {
16
- return NATIVE_PATHS?.gemini?.tmp || joinNativeBasePath(getGeminiDir(), 'tmp');
16
+ return path.join(getGeminiDir(), 'tmp');
17
17
  }
18
18
 
19
19
  /**
@@ -63,7 +63,7 @@ function scanDirForHashes(dir, targetHashes, maxDepth, results, currentDepth = 0
63
63
 
64
64
  if (item.isDirectory()) {
65
65
  scanDirForHashes(
66
- joinNativeBasePath(dir, item.name),
66
+ path.join(dir, item.name),
67
67
  targetHashes,
68
68
  maxDepth,
69
69
  results,
@@ -108,17 +108,17 @@ function buildPathMapping() {
108
108
  // 深度说明:depth=3 表示可以扫描到 Desktop/a/b/c 这样的 4 层目录
109
109
  const searchPaths = [
110
110
  { dir: homeDir, depth: 0 }, // 只检查 home 目录本身
111
- { dir: joinNativeBasePath(homeDir, 'Desktop'), depth: 4 }, // Desktop 及 4 层子目录
112
- { dir: joinNativeBasePath(homeDir, 'Documents'), depth: 4 }, // Documents 及 4 层子目录
113
- { dir: joinNativeBasePath(homeDir, 'Downloads'), depth: 3 }, // Downloads 及 3 层子目录
114
- { dir: joinNativeBasePath(homeDir, 'Projects'), depth: 4 }, // Projects 及 4 层子目录
115
- { dir: joinNativeBasePath(homeDir, 'Code'), depth: 4 }, // Code 及 4 层子目录
116
- { dir: joinNativeBasePath(homeDir, 'workspace'), depth: 4 }, // workspace 及 4 层子目录
117
- { dir: joinNativeBasePath(homeDir, 'dev'), depth: 4 }, // dev 及 4 层子目录
118
- { dir: joinNativeBasePath(homeDir, 'src'), depth: 4 }, // src 及 4 层子目录
119
- { dir: joinNativeBasePath(homeDir, 'work'), depth: 4 }, // work 及 4 层子目录
120
- { dir: joinNativeBasePath(homeDir, 'repos'), depth: 4 }, // repos 及 4 层子目录
121
- { dir: joinNativeBasePath(homeDir, 'github'), depth: 4 }, // github 及 4 层子目录
111
+ { dir: path.join(homeDir, 'Desktop'), depth: 4 }, // Desktop 及 4 层子目录
112
+ { dir: path.join(homeDir, 'Documents'), depth: 4 }, // Documents 及 4 层子目录
113
+ { dir: path.join(homeDir, 'Downloads'), depth: 3 }, // Downloads 及 3 层子目录
114
+ { dir: path.join(homeDir, 'Projects'), depth: 4 }, // Projects 及 4 层子目录
115
+ { dir: path.join(homeDir, 'Code'), depth: 4 }, // Code 及 4 层子目录
116
+ { dir: path.join(homeDir, 'workspace'), depth: 4 }, // workspace 及 4 层子目录
117
+ { dir: path.join(homeDir, 'dev'), depth: 4 }, // dev 及 4 层子目录
118
+ { dir: path.join(homeDir, 'src'), depth: 4 }, // src 及 4 层子目录
119
+ { dir: path.join(homeDir, 'work'), depth: 4 }, // work 及 4 层子目录
120
+ { dir: path.join(homeDir, 'repos'), depth: 4 }, // repos 及 4 层子目录
121
+ { dir: path.join(homeDir, 'github'), depth: 4 }, // github 及 4 层子目录
122
122
  ];
123
123
 
124
124
  for (const { dir, depth } of searchPaths) {
@@ -164,7 +164,7 @@ function scanProjects() {
164
164
  * @returns {Array} 会话文件路径数组
165
165
  */
166
166
  function scanProjectSessions(projectHash) {
167
- const chatsDir = joinNativeBasePath(getTmpDir(), projectHash, 'chats');
167
+ const chatsDir = path.join(getTmpDir(), projectHash, 'chats');
168
168
 
169
169
  if (!fs.existsSync(chatsDir)) {
170
170
  return [];
@@ -175,7 +175,7 @@ function scanProjectSessions(projectHash) {
175
175
  return entries
176
176
  .filter(entry => entry.isFile() && entry.name.match(/^session-.*\.json$/))
177
177
  .map(entry => {
178
- const filePath = joinNativeBasePath(chatsDir, entry.name);
178
+ const filePath = path.join(chatsDir, entry.name);
179
179
  // 文件名格式:session-2025-11-23T02-09-87570eb4.json
180
180
  // session-{timestamp}-{shortId}.json
181
181
  const match = entry.name.match(/^session-(.*)-([a-f0-9]+)\.json$/);
@@ -451,7 +451,7 @@ function deleteSession(sessionId) {
451
451
  * @returns {Object} 删除结果
452
452
  */
453
453
  function deleteProject(projectHash) {
454
- const projectDir = joinNativeBasePath(getTmpDir(), projectHash);
454
+ const projectDir = path.join(getTmpDir(), projectHash);
455
455
 
456
456
  if (!fs.existsSync(projectDir)) {
457
457
  throw new Error('Project not found');
@@ -604,7 +604,7 @@ function saveSessionOrder(projectHash, order) {
604
604
  * @param {string} sessionId - 原会话 ID
605
605
  * @returns {Object} Fork 结果
606
606
  */
607
- function forkSession(sessionId) {
607
+ function forkSession(sessionId, options = {}) {
608
608
  const allSessions = getAllSessions();
609
609
  const sourceSession = allSessions.find(s => s.sessionId === sessionId);
610
610
 
@@ -618,6 +618,27 @@ function forkSession(sessionId) {
618
618
  throw new Error('Failed to read source session');
619
619
  }
620
620
 
621
+ const sourceMessages = Array.isArray(fullSession.messages) ? fullSession.messages : [];
622
+ let truncatedMessages = sourceMessages;
623
+ if (Number.isInteger(options.afterUserMessageNumber) && options.afterUserMessageNumber > 0) {
624
+ let matchedUserMessages = 0;
625
+ let stopIndex = -1;
626
+ for (let index = 0; index < sourceMessages.length; index += 1) {
627
+ if (sourceMessages[index]?.type !== 'user') continue;
628
+ matchedUserMessages += 1;
629
+ if (matchedUserMessages >= options.afterUserMessageNumber) {
630
+ stopIndex = index;
631
+ break;
632
+ }
633
+ }
634
+
635
+ if (stopIndex < 0) {
636
+ throw new Error(`afterUserMessageNumber ${options.afterUserMessageNumber} exceeds available user messages (${matchedUserMessages})`);
637
+ }
638
+
639
+ truncatedMessages = sourceMessages.slice(0, stopIndex + 1);
640
+ }
641
+
621
642
  // 生成新的会话 ID
622
643
  const newSessionId = crypto.randomUUID();
623
644
  const timestamp = new Date().toISOString().replace(/:/g, '-').replace(/\..+/, '');
@@ -630,21 +651,28 @@ function forkSession(sessionId) {
630
651
  sessionId: newSessionId,
631
652
  startTime: new Date().toISOString(),
632
653
  lastUpdated: new Date().toISOString(),
633
- forkedFrom: sessionId
654
+ forkedFrom: sessionId,
655
+ messages: truncatedMessages
634
656
  };
635
657
 
636
658
  // 写入新文件
637
- const chatsDir = joinNativeBasePath(getTmpDir(), sourceSession.projectHash, 'chats');
638
- const newFilePath = joinNativeBasePath(chatsDir, newFileName);
659
+ const chatsDir = path.join(getTmpDir(), sourceSession.projectHash, 'chats');
660
+ const newFilePath = path.join(chatsDir, newFileName);
639
661
 
640
662
  try {
641
663
  fs.writeFileSync(newFilePath, JSON.stringify(newSession, null, 2), 'utf8');
664
+ if (options.alias) {
665
+ const { setAlias } = require('./alias');
666
+ setAlias(newSessionId, options.alias);
667
+ }
642
668
 
643
669
  return {
644
670
  success: true,
645
671
  sessionId: newSessionId,
646
672
  filePath: newFilePath,
647
- forkedFrom: sessionId
673
+ forkedFrom: sessionId,
674
+ alias: options.alias || null,
675
+ afterUserMessageNumber: options.afterUserMessageNumber || null
648
676
  };
649
677
  } catch (err) {
650
678
  throw new Error('Failed to fork session: ' + err.message);
@@ -685,8 +713,5 @@ module.exports = {
685
713
  saveSessionOrder,
686
714
  forkSession,
687
715
  getProjectPath,
688
- getProjectAndSessionCounts,
689
- _test: {
690
- getTmpDir
691
- }
716
+ getProjectAndSessionCounts
692
717
  };
@@ -1,4 +1,5 @@
1
1
  const fs = require('fs');
2
+ const path = require('path');
2
3
  const { NATIVE_PATHS } = require('../../config/paths');
3
4
 
4
5
  // Gemini 配置文件路径
@@ -7,7 +8,7 @@ function getEnvPath() {
7
8
  }
8
9
 
9
10
  function getSettingsPath() {
10
- return NATIVE_PATHS.gemini.settings;
11
+ return path.join(path.dirname(NATIVE_PATHS.gemini.env), 'settings.json');
11
12
  }
12
13
 
13
14
  // 备份文件路径
@@ -16,7 +17,7 @@ function getEnvBackupPath() {
16
17
  }
17
18
 
18
19
  function getSettingsBackupPath() {
19
- return NATIVE_PATHS.gemini.settingsBackup;
20
+ return path.join(path.dirname(NATIVE_PATHS.gemini.env), 'settings.json.cc-tool-backup');
20
21
  }
21
22
 
22
23
  // 检查配置文件是否存在