@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.
Files changed (34) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/dist/web/assets/{icons-BlzwYoRU.js → icons-CO_2OFES.js} +1 -1
  3. package/dist/web/assets/index-DI8QOi-E.js +14 -0
  4. package/dist/web/assets/index-uLHGdeZh.css +41 -0
  5. package/dist/web/assets/{naive-ui-B1TP-0TP.js → naive-ui-B1re3c-e.js} +1 -1
  6. package/dist/web/index.html +4 -4
  7. package/package.json +1 -1
  8. package/src/commands/daemon.js +11 -1
  9. package/src/commands/ui.js +8 -1
  10. package/src/index.js +3 -1
  11. package/src/server/api/channels.js +3 -0
  12. package/src/server/api/codex-channels.js +40 -0
  13. package/src/server/api/config-registry.js +341 -0
  14. package/src/server/api/gemini-channels.js +40 -0
  15. package/src/server/api/oauth.js +294 -0
  16. package/src/server/api/permissions.js +30 -15
  17. package/src/server/codex-proxy-server.js +30 -4
  18. package/src/server/config/oauth-providers.js +68 -0
  19. package/src/server/gemini-proxy-server.js +64 -2
  20. package/src/server/index.js +15 -3
  21. package/src/server/proxy-server.js +31 -4
  22. package/src/server/services/channels.js +33 -2
  23. package/src/server/services/codex-channels.js +35 -4
  24. package/src/server/services/config-registry-service.js +762 -0
  25. package/src/server/services/config-sync-manager.js +456 -0
  26. package/src/server/services/config-templates-service.js +38 -3
  27. package/src/server/services/gemini-channels.js +40 -1
  28. package/src/server/services/model-detector.js +116 -23
  29. package/src/server/services/oauth-callback-server.js +284 -0
  30. package/src/server/services/oauth-service.js +378 -0
  31. package/src/server/services/oauth-token-storage.js +135 -0
  32. package/src/server/services/permission-templates-service.js +0 -31
  33. package/dist/web/assets/index-19ZPjh5b.css +0 -41
  34. package/dist/web/assets/index-B4w1yh7H.js +0 -14
@@ -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-B4w1yh7H.js"></script>
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-BlzwYoRU.js">
12
- <link rel="modulepreload" crossorigin href="/assets/naive-ui-B1TP-0TP.js">
13
- <link rel="stylesheet" crossorigin href="/assets/index-19ZPjh5b.css">
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adversity/coding-tool-x",
3
- "version": "3.0.5",
3
+ "version": "3.1.0",
4
4
  "description": "Vibe Coding 增强工作助手 - 智能会话管理、动态渠道切换、全局搜索、实时监控",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -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: ['ui', '--daemon'],
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(' - 查看服务状态'));
@@ -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) {