coding-tool-x 3.5.9 → 3.5.11
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/CHANGELOG.md +11 -0
- package/README.md +4 -25
- package/bin/ctx.js +1 -6
- package/dist/web/assets/{Analytics-C5W3axXs.js → Analytics-Bw-0r4Ys.js} +2 -2
- package/dist/web/assets/Analytics-RNn1BUbG.css +1 -0
- package/dist/web/assets/{ConfigTemplates-DzyVFDx9.js → ConfigTemplates-C6QRIeDj.js} +1 -1
- package/dist/web/assets/Home-BQxQ1LhR.css +1 -0
- package/dist/web/assets/{Home-C9TQNB6f.js → Home-B_Z9ythw.js} +1 -1
- package/dist/web/assets/{PluginManager-9B_brLWT.js → PluginManager-Ce-mws6Q.js} +1 -1
- package/dist/web/assets/ProjectList-9Z7FFCIT.js +1 -0
- package/dist/web/assets/ProjectList-DL4JK6ci.css +1 -0
- package/dist/web/assets/SessionList-CyETQcs8.js +1 -0
- package/dist/web/assets/SessionList-D12muXvx.css +1 -0
- package/dist/web/assets/{SkillManager-vST8DRRg.js → SkillManager-DSUcP1se.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-ov1KgRXR.js → WorkspaceManager-Cvbc11bJ.js} +1 -1
- package/dist/web/assets/icons-lKCTySl7.js +1 -0
- package/dist/web/assets/index-BS7aplYp.css +1 -0
- package/dist/web/assets/index-CTeg1xG-.js +2 -0
- package/dist/web/assets/{markdown-DyTJGI4N.js → markdown-BNJfEgCA.js} +1 -1
- package/dist/web/assets/{naive-ui-Cg4_ZeoT.js → naive-ui-DM897mIP.js} +1 -1
- package/dist/web/assets/vendors-DQA4Hw9v.js +7 -0
- package/dist/web/assets/vue-vendor-B5RqjEjQ.js +45 -0
- package/dist/web/index.html +8 -8
- package/package.json +1 -2
- package/src/commands/daemon.js +5 -33
- package/src/commands/export-config.js +6 -6
- package/src/commands/stats.js +4 -41
- package/src/commands/ui.js +3 -12
- package/src/config/channel-preset-websites.js +114 -0
- package/src/config/default.js +9 -5
- package/src/config/loader.js +2 -2
- package/src/config/model-metadata.json +16 -20
- package/src/config/paths.js +33 -166
- package/src/index.js +34 -125
- package/src/server/api/agents.js +2 -52
- package/src/server/api/codex-channels.js +4 -0
- package/src/server/api/codex-proxy.js +45 -2
- package/src/server/api/codex-sessions.js +89 -12
- package/src/server/api/commands.js +2 -38
- package/src/server/api/dashboard.js +1 -25
- package/src/server/api/gemini-proxy.js +4 -2
- package/src/server/api/gemini-sessions.js +86 -9
- package/src/server/api/hooks.js +1 -17
- package/src/server/api/opencode-proxy.js +7 -2
- package/src/server/api/opencode-sessions.js +83 -10
- package/src/server/api/plugins.js +34 -128
- package/src/server/api/proxy.js +5 -3
- package/src/server/api/sessions.js +181 -27
- package/src/server/api/workspaces.js +0 -14
- package/src/server/index.js +9 -31
- package/src/server/services/agents-service.js +62 -269
- package/src/server/services/base/base-channel-service.js +3 -1
- package/src/server/services/channels.js +5 -0
- package/src/server/services/codex-channels.js +2 -0
- package/src/server/services/codex-sessions.js +88 -108
- package/src/server/services/commands-service.js +81 -281
- package/src/server/services/config-export-service.js +7 -7
- package/src/server/services/config-registry-service.js +9 -8
- package/src/server/services/config-sync-manager.js +41 -61
- package/src/server/services/config-sync-service.js +3 -3
- package/src/server/services/gemini-channels.js +17 -6
- package/src/server/services/gemini-config.js +4 -3
- package/src/server/services/gemini-sessions.js +59 -26
- package/src/server/services/gemini-settings-manager.js +3 -2
- package/src/server/services/mcp-service.js +14 -9
- package/src/server/services/native-oauth-adapters.js +3 -3
- package/src/server/services/network-access.js +2 -61
- package/src/server/services/notification-hooks.js +19 -184
- package/src/server/services/opencode-channels.js +9 -0
- package/src/server/services/opencode-sessions.js +6 -16
- package/src/server/services/opencode-settings-manager.js +3 -3
- package/src/server/services/plugins-service.js +62 -996
- package/src/server/services/prompts-service.js +9 -5
- package/src/server/services/sessions.js +171 -183
- package/src/server/services/skill-service.js +18 -155
- package/src/server/websocket-server.js +4 -35
- package/dist/web/assets/Analytics-vQS5IWvs.css +0 -1
- package/dist/web/assets/Home-qzk118Of.css +0 -1
- package/dist/web/assets/ProjectList-Bjt6mrsV.js +0 -1
- package/dist/web/assets/ProjectList-GCC2QOmq.css +0 -1
- package/dist/web/assets/SessionList-BsHPgmUR.css +0 -1
- package/dist/web/assets/SessionList-DcBH13uA.js +0 -1
- package/dist/web/assets/icons-CEq2hYB-.js +0 -1
- package/dist/web/assets/index-Dih_bOsv.css +0 -1
- package/dist/web/assets/index-Duc7QP4e.js +0 -2
- package/dist/web/assets/vendors-Bsp-dq2d.js +0 -7
- package/dist/web/assets/vue-vendor-BxIT0uQq.js +0 -45
- package/docs/Caddyfile.example +0 -19
- package/docs/reverse-proxy-https.md +0 -57
- package/src/server/services/https-cert.js +0 -171
- package/src/server/services/session-launch-command.js +0 -58
- 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 =
|
|
37
|
-
const CODEX_DIR =
|
|
38
|
-
const GEMINI_DIR =
|
|
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 =
|
|
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(
|
|
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(
|
|
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 =
|
|
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(
|
|
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 =
|
|
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 =
|
|
314
|
-
this._ensureDir(
|
|
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 =
|
|
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(
|
|
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 =
|
|
437
|
-
this._ensureDir(
|
|
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 =
|
|
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 =
|
|
492
|
+
const targetPath = path.join(targetBaseDir, safeName);
|
|
515
493
|
|
|
516
494
|
if (config.isDirectory) {
|
|
517
|
-
this._ensureDir(
|
|
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(
|
|
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(
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
803
|
-
const
|
|
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 (!
|
|
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(
|
|
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 (
|
|
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
|
|
863
|
+
return path.join(HOME, normalized.slice(2));
|
|
885
864
|
}
|
|
886
865
|
|
|
887
|
-
if (
|
|
866
|
+
if (path.isAbsolute(normalized)) {
|
|
888
867
|
return normalized;
|
|
889
868
|
}
|
|
890
869
|
|
|
891
|
-
return
|
|
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
|
-
|
|
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
|
|
882
|
+
return path.join(this.codexDir, 'agents');
|
|
903
883
|
}
|
|
904
884
|
|
|
905
885
|
_getCodexManagedAgentConfigPath(fileName) {
|
|
906
|
-
return
|
|
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(
|
|
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(
|
|
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 {
|
|
18
|
+
const { HOME_DIR } = require('../../config/paths');
|
|
19
19
|
|
|
20
20
|
// 全局配置目录
|
|
21
|
-
const GLOBAL_CONFIG_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 =
|
|
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.
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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.
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
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:
|
|
112
|
-
{ dir:
|
|
113
|
-
{ dir:
|
|
114
|
-
{ dir:
|
|
115
|
-
{ dir:
|
|
116
|
-
{ dir:
|
|
117
|
-
{ dir:
|
|
118
|
-
{ dir:
|
|
119
|
-
{ dir:
|
|
120
|
-
{ dir:
|
|
121
|
-
{ dir:
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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,35 @@ 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 targetUserIndex = -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
|
+
targetUserIndex = index;
|
|
631
|
+
break;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
if (targetUserIndex < 0) {
|
|
636
|
+
throw new Error(`afterUserMessageNumber ${options.afterUserMessageNumber} exceeds available user messages (${matchedUserMessages})`);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
let nextUserIndex = sourceMessages.length;
|
|
640
|
+
for (let index = targetUserIndex + 1; index < sourceMessages.length; index += 1) {
|
|
641
|
+
if (sourceMessages[index]?.type === 'user') {
|
|
642
|
+
nextUserIndex = index;
|
|
643
|
+
break;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
truncatedMessages = sourceMessages.slice(0, nextUserIndex);
|
|
648
|
+
}
|
|
649
|
+
|
|
621
650
|
// 生成新的会话 ID
|
|
622
651
|
const newSessionId = crypto.randomUUID();
|
|
623
652
|
const timestamp = new Date().toISOString().replace(/:/g, '-').replace(/\..+/, '');
|
|
@@ -630,21 +659,28 @@ function forkSession(sessionId) {
|
|
|
630
659
|
sessionId: newSessionId,
|
|
631
660
|
startTime: new Date().toISOString(),
|
|
632
661
|
lastUpdated: new Date().toISOString(),
|
|
633
|
-
forkedFrom: sessionId
|
|
662
|
+
forkedFrom: sessionId,
|
|
663
|
+
messages: truncatedMessages
|
|
634
664
|
};
|
|
635
665
|
|
|
636
666
|
// 写入新文件
|
|
637
|
-
const chatsDir =
|
|
638
|
-
const newFilePath =
|
|
667
|
+
const chatsDir = path.join(getTmpDir(), sourceSession.projectHash, 'chats');
|
|
668
|
+
const newFilePath = path.join(chatsDir, newFileName);
|
|
639
669
|
|
|
640
670
|
try {
|
|
641
671
|
fs.writeFileSync(newFilePath, JSON.stringify(newSession, null, 2), 'utf8');
|
|
672
|
+
if (options.alias) {
|
|
673
|
+
const { setAlias } = require('./alias');
|
|
674
|
+
setAlias(newSessionId, options.alias);
|
|
675
|
+
}
|
|
642
676
|
|
|
643
677
|
return {
|
|
644
678
|
success: true,
|
|
645
679
|
sessionId: newSessionId,
|
|
646
680
|
filePath: newFilePath,
|
|
647
|
-
forkedFrom: sessionId
|
|
681
|
+
forkedFrom: sessionId,
|
|
682
|
+
alias: options.alias || null,
|
|
683
|
+
afterUserMessageNumber: options.afterUserMessageNumber || null
|
|
648
684
|
};
|
|
649
685
|
} catch (err) {
|
|
650
686
|
throw new Error('Failed to fork session: ' + err.message);
|
|
@@ -685,8 +721,5 @@ module.exports = {
|
|
|
685
721
|
saveSessionOrder,
|
|
686
722
|
forkSession,
|
|
687
723
|
getProjectPath,
|
|
688
|
-
getProjectAndSessionCounts
|
|
689
|
-
_test: {
|
|
690
|
-
getTmpDir
|
|
691
|
-
}
|
|
724
|
+
getProjectAndSessionCounts
|
|
692
725
|
};
|
|
@@ -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.
|
|
20
|
+
return path.join(path.dirname(NATIVE_PATHS.gemini.env), 'settings.json.cc-tool-backup');
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
// 检查配置文件是否存在
|