@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.
@@ -5,12 +5,12 @@
5
5
  <link rel="icon" href="/favicon.ico">
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
7
  <title>CC-TOOL - ClaudeCode增强工作助手</title>
8
- <script type="module" crossorigin src="/assets/index-Bpjcdalh.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-D2VfwJBa.js"></script>
9
9
  <link rel="modulepreload" crossorigin href="/assets/vue-vendor-6JaYHOiI.js">
10
10
  <link rel="modulepreload" crossorigin href="/assets/vendors-D2HHw_aW.js">
11
- <link rel="modulepreload" crossorigin href="/assets/icons-BlzwYoRU.js">
12
- <link rel="modulepreload" crossorigin href="/assets/naive-ui-B1TP-0TP.js">
13
- <link rel="stylesheet" crossorigin href="/assets/index-CB782_71.css">
11
+ <link rel="modulepreload" crossorigin href="/assets/icons-BxudHPiX.js">
12
+ <link rel="modulepreload" crossorigin href="/assets/naive-ui-DT-Uur8K.js">
13
+ <link rel="stylesheet" crossorigin href="/assets/index-oXBzu0bd.css">
14
14
  </head>
15
15
  <body>
16
16
  <div id="app"></div>
@@ -0,0 +1,251 @@
1
+ # 模型重定向功能
2
+
3
+ ## 功能概述
4
+
5
+ 模型重定向功能允许在代理模式下自动将高成本模型请求重定向到低成本模型,从而节省 token 消耗。
6
+
7
+ 例如:将 `claude-opus-4` 重定向到 `claude-sonnet-4-5`,可以大幅降低成本,同时保持良好的性能。
8
+
9
+ ## 使用场景
10
+
11
+ - GitHub 插件或 oh-my-claudecode skills 默认使用 opus 模型
12
+ - 修改这些插件/skills 的模型配置过于复杂
13
+ - 希望在不修改代码的情况下降低 token 消耗
14
+
15
+ ## 工作原理
16
+
17
+ ### 双重语义
18
+
19
+ `modelConfig` 字段根据代理状态有不同的含义:
20
+
21
+ | 代理状态 | 语义 | 行为 |
22
+ |---------|------|------|
23
+ | **代理关闭** | 模型映射 | 写入 `~/.claude/settings.json`,Claude Code CLI 读取环境变量 |
24
+ | **代理开启** | 模型重定向 | 在代理服务器中拦截请求,修改 `model` 字段 |
25
+
26
+ ### 重定向规则
27
+
28
+ 1. **层级检测**:根据模型名称检测层级(opus/sonnet/haiku)
29
+ 2. **优先级匹配**:
30
+ - 优先使用层级特定配置(如 `opusModel`)
31
+ - 回退到通用配置(`model`)
32
+ - 无配置则保持原样
33
+
34
+ ### 示例
35
+
36
+ **配置**:
37
+ ```json
38
+ {
39
+ "modelConfig": {
40
+ "opusModel": "claude-sonnet-4-5",
41
+ "sonnetModel": "",
42
+ "haikuModel": ""
43
+ }
44
+ }
45
+ ```
46
+
47
+ **重定向结果**:
48
+ - `claude-opus-4-20250514` → `claude-sonnet-4-5`
49
+ - `claude-sonnet-4-5` → `claude-sonnet-4-5`(不变)
50
+ - `claude-3-5-haiku-20241022` → `claude-3-5-haiku-20241022`(不变)
51
+
52
+ ## 配置方法
53
+
54
+ ### 1. 官方渠道
55
+
56
+ 在渠道编辑面板中,展开 **"模型重定向"** 部分:
57
+
58
+ - **Haiku 重定向**:将所有 haiku 模型重定向到指定模型
59
+ - **Sonnet 重定向**:将所有 sonnet 模型重定向到指定模型
60
+ - **Opus 重定向**:将所有 opus 模型重定向到指定模型(推荐配置为 sonnet)
61
+
62
+ ### 2. 非官方渠道
63
+
64
+ 在渠道编辑面板中,展开 **"模型配置"** 部分:
65
+
66
+ - 代理关闭时:作为模型映射使用
67
+ - 代理开启时:作为模型重定向使用
68
+
69
+ ## 测试步骤
70
+
71
+ ### 前置条件
72
+
73
+ 1. 启动代理:`ctx proxy start`
74
+ 2. 配置渠道的模型重定向规则
75
+ 3. 确保渠道已启用
76
+
77
+ ### 测试用例
78
+
79
+ #### 测试 1:Opus → Sonnet 重定向
80
+
81
+ **配置**:
82
+ ```json
83
+ {
84
+ "opusModel": "claude-sonnet-4-5"
85
+ }
86
+ ```
87
+
88
+ **测试命令**:
89
+ ```bash
90
+ curl -X POST http://localhost:<proxy-port>/v1/messages \
91
+ -H "Content-Type: application/json" \
92
+ -H "x-api-key: sk-test" \
93
+ -d '{
94
+ "model": "claude-opus-4-20250514",
95
+ "max_tokens": 100,
96
+ "messages": [{"role": "user", "content": "Hello"}]
97
+ }'
98
+ ```
99
+
100
+ **预期结果**:
101
+ - 控制台输出:`[Model Redirect] claude-opus-4-20250514 → claude-sonnet-4-5 (channel: <渠道名>)`
102
+ - 请求成功返回
103
+
104
+ #### 测试 2:无重定向配置
105
+
106
+ **配置**:
107
+ ```json
108
+ {
109
+ "opusModel": "",
110
+ "sonnetModel": "",
111
+ "haikuModel": ""
112
+ }
113
+ ```
114
+
115
+ **测试命令**:同上
116
+
117
+ **预期结果**:
118
+ - 无控制台输出(不重定向)
119
+ - 请求使用原始模型
120
+
121
+ #### 测试 3:Haiku 保持不变
122
+
123
+ **配置**:
124
+ ```json
125
+ {
126
+ "opusModel": "claude-sonnet-4-5",
127
+ "haikuModel": ""
128
+ }
129
+ ```
130
+
131
+ **测试命令**:
132
+ ```bash
133
+ curl -X POST http://localhost:<proxy-port>/v1/messages \
134
+ -H "Content-Type: application/json" \
135
+ -H "x-api-key: sk-test" \
136
+ -d '{
137
+ "model": "claude-3-5-haiku-20241022",
138
+ "max_tokens": 100,
139
+ "messages": [{"role": "user", "content": "Hello"}]
140
+ }'
141
+ ```
142
+
143
+ **预期结果**:
144
+ - 无控制台输出(haiku 未配置重定向)
145
+ - 请求使用原始 haiku 模型
146
+
147
+ ## 实现细节
148
+
149
+ ### 代码位置
150
+
151
+ - **后端逻辑**:
152
+ - `src/server/proxy-server.js` (Claude 代理)
153
+ - `src/server/codex-proxy-server.js` (Codex 代理)
154
+
155
+ - **前端 UI**:
156
+ - `src/web/src/components/channel/channelPanelFactories.js`
157
+
158
+ ### 核心函数
159
+
160
+ ```javascript
161
+ // 检测模型层级
162
+ function detectModelTier(modelName) {
163
+ if (!modelName) return null;
164
+ const lower = modelName.toLowerCase();
165
+ if (lower.includes('opus')) return 'opus';
166
+ if (lower.includes('sonnet')) return 'sonnet';
167
+ if (lower.includes('haiku')) return 'haiku';
168
+ return null;
169
+ }
170
+
171
+ // 应用模型重定向
172
+ function redirectModel(originalModel, modelConfig) {
173
+ if (!modelConfig || !originalModel) return originalModel;
174
+
175
+ const tier = detectModelTier(originalModel);
176
+
177
+ // 优先级:层级特定配置 > 通用模型覆盖
178
+ if (tier === 'opus' && modelConfig.opusModel) {
179
+ return modelConfig.opusModel;
180
+ }
181
+ if (tier === 'sonnet' && modelConfig.sonnetModel) {
182
+ return modelConfig.sonnetModel;
183
+ }
184
+ if (tier === 'haiku' && modelConfig.haikuModel) {
185
+ return modelConfig.haikuModel;
186
+ }
187
+
188
+ // 回退到通用模型覆盖
189
+ if (modelConfig.model) {
190
+ return modelConfig.model;
191
+ }
192
+
193
+ return originalModel;
194
+ }
195
+ ```
196
+
197
+ ### 请求流程
198
+
199
+ ```
200
+ Client Request
201
+
202
+ Proxy Server (allocate channel)
203
+
204
+ Check req.body.model
205
+
206
+ Apply redirectModel(originalModel, channel.modelConfig)
207
+
208
+ Update req.body.model & req.rawBody
209
+
210
+ Forward to upstream API
211
+ ```
212
+
213
+ ## 注意事项
214
+
215
+ 1. **仅在代理开启时生效**:代理关闭时,modelConfig 用于模型映射
216
+ 2. **不修改响应**:只修改请求中的 model 字段,不影响响应
217
+ 3. **日志记录**:每次重定向都会在控制台输出日志
218
+ 4. **向后兼容**:未配置重定向时,保持原有行为
219
+
220
+ ## 常见问题
221
+
222
+ ### Q: 为什么我的重定向没有生效?
223
+
224
+ A: 检查以下几点:
225
+ 1. 代理是否已启动(`ctx proxy status`)
226
+ 2. 渠道的 modelConfig 是否正确配置
227
+ 3. 渠道是否已启用
228
+ 4. 查看控制台是否有重定向日志
229
+
230
+ ### Q: 可以将 sonnet 重定向到 opus 吗?
231
+
232
+ A: 可以,但不推荐。重定向的目的是降低成本,将低成本模型重定向到高成本模型会增加开销。
233
+
234
+ ### Q: 重定向会影响响应质量吗?
235
+
236
+ A: 取决于重定向的目标模型。例如 opus → sonnet 可能会略微降低质量,但通常差异不大。建议根据实际场景测试。
237
+
238
+ ### Q: 可以为不同渠道配置不同的重定向规则吗?
239
+
240
+ A: 可以。每个渠道都有独立的 modelConfig,可以配置不同的重定向规则。
241
+
242
+ ## 成本节省示例
243
+
244
+ 假设使用 oh-my-claudecode 的 opus 级别 agent:
245
+
246
+ | 场景 | 原始模型 | 重定向模型 | 输入成本 | 输出成本 | 节省比例 |
247
+ |------|---------|-----------|---------|---------|---------|
248
+ | 默认 | opus-4 | - | $15/M | $75/M | - |
249
+ | 重定向 | opus-4 | sonnet-4-5 | $3/M | $15/M | 80% |
250
+
251
+ 对于大量使用 opus 的场景,成本节省非常显著。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adversity/coding-tool-x",
3
- "version": "3.0.4",
3
+ "version": "3.0.6",
4
4
  "description": "Vibe Coding 增强工作助手 - 智能会话管理、动态渠道切换、全局搜索、实时监控",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -15,6 +15,7 @@ const { getChannelHealthStatus, getAllChannelHealthStatus, resetChannelHealth }
