@adversity/coding-tool-x 3.0.5 → 3.1.0
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 +42 -0
- package/dist/web/assets/{icons-BlzwYoRU.js → icons-CO_2OFES.js} +1 -1
- package/dist/web/assets/index-DI8QOi-E.js +14 -0
- package/dist/web/assets/index-uLHGdeZh.css +41 -0
- package/dist/web/assets/{naive-ui-B1TP-0TP.js → naive-ui-B1re3c-e.js} +1 -1
- package/dist/web/index.html +4 -4
- package/package.json +1 -1
- package/src/commands/daemon.js +11 -1
- package/src/commands/ui.js +8 -1
- package/src/index.js +3 -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/oauth.js +294 -0
- package/src/server/api/permissions.js +30 -15
- package/src/server/codex-proxy-server.js +30 -4
- package/src/server/config/oauth-providers.js +68 -0
- package/src/server/gemini-proxy-server.js +64 -2
- package/src/server/index.js +15 -3
- package/src/server/proxy-server.js +31 -4
- package/src/server/services/channels.js +33 -2
- package/src/server/services/codex-channels.js +35 -4
- 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 +40 -1
- package/src/server/services/model-detector.js +116 -23
- package/src/server/services/oauth-callback-server.js +284 -0
- package/src/server/services/oauth-service.js +378 -0
- package/src/server/services/oauth-token-storage.js +135 -0
- 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-DI8QOi-E.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-CO_2OFES.js">
|
|
12
|
+
<link rel="modulepreload" crossorigin href="/assets/naive-ui-B1re3c-e.js">
|
|
13
|
+
<link rel="stylesheet" crossorigin href="/assets/index-uLHGdeZh.css">
|
|
14
14
|
</head>
|
|
15
15
|
<body>
|
|
16
16
|
<div id="app"></div>
|
package/package.json
CHANGED
package/src/commands/daemon.js
CHANGED
|
@@ -72,11 +72,18 @@ async function handleStart() {
|
|
|
72
72
|
const config = loadConfig();
|
|
73
73
|
const port = config.ports?.webUI || 10099;
|
|
74
74
|
|
|
75
|
+
// 检查是否启用 LAN 访问 (--host 标志)
|
|
76
|
+
const enableHost = process.argv.includes('--host');
|
|
77
|
+
const pmArgs = ['ui', '--daemon'];
|
|
78
|
+
if (enableHost) {
|
|
79
|
+
pmArgs.push('--host');
|
|
80
|
+
}
|
|
81
|
+
|
|
75
82
|
// 启动 PM2 进程
|
|
76
83
|
pm2.start({
|
|
77
84
|
name: PM2_APP_NAME,
|
|
78
85
|
script: path.join(__dirname, '../index.js'),
|
|
79
|
-
args:
|
|
86
|
+
args: pmArgs,
|
|
80
87
|
interpreter: 'node',
|
|
81
88
|
autorestart: true,
|
|
82
89
|
max_memory_restart: '500M',
|
|
@@ -97,6 +104,9 @@ async function handleStart() {
|
|
|
97
104
|
|
|
98
105
|
console.log(chalk.green('\n✅ Coding-Tool 服务已启动(后台运行)\n'));
|
|
99
106
|
console.log(chalk.gray(`Web UI: http://localhost:${port}`));
|
|
107
|
+
if (enableHost) {
|
|
108
|
+
console.log(chalk.yellow(`⚠️ LAN 访问已启用 (http://<your-ip>:${port})`));
|
|
109
|
+
}
|
|
100
110
|
console.log(chalk.gray('\n可以安全关闭此终端窗口'));
|
|
101
111
|
console.log(chalk.gray('\n常用命令:'));
|
|
102
112
|
console.log(chalk.gray(' ') + chalk.cyan('ctx status') + chalk.gray(' - 查看服务状态'));
|
package/src/commands/ui.js
CHANGED
|
@@ -8,9 +8,16 @@ async function handleUI() {
|
|
|
8
8
|
// 检查是否为 daemon 模式(PM2 启动)
|
|
9
9
|
const isDaemon = process.argv.includes('--daemon');
|
|
10
10
|
|
|
11
|
+
// 检查是否启用 LAN 访问 (--host 标志)
|
|
12
|
+
const enableHost = process.argv.includes('--host');
|
|
13
|
+
const host = enableHost ? '0.0.0.0' : '127.0.0.1';
|
|
14
|
+
|
|
11
15
|
if (!isDaemon) {
|
|
12
16
|
console.clear();
|
|
13
17
|
console.log(chalk.cyan.bold('\n🌐 启动 Coding-Tool Web UI...\n'));
|
|
18
|
+
if (enableHost) {
|
|
19
|
+
console.log(chalk.yellow('⚠️ LAN 访问已启用 (--host)\n'));
|
|
20
|
+
}
|
|
14
21
|
}
|
|
15
22
|
|
|
16
23
|
// 从配置加载端口
|
|
@@ -19,7 +26,7 @@ async function handleUI() {
|
|
|
19
26
|
const url = `http://localhost:${port}`;
|
|
20
27
|
|
|
21
28
|
try {
|
|
22
|
-
await startServer(port);
|
|
29
|
+
await startServer(port, host);
|
|
23
30
|
|
|
24
31
|
// 自动打开浏览器(仅非 daemon 模式)
|
|
25
32
|
if (!isDaemon) {
|
package/src/index.js
CHANGED
|
@@ -48,8 +48,10 @@ function showHelp() {
|
|
|
48
48
|
console.log(' ctx status 查看服务状态\n');
|
|
49
49
|
|
|
50
50
|
console.log(chalk.yellow('📱 UI 管理:'));
|
|
51
|
-
console.log(' ctx ui 前台启动 Web UI
|
|
51
|
+
console.log(' ctx ui 前台启动 Web UI(仅本地访问)');
|
|
52
|
+
console.log(' ctx ui --host 前台启动 Web UI(允许 LAN 访问)');
|
|
52
53
|
console.log(' ctx ui start 后台启动 Web UI');
|
|
54
|
+
console.log(' ctx ui start --host 后台启动 Web UI(允许 LAN 访问)');
|
|
53
55
|
console.log(' ctx ui stop 停止 Web UI');
|
|
54
56
|
console.log(' ctx ui restart 重启 Web UI\n');
|
|
55
57
|
|
|
@@ -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) {
|