coding-tool-x 3.7.11 → 3.7.12

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 (42) hide show
  1. package/CHANGELOG.md +3 -0
  2. package/README.md +1 -1
  3. package/dist/web/assets/{Analytics-Ck_00Brj.js → Analytics-8dRv1aYW.js} +1 -1
  4. package/dist/web/assets/{ConfigTemplates-Cpw2vUa8.js → ConfigTemplates-BQRXSICO.js} +1 -1
  5. package/dist/web/assets/Home-BtzWhKIa.js +1 -0
  6. package/dist/web/assets/Home-DYVy4cGZ.css +1 -0
  7. package/dist/web/assets/{PluginManager-2RlvEJCT.js → PluginManager-BTa-Ygf6.js} +1 -1
  8. package/dist/web/assets/{ProjectList-_ecCngpl.js → ProjectList-F_cB8ldA.js} +1 -1
  9. package/dist/web/assets/{SessionList-DlWcw3hO.js → SessionList-LOrbPQvu.js} +1 -1
  10. package/dist/web/assets/SkillManager-BsX8Qpj0.css +1 -0
  11. package/dist/web/assets/SkillManager-BzI_t-yV.js +1 -0
  12. package/dist/web/assets/{WorkspaceManager-3mqghi09.js → WorkspaceManager-Db9RGiz7.js} +1 -1
  13. package/dist/web/assets/index-DIWJmyL6.js +2 -0
  14. package/dist/web/assets/{index-BceXgSSe.css → index-bJRG3RXH.css} +1 -1
  15. package/dist/web/index.html +2 -2
  16. package/docs/superpowers/plans/2026-08-01-omp-skill-scan-settings.md +759 -0
  17. package/docs/superpowers/specs/2026-08-01-omp-skill-scan-settings-design.md +224 -0
  18. package/package.json +1 -1
  19. package/src/commands/proxy-control.js +12 -12
  20. package/src/commands/toggle-proxy.js +9 -9
  21. package/src/config/paths.js +52 -29
  22. package/src/index.js +1 -1
  23. package/src/server/api/plugins.js +119 -47
  24. package/src/server/api/skills.js +63 -10
  25. package/src/server/omp-proxy-server.js +14 -1
  26. package/src/server/services/model-definition-schema.js +2 -1
  27. package/src/server/services/omp-channels.js +71 -12
  28. package/src/server/services/omp-config.js +71 -6
  29. package/src/server/services/omp-native-plugin-adapter.js +456 -0
  30. package/src/server/services/omp-settings-manager.js +3 -1
  31. package/src/server/services/omp-skill-discovery.js +358 -0
  32. package/src/server/services/omp-skill-settings-service.js +97 -0
  33. package/src/server/services/pi-omp-migration.js +273 -0
  34. package/src/server/services/platform-resolution.js +44 -0
  35. package/src/server/services/plugins-service.js +85 -561
  36. package/src/server/services/project-path-validation.js +65 -0
  37. package/src/server/services/skill-service.js +169 -77
  38. package/dist/web/assets/Home-Bft3Kztl.js +0 -1
  39. package/dist/web/assets/Home-CHZJkkjH.css +0 -1
  40. package/dist/web/assets/SkillManager-CnY7IXkX.js +0 -1
  41. package/dist/web/assets/SkillManager-D7pd-d_P.css +0 -1
  42. package/dist/web/assets/index-k5oP7c-A.js +0 -2
@@ -8,13 +8,14 @@ const express = require('express');
8
8
  const { PluginsService } = require('../services/plugins-service');
9
9
  const { maskToken } = require('../services/oauth-utils');
10
10
  const { sendApiError } = require('./validation-errors');
11
+ const { resolveManagedPlatform } = require('../services/platform-resolution');
12
+ const { validateKnownProjectCwd } = require('../services/project-path-validation');
11
13
 
12
14
  const router = express.Router();
13
- const SUPPORTED_PLATFORMS = ['claude', 'codex', 'gemini', 'opencode', 'omp'];
14
15
  const pluginServices = new Map();
15
16
 
16
17
  function resolvePlatform(rawPlatform) {
17
- return SUPPORTED_PLATFORMS.includes(rawPlatform) ? rawPlatform : 'claude';
18
+ return resolveManagedPlatform(rawPlatform);
18
19
  }
