@adversity/coding-tool-x 3.0.0 → 3.0.2
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 +31 -0
- package/dist/web/assets/{index-AtwYwBZD.js → index-DfPKAt9R.js} +2 -2
- package/dist/web/assets/{index-BNHWEpD4.css → index-TjhcaFRe.css} +1 -1
- package/dist/web/assets/naive-ui-B1TP-0TP.js +1 -0
- package/dist/web/index.html +3 -3
- package/package.json +1 -1
- package/src/server/api/channels.js +52 -1
- package/src/server/api/config-export.js +7 -1
- package/src/server/api/proxy.js +63 -6
- package/src/server/services/channels.js +47 -3
- package/src/server/services/codex-channels.js +31 -0
- package/src/server/services/config-export-service.js +324 -1
- package/src/server/services/gemini-channels.js +95 -5
- package/src/server/services/model-detector.js +269 -0
- package/src/server/services/speed-test.js +72 -24
- package/dist/web/assets/naive-ui-BcSq2wzw.js +0 -1
|
@@ -15,13 +15,28 @@ const { CommandsService } = require('./commands-service');
|
|
|
15
15
|
const { RulesService } = require('./rules-service');
|
|
16
16
|
const { SkillService } = require('./skill-service');
|
|
17
17
|
|
|
18
|
-
const CONFIG_VERSION = '1.
|
|
18
|
+
const CONFIG_VERSION = '1.2.0';
|
|
19
19
|
const SKILL_FILE_ENCODING = 'base64';
|
|
20
20
|
const SKILL_IGNORE_DIRS = new Set(['.git']);
|
|
21
21
|
const SKILL_IGNORE_FILES = new Set(['.DS_Store']);
|
|
22
22
|
const CC_TOOL_DIR = path.join(os.homedir(), '.claude', 'cc-tool');
|
|
23
23
|
const LEGACY_CC_TOOL_DIR = path.join(os.homedir(), '.cc-tool');
|
|
24
24
|
const CLAUDE_SETTINGS_PATH = path.join(os.homedir(), '.claude', 'settings.json');
|
|
25
|
+
const LEGACY_PLUGINS_DIR = path.join(LEGACY_CC_TOOL_DIR, 'plugins', 'installed');
|
|
26
|
+
const LEGACY_PLUGINS_REGISTRY = path.join(LEGACY_CC_TOOL_DIR, 'plugins', 'registry.json');
|
|
27
|
+
const NATIVE_PLUGINS_REGISTRY = path.join(os.homedir(), '.claude', 'plugins', 'installed_plugins.json');
|
|
28
|
+
const PLUGIN_IGNORE_DIRS = new Set(['.git', 'node_modules', '.DS_Store']);
|
|
29
|
+
const PLUGIN_IGNORE_FILES = new Set(['.DS_Store']);
|
|
30
|
+
const PLUGIN_SENSITIVE_PATTERNS = [
|
|
31
|
+
/\.env$/i,
|
|
32
|
+
/\.env\./i,
|
|
33
|
+
/credentials?\.json$/i,
|
|
34
|
+
/secrets?\.json$/i,
|
|
35
|
+
/\.key$/i,
|
|
36
|
+
/\.pem$/i,
|
|
37
|
+
/\.p12$/i,
|
|
38
|
+
/\.pfx$/i
|
|
39
|
+
];
|
|
25
40
|
const CC_UI_CONFIG_PATH = path.join(CC_TOOL_DIR, 'ui-config.json');
|
|
26
41
|
const CC_TERMINAL_CONFIG_PATH = path.join(CC_TOOL_DIR, 'terminal-config.json');
|
|
27
42
|
const CC_TERMINAL_COMMANDS_PATH = path.join(CC_TOOL_DIR, 'terminal-commands.json');
|
|
@@ -108,6 +123,7 @@ function buildExportReadme(exportData) {
|
|
|
108
123
|
## 📦 包含内容
|
|
109
124
|
- 权限模板、配置模板、频道配置、工作区、收藏
|
|
110
125
|
- Agents / Skills / Commands / Rules
|
|
126
|
+
- 插件 (Plugins)
|
|
111
127
|
- MCP 服务器配置
|
|
112
128
|
- UI 配置(主题、面板显示、排序等)
|
|
113
129
|
- 终端配置与 CLI 命令配置
|
|
@@ -224,6 +240,56 @@ function collectSkillFiles(baseDir) {
|
|
|
224
240
|
return files;
|
|
225
241
|
}
|
|
226
242
|
|
|
243
|
+
function collectPluginFiles(pluginDir, basePath = '') {
|
|
244
|
+
const files = [];
|
|
245
|
+
const stack = [pluginDir];
|
|
246
|
+
|
|
247
|
+
while (stack.length > 0) {
|
|
248
|
+
const currentDir = stack.pop();
|
|
249
|
+
let entries = [];
|
|
250
|
+
try {
|
|
251
|
+
entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
252
|
+
} catch (err) {
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
for (const entry of entries) {
|
|
257
|
+
if (entry.isDirectory()) {
|
|
258
|
+
if (PLUGIN_IGNORE_DIRS.has(entry.name)) {
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
stack.push(path.join(currentDir, entry.name));
|
|
262
|
+
} else if (entry.isFile()) {
|
|
263
|
+
if (PLUGIN_IGNORE_FILES.has(entry.name)) {
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
267
|
+
const relativePath = path.relative(pluginDir, fullPath);
|
|
268
|
+
|
|
269
|
+
// Skip sensitive files
|
|
270
|
+
const isSensitive = PLUGIN_SENSITIVE_PATTERNS.some(pattern => pattern.test(entry.name));
|
|
271
|
+
if (isSensitive) {
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
try {
|
|
276
|
+
const content = fs.readFileSync(fullPath);
|
|
277
|
+
files.push({
|
|
278
|
+
path: relativePath,
|
|
279
|
+
encoding: SKILL_FILE_ENCODING,
|
|
280
|
+
content: content.toString(SKILL_FILE_ENCODING)
|
|
281
|
+
});
|
|
282
|
+
} catch (err) {
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
files.sort((a, b) => a.path.localeCompare(b.path));
|
|
290
|
+
return files;
|
|
291
|
+
}
|
|
292
|
+
|
|
227
293
|
function exportSkillsSnapshot() {
|
|
228
294
|
const skillService = new SkillService();
|
|
229
295
|
const installedSkills = skillService.getInstalledSkills();
|
|
@@ -244,6 +310,113 @@ function exportSkillsSnapshot() {
|
|
|
244
310
|
}).filter(Boolean);
|
|
245
311
|
}
|
|
246
312
|
|
|
313
|
+
function exportLegacyPlugins() {
|
|
314
|
+
const plugins = [];
|
|
315
|
+
|
|
316
|
+
// Check if legacy plugins directory exists
|
|
317
|
+
if (!fs.existsSync(LEGACY_PLUGINS_DIR)) {
|
|
318
|
+
return plugins;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Read registry.json if it exists
|
|
322
|
+
const registry = readJsonFileSafe(LEGACY_PLUGINS_REGISTRY) || {};
|
|
323
|
+
|
|
324
|
+
try {
|
|
325
|
+
const pluginDirs = fs.readdirSync(LEGACY_PLUGINS_DIR, { withFileTypes: true });
|
|
326
|
+
|
|
327
|
+
for (const entry of pluginDirs) {
|
|
328
|
+
if (!entry.isDirectory()) {
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const pluginName = entry.name;
|
|
333
|
+
const pluginDir = path.join(LEGACY_PLUGINS_DIR, pluginName);
|
|
334
|
+
const manifestPath = path.join(pluginDir, 'plugin.json');
|
|
335
|
+
|
|
336
|
+
// Read plugin.json manifest
|
|
337
|
+
const manifest = readJsonFileSafe(manifestPath);
|
|
338
|
+
if (!manifest) {
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Collect all files in the plugin directory
|
|
343
|
+
const files = collectPluginFiles(pluginDir);
|
|
344
|
+
|
|
345
|
+
plugins.push({
|
|
346
|
+
type: 'legacy',
|
|
347
|
+
name: manifest.name || pluginName,
|
|
348
|
+
version: manifest.version || '1.0.0',
|
|
349
|
+
description: manifest.description || '',
|
|
350
|
+
author: manifest.author || '',
|
|
351
|
+
directory: pluginName,
|
|
352
|
+
manifest: manifest,
|
|
353
|
+
files: files,
|
|
354
|
+
registryEntry: registry[pluginName] || null
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
} catch (err) {
|
|
358
|
+
console.warn('[ConfigExport] Failed to export legacy plugins:', err.message);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return plugins;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function exportNativePlugins() {
|
|
365
|
+
const plugins = [];
|
|
366
|
+
|
|
367
|
+
// Read installed_plugins.json
|
|
368
|
+
const installedPlugins = readJsonFileSafe(NATIVE_PLUGINS_REGISTRY);
|
|
369
|
+
if (!installedPlugins || typeof installedPlugins !== 'object') {
|
|
370
|
+
return plugins;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
try {
|
|
374
|
+
for (const [pluginId, pluginInfo] of Object.entries(installedPlugins)) {
|
|
375
|
+
if (!pluginInfo || !pluginInfo.installPath) {
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const pluginDir = pluginInfo.installPath;
|
|
380
|
+
if (!fs.existsSync(pluginDir)) {
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Read package.json manifest
|
|
385
|
+
const manifestPath = path.join(pluginDir, 'package.json');
|
|
386
|
+
const manifest = readJsonFileSafe(manifestPath);
|
|
387
|
+
if (!manifest) {
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Collect all files in the plugin directory
|
|
392
|
+
const files = collectPluginFiles(pluginDir);
|
|
393
|
+
|
|
394
|
+
plugins.push({
|
|
395
|
+
type: 'native',
|
|
396
|
+
id: pluginId,
|
|
397
|
+
name: manifest.name || pluginId,
|
|
398
|
+
version: manifest.version || '1.0.0',
|
|
399
|
+
description: manifest.description || '',
|
|
400
|
+
author: manifest.author || '',
|
|
401
|
+
installPath: pluginDir,
|
|
402
|
+
manifest: manifest,
|
|
403
|
+
files: files,
|
|
404
|
+
registryEntry: pluginInfo
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
} catch (err) {
|
|
408
|
+
console.warn('[ConfigExport] Failed to export native plugins:', err.message);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return plugins;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function exportPluginsSnapshot() {
|
|
415
|
+
const legacyPlugins = exportLegacyPlugins();
|
|
416
|
+
const nativePlugins = exportNativePlugins();
|
|
417
|
+
return [...legacyPlugins, ...nativePlugins];
|
|
418
|
+
}
|
|
419
|
+
|
|
247
420
|
function writeTextFile(baseDir, relativePath, content, overwrite) {
|
|
248
421
|
if (!content && content !== '') {
|
|
249
422
|
return 'failed';
|
|
@@ -332,6 +505,9 @@ function exportAllConfigs() {
|
|
|
332
505
|
const mcpService = require('./mcp-service');
|
|
333
506
|
const mcpServers = mcpService.getAllServers();
|
|
334
507
|
|
|
508
|
+
// 获取 Plugins 配置
|
|
509
|
+
const plugins = exportPluginsSnapshot();
|
|
510
|
+
|
|
335
511
|
// 读取 Markdown 配置文件
|
|
336
512
|
const { PATHS } = require('../../config/paths');
|
|
337
513
|
const markdownFiles = {};
|
|
@@ -383,6 +559,7 @@ function exportAllConfigs() {
|
|
|
383
559
|
commands: commands || [],
|
|
384
560
|
rules: rules || [],
|
|
385
561
|
mcpServers: mcpServers || [],
|
|
562
|
+
plugins: plugins || [],
|
|
386
563
|
markdownFiles: markdownFiles,
|
|
387
564
|
uiConfig: uiConfig,
|
|
388
565
|
terminalConfig: terminalConfig,
|
|
@@ -450,6 +627,7 @@ function importConfigs(importData, options = {}) {
|
|
|
450
627
|
commands: { success: 0, failed: 0, skipped: 0 },
|
|
451
628
|
rules: { success: 0, failed: 0, skipped: 0 },
|
|
452
629
|
mcpServers: { success: 0, failed: 0, skipped: 0 },
|
|
630
|
+
plugins: { success: 0, failed: 0, skipped: 0 },
|
|
453
631
|
markdownFiles: { success: 0, failed: 0, skipped: 0 },
|
|
454
632
|
uiConfig: { success: 0, failed: 0, skipped: 0 },
|
|
455
633
|
terminalConfig: { success: 0, failed: 0, skipped: 0 },
|
|
@@ -675,6 +853,143 @@ function importConfigs(importData, options = {}) {
|
|
|
675
853
|
}
|
|
676
854
|
}
|
|
677
855
|
|
|
856
|
+
// 导入 Plugins
|
|
857
|
+
if (importData.data.plugins && importData.data.plugins.length > 0) {
|
|
858
|
+
const plugins = importData.data.plugins;
|
|
859
|
+
|
|
860
|
+
try {
|
|
861
|
+
for (const plugin of plugins) {
|
|
862
|
+
try {
|
|
863
|
+
const pluginType = plugin.type || 'legacy';
|
|
864
|
+
|
|
865
|
+
// Determine target directory based on plugin type
|
|
866
|
+
let targetDir;
|
|
867
|
+
let registryPath;
|
|
868
|
+
|
|
869
|
+
if (pluginType === 'legacy') {
|
|
870
|
+
targetDir = path.join(LEGACY_PLUGINS_DIR, plugin.directory || plugin.name);
|
|
871
|
+
registryPath = LEGACY_PLUGINS_REGISTRY;
|
|
872
|
+
} else if (pluginType === 'native') {
|
|
873
|
+
// SECURITY: Never trust installPath from import data - construct it safely
|
|
874
|
+
const pluginId = plugin.id || plugin.name;
|
|
875
|
+
if (!pluginId) {
|
|
876
|
+
console.warn('[ConfigImport] Native plugin missing id/name, skipping');
|
|
877
|
+
results.plugins.failed++;
|
|
878
|
+
continue;
|
|
879
|
+
}
|
|
880
|
+
targetDir = path.join(os.homedir(), '.claude', 'plugins', pluginId);
|
|
881
|
+
registryPath = NATIVE_PLUGINS_REGISTRY;
|
|
882
|
+
} else {
|
|
883
|
+
console.warn(`[ConfigImport] Unknown plugin type: ${pluginType}`);
|
|
884
|
+
results.plugins.failed++;
|
|
885
|
+
continue;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
// Check if plugin exists
|
|
889
|
+
if (fs.existsSync(targetDir)) {
|
|
890
|
+
if (!overwrite) {
|
|
891
|
+
results.plugins.skipped++;
|
|
892
|
+
continue;
|
|
893
|
+
}
|
|
894
|
+
// Remove existing plugin directory
|
|
895
|
+
fs.rmSync(targetDir, { recursive: true, force: true });
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// Create plugin directory
|
|
899
|
+
ensureDir(targetDir);
|
|
900
|
+
|
|
901
|
+
// Write all plugin files from base64 content
|
|
902
|
+
const files = Array.isArray(plugin.files) ? plugin.files : [];
|
|
903
|
+
let failed = false;
|
|
904
|
+
|
|
905
|
+
for (const file of files) {
|
|
906
|
+
const filePath = resolveSafePath(targetDir, file.path);
|
|
907
|
+
if (!filePath) {
|
|
908
|
+
failed = true;
|
|
909
|
+
break;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
ensureDir(path.dirname(filePath));
|
|
913
|
+
|
|
914
|
+
try {
|
|
915
|
+
if (file.encoding === SKILL_FILE_ENCODING) {
|
|
916
|
+
fs.writeFileSync(filePath, Buffer.from(file.content || '', SKILL_FILE_ENCODING));
|
|
917
|
+
} else {
|
|
918
|
+
fs.writeFileSync(filePath, file.content || '', file.encoding || 'utf8');
|
|
919
|
+
}
|
|
920
|
+
} catch (err) {
|
|
921
|
+
console.error(`[ConfigImport] Failed to write plugin file ${file.path}:`, err);
|
|
922
|
+
failed = true;
|
|
923
|
+
break;
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
if (failed || files.length === 0) {
|
|
928
|
+
results.plugins.failed++;
|
|
929
|
+
continue;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// Update appropriate registry
|
|
933
|
+
try {
|
|
934
|
+
ensureDir(path.dirname(registryPath));
|
|
935
|
+
|
|
936
|
+
if (pluginType === 'legacy') {
|
|
937
|
+
// Update legacy registry.json
|
|
938
|
+
const registry = readJsonFileSafe(registryPath) || {};
|
|
939
|
+
const pluginKey = plugin.directory || plugin.name;
|
|
940
|
+
|
|
941
|
+
if (plugin.registryEntry) {
|
|
942
|
+
registry[pluginKey] = plugin.registryEntry;
|
|
943
|
+
} else {
|
|
944
|
+
registry[pluginKey] = {
|
|
945
|
+
name: plugin.name,
|
|
946
|
+
version: plugin.version,
|
|
947
|
+
description: plugin.description,
|
|
948
|
+
author: plugin.author,
|
|
949
|
+
installedAt: new Date().toISOString()
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
fs.writeFileSync(registryPath, JSON.stringify(registry, null, 2), 'utf8');
|
|
954
|
+
} else if (pluginType === 'native') {
|
|
955
|
+
// Update native installed_plugins.json
|
|
956
|
+
const installedPlugins = readJsonFileSafe(registryPath) || {};
|
|
957
|
+
const pluginId = plugin.id || plugin.name;
|
|
958
|
+
|
|
959
|
+
if (plugin.registryEntry) {
|
|
960
|
+
installedPlugins[pluginId] = {
|
|
961
|
+
...plugin.registryEntry,
|
|
962
|
+
installPath: targetDir
|
|
963
|
+
};
|
|
964
|
+
} else {
|
|
965
|
+
installedPlugins[pluginId] = {
|
|
966
|
+
name: plugin.name,
|
|
967
|
+
version: plugin.version,
|
|
968
|
+
description: plugin.description,
|
|
969
|
+
author: plugin.author,
|
|
970
|
+
installPath: targetDir,
|
|
971
|
+
installedAt: new Date().toISOString()
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
fs.writeFileSync(registryPath, JSON.stringify(installedPlugins, null, 2), 'utf8');
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
results.plugins.success++;
|
|
979
|
+
} catch (err) {
|
|
980
|
+
console.error(`[ConfigImport] Failed to update plugin registry for ${plugin.name}:`, err);
|
|
981
|
+
results.plugins.failed++;
|
|
982
|
+
}
|
|
983
|
+
} catch (err) {
|
|
984
|
+
console.error(`[ConfigImport] Failed to import plugin ${plugin.name}:`, err);
|
|
985
|
+
results.plugins.failed++;
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
} catch (err) {
|
|
989
|
+
console.error('[ConfigImport] 导入 Plugins 失败:', err);
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
|
|
678
993
|
// 导入 Commands
|
|
679
994
|
if (commands && commands.length > 0) {
|
|
680
995
|
try {
|
|
@@ -752,8 +1067,15 @@ function importConfigs(importData, options = {}) {
|
|
|
752
1067
|
// 导入 Markdown 文件
|
|
753
1068
|
if (markdownFiles && Object.keys(markdownFiles).length > 0 && overwrite) {
|
|
754
1069
|
const { PATHS } = require('../../config/paths');
|
|
1070
|
+
// SECURITY: Whitelist allowed markdown files to prevent path traversal
|
|
1071
|
+
const ALLOWED_MARKDOWN_FILES = new Set(['CLAUDE.md', 'AGENTS.md', 'GEMINI.md']);
|
|
755
1072
|
for (const [fileName, content] of Object.entries(markdownFiles)) {
|
|
756
1073
|
try {
|
|
1074
|
+
if (!ALLOWED_MARKDOWN_FILES.has(fileName)) {
|
|
1075
|
+
console.warn(`[ConfigImport] Skipping disallowed markdown file: ${fileName}`);
|
|
1076
|
+
results.markdownFiles.failed++;
|
|
1077
|
+
continue;
|
|
1078
|
+
}
|
|
757
1079
|
const filePath = path.join(PATHS.base, fileName);
|
|
758
1080
|
fs.writeFileSync(filePath, content, 'utf8');
|
|
759
1081
|
results.markdownFiles.success++;
|
|
@@ -952,6 +1274,7 @@ function generateImportSummary(results) {
|
|
|
952
1274
|
{ key: 'commands', label: 'Commands' },
|
|
953
1275
|
{ key: 'rules', label: 'Rules' },
|
|
954
1276
|
{ key: 'mcpServers', label: 'MCP服务器' },
|
|
1277
|
+
{ key: 'plugins', label: '插件' },
|
|
955
1278
|
{ key: 'markdownFiles', label: 'Markdown文件' },
|
|
956
1279
|
{ key: 'uiConfig', label: 'UI配置' },
|
|
957
1280
|
{ key: 'terminalConfig', label: '终端配置' },
|
|
@@ -193,10 +193,10 @@ function updateChannel(channelId, updates) {
|
|
|
193
193
|
throw new Error('Channel not found');
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
-
const
|
|
196
|
+
const oldChannel = data.channels[index];
|
|
197
197
|
|
|
198
198
|
// 检查名称冲突
|
|
199
|
-
if (updates.name && updates.name !==
|
|
199
|
+
if (updates.name && updates.name !== oldChannel.name) {
|
|
200
200
|
const existing = data.channels.find(c => c.name === updates.name && c.id !== channelId);
|
|
201
201
|
if (existing) {
|
|
202
202
|
throw new Error(`Channel name "${updates.name}" already exists`);
|
|
@@ -204,13 +204,43 @@ function updateChannel(channelId, updates) {
|
|
|
204
204
|
}
|
|
205
205
|
|
|
206
206
|
data.channels[index] = {
|
|
207
|
-
...
|
|
207
|
+
...oldChannel,
|
|
208
208
|
...updates,
|
|
209
209
|
id: channelId, // 保持 ID 不变
|
|
210
|
-
createdAt:
|
|
210
|
+
createdAt: oldChannel.createdAt, // 保持创建时间
|
|
211
211
|
updatedAt: Date.now()
|
|
212
212
|
};
|
|
213
213
|
|
|
214
|
+
// Get proxy status
|
|
215
|
+
const { getGeminiProxyStatus } = require('../gemini-proxy-server');
|
|
216
|
+
const proxyStatus = getGeminiProxyStatus();
|
|
217
|
+
const isProxyRunning = proxyStatus.running;
|
|
218
|
+
|
|
219
|
+
// Fix 1: Detect enabled toggle (false → true) when proxy is OFF
|
|
220
|
+
if (!isProxyRunning && !oldChannel.enabled && data.channels[index].enabled) {
|
|
221
|
+
console.log(`[Gemini Settings-sync] Proxy is OFF and channel "${data.channels[index].name}" was enabled, syncing .env...`);
|
|
222
|
+
applyChannelToSettings(channelId, data.channels);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Fix 2: Single-channel enforcement when proxy is OFF
|
|
226
|
+
if (!isProxyRunning && data.channels[index].enabled && !oldChannel.enabled) {
|
|
227
|
+
// Disable all other channels
|
|
228
|
+
data.channels.forEach((ch, i) => {
|
|
229
|
+
if (i !== index && ch.enabled) {
|
|
230
|
+
ch.enabled = false;
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
console.log(`[Gemini Single-channel mode] Enabled "${data.channels[index].name}", disabled all others`);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Fix 3: Prevent disabling last enabled channel when proxy is OFF
|
|
237
|
+
if (!isProxyRunning && !data.channels[index].enabled && oldChannel.enabled) {
|
|
238
|
+
const enabledCount = data.channels.filter(ch => ch.enabled).length;
|
|
239
|
+
if (enabledCount === 0) {
|
|
240
|
+
throw new Error('无法禁用最后一个启用的渠道。请先启用其他渠道或启动动态切换。');
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
214
244
|
saveChannels(data);
|
|
215
245
|
|
|
216
246
|
// 更新 Gemini 配置文件
|
|
@@ -219,6 +249,65 @@ function updateChannel(channelId, updates) {
|
|
|
219
249
|
return data.channels[index];
|
|
220
250
|
}
|
|
221
251
|
|
|
252
|
+
/**
|
|
253
|
+
* 将指定渠道应用到 Gemini 配置文件
|
|
254
|
+
*
|
|
255
|
+
* @param {string} channelId - 渠道 ID
|
|
256
|
+
* @param {Array} channels - 渠道列表(可选,避免重复读取)
|
|
257
|
+
* @returns {Object} 应用的渠道
|
|
258
|
+
*/
|
|
259
|
+
function applyChannelToSettings(channelId, channels = null) {
|
|
260
|
+
const data = channels ? { channels } : loadChannels();
|
|
261
|
+
const channel = data.channels.find(c => c.id === channelId);
|
|
262
|
+
|
|
263
|
+
if (!channel) {
|
|
264
|
+
throw new Error('Channel not found');
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const geminiDir = getGeminiDir();
|
|
268
|
+
|
|
269
|
+
if (!fs.existsSync(geminiDir)) {
|
|
270
|
+
fs.mkdirSync(geminiDir, { recursive: true });
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const envPath = path.join(geminiDir, '.env');
|
|
274
|
+
|
|
275
|
+
// 构建 .env 内容
|
|
276
|
+
const envContent = `GOOGLE_GEMINI_BASE_URL=${channel.baseUrl}
|
|
277
|
+
GEMINI_API_KEY=${channel.apiKey}
|
|
278
|
+
GEMINI_MODEL=${channel.model}
|
|
279
|
+
`;
|
|
280
|
+
|
|
281
|
+
fs.writeFileSync(envPath, envContent, 'utf8');
|
|
282
|
+
|
|
283
|
+
// 设置 .env 文件权限为 600 (仅所有者可读写)
|
|
284
|
+
if (process.platform !== 'win32') {
|
|
285
|
+
fs.chmodSync(envPath, 0o600);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// 确保 settings.json 存在并配置正确的认证模式
|
|
289
|
+
const settingsPath = path.join(geminiDir, 'settings.json');
|
|
290
|
+
let settings = {};
|
|
291
|
+
|
|
292
|
+
if (fs.existsSync(settingsPath)) {
|
|
293
|
+
try {
|
|
294
|
+
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
295
|
+
} catch (err) {
|
|
296
|
+
console.warn('[Gemini Channels] Failed to read settings.json, creating new');
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// 设置认证模式为 gemini-api-key(第三方 API)
|
|
301
|
+
settings.security = settings.security || {};
|
|
302
|
+
settings.security.auth = settings.security.auth || {};
|
|
303
|
+
settings.security.auth.selectedType = 'gemini-api-key';
|
|
304
|
+
|
|
305
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf8');
|
|
306
|
+
|
|
307
|
+
console.log(`[Gemini Channels] Applied channel ${channel.name} to .env`);
|
|
308
|
+
return channel;
|
|
309
|
+
}
|
|
310
|
+
|
|
222
311
|
// 删除渠道
|
|
223
312
|
function deleteChannel(channelId) {
|
|
224
313
|
const data = loadChannels();
|
|
@@ -329,5 +418,6 @@ module.exports = {
|
|
|
329
418
|
getEnabledChannels,
|
|
330
419
|
saveChannelOrder,
|
|
331
420
|
isProxyConfig,
|
|
332
|
-
getGeminiDir
|
|
421
|
+
getGeminiDir,
|
|
422
|
+
applyChannelToSettings
|
|
333
423
|
};
|