@adversity/coding-tool-x 3.0.4 → 3.0.6

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.
@@ -0,0 +1,456 @@
1
+ /**
2
+ * Config Sync Manager
3
+ *
4
+ * Manages file synchronization between cc-tool central storage and CLI directories:
5
+ * - Claude Code: ~/.claude/{skills,commands,agents,rules}/
6
+ * - Codex CLI: ~/.codex/skills/, ~/.codex/prompts/
7
+ *
8
+ * Config types:
9
+ * - skills: directory-based (each skill is a dir with SKILL.md)
10
+ * - commands: file-based (.md), may be nested in subdirectories
11
+ * - agents: file-based (.md), flat directory
12
+ * - rules: file-based (.md), may be nested in subdirectories
13
+ */
14
+
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+ const os = require('os');
18
+ const { convertSkillToCodex, convertCommandToCodex } = require('./format-converter');
19
+
20
+ // Paths
21
+ const HOME = os.homedir();
22
+ const CC_TOOL_CONFIGS = path.join(HOME, '.claude', 'cc-tool', 'configs');
23
+ const CLAUDE_CODE_DIR = path.join(HOME, '.claude');
24
+ const CODEX_DIR = path.join(HOME, '.codex');
25
+
26
+ // Config type definitions
27
+ const CONFIG_TYPES = {
28
+ skills: {
29
+ isDirectory: true,
30
+ markerFile: 'SKILL.md',
31
+ claudeTarget: 'skills',
32
+ codexTarget: 'skills',
33
+ codexSupported: true,
34
+ convertForCodex: true
35
+ },
36
+ commands: {
37
+ isDirectory: false,
38
+ extension: '.md',
39
+ claudeTarget: 'commands',
40
+ codexTarget: 'prompts',
41
+ codexSupported: true,
42
+ convertForCodex: true
43
+ },
44
+ agents: {
45
+ isDirectory: false,
46
+ extension: '.md',
47
+ claudeTarget: 'agents',
48
+ codexSupported: false
49
+ },
50
+ rules: {
51
+ isDirectory: false,
52
+ extension: '.md',
53
+ claudeTarget: 'rules',
54
+ codexSupported: false
55
+ }
56
+ };
57
+
58
+ class ConfigSyncManager {
59
+ constructor() {
60
+ this.ccToolConfigs = CC_TOOL_CONFIGS;
61
+ this.claudeDir = CLAUDE_CODE_DIR;
62
+ this.codexDir = CODEX_DIR;
63
+ this.configTypes = CONFIG_TYPES;
64
+ }
65
+
66
+ /**
67
+ * Sync a config item to Claude Code
68
+ * @param {string} type - Config type (skills, commands, agents, rules)
69
+ * @param {string} name - Item name (directory name for skills, file path for others)
70
+ * @returns {Object} Result with success status
71
+ */
72
+ syncToClaude(type, name) {
73
+ const config = this.configTypes[type];
74
+ if (!config) {
75
+ console.log(`[ConfigSyncManager] Unknown config type: ${type}`);
76
+ return { success: false, error: `Unknown config type: ${type}` };
77
+ }
78
+
79
+ const sourcePath = path.join(this.ccToolConfigs, type, name);
80
+ const targetPath = path.join(this.claudeDir, config.claudeTarget, name);
81
+
82
+ // Check if source exists
83
+ if (!fs.existsSync(sourcePath)) {
84
+ console.log(`[ConfigSyncManager] Source not found: ${sourcePath}`);
85
+ return { success: false, error: 'Source not found' };
86
+ }
87
+
88
+ try {
89
+ if (config.isDirectory) {
90
+ // Copy entire directory recursively
91
+ this._ensureDir(path.dirname(targetPath));
92
+ this._copyDirRecursive(sourcePath, targetPath);
93
+ console.log(`[ConfigSyncManager] Synced ${type}/${name} to Claude Code (directory)`);
94
+ } else {
95
+ // Copy single file, preserving subdirectory structure
96
+ this._ensureDir(path.dirname(targetPath));
97
+ this._copyFile(sourcePath, targetPath);
98
+ console.log(`[ConfigSyncManager] Synced ${type}/${name} to Claude Code (file)`);
99
+ }
100
+
101
+ return { success: true, target: targetPath };
102
+ } catch (err) {
103
+ console.error(`[ConfigSyncManager] Sync to Claude failed:`, err.message);
104
+ return { success: false, error: err.message };
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Remove a config item from Claude Code
110
+ * @param {string} type - Config type
111
+ * @param {string} name - Item name
112
+ * @returns {Object} Result with success status
113
+ */
114
+ removeFromClaude(type, name) {
115
+ const config = this.configTypes[type];
116
+ if (!config) {
117
+ return { success: false, error: `Unknown config type: ${type}` };
118
+ }
119
+
120
+ const targetPath = path.join(this.claudeDir, config.claudeTarget, name);
121
+
122
+ if (!fs.existsSync(targetPath)) {
123
+ console.log(`[ConfigSyncManager] Target not found (already removed): ${targetPath}`);
124
+ return { success: true, message: 'Already removed' };
125
+ }
126
+
127
+ try {
128
+ if (config.isDirectory) {
129
+ // Remove entire directory
130
+ this._removeRecursive(targetPath);
131
+ console.log(`[ConfigSyncManager] Removed ${type}/${name} from Claude Code (directory)`);
132
+ } else {
133
+ // Remove file
134
+ fs.unlinkSync(targetPath);
135
+ console.log(`[ConfigSyncManager] Removed ${type}/${name} from Claude Code (file)`);
136
+
137
+ // Clean up empty parent directories for commands/rules
138
+ this._cleanupEmptyParents(path.dirname(targetPath), path.join(this.claudeDir, config.claudeTarget));
139
+ }
140
+
141
+ return { success: true };
142
+ } catch (err) {
143
+ console.error(`[ConfigSyncManager] Remove from Claude failed:`, err.message);
144
+ return { success: false, error: err.message };
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Sync a config item to Codex CLI
150
+ * Only skills and commands are supported
151
+ * @param {string} type - Config type
152
+ * @param {string} name - Item name
153
+ * @returns {Object} Result with success status and any warnings
154
+ */
155
+ syncToCodex(type, name) {
156
+ const config = this.configTypes[type];
157
+ if (!config) {
158
+ return { success: false, error: `Unknown config type: ${type}` };
159
+ }
160
+
161
+ if (!config.codexSupported) {
162
+ console.log(`[ConfigSyncManager] ${type} not supported by Codex, skipping`);
163
+ return { success: true, skipped: true, reason: 'Not supported by Codex' };
164
+ }
165
+
166
+ const sourcePath = path.join(this.ccToolConfigs, type, name);
167
+
168
+ if (!fs.existsSync(sourcePath)) {
169
+ console.log(`[ConfigSyncManager] Source not found: ${sourcePath}`);
170
+ return { success: false, error: 'Source not found' };
171
+ }
172
+
173
+ try {
174
+ const warnings = [];
175
+
176
+ if (type === 'skills') {
177
+ // Skills: copy directory, convert SKILL.md content
178
+ const targetPath = path.join(this.codexDir, config.codexTarget, name);
179
+ this._ensureDir(targetPath);
180
+
181
+ // Copy all files, converting SKILL.md
182
+ this._copyDirWithConversion(sourcePath, targetPath, (filePath, content) => {
183
+ if (path.basename(filePath) === 'SKILL.md') {
184
+ const result = convertSkillToCodex(content);
185
+ if (result.warnings && result.warnings.length > 0) {
186
+ warnings.push(...result.warnings);
187
+ }
188
+ return result.content;
189
+ }
190
+ return content;
191
+ });
192
+
193
+ console.log(`[ConfigSyncManager] Synced ${type}/${name} to Codex (skill directory)`);
194
+ return { success: true, target: targetPath, warnings };
195
+
196
+ } else if (type === 'commands') {
197
+ // Commands: convert and write to prompts directory
198
+ const content = fs.readFileSync(sourcePath, 'utf-8');
199
+ const result = convertCommandToCodex(content);
200
+
201
+ if (result.warnings && result.warnings.length > 0) {
202
+ warnings.push(...result.warnings);
203
+ }
204
+
205
+ // Target path in codex prompts (same relative path structure)
206
+ const targetPath = path.join(this.codexDir, config.codexTarget, name);
207
+ this._ensureDir(path.dirname(targetPath));
208
+ fs.writeFileSync(targetPath, result.content, 'utf-8');
209
+
210
+ console.log(`[ConfigSyncManager] Synced ${type}/${name} to Codex (prompt)`);
211
+ return { success: true, target: targetPath, warnings };
212
+ }
213
+
214
+ return { success: false, error: 'Unexpected type' };
215
+ } catch (err) {
216
+ console.error(`[ConfigSyncManager] Sync to Codex failed:`, err.message);
217
+ return { success: false, error: err.message };
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Remove a config item from Codex CLI
223
+ * @param {string} type - Config type
224
+ * @param {string} name - Item name
225
+ * @returns {Object} Result with success status
226
+ */
227
+ removeFromCodex(type, name) {
228
+ const config = this.configTypes[type];
229
+ if (!config) {
230
+ return { success: false, error: `Unknown config type: ${type}` };
231
+ }
232
+
233
+ if (!config.codexSupported) {
234
+ return { success: true, skipped: true, reason: 'Not supported by Codex' };
235
+ }
236
+
237
+ const targetPath = path.join(this.codexDir, config.codexTarget, name);
238
+
239
+ if (!fs.existsSync(targetPath)) {
240
+ console.log(`[ConfigSyncManager] Target not found (already removed): ${targetPath}`);
241
+ return { success: true, message: 'Already removed' };
242
+ }
243
+
244
+ try {
245
+ if (type === 'skills') {
246
+ // Remove entire directory
247
+ this._removeRecursive(targetPath);
248
+ console.log(`[ConfigSyncManager] Removed ${type}/${name} from Codex (skill directory)`);
249
+ } else {
250
+ // Remove file
251
+ fs.unlinkSync(targetPath);
252
+ console.log(`[ConfigSyncManager] Removed ${type}/${name} from Codex (prompt)`);
253
+
254
+ // Clean up empty parent directories
255
+ this._cleanupEmptyParents(path.dirname(targetPath), path.join(this.codexDir, config.codexTarget));
256
+ }
257
+
258
+ return { success: true };
259
+ } catch (err) {
260
+ console.error(`[ConfigSyncManager] Remove from Codex failed:`, err.message);
261
+ return { success: false, error: err.message };
262
+ }
263
+ }
264
+
265
+ /**
266
+ * Batch sync based on registry data
267
+ * @param {string} type - Config type
268
+ * @param {Object} registryItems - Registry items { name: { enabled, platforms: { claude, codex } } }
269
+ * @returns {Object} Results summary
270
+ */
271
+ syncAll(type, registryItems) {
272
+ const results = {
273
+ synced: [],
274
+ removed: [],
275
+ errors: [],
276
+ warnings: []
277
+ };
278
+
279
+ if (!registryItems || typeof registryItems !== 'object') {
280
+ return results;
281
+ }
282
+
283
+ for (const [name, item] of Object.entries(registryItems)) {
284
+ if (!item || typeof item !== 'object') continue;
285
+
286
+ const { enabled, platforms } = item;
287
+
288
+ if (enabled && platforms) {
289
+ // Sync to enabled platforms
290
+ if (platforms.claude) {
291
+ const result = this.syncToClaude(type, name);
292
+ if (result.success && !result.skipped) {
293
+ results.synced.push({ type, name, platform: 'claude' });
294
+ } else if (!result.success) {
295
+ results.errors.push({ type, name, platform: 'claude', error: result.error });
296
+ }
297
+ } else {
298
+ // Platform disabled, remove
299
+ const result = this.removeFromClaude(type, name);
300
+ if (result.success && !result.message) {
301
+ results.removed.push({ type, name, platform: 'claude' });
302
+ }
303
+ }
304
+
305
+ if (platforms.codex) {
306
+ const result = this.syncToCodex(type, name);
307
+ if (result.success && !result.skipped) {
308
+ results.synced.push({ type, name, platform: 'codex' });
309
+ if (result.warnings && result.warnings.length > 0) {
310
+ results.warnings.push({ type, name, platform: 'codex', warnings: result.warnings });
311
+ }
312
+ } else if (!result.success) {
313
+ results.errors.push({ type, name, platform: 'codex', error: result.error });
314
+ }
315
+ } else {
316
+ // Platform disabled, remove
317
+ const result = this.removeFromCodex(type, name);
318
+ if (result.success && !result.message && !result.skipped) {
319
+ results.removed.push({ type, name, platform: 'codex' });
320
+ }
321
+ }
322
+ } else {
323
+ // Item disabled, remove from all platforms
324
+ const claudeResult = this.removeFromClaude(type, name);
325
+ if (claudeResult.success && !claudeResult.message) {
326
+ results.removed.push({ type, name, platform: 'claude' });
327
+ }
328
+
329
+ const codexResult = this.removeFromCodex(type, name);
330
+ if (codexResult.success && !codexResult.message && !codexResult.skipped) {
331
+ results.removed.push({ type, name, platform: 'codex' });
332
+ }
333
+ }
334
+ }
335
+
336
+ console.log(`[ConfigSyncManager] syncAll(${type}): synced=${results.synced.length}, removed=${results.removed.length}, errors=${results.errors.length}`);
337
+ return results;
338
+ }
339
+
340
+ // ==================== Helper Methods ====================
341
+
342
+ /**
343
+ * Ensure a directory exists
344
+ */
345
+ _ensureDir(dir) {
346
+ if (!fs.existsSync(dir)) {
347
+ fs.mkdirSync(dir, { recursive: true });
348
+ }
349
+ }
350
+
351
+ /**
352
+ * Recursively copy a directory
353
+ */
354
+ _copyDirRecursive(src, dest) {
355
+ this._ensureDir(dest);
356
+
357
+ const entries = fs.readdirSync(src, { withFileTypes: true });
358
+
359
+ for (const entry of entries) {
360
+ const srcPath = path.join(src, entry.name);
361
+ const destPath = path.join(dest, entry.name);
362
+
363
+ if (entry.isDirectory()) {
364
+ this._copyDirRecursive(srcPath, destPath);
365
+ } else {
366
+ fs.copyFileSync(srcPath, destPath);
367
+ }
368
+ }
369
+ }
370
+
371
+ /**
372
+ * Copy a directory with content transformation
373
+ * @param {string} src - Source directory
374
+ * @param {string} dest - Destination directory
375
+ * @param {Function} transform - Function(filePath, content) => transformedContent
376
+ */
377
+ _copyDirWithConversion(src, dest, transform) {
378
+ this._ensureDir(dest);
379
+
380
+ const entries = fs.readdirSync(src, { withFileTypes: true });
381
+
382
+ for (const entry of entries) {
383
+ const srcPath = path.join(src, entry.name);
384
+ const destPath = path.join(dest, entry.name);
385
+
386
+ if (entry.isDirectory()) {
387
+ this._copyDirWithConversion(srcPath, destPath, transform);
388
+ } else {
389
+ // Check if it's a text file that should be transformed
390
+ const ext = path.extname(entry.name).toLowerCase();
391
+ const textExtensions = ['.md', '.txt', '.json', '.js', '.ts', '.py', '.sh', '.yaml', '.yml'];
392
+
393
+ if (textExtensions.includes(ext)) {
394
+ const content = fs.readFileSync(srcPath, 'utf-8');
395
+ const transformed = transform(srcPath, content);
396
+ fs.writeFileSync(destPath, transformed, 'utf-8');
397
+ } else {
398
+ // Binary file, copy as-is
399
+ fs.copyFileSync(srcPath, destPath);
400
+ }
401
+ }
402
+ }
403
+ }
404
+
405
+ /**
406
+ * Copy a single file
407
+ */
408
+ _copyFile(src, dest) {
409
+ fs.copyFileSync(src, dest);
410
+ }
411
+
412
+ /**
413
+ * Recursively remove a file or directory
414
+ */
415
+ _removeRecursive(target) {
416
+ if (!fs.existsSync(target)) {
417
+ return;
418
+ }
419
+
420
+ fs.rmSync(target, { recursive: true, force: true });
421
+ }
422
+
423
+ /**
424
+ * Clean up empty parent directories up to the base directory
425
+ */
426
+ _cleanupEmptyParents(dir, baseDir) {
427
+ // Normalize paths for comparison
428
+ const normalizedDir = path.resolve(dir);
429
+ const normalizedBase = path.resolve(baseDir);
430
+
431
+ // Don't go above base directory
432
+ if (!normalizedDir.startsWith(normalizedBase) || normalizedDir === normalizedBase) {
433
+ return;
434
+ }
435
+
436
+ try {
437
+ const entries = fs.readdirSync(dir);
438
+ if (entries.length === 0) {
439
+ fs.rmdirSync(dir);
440
+ console.log(`[ConfigSyncManager] Removed empty directory: ${dir}`);
441
+ // Recurse to parent
442
+ this._cleanupEmptyParents(path.dirname(dir), baseDir);
443
+ }
444
+ } catch (err) {
445
+ // Ignore errors (directory might not exist or permission issues)
446
+ }
447
+ }
448
+ }
449
+
450
+ module.exports = {
451
+ ConfigSyncManager,
452
+ CONFIG_TYPES,
453
+ CC_TOOL_CONFIGS,
454
+ CLAUDE_CODE_DIR,
455
+ CODEX_DIR
456
+ };
@@ -13,8 +13,10 @@ const { AgentsService } = require('./agents-service');
13
13
  const { CommandsService } = require('./commands-service');
14
14
  const { RulesService } = require('./rules-service');
15
15
  const { SkillService } = require('./skill-service');
16
+ const { PluginsService } = require('./plugins-service');
16
17
  const mcpService = require('./mcp-service');
17
18
  const skillService = new SkillService();
19
+ const pluginsService = new PluginsService();
18
20
 
19
21
  // 配置模板文件路径
20
22
  const TEMPLATES_FILE = path.join(PATHS.config, 'config-templates.json');
@@ -149,6 +151,7 @@ You are an experienced full-stack developer focused on delivering high-quality c
149
151
  rules: [],
150
152
  commands: [],
151
153
  agents: [],
154
+ plugins: [],
152
155
  mcpServers: ['github', 'context7', 'fetch', 'memory'],
153
156
  isBuiltin: true
154
157
  },
@@ -297,6 +300,7 @@ You are a senior technical architect focused on system design and technical plan
297
300
  rules: [],
298
301
  commands: [],
299
302
  agents: [],
303
+ plugins: [],
300
304
  mcpServers: ['context7', 'fetch', 'memory'],
301
305
  isBuiltin: true
302
306
  },
@@ -455,6 +459,7 @@ For each issue:
455
459
  rules: [],
456
460
  commands: [],
457
461
  agents: [],
462
+ plugins: [],
458
463
  mcpServers: ['github'],
459
464
  isBuiltin: true
460
465
  },
@@ -472,6 +477,7 @@ For each issue:
472
477
  rules: [],
473
478
  commands: [],
474
479
  agents: [],
480
+ plugins: [],
475
481
  mcpServers: [],
476
482
  isBuiltin: true
477
483
  }
@@ -567,6 +573,7 @@ function createCustomTemplate(template) {
567
573
  rules: template.rules || [],
568
574
  commands: template.commands || [],
569
575
  agents: template.agents || [],
576
+ plugins: template.plugins || [],
570
577
  mcpServers: template.mcpServers || [],
571
578
  isBuiltin: false,
572
579
  createdAt: new Date().toISOString()
@@ -637,6 +644,7 @@ function applyTemplate(targetDir, templateId) {
637
644
  rules: 0,
638
645
  commands: 0,
639
646
  agents: 0,
647
+ plugins: 0,
640
648
  mcpServers: 0
641
649
  };
642
650
 
@@ -656,6 +664,7 @@ function applyTemplate(targetDir, templateId) {
656
664
  rules: template.rules,
657
665
  commands: template.commands,
658
666
  agents: template.agents,
667
+ plugins: template.plugins,
659
668
  mcpServers: template.mcpServers
660
669
  };
661
670
 
@@ -693,7 +702,7 @@ function readCurrentConfig(targetDir) {
693
702
 
694
703
  /**
695
704
  * 获取所有可用配置(用于模板编辑器选择)
696
- * 返回用户级的 agents, commands, rules + MCP 服务器列表
705
+ * 返回用户级的 agents, commands, rules, plugins + MCP 服务器列表
697
706
  */
698
707
  function getAvailableConfigs() {
699
708
  const agentsService = new AgentsService();
@@ -706,6 +715,9 @@ function getAvailableConfigs() {
706
715
  const { rules } = rulesService.listRules();
707
716
  const installedSkills = skillService.getInstalledSkills();
708
717
 
718
+ // 获取已安装的插件和市场插件
719
+ const { plugins: installedPlugins } = pluginsService.listPlugins();
720
+
709
721
  // 获取 MCP 服务器
710
722
  const mcpServers = mcpService.getAllServers();
711
723
  const mcpServerList = Object.values(mcpServers).map(s => ({
@@ -754,6 +766,14 @@ function getAvailableConfigs() {
754
766
  paths: r.paths,
755
767
  body: r.body
756
768
  })),
769
+ plugins: installedPlugins.map(p => ({
770
+ name: p.name,
771
+ description: p.description || '',
772
+ version: p.version || '1.0.0',
773
+ marketplace: p.marketplace || null,
774
+ source: p.source || null,
775
+ repoUrl: p.repoUrl || null
776
+ })),
757
777
  mcpServers: mcpServerList,
758
778
  mcpPresets
759
779
  };
@@ -819,6 +839,7 @@ function applyTemplateToProject(targetDir, templateId, options = {}) {
819
839
  agents: { applied: 0, files: [] },
820
840
  commands: { applied: 0, files: [] },
821
841
  rules: { applied: 0, files: [] },
842
+ plugins: { applied: 0, items: [] },
822
843
  mcpServers: { applied: 0 }
823
844
  };
824
845
 
@@ -910,7 +931,14 @@ function applyTemplateToProject(targetDir, templateId, options = {}) {
910
931
  }
911
932
  }
912
933
 
913
- // 5. 写入 MCP 配置到 .mcp.json
934
+ // 5. 记录 Plugins(插件只记录,不自动安装)
935
+ // 插件安装需要用户手动确认,这里只记录模板中包含的插件信息
936
+ if (template.plugins?.length > 0) {
937
+ results.plugins.applied = template.plugins.length;
938
+ results.plugins.items = template.plugins.map(p => p.name);
939
+ }
940
+
941
+ // 6. 写入 MCP 配置到 .mcp.json
914
942
  if (template.mcpServers?.length > 0) {
915
943
  const mcpConfig = { mcpServers: {} };
916
944
  const allServers = mcpService.getAllServers();
@@ -938,7 +966,7 @@ function applyTemplateToProject(targetDir, templateId, options = {}) {
938
966
  }
939
967
  }
940
968
 
941
- // 6. 创建配置记录文件
969
+ // 7. 创建配置记录文件
942
970
  const configRecord = {
943
971
  templateId: template.id,
944
972
  templateName: template.name,
@@ -949,6 +977,7 @@ function applyTemplateToProject(targetDir, templateId, options = {}) {
949
977
  agents: template.agents?.map(a => a.fileName || a.name) || [],
950
978
  commands: template.commands?.map(c => c.name) || [],
951
979
  rules: template.rules?.map(r => r.fileName) || [],
980
+ plugins: template.plugins?.map(p => p.name) || [],
952
981
  mcpServers: template.mcpServers || []
953
982
  };
954
983
  const recordPath = path.join(targetDir, '.ctx-config.json');
@@ -984,6 +1013,7 @@ function previewTemplateApplication(targetDir, templateId, options = {}) {
984
1013
  agents: 0,
985
1014
  commands: 0,
986
1015
  rules: 0,
1016
+ plugins: 0,
987
1017
  mcpServers: 0
988
1018
  }
989
1019
  };
@@ -1088,6 +1118,11 @@ function previewTemplateApplication(targetDir, templateId, options = {}) {
1088
1118
  preview.summary.mcpServers = template.mcpServers.length;
1089
1119
  }
1090
1120
 
1121
+ // 统计 Plugins(插件不写入文件,只记录数量)
1122
+ if (template.plugins?.length > 0) {
1123
+ preview.summary.plugins = template.plugins.length;
1124
+ }
1125
+
1091
1126
  return preview;
1092
1127
  }
1093
1128
 
@@ -70,7 +70,9 @@ function loadChannels() {
70
70
  ...ch,
71
71
  enabled: ch.enabled !== false, // 默认启用
72
72
  weight: ch.weight || 1,
73
- maxConcurrency: ch.maxConcurrency || null
73
+ maxConcurrency: ch.maxConcurrency || null,
74
+ modelRedirects: ch.modelRedirects || [],
75
+ speedTestModel: ch.speedTestModel || null
74
76
  }));
75
77
  }
76
78
  return data;
@@ -171,6 +173,8 @@ function createChannel(name, baseUrl, apiKey, model = 'gemini-2.5-pro', extraCon
171
173
  enabled: extraConfig.enabled !== false, // 默认启用
172
174
  weight: extraConfig.weight || 1,
173
175
  maxConcurrency: extraConfig.maxConcurrency || null,
176
+ modelRedirects: extraConfig.modelRedirects || [],
177
+ speedTestModel: extraConfig.speedTestModel || null,
174
178
  createdAt: Date.now(),
175
179
  updatedAt: Date.now()
176
180
  };
@@ -208,6 +212,8 @@ function updateChannel(channelId, updates) {
208
212
  ...updates,
209
213
  id: channelId, // 保持 ID 不变
210
214
  createdAt: oldChannel.createdAt, // 保持创建时间
215
+ modelRedirects: updates.modelRedirects !== undefined ? updates.modelRedirects : (oldChannel.modelRedirects || []),
216
+ speedTestModel: updates.speedTestModel !== undefined ? updates.speedTestModel : (oldChannel.speedTestModel || null),
211
217
  updatedAt: Date.now()
212
218
  };
213
219