19
20
 
20
21
  function getPlatform(req) {
@@ -22,11 +23,31 @@ function getPlatform(req) {
22
23
  }
23
24
 
24
25
  function getPluginsService(req) {
25
- const platform = getPlatform(req);
26
+ const resolution = getPlatform(req);
27
+ const platform = resolution.platform;
26
28
  if (!pluginServices.has(platform)) {
27
29
  pluginServices.set(platform, new PluginsService(platform));
28
30
  }
29
- return { platform, service: pluginServices.get(platform) };
31
+ return {
32
+ platform,
33
+ service: pluginServices.get(platform),
34
+ warning: resolution.warning
35
+ };
36
+ }
37
+
38
+ async function getRequestOptions(req) {
39
+ const scope = String(req.query?.scope || req.body?.scope || '').trim();
40
+ if (scope && scope !== 'user' && scope !== 'project') {
41
+ throw new Error('Invalid scope: expected "user" or "project"');
42
+ }
43
+ const cwd = await validateKnownProjectCwd(req.query?.cwd || req.body?.cwd);
44
+ if (scope === 'project' && !cwd) {
45
+ throw new Error('Project scope requires a valid cwd');
46
+ }
47
+ return {
48
+ ...(cwd ? { cwd } : {}),
49
+ ...(scope ? { scope } : {})
50
+ };
30
51
  }
31
52
 
32
53
  function extractRepoPayload(source = {}) {
@@ -43,7 +64,8 @@ function extractRepoPayload(source = {}) {
43
64
  localPath: repo.localPath || source.localPath || '',
44
65
  repoUrl: repo.repoUrl || repo.url || source.repoUrl || source.url || '',
45
66
  token: repo.token || source.token || '',
46
- marketplace: repo.marketplace || source.marketplace || ''
67
+ marketplace: repo.marketplace || source.marketplace || '',
68
+ source: repo.source || repo.sourceUri || source.source || source.sourceUri || ''
47
69
  };
48
70
  }
49
71
 
@@ -65,6 +87,10 @@ function sanitizeRepos(service, repos = []) {
65
87
  return (Array.isArray(repos) ? repos : []).map(sanitizeRepo);
66
88
  }
67
89
 
90
+ function hasRequestOptions(options = {}) {
91
+ return Object.keys(options).length > 0;
92
+ }
93
+
68
94
  /**
69
95
  * 获取平台插件能力
70
96
  * GET /api/plugins/capabilities
@@ -91,15 +117,19 @@ router.get('/capabilities', (req, res) => {
91
117
  * 获取插件列表
92
118
  * GET /api/plugins
93
119
  */
94
- router.get('/', (req, res) => {
120
+ router.get('/', async (req, res) => {
95
121
  try {
96
- const { platform, service } = getPluginsService(req);
97
- const result = service.listPlugins();
122
+ const { platform, service, warning } = getPluginsService(req);
123
+ const options = await getRequestOptions(req);
124
+ const result = platform === 'omp' || hasRequestOptions(options)
125
+ ? service.listPlugins(options)
126
+ : service.listPlugins();
98
127
 
99
128
  res.json({
100
129
  success: true,
101
130
  platform,
102
- ...result
131
+ ...result,
132
+ ...(warning ? { warnings: [warning] } : {})
103
133
  });
104
134
  } catch (err) {
105
135
  console.error('[Plugins API] List plugins error:', err);
@@ -113,18 +143,22 @@ router.get('/', (req, res) => {
113
143
  */
114
144
  router.get('/market', async (req, res) => {
115
145
  try {
116
- const { platform, service } = getPluginsService(req);
146
+ const { platform, service, warning } = getPluginsService(req);
147
+ const options = await getRequestOptions(req);
117
148
  const forceRefresh = req.query.refresh === '1';
118
149
  if (forceRefresh) {
119
150
  console.log(`[Plugins API] Refreshing market plugins for ${platform}...`);
120
151
  }
121
- const plugins = await service.getMarketPlugins(forceRefresh);
152
+ const plugins = platform === 'omp' || hasRequestOptions(options)
153
+ ? await service.getMarketPlugins(forceRefresh, options)
154
+ : await service.getMarketPlugins(forceRefresh);
122
155
  console.log(`[Plugins API] ${platform}: ${plugins.length} market plugins loaded (refresh=${forceRefresh})`);
123
156
 
124
157
  res.json({
125
158
  success: true,
126
159
  platform,
127
- plugins
160
+ plugins,
161
+ ...(warning ? { warnings: [warning] } : {})
128
162
  });
129
163
  } catch (err) {
130
164
  console.error('[Plugins API] Get market plugins error:', err);
@@ -139,14 +173,15 @@ router.get('/market', async (req, res) => {
139
173
  */
140
174
  router.post('/install', async (req, res) => {
141
175
  try {
142
- const { platform, service } = getPluginsService(req);
143
- const { directory, repo, gitUrl, source } = req.body;
176
+ const { platform, service, warning } = getPluginsService(req);
177
+ const options = await getRequestOptions(req);
178
+ const { directory, repo, gitUrl, source, pluginId } = req.body;
144
179
  const hasDirectoryField = Object.prototype.hasOwnProperty.call(req.body, 'directory');
145
180
 
146
181
  // Support both new format (directory + repo) and legacy format (gitUrl)
147
182
  let installUrl;
148
- if (source) {
149
- installUrl = source;
183
+ if (pluginId || source) {
184
+ installUrl = pluginId || source;
150
185
  } else if (repo && hasDirectoryField) {
151
186
  installUrl = '';
152
187
  } else if (gitUrl) {
@@ -158,15 +193,25 @@ router.post('/install', async (req, res) => {
158
193
  });
159
194
  }
160
195
 
161
- const result = await service.installPlugin(
162
- installUrl,
163
- repo && hasDirectoryField
164
- ? {
165
- ...extractRepoPayload({ repo }),
166
- directory: directory || ''
167
- }
168
- : null
169
- );
196
+ const repoMetadata = repo && hasDirectoryField
197
+ ? {
198
+ ...extractRepoPayload({ repo }),
199
+ directory: directory || ''
200
+ }
201
+ : platform === 'omp' ? {
202
+ ...(req.body.metadata && typeof req.body.metadata === 'object' ? req.body.metadata : {}),
203
+ name: req.body.name || req.body.metadata?.name,
204
+ pluginId: pluginId || req.body.metadata?.pluginId,
205
+ pluginKind: req.body.pluginKind || req.body.metadata?.pluginKind,
206
+ marketplace: req.body.marketplace || req.body.metadata?.marketplace,
207
+ installSource: req.body.installSource || req.body.metadata?.installSource,
208
+ version: req.body.version || req.body.metadata?.version,
209
+ description: req.body.description || req.body.metadata?.description,
210
+ resourceTypes: req.body.resourceTypes || req.body.metadata?.resourceTypes
211
+ } : null;
212
+ const result = platform === 'omp' || hasRequestOptions(options)
213
+ ? await service.installPlugin(installUrl, repoMetadata, options)
214
+ : await service.installPlugin(installUrl, repoMetadata);
170
215
 
171
216
  if (!result.success) {
172
217
  return res.status(400).json({
@@ -179,7 +224,8 @@ router.post('/install', async (req, res) => {
179
224
  success: true,
180
225
  platform,
181
226
  plugin: result.plugin,
182
- message: `Plugin "${result.plugin.name}" installed successfully`
227
+ message: `Plugin "${result.plugin.name}" installed successfully`,
228
+ ...(warning ? { warnings: [warning] } : {})
183
229
  });
184
230
  } catch (err) {
185
231
  console.error('[Plugins API] Install plugin error:', err);
@@ -193,14 +239,23 @@ router.post('/install', async (req, res) => {
193
239
  * 获取插件仓库列表
194
240
  * GET /api/plugins/repos
195
241
  */
196
- router.get('/repos', (req, res) => {
242
+ router.get('/repos', async (req, res) => {
197
243
  try {
198
- const { platform, service } = getPluginsService(req);
199
- const repos = service.getRepos();
244
+ const { platform, service, warning } = getPluginsService(req);
245
+ const options = await getRequestOptions(req);
246
+ const repos = platform === 'omp' || hasRequestOptions(options)
247
+ ? service.getRepos(options)
248
+ : service.getRepos();
200
249
  res.json({
201
250
  success: true,
202
251
  platform,
203
- repos: sanitizeRepos(service, repos)
252
+ repos: sanitizeRepos(service, repos),
253
+ ...((warning || service.getMigrationWarnings?.().length) ? {
254
+ warnings: [
255
+ ...(warning ? [warning] : []),
256
+ ...(service.getMigrationWarnings?.() || [])
257
+ ]
258
+ } : {})
204
259
  });
205
260
  } catch (err) {
206
261
  console.error('[Plugins API] Get repos error:', err);
@@ -213,20 +268,23 @@ router.get('/repos', (req, res) => {
213
268
  * POST /api/plugins/repos
214
269
  * Body: { url, name, description }
215
270
  */
216
- router.post('/repos', (req, res) => {
271
+ router.post('/repos', async (req, res) => {
217
272
  try {
218
273
  const { platform, service } = getPluginsService(req);
274
+ const options = await getRequestOptions(req);
219
275
  const repo = extractRepoPayload(req.body);
220
276
  repo.enabled = req.body.enabled !== false;
221
277
 
222
- if (!repo.localPath && !repo.projectPath && (!repo.owner || !repo.name) && !repo.repoUrl) {
278
+ if (!repo.source && !repo.localPath && !repo.projectPath && (!repo.owner || !repo.name) && !repo.repoUrl) {
223
279
  return res.status(400).json({
224
280
  success: false,
225
281
  message: 'Missing repo info'
226
282
  });
227
283
  }
228
284
 
229
- const repos = service.addRepo(repo);
285
+ const repos = platform === 'omp' || hasRequestOptions(options)
286
+ ? service.addRepo(repo, options)
287
+ : service.addRepo(repo);
230
288
 
231
289
  res.json({
232
290
  success: true,
@@ -240,11 +298,14 @@ router.post('/repos', (req, res) => {
240
298
  }
241
299
  });
242
300
 
243
- router.delete('/repos', (req, res) => {
301
+ router.delete('/repos', async (req, res) => {
244
302
  try {
245
303
  const { platform, service } = getPluginsService(req);
304
+ const options = await getRequestOptions(req);
246
305
  const { id = '', owner = '', name = '' } = req.query;
247
- const repos = service.removeRepo(owner, name, id);
306
+ const repos = platform === 'omp' || hasRequestOptions(options)
307
+ ? service.removeRepo(owner, name, id, options)
308
+ : service.removeRepo(owner, name, id);
248
309
 
249
310
  res.json({
250
311
  success: true,
@@ -376,7 +437,8 @@ router.put('/repos/auth', (req, res) => {
376
437
  router.post('/repos/sync', async (req, res) => {
377
438
  try {
378
439
  const { platform, service } = getPluginsService(req);
379
- const result = await service.syncRepos();
440
+ const options = await getRequestOptions(req);
441
+ const result = await service.syncRepos(options);
380
442
 
381
443
  res.json({
382
444
  success: true,
@@ -397,7 +459,8 @@ router.post('/repos/sync', async (req, res) => {
397
459
  router.post('/sync', async (req, res) => {
398
460
  try {
399
461
  const { platform, service } = getPluginsService(req);
400
- const result = await service.syncPlugins();
462
+ const options = await getRequestOptions(req);
463
+ const result = await service.syncPlugins(options);
401
464
 
402
465
  res.json({
403
466
  success: true,
@@ -472,12 +535,13 @@ router.get('/:name/readme', async (req, res) => {
472
535
  * 获取单个插件详情
473
536
  * GET /api/plugins/:name
474
537
  */
475
- router.get('/:name', (req, res) => {
538
+ router.get('/:name', async (req, res) => {
476
539
  try {
477
540
  const { platform, service } = getPluginsService(req);
478
541
  const { name } = req.params;
542
+ const options = await getRequestOptions(req);
479
543
 
480
- const plugin = service.getPlugin(name);
544
+ const plugin = service.getPlugin(req.query.pluginId || name, options);
481
545
 
482
546
  if (!plugin) {
483
547
  return res.status(404).json({
@@ -501,12 +565,14 @@ router.get('/:name', (req, res) => {
501
565
  * 卸载插件
502
566
  * DELETE /api/plugins/:name
503
567
  */
504
- router.delete('/:name', (req, res) => {
568
+ router.delete('/:name', async (req, res) => {
505
569
  try {
506
570
  const { platform, service } = getPluginsService(req);
507
571
  const { name } = req.params;
572
+ const options = await getRequestOptions(req);
573
+ const pluginId = req.query.pluginId || name;
508
574
 
509
- const result = service.uninstallPlugin(name);
575
+ const result = service.uninstallPlugin(pluginId, options);
510
576
 
511
577
  if (!result.success) {
512
578
  return res.status(400).json({
@@ -531,11 +597,12 @@ router.delete('/:name', (req, res) => {
531
597
  * PUT /api/plugins/:name/toggle
532
598
  * Body: { enabled }
533
599
  */
534
- router.put('/:name/toggle', (req, res) => {
600
+ router.put('/:name/toggle', async (req, res) => {
535
601
  try {
536
602
  const { platform, service } = getPluginsService(req);
537
603
  const { name } = req.params;
538
- const { enabled } = req.body;
604
+ const { enabled, pluginId = name } = req.body;
605
+ const options = await getRequestOptions(req);
539
606
 
540
607
  if (typeof enabled !== 'boolean') {
541
608
  return res.status(400).json({
@@ -544,7 +611,9 @@ router.put('/:name/toggle', (req, res) => {
544
611
  });
545
612
  }
546
613
 
547
- const plugin = service.togglePlugin(name, enabled);
614
+ const plugin = platform === 'omp' || hasRequestOptions(options)
615
+ ? service.togglePlugin(pluginId, enabled, options)
616
+ : service.togglePlugin(pluginId, enabled);
548
617
 
549
618
  res.json({
550
619
  success: true,
@@ -563,11 +632,12 @@ router.put('/:name/toggle', (req, res) => {
563
632
  * PUT /api/plugins/:name/config
564
633
  * Body: { config }
565
634
  */
566
- router.put('/:name/config', (req, res) => {
635
+ router.put('/:name/config', async (req, res) => {
567
636
  try {
568
637
  const { platform, service } = getPluginsService(req);
569
638
  const { name } = req.params;
570
- const { config } = req.body;
639
+ const { config, pluginId = name } = req.body;
640
+ const options = await getRequestOptions(req);
571
641
 
572
642
  if (!config || typeof config !== 'object') {
573
643
  return res.status(400).json({
@@ -576,7 +646,9 @@ router.put('/:name/config', (req, res) => {
576
646
  });
577
647
  }
578
648
 
579
- const result = service.updatePluginConfig(name, config);
649
+ const result = platform === 'omp' || hasRequestOptions(options)
650
+ ? service.updatePluginConfig(pluginId, config, options)
651
+ : service.updatePluginConfig(pluginId, config);
580
652
 
581
653
  res.json({
582
654
  success: true,
@@ -6,13 +6,18 @@ const express = require('express');
6
6
  const { SkillService } = require('../services/skill-service');
7
7
  const { maskToken } = require('../services/oauth-utils');
8
8
  const { sendApiError } = require('./validation-errors');
9
+ const { resolveManagedPlatform } = require('../services/platform-resolution');
10
+ const { validateKnownProjectCwd } = require('../services/project-path-validation');
11
+ const {
12
+ readOmpSkillSettings,
13
+ updateOmpSkillSettings
14
+ } = require('../services/omp-skill-settings-service');
9
15
 
10
16
  const router = express.Router();
11
- const SUPPORTED_PLATFORMS = ['claude', 'codex', 'gemini', 'opencode', 'omp'];
12
17
  const skillServices = new Map();
13
18
 
14
19
  function resolvePlatform(rawPlatform) {
15
- return SUPPORTED_PLATFORMS.includes(rawPlatform) ? rawPlatform : 'claude';
20
+ return resolveManagedPlatform(rawPlatform);
16
21
  }
17
22
 
18
23
  function getPlatform(req) {
@@ -20,11 +25,16 @@ function getPlatform(req) {
20
25
  }
21
26
 
22
27
  function getSkillService(req) {
23
- const platform = getPlatform(req);
28
+ const resolution = getPlatform(req);
29
+ const platform = resolution.platform;
24
30
  if (!skillServices.has(platform)) {
25
31
  skillServices.set(platform, new SkillService(platform));
26
32
  }
27
- return { platform, service: skillServices.get(platform) };
33
+ return {
34
+ platform,
35
+ service: skillServices.get(platform),
36
+ warning: resolution.warning
37
+ };
28
38
  }
29
39
 
30
40
  function extractRepoPayload(source = {}) {
@@ -69,19 +79,21 @@ function sanitizeRepos(service, repos = []) {
69
79
  */
70
80
  router.get('/', async (req, res) => {
71
81
  try {
72
- const { platform, service } = getSkillService(req);
82
+ const { platform, service, warning } = getSkillService(req);
73
83
  const forceRefresh = req.query.refresh === '1';
84
+ const cwd = await validateKnownProjectCwd(req.query.cwd);
74
85
  if (forceRefresh) {
75
86
  console.log(`[Skills API] Refreshing skills for ${platform}...`);
76
87
  }
77
- const skills = await service.listSkills(forceRefresh);
88
+ const skills = await service.listSkills(forceRefresh, { cwd });
78
89
  console.log(`[Skills API] ${platform}: ${skills.length} skills loaded (refresh=${forceRefresh})`);
79
90
  res.json({
80
91
  success: true,
81
92
  platform,
82
93
  skills,
83
94
  total: skills.length,
84
- installed: skills.filter(s => s.installed).length
95
+ installed: skills.filter(s => s.installed).length,
96
+ ...(warning ? { warnings: [warning] } : {})
85
97
  });
86
98
  } catch (err) {
87
99
  console.error('[Skills API] List skills error:', err);
@@ -89,6 +101,34 @@ router.get('/', async (req, res) => {
89
101
  }
90
102
  });
91
103
 
104
+ /**
105
+ * 获取 OMP 技能设置
106
+ * GET /api/skills/omp-settings
107
+ */
108
+ router.get('/omp-settings', (req, res) => {
109
+ try {
110
+ const settings = readOmpSkillSettings();
111
+ res.json({ success: true, settings });
112
+ } catch (err) {
113
+ console.error('[Skills API] Get OMP skill settings error:', err);
114
+ sendApiError(res, err);
115
+ }
116
+ });
117
+
118
+ /**
119
+ * 更新 OMP 技能设置
120
+ * PUT /api/skills/omp-settings
121
+ */
122
+ router.put('/omp-settings', (req, res) => {
123
+ try {
124
+ const settings = updateOmpSkillSettings(req.body);
125
+ res.json({ success: true, settings });
126
+ } catch (err) {
127
+ console.error('[Skills API] Update OMP skill settings error:', err);
128
+ sendApiError(res, err);
129
+ }
130
+ });
131
+
92
132
  /**
93
133
  * 获取技能详情(完整内容)
94
134
  * GET /api/skills/detail/:directory
@@ -257,10 +297,10 @@ router.post('/create', (req, res) => {
257
297
  * POST /api/skills/uninstall
258
298
  * Body: { directory }
259
299
  */
260
- router.post('/uninstall', (req, res) => {
300
+ router.post('/uninstall', async (req, res) => {
261
301
  try {
262
302
  const { platform, service } = getSkillService(req);
263
- const { directory } = req.body;
303
+ const { directory, scope } = req.body;
264
304
 
265
305
  if (!directory) {
266
306
  return res.status(400).json({
@@ -269,7 +309,20 @@ router.post('/uninstall', (req, res) => {
269
309
  });
270
310
  }
271
311
 
272
- const result = service.uninstallSkill(directory);
312
+ const cwd = await validateKnownProjectCwd(req.body.cwd);
313
+ if (scope && scope !== 'user' && scope !== 'project') {
314
+ throw new Error('Invalid scope: expected "user" or "project"');
315
+ }
316
+ if (scope === 'project' && !cwd) {
317
+ throw new Error('Project scope requires a valid cwd');
318
+ }
319
+ const options = {
320
+ ...(cwd ? { cwd } : {}),
321
+ ...(scope ? { scope } : {})
322
+ };
323
+ const result = platform === 'omp' && Object.keys(options).length > 0
324
+ ? service.uninstallSkill(directory, options)
325
+ : service.uninstallSkill(directory);
273
326
 
274
327
  res.json({
275
328
  success: true,
@@ -4,6 +4,7 @@ const { saveProxyStartTime, clearProxyStartTime, getProxyStartTime, getProxyRunt
4
4
  const {
5
5
  syncManagedOmpProviders,
6
6
  disableManagedOmpProviders,
7
+ activateStaticOmpChannel,
7
8
  getEnabledChannels,
8
9
  isManagedOmpModeEnabled,
9
10
  enableManagedOmpMode,
@@ -115,10 +116,22 @@ async function stopOmpProxyServerUnlocked(options = {}) {
115
116
  warnings: lastSyncResult?.warnings || []
116
117
  };
117
118
  }
119
+ const state = loadManagedOmpModeState?.() || {
120
+ activeChannelId: loadManagedOmpActiveChannelId()
121
+ };
122
+ const enabledChannels = getEnabledChannels();
123
+ const activeChannel = enabledChannels.find(channel => channel.id === state.activeChannelId)
124
+ || enabledChannels[0]
125
+ || null;
118
126
  gateway.beginDraining();
119
127
  stopOmpSessionLogObserver();
120
128
  try {
121
- lastSyncResult = disableManagedOmpProviders();
129
+ if (activeChannel) {
130
+ const handoff = activateStaticOmpChannel(activeChannel.id);
131
+ lastSyncResult = handoff?.sync || lastSyncResult;
132
+ } else {
133
+ lastSyncResult = disableManagedOmpProviders();
134
+ }
122
135
  } catch (error) {
123
136
  startOmpSessionLogObserver();
124
137
  gateway.cancelDraining();
@@ -390,7 +390,8 @@ function normalizeCatalogModelList(value) {
390
390
  const warnings = [];
391
391
  for (const rawModel of Array.isArray(value) ? value : []) {
392
392
  const rawId = String(rawModel?.id || rawModel?.name || '').trim();
393
- const normalized = normalizeModelSpec(rawModel, rawId);
393
+ const { provider: _provider, selector: _selector, ...modelFields } = isPlainObject(rawModel) ? rawModel : {};
394
+ const normalized = normalizeModelSpec(modelFields, rawId);
394
395
  if (!normalized.spec?.id) continue;
395
396
  warnings.push(...normalized.warnings.map(message => `${normalized.spec.id}: ${message}`));
396
397
  const key = normalized.spec.id.toLowerCase();
@@ -22,6 +22,24 @@ const {
22
22
 
23
23
  const OMP_THINKING_SUFFIX_RE = /:(minimal|low|medium|high|xhigh|off)$/;
24
24
 
25
+ function clearOmpChannelBalanceCache(channel) {
26
+ try {
27
+ require('./channel-balance').clearChannelBalanceCache('omp', channel);
28
+ } catch (_) {
29
+ // Balance cache invalidation is an optimization; channel activation must still succeed.
30
+ }
31
+ }
32
+
33
+ function selectLatestEnabledChannel(channels = []) {
34
+ const enabledChannels = (channels || []).filter(channel => channel?.enabled !== false);
35
+ if (enabledChannels.length === 0) return null;
36
+ return enabledChannels.reduce((latest, current) => {
37
+ const latestTs = Number(latest?.updatedAt || latest?.createdAt || 0);
38
+ const currentTs = Number(current?.updatedAt || current?.createdAt || 0);
39
+ return currentTs > latestTs ? current : latest;
40
+ }, enabledChannels[0]);
41
+ }
42
+
25
43
  function loadManagedOmpModeState() {
26
44
  ensureStorageDirMigrated();
27
45
  try {
@@ -140,30 +158,33 @@ class OmpChannelService extends BaseChannelService {
140
158
  gateway: state.gateway,
141
159
  activeChannelId: channel.id
142
160
  } : {});
161
+ return;
143
162
  }
163
+ writeManagedOmpProviders([channel], {});
144
164
  }
145
165
 
146
166
  _onAfterCreate(_channel, allChannels) {
147
- this.syncManagedOmpProvidersIfEnabled(allChannels);
167
+ this.syncOmpProvidersForCurrentMode(allChannels);
148
168
  }
149
169
 
150
170
  _onAfterUpdate(_oldChannel, _newChannel, allChannels) {
151
- this.syncManagedOmpProvidersIfEnabled(allChannels);
171
+ this.syncOmpProvidersForCurrentMode(allChannels);
152
172
  }
153
173
 
154
174
  _onAfterDelete(_channel, allChannels) {
155
- this.syncManagedOmpProvidersIfEnabled(allChannels);
175
+ this.syncOmpProvidersForCurrentMode(allChannels);
156
176
  }
157
177
 
158
- syncManagedOmpProvidersIfEnabled(channels = this.getChannels().channels) {
159
- if (!isManagedOmpModeEnabled()) {
160
- return getLastManagedOmpSyncResult();
178
+ syncOmpProvidersForCurrentMode(channels = this.getChannels().channels) {
179
+ if (isManagedOmpModeEnabled()) {
180
+ const state = loadManagedOmpModeState();
181
+ return this.syncManagedOmpProviders(channels, state?.gateway ? {
182
+ gateway: state.gateway,
183
+ activeChannelId: state.activeChannelId
184
+ } : {});
161
185
  }
162
- const state = loadManagedOmpModeState();
163
- return this.syncManagedOmpProviders(channels, state?.gateway ? {
164
- gateway: state.gateway,
165
- activeChannelId: state.activeChannelId
166
- } : {});
186
+ const activeChannel = selectLatestEnabledChannel(channels);
187
+ return this.syncManagedOmpProviders(activeChannel ? [activeChannel] : [], {});
167
188
  }
168
189
 
169
190
  syncManagedOmpProviders(channels = this.getChannels().channels, options = {}) {
@@ -181,8 +202,45 @@ class OmpChannelService extends BaseChannelService {
181
202
  return getLastManagedOmpSyncResult();
182
203
  }
183
204
 
205
+ activateStaticOmpChannel(channelId) {
206
+ const data = this.loadChannels();
207
+ const channel = data.channels.find(item => item.id === channelId);
208
+ if (!channel) {
209
+ throw new Error('Channel not found');
210
+ }
211
+
212
+ const previousData = JSON.parse(JSON.stringify(data));
213
+ const wasEnabled = channel.enabled !== false;
214
+ data.channels.forEach(item => {
215
+ item.enabled = item.id === channelId;
216
+ });
217
+
218
+ try {
219
+ this.saveChannels(data);
220
+ const sync = this.syncManagedOmpProviders([channel], {});
221
+ if (!wasEnabled) {
222
+ clearOmpChannelBalanceCache(channel);
223
+ }
224
+ return { channel, sync };
225
+ } catch (error) {
226
+ try {
227
+ this.saveChannels(previousData);
228
+ } catch (rollbackError) {
229
+ error.rollbackError = rollbackError;
230
+ }
231
+ throw error;
232
+ }
233
+ }
234
+
235
+ applyChannelToSettings(channelId) {
236
+ if (isManagedOmpModeEnabled()) {
237
+ return super.applyChannelToSettings(channelId);
238
+ }
239
+ return this.activateStaticOmpChannel(channelId).channel;
240
+ }
241
+
184
242
  syncManagedProviderExtension(channels = this.getChannels().channels) {
185
- return this.syncManagedOmpProvidersIfEnabled(channels);
243
+ return this.syncOmpProvidersForCurrentMode(channels);
186
244
  }
187
245
 
188
246
  disableManagedProviderExtension() {
@@ -486,6 +544,7 @@ module.exports = {
486
544
  getEnabledChannels: () => service.getEnabledChannels(),
487
545
  saveChannelOrder: (order) => service.saveChannelOrder(order),
488
546
  applyChannelToSettings: (id) => service.applyChannelToSettings(id),
547
+ activateStaticOmpChannel: (id) => service.activateStaticOmpChannel(id),
489
548
  disableAllChannels: () => service.disableAllChannels(),
490
549
  getEffectiveApiKey: (channel) => channel?.apiKey || null,
491
550
  syncManagedOmpProviders: (channels, options) => service.syncManagedOmpProviders(channels, options),