15
15
  const { testChannelSpeed, testMultipleChannels, getLatencyLevel } = require('../services/speed-test');
16
16
  const { fetchModelsFromProvider } = require('../services/model-detector');
17
17
  const { broadcastLog, broadcastProxyState, broadcastSchedulerState } = require('../websocket-server');
18
+ const { clearRedirectCache } = require('../proxy-server');
18
19
 
19
20
  // GET /api/channels - Get all channels with health status
20
21
  router.get('/', (req, res) => {
@@ -102,6 +103,8 @@ router.put('/:id', (req, res) => {
102
103
  const updates = req.body;
103
104
 
104
105
  const channel = updateChannel(id, updates);
106
+ // 清除该渠道的模型重定向日志缓存,使下次请求时重新打印
107
+ clearRedirectCache(id);
105
108
  res.json({ channel });
106
109
  broadcastSchedulerState('claude', getSchedulerState('claude'));
107
110
  } catch (error) {
@@ -14,6 +14,8 @@ const { getChannelHealthStatus, resetChannelHealth } = require('../services/chan
14
14
  const { broadcastSchedulerState, broadcastLog } = require('../websocket-server');
15
15
  const { isCodexInstalled } = require('../services/codex-config');
16
16
  const { testChannelSpeed, testMultipleChannels, getLatencyLevel } = require('../services/speed-test');
17
+ const { clearCodexRedirectCache } = require('../codex-proxy-server');
18
+ const { fetchModelsFromProvider } = require('../services/model-detector');
17
19
 
18
20
  module.exports = (config) => {
19
21
  /**
@@ -42,6 +44,42 @@ module.exports = (config) => {
42
44
  }
43
45
  });
44
46
 
47
+ /**
48
+ * GET /api/codex/channels/:id/models
49
+ * 获取渠道可用模型列表
50
+ */
51
+ router.get('/:id/models', async (req, res) => {
52
+ try {
53
+ const { id } = req.params;
54
+ const channels = getChannels().channels || [];
55
+ const channel = channels.find(ch => ch.id === id);
56
+
57
+ if (!channel) {
58
+ return res.status(404).json({ error: '渠道不存在' });
59
+ }
60
+
61
+ // Codex 渠道大多数是 OpenAI 兼容的
62
+ const result = await fetchModelsFromProvider(channel, 'openai_compatible');
63
+
64
+ res.json({
65
+ channelId: id,
66
+ models: result.models,
67
+ supported: result.supported,
68
+ cached: result.cached,
69
+ fallbackUsed: result.fallbackUsed,
70
+ fetchedAt: result.lastChecked || new Date().toISOString(),
71
+ error: result.error,
72
+ errorHint: result.errorHint
73
+ });
74
+ } catch (error) {
75
+ console.error('[Codex Channels API] Error fetching models:', error);
76
+ res.status(500).json({
77
+ error: '获取模型列表失败',
78
+ channelId: req.params.id
79
+ });
80
+ }
81
+ });
82
+
45
83
  /**
46
84
  * POST /api/codex/channels
47
85
  * 创建新渠道
@@ -88,6 +126,8 @@ module.exports = (config) => {
88
126
  const updates = req.body;
89
127
 
90
128
  const channel = updateChannel(channelId, updates);
129
+ // 清除该渠道的模型重定向日志缓存,使下次请求时重新打印
130
+ clearCodexRedirectCache(channelId);
91
131
  res.json(channel);
92
132
  broadcastSchedulerState('codex', getSchedulerState('codex'));
93
133
  } catch (err) {
@@ -0,0 +1,341 @@
1
+ /**
2
+ * Config Registry API 路由
3
+ *
4
+ * Exposes config registry functionality via REST API.
5
+ * Manages skills, commands, agents, rules with enable/disable and per-platform support.
6
+ */
7
+
8
+ const express = require('express');
9
+ const { ConfigRegistryService, CONFIG_TYPES } = require('../services/config-registry-service');
10
+ const { ConfigSyncManager } = require('../services/config-sync-manager');
11
+
12
+ const router = express.Router();
13
+ const registryService = new ConfigRegistryService();
14
+ const syncManager = new ConfigSyncManager();
15
+
16
+ // Valid config types
17
+ const VALID_TYPES = CONFIG_TYPES;
18
+
19
+ // Valid platforms
20
+ const VALID_PLATFORMS = ['claude', 'codex'];
21
+
22
+ /**
23
+ * Validate config type parameter
24
+ * @param {string} type - Config type
25
+ * @returns {string|null} Error message or null if valid
26
+ */
27
+ function validateType(type) {
28
+ if (!VALID_TYPES.includes(type)) {
29
+ return `Invalid config type: ${type}. Must be one of: ${VALID_TYPES.join(', ')}`;
30
+ }
31
+ return null;
32
+ }
33
+
34
+ /**
35
+ * Validate platform parameter
36
+ * @param {string} platform - Platform name
37
+ * @returns {string|null} Error message or null if valid
38
+ */
39
+ function validatePlatform(platform) {
40
+ if (!VALID_PLATFORMS.includes(platform)) {
41
+ return `Invalid platform: ${platform}. Must be one of: ${VALID_PLATFORMS.join(', ')}`;
42
+ }
43
+ return null;
44
+ }
45
+
46
+ /**
47
+ * GET /api/config-registry/stats
48
+ * Get statistics for all config types
49
+ */
50
+ router.get('/stats', async (req, res) => {
51
+ try {
52
+ const stats = registryService.getStats();
53
+
54
+ res.json({
55
+ success: true,
56
+ stats
57
+ });
58
+ } catch (err) {
59
+ console.error('[ConfigRegistry API] Get stats error:', err);
60
+ res.status(500).json({
61
+ success: false,
62
+ message: err.message
63
+ });
64
+ }
65
+ });
66
+
67
+ /**
68
+ * GET /api/config-registry/:type
69
+ * List all items for a config type (skills, commands, agents, rules)
70
+ * Returns { success: true, items: { name: registryEntry } }
71
+ */
72
+ router.get('/:type', async (req, res) => {
73
+ try {
74
+ const { type } = req.params;
75
+
76
+ const typeError = validateType(type);
77
+ if (typeError) {
78
+ return res.status(400).json({
79
+ success: false,
80
+ message: typeError
81
+ });
82
+ }
83
+
84
+ const items = registryService.listItems(type);
85
+
86
+ res.json({
87
+ success: true,
88
+ type,
89
+ items
90
+ });
91
+ } catch (err) {
92
+ console.error('[ConfigRegistry API] List items error:', err);
93
+ res.status(500).json({
94
+ success: false,
95
+ message: err.message
96
+ });
97
+ }
98
+ });
99
+
100
+ /**
101
+ * POST /api/config-registry/:type/import
102
+ * Import configs from Claude Code native directories to cc-tool
103
+ * - Scans ~/.claude/{type}/
104
+ * - Copies new items to cc-tool/configs/{type}/
105
+ * - Registers them with enabled: true
106
+ * - Syncs back to Claude (since they were already there)
107
+ * Returns { success: true, imported: number, skipped: number, items: [...] }
108
+ */
109
+ router.post('/:type/import', async (req, res) => {
110
+ try {
111
+ const { type } = req.params;
112
+
113
+ const typeError = validateType(type);
114
+ if (typeError) {
115
+ return res.status(400).json({
116
+ success: false,
117
+ message: typeError
118
+ });
119
+ }
120
+
121
+ // Import from Claude Code directories
122
+ const result = registryService.importFromClaude(type);
123
+
124
+ // Sync imported items back to Claude (they were already there but now managed by cc-tool)
125
+ // This ensures consistency between registry and actual files
126
+ if (result.imported > 0) {
127
+ for (const name of result.items) {
128
+ const item = registryService.getItem(type, name);
129
+ if (item && item.enabled && item.platforms?.claude) {
130
+ syncManager.syncToClaude(type, name);
131
+ }
132
+ }
133
+ }
134
+
135
+ res.json({
136
+ success: true,
137
+ type,
138
+ imported: result.imported,
139
+ skipped: result.skipped,
140
+ items: result.items
141
+ });
142
+ } catch (err) {
143
+ console.error('[ConfigRegistry API] Import error:', err);
144
+ res.status(500).json({
145
+ success: false,
146
+ message: err.message
147
+ });
148
+ }
149
+ });
150
+
151
+ /**
152
+ * PUT /api/config-registry/:type/:name/toggle
153
+ * Toggle enabled/disabled status
154
+ * Body: { enabled: boolean }
155
+ * - Updates registry
156
+ * - If enabling: sync to platforms where platform=true
157
+ * - If disabling: remove from all platforms
158
+ * Returns { success: true, item: updatedEntry }
159
+ */
160
+ router.put('/:type/:name/toggle', async (req, res) => {
161
+ try {
162
+ const { type } = req.params;
163
+ const name = decodeURIComponent(req.params.name);
164
+ const { enabled } = req.body;
165
+
166
+ const typeError = validateType(type);
167
+ if (typeError) {
168
+ return res.status(400).json({
169
+ success: false,
170
+ message: typeError
171
+ });
172
+ }
173
+
174
+ if (typeof enabled !== 'boolean') {
175
+ return res.status(400).json({
176
+ success: false,
177
+ message: 'enabled must be a boolean'
178
+ });
179
+ }
180
+
181
+ // Check if item exists
182
+ const existing = registryService.getItem(type, name);
183
+ if (!existing) {
184
+ return res.status(404).json({
185
+ success: false,
186
+ message: `Item "${name}" not found in ${type}`
187
+ });
188
+ }
189
+
190
+ // Update registry
191
+ const item = registryService.toggleEnabled(type, name, enabled);
192
+
193
+ // Apply sync based on new state
194
+ if (enabled) {
195
+ // Sync to platforms where platform=true
196
+ if (item.platforms?.claude) {
197
+ syncManager.syncToClaude(type, name);
198
+ }
199
+ if (item.platforms?.codex) {
200
+ syncManager.syncToCodex(type, name);
201
+ }
202
+ } else {
203
+ // Remove from all platforms
204
+ syncManager.removeFromClaude(type, name);
205
+ syncManager.removeFromCodex(type, name);
206
+ }
207
+
208
+ res.json({
209
+ success: true,
210
+ item
211
+ });
212
+ } catch (err) {
213
+ console.error('[ConfigRegistry API] Toggle enabled error:', err);
214
+ res.status(500).json({
215
+ success: false,
216
+ message: err.message
217
+ });
218
+ }
219
+ });
220
+
221
+ /**
222
+ * PUT /api/config-registry/:type/:name/platform/:platform
223
+ * Toggle platform (claude/codex) for an item
224
+ * Body: { enabled: boolean }
225
+ * - Updates registry
226
+ * - If enabling platform: sync to that platform (if item is enabled)
227
+ * - If disabling platform: remove from that platform
228
+ * Returns { success: true, item: updatedEntry }
229
+ */
230
+ router.put('/:type/:name/platform/:platform', async (req, res) => {
231
+ try {
232
+ const { type, platform } = req.params;
233
+ const name = decodeURIComponent(req.params.name);
234
+ const { enabled } = req.body;
235
+
236
+ const typeError = validateType(type);
237
+ if (typeError) {
238
+ return res.status(400).json({
239
+ success: false,
240
+ message: typeError
241
+ });
242
+ }
243
+
244
+ const platformError = validatePlatform(platform);
245
+ if (platformError) {
246
+ return res.status(400).json({
247
+ success: false,
248
+ message: platformError
249
+ });
250
+ }
251
+
252
+ if (typeof enabled !== 'boolean') {
253
+ return res.status(400).json({
254
+ success: false,
255
+ message: 'enabled must be a boolean'
256
+ });
257
+ }
258
+
259
+ // Check if item exists
260
+ const existing = registryService.getItem(type, name);
261
+ if (!existing) {
262
+ return res.status(404).json({
263
+ success: false,
264
+ message: `Item "${name}" not found in ${type}`
265
+ });
266
+ }
267
+
268
+ // Update registry
269
+ const item = registryService.togglePlatform(type, name, platform, enabled);
270
+
271
+ // Apply sync based on new state
272
+ if (enabled && item.enabled) {
273
+ // Sync to this platform (only if item is enabled)
274
+ if (platform === 'claude') {
275
+ syncManager.syncToClaude(type, name);
276
+ } else if (platform === 'codex') {
277
+ syncManager.syncToCodex(type, name);
278
+ }
279
+ } else {
280
+ // Remove from this platform
281
+ if (platform === 'claude') {
282
+ syncManager.removeFromClaude(type, name);
283
+ } else if (platform === 'codex') {
284
+ syncManager.removeFromCodex(type, name);
285
+ }
286
+ }
287
+
288
+ res.json({
289
+ success: true,
290
+ item
291
+ });
292
+ } catch (err) {
293
+ console.error('[ConfigRegistry API] Toggle platform error:', err);
294
+ res.status(500).json({
295
+ success: false,
296
+ message: err.message
297
+ });
298
+ }
299
+ });
300
+
301
+ /**
302
+ * POST /api/config-registry/:type/sync
303
+ * Force sync all items of a type to their platforms based on registry
304
+ * Returns { success: true, synced: number }
305
+ */
306
+ router.post('/:type/sync', async (req, res) => {
307
+ try {
308
+ const { type } = req.params;
309
+
310
+ const typeError = validateType(type);
311
+ if (typeError) {
312
+ return res.status(400).json({
313
+ success: false,
314
+ message: typeError
315
+ });
316
+ }
317
+
318
+ // Get all items for this type
319
+ const items = registryService.listItems(type);
320
+
321
+ // Sync all items based on their registry state
322
+ const result = syncManager.syncAll(type, items);
323
+
324
+ res.json({
325
+ success: true,
326
+ type,
327
+ synced: result.synced.length,
328
+ removed: result.removed.length,
329
+ errors: result.errors,
330
+ warnings: result.warnings
331
+ });
332
+ } catch (err) {
333
+ console.error('[ConfigRegistry API] Sync all error:', err);
334
+ res.status(500).json({
335
+ success: false,
336
+ message: err.message
337
+ });
338
+ }
339
+ });
340
+
341
+ module.exports = router;