@adversity/coding-tool-x 3.0.5 → 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.
- package/CHANGELOG.md +19 -0
- package/dist/web/assets/{icons-BlzwYoRU.js → icons-BxudHPiX.js} +1 -1
- package/dist/web/assets/index-D2VfwJBa.js +14 -0
- package/dist/web/assets/index-oXBzu0bd.css +41 -0
- package/dist/web/assets/{naive-ui-B1TP-0TP.js → naive-ui-DT-Uur8K.js} +1 -1
- package/dist/web/index.html +4 -4
- package/package.json +1 -1
- package/src/server/api/channels.js +3 -0
- package/src/server/api/codex-channels.js +40 -0
- package/src/server/api/config-registry.js +341 -0
- package/src/server/api/gemini-channels.js +40 -0
- package/src/server/api/permissions.js +30 -15
- package/src/server/codex-proxy-server.js +27 -2
- package/src/server/gemini-proxy-server.js +61 -1
- package/src/server/index.js +3 -0
- package/src/server/proxy-server.js +27 -2
- package/src/server/services/codex-channels.js +9 -3
- package/src/server/services/config-registry-service.js +762 -0
- package/src/server/services/config-sync-manager.js +456 -0
- package/src/server/services/config-templates-service.js +38 -3
- package/src/server/services/gemini-channels.js +7 -1
- package/src/server/services/model-detector.js +116 -23
- package/src/server/services/permission-templates-service.js +0 -31
- package/dist/web/assets/index-19ZPjh5b.css +0 -41
- package/dist/web/assets/index-B4w1yh7H.js +0 -14
package/dist/web/index.html
CHANGED
|
@@ -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-
|
|
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-
|
|
12
|
-
<link rel="modulepreload" crossorigin href="/assets/naive-ui-
|
|
13
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
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>
|
package/package.json
CHANGED
|
@@ -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;
|
|
@@ -13,6 +13,8 @@ const { getChannelHealthStatus, resetChannelHealth } = require('../services/chan
|
|
|
13
13
|
const { broadcastSchedulerState } = require('../websocket-server');
|
|
14
14
|
const { isGeminiInstalled } = require('../services/gemini-config');
|
|
15
15
|
const { testChannelSpeed, testMultipleChannels, getLatencyLevel } = require('../services/speed-test');
|
|
16
|
+
const { clearGeminiRedirectCache } = require('../gemini-proxy-server');
|
|
17
|
+
const { fetchModelsFromProvider } = require('../services/model-detector');
|
|
16
18
|
|
|
17
19
|
module.exports = (config) => {
|
|
18
20
|
/**
|
|
@@ -41,6 +43,42 @@ module.exports = (config) => {
|
|
|
41
43
|
}
|
|
42
44
|
});
|
|
43
45
|
|
|
46
|
+
/**
|
|
47
|
+
* GET /api/gemini/channels/:id/models
|
|
48
|
+
* 获取渠道可用模型列表
|
|
49
|
+
*/
|
|
50
|
+
router.get('/:id/models', async (req, res) => {
|
|
51
|
+
try {
|
|
52
|
+
const { id } = req.params;
|
|
53
|
+
const channels = getChannels().channels || [];
|
|
54
|
+
const channel = channels.find(ch => ch.id === id);
|
|
55
|
+
|
|
56
|
+
if (!channel) {
|
|
57
|
+
return res.status(404).json({ error: '渠道不存在' });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Gemini 渠道尝试作为 OpenAI 兼容获取,失败则回退
|
|
61
|
+
const result = await fetchModelsFromProvider(channel, 'openai_compatible');
|
|
62
|
+
|
|
63
|
+
res.json({
|
|
64
|
+
channelId: id,
|
|
65
|
+
models: result.models,
|
|
66
|
+
supported: result.supported,
|
|
67
|
+
cached: result.cached,
|
|
68
|
+
fallbackUsed: result.fallbackUsed,
|
|
69
|
+
fetchedAt: result.lastChecked || new Date().toISOString(),
|
|
70
|
+
error: result.error,
|
|
71
|
+
errorHint: result.errorHint
|
|
72
|
+
});
|
|
73
|
+
} catch (error) {
|
|
74
|
+
console.error('[Gemini Channels API] Error fetching models:', error);
|
|
75
|
+
res.status(500).json({
|
|
76
|
+
error: '获取模型列表失败',
|
|
77
|
+
channelId: req.params.id
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
44
82
|
/**
|
|
45
83
|
* POST /api/gemini/channels
|
|
46
84
|
* 创建新渠道
|
|
@@ -86,6 +124,8 @@ module.exports = (config) => {
|
|
|
86
124
|
const updates = req.body;
|
|
87
125
|
|
|
88
126
|
const channel = updateChannel(channelId, updates);
|
|
127
|
+
// 清除该渠道的模型重定向日志缓存,使下次请求时重新打印
|
|
128
|
+
clearGeminiRedirectCache(channelId);
|
|
89
129
|
res.json(channel);
|
|
90
130
|
broadcastSchedulerState('gemini', getSchedulerState('gemini'));
|
|
91
131
|
} catch (err) {
|
|
@@ -30,16 +30,16 @@ const permissionTemplatesService = require('../services/permission-templates-ser
|
|
|
30
30
|
const router = express.Router();
|
|
31
31
|
|
|
32
32
|
// Claude Code 设置文件路径
|
|
33
|
-
function getClaudeSettingsPath(projectPath
|
|
33
|
+
function getClaudeSettingsPath(projectPath) {
|
|
34
34
|
if (projectPath) {
|
|
35
|
-
return path.join(projectPath, '.claude',
|
|
35
|
+
return path.join(projectPath, '.claude', 'settings.json');
|
|
36
36
|
}
|
|
37
37
|
return path.join(os.homedir(), '.claude', 'settings.json');
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
// 读取 Claude Code settings.json
|
|
41
|
-
function readClaudeSettings(projectPath
|
|
42
|
-
const settingsPath = getClaudeSettingsPath(projectPath
|
|
41
|
+
function readClaudeSettings(projectPath) {
|
|
42
|
+
const settingsPath = getClaudeSettingsPath(projectPath);
|
|
43
43
|
try {
|
|
44
44
|
if (fs.existsSync(settingsPath)) {
|
|
45
45
|
const content = fs.readFileSync(settingsPath, 'utf-8');
|
|
@@ -52,15 +52,29 @@ function readClaudeSettings(projectPath, isLocal = false) {
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
// 保存 Claude Code settings.json
|
|
55
|
-
function saveClaudeSettings(projectPath, settings
|
|
56
|
-
const settingsPath = getClaudeSettingsPath(projectPath
|
|
55
|
+
function saveClaudeSettings(projectPath, settings) {
|
|
56
|
+
const settingsPath = getClaudeSettingsPath(projectPath);
|
|
57
57
|
const settingsDir = path.dirname(settingsPath);
|
|
58
58
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
try {
|
|
60
|
+
// 确保目录存在
|
|
61
|
+
if (!fs.existsSync(settingsDir)) {
|
|
62
|
+
fs.mkdirSync(settingsDir, { recursive: true });
|
|
63
|
+
}
|
|
62
64
|
|
|
63
|
-
|
|
65
|
+
// 写入文件
|
|
66
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf-8');
|
|
67
|
+
|
|
68
|
+
// 验证文件已创建
|
|
69
|
+
if (!fs.existsSync(settingsPath)) {
|
|
70
|
+
throw new Error('文件写入后验证失败,文件未被创建');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return { success: true, path: settingsPath };
|
|
74
|
+
} catch (err) {
|
|
75
|
+
console.error('[Permissions API] Error saving Claude settings:', err);
|
|
76
|
+
throw new Error(`保存配置文件失败: ${err.message}`);
|
|
77
|
+
}
|
|
64
78
|
}
|
|
65
79
|
|
|
66
80
|
// 全局 all-allow 状态(内存中)
|
|
@@ -117,11 +131,11 @@ router.get('/', (req, res) => {
|
|
|
117
131
|
/**
|
|
118
132
|
* 保存项目的命令执行权限设置
|
|
119
133
|
* POST /api/permissions
|
|
120
|
-
* Body: { projectPath, settings: { allow, deny }
|
|
134
|
+
* Body: { projectPath, settings: { allow, deny } }
|
|
121
135
|
*/
|
|
122
136
|
router.post('/', (req, res) => {
|
|
123
137
|
try {
|
|
124
|
-
const { projectPath, settings: newPermissions
|
|
138
|
+
const { projectPath, settings: newPermissions } = req.body;
|
|
125
139
|
|
|
126
140
|
if (!projectPath) {
|
|
127
141
|
return res.status(400).json({
|
|
@@ -138,7 +152,7 @@ router.post('/', (req, res) => {
|
|
|
138
152
|
}
|
|
139
153
|
|
|
140
154
|
// 读取现有设置
|
|
141
|
-
const settings = readClaudeSettings(projectPath
|
|
155
|
+
const settings = readClaudeSettings(projectPath);
|
|
142
156
|
|
|
143
157
|
// 更新权限设置(使用 Claude Code 的标准格式)
|
|
144
158
|
settings.permissions = {
|
|
@@ -147,12 +161,13 @@ router.post('/', (req, res) => {
|
|
|
147
161
|
};
|
|
148
162
|
|
|
149
163
|
// 保存设置
|
|
150
|
-
saveClaudeSettings(projectPath, settings
|
|
164
|
+
const saveResult = saveClaudeSettings(projectPath, settings);
|
|
151
165
|
|
|
152
166
|
res.json({
|
|
153
167
|
success: true,
|
|
154
168
|
message: '权限设置已保存',
|
|
155
|
-
savedTo:
|
|
169
|
+
savedTo: '.claude/settings.json',
|
|
170
|
+
fullPath: saveResult.path
|
|
156
171
|
});
|
|
157
172
|
} catch (err) {
|
|
158
173
|
console.error('[Permissions API] Save permissions error:', err);
|
|
@@ -20,6 +20,10 @@ let currentPort = null;
|
|
|
20
20
|
// 用于存储每个请求的元数据
|
|
21
21
|
const requestMetadata = new Map();
|
|
22
22
|
|
|
23
|
+
// 用于缓存已打印过的模型重定向规则,避免重复打印
|
|
24
|
+
// 格式: { channelId: { "originalModel": "redirectedModel", ... } }
|
|
25
|
+
const printedRedirectCache = new Map();
|
|
26
|
+
|
|
23
27
|
// OpenAI 模型定价(每百万 tokens 的价格,单位:美元)
|
|
24
28
|
// Claude 模型使用 config/model-pricing.js 中的集中定价
|
|
25
29
|
const PRICING = {
|
|
@@ -283,7 +287,14 @@ async function startCodexProxyServer(options = {}) {
|
|
|
283
287
|
req.body.model = redirectedModel;
|
|
284
288
|
// 更新 rawBody 以匹配修改后的 body
|
|
285
289
|
req.rawBody = Buffer.from(JSON.stringify(req.body));
|
|
286
|
-
|
|
290
|
+
|
|
291
|
+
// 只在重定向规则变化时打印日志(避免每次请求都打印)
|
|
292
|
+
const cachedRedirects = printedRedirectCache.get(channel.id) || {};
|
|
293
|
+
if (cachedRedirects[originalModel] !== redirectedModel) {
|
|
294
|
+
cachedRedirects[originalModel] = redirectedModel;
|
|
295
|
+
printedRedirectCache.set(channel.id, cachedRedirects);
|
|
296
|
+
console.log(`[Codex Model Redirect] ${originalModel} → ${redirectedModel} (channel: ${channel.name})`);
|
|
297
|
+
}
|
|
287
298
|
}
|
|
288
299
|
}
|
|
289
300
|
|
|
@@ -648,8 +659,22 @@ function getCodexProxyStatus() {
|
|
|
648
659
|
};
|
|
649
660
|
}
|
|
650
661
|
|
|
662
|
+
/**
|
|
663
|
+
* 清除指定渠道的模型重定向日志缓存
|
|
664
|
+
* 用于在渠道配置更新后触发重新打印日志
|
|
665
|
+
* @param {string} channelId - 渠道 ID
|
|
666
|
+
*/
|
|
667
|
+
function clearCodexRedirectCache(channelId) {
|
|
668
|
+
if (channelId) {
|
|
669
|
+
printedRedirectCache.delete(channelId);
|
|
670
|
+
} else {
|
|
671
|
+
printedRedirectCache.clear();
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
651
675
|
module.exports = {
|
|
652
676
|
startCodexProxyServer,
|
|
653
677
|
stopCodexProxyServer,
|
|
654
|
-
getCodexProxyStatus
|
|
678
|
+
getCodexProxyStatus,
|
|
679
|
+
clearCodexRedirectCache
|
|
655
680
|
};
|