coding-tool-x 3.9.0 → 3.9.2

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 (84) hide show
  1. package/README.md +2 -0
  2. package/dist/web/assets/AgentsDrawer-Dubq1kQI.css +1 -0
  3. package/dist/web/assets/AgentsDrawer-x29vGIIi.js +1 -0
  4. package/dist/web/assets/{Analytics-BXRwqC-P.js → Analytics-BJbR6XJO.js} +1 -1
  5. package/dist/web/assets/{AssetPathField-B-LHvOxq.js → AssetPathField-2W-16zgR.js} +1 -1
  6. package/dist/web/assets/{ChatHistoryDrawer-CoHlGAxb.js → ChatHistoryDrawer-C1URzQ_4.js} +1 -1
  7. package/dist/web/assets/{CollapseItem-kJkiHxFl.js → CollapseItem-DjkOkbwE.js} +1 -1
  8. package/dist/web/assets/{CommandsDrawer-DLQrikcw.js → CommandsDrawer-DcnfrHC4.js} +1 -1
  9. package/dist/web/assets/{ConfigExportDrawer-BItwo0FQ.js → ConfigExportDrawer-CKwG_RGJ.js} +1 -1
  10. package/dist/web/assets/{ConfigTemplates-R3YzzBT3.js → ConfigTemplates-C1tNpfzi.js} +1 -1
  11. package/dist/web/assets/{ConfigTemplatesDrawer-C_nPKrhx.js → ConfigTemplatesDrawer-gZNPbcUR.js} +1 -1
  12. package/dist/web/assets/{ConfigTemplatesPanel-BiwrNyYN.js → ConfigTemplatesPanel-C-Fy53Qo.js} +1 -1
  13. package/dist/web/assets/{FavoritesDrawer-BAJ-AOpN.js → FavoritesDrawer-Bp8FYHPk.js} +1 -1
  14. package/dist/web/assets/GatewayConvertDrawer-CvUkvNQy.css +1 -0
  15. package/dist/web/assets/GatewayConvertDrawer-j5IJzb2Y.js +1 -0
  16. package/dist/web/assets/Home-2MAvBnml.js +1 -0
  17. package/dist/web/assets/Home-BRNWE0Qy.css +1 -0
  18. package/dist/web/assets/{MarkdownEditor-D75GGt5X.js → MarkdownEditor-BA_PcLed.js} +1 -1
  19. package/dist/web/assets/{McpDrawer-xNzJtT4X.js → McpDrawer-DAep5PP_.js} +1 -1
  20. package/dist/web/assets/{McpFormDrawer-B240mJ5y.js → McpFormDrawer-BRm-hzv1.js} +1 -1
  21. package/dist/web/assets/{PluginManager-CgLjBdlr.js → PluginManager-C2yY8-FP.js} +1 -1
  22. package/dist/web/assets/{PluginsDrawer-DvJdwMXx.js → PluginsDrawer-CxKnWzTO.js} +1 -1
  23. package/dist/web/assets/{PluginsPanel-CzGMK-Ap.css → PluginsPanel-DsmiW9TU.css} +1 -1
  24. package/dist/web/assets/PluginsPanel-DtbXSoXu.js +1 -0
  25. package/dist/web/assets/{ProjectList-CRtc3Hhx.js → ProjectList-Cb25pa2s.js} +1 -1
  26. package/dist/web/assets/{PromptsDrawer-CvT9dA2N.js → PromptsDrawer-D39OJVRP.js} +1 -1
  27. package/dist/web/assets/{RecentSessionsDrawer-DM1gf584.js → RecentSessionsDrawer-DkCjc4kS.js} +1 -1
  28. package/dist/web/assets/{SessionCard-cZR2V4Br.js → SessionCard-5Mwig8zy.js} +1 -1
  29. package/dist/web/assets/{SessionList-D7-P4Zme.js → SessionList-DVkIlraF.js} +1 -1
  30. package/dist/web/assets/{SettingsDrawer-gW8kh-S8.js → SettingsDrawer-Cr9qZDgO.js} +1 -1
  31. package/dist/web/assets/{SkillManager-D0BlI7CG.js → SkillManager-D935sPSV.js} +1 -1
  32. package/dist/web/assets/{SkillsDrawer-BMB6uzqP.js → SkillsDrawer-D1_3j74b.js} +1 -1
  33. package/dist/web/assets/{SkillsPanel-BzR828De.js → SkillsPanel-6SehnNG8.js} +1 -1
  34. package/dist/web/assets/{Upload-3fNyOQrx.js → Upload-38eKB6wv.js} +1 -1
  35. package/dist/web/assets/{WorkspaceDrawer-DvtBA58Q.js → WorkspaceDrawer-Bkfmlr5M.js} +1 -1
  36. package/dist/web/assets/{WorkspaceManager-BW5KtsHO.js → WorkspaceManager-DFVQ5YoE.js} +1 -1
  37. package/dist/web/assets/{WorkspacePanel-BC_xpAEZ.js → WorkspacePanel-CEnogXEv.js} +1 -1
  38. package/dist/web/assets/client-oU6RT-6I.js +1 -0
  39. package/dist/web/assets/dsh-agent-presets-DjJRVMT6.js +1 -0
  40. package/dist/web/assets/index-CCATqHAD.js +2 -0
  41. package/dist/web/assets/index-p-yggtLj.css +1 -0
  42. package/dist/web/assets/{platforms-DrzAtnjY.js → platforms-DILqyP72.js} +1 -1
  43. package/dist/web/assets/platforms-JV36QBCw.js +1 -0
  44. package/dist/web/assets/{project-config-BVt5s-pH.js → project-config-CzFKBw_m.js} +1 -1
  45. package/dist/web/assets/{sessions-CaWUaysM.js → sessions-WXODMvLY.js} +1 -1
  46. package/dist/web/assets/{sessions-53-Fr68H.js → sessions-wvj5oaYr.js} +1 -1
  47. package/dist/web/assets/{useResponsiveDrawer-Ckgj760z.js → useResponsiveDrawer-BWRBXImg.js} +1 -1
  48. package/dist/web/assets/{workspaces-DQuZd4qw.js → workspaces-D2yLnT-B.js} +1 -1
  49. package/dist/web/index.html +5 -5
  50. package/package.json +4 -4
  51. package/src/platforms/agents-service.js +50 -12
  52. package/src/platforms/drivers/dsh/api-operations.js +60 -2
  53. package/src/platforms/drivers/dsh/common.js +222 -11
  54. package/src/platforms/drivers/dsh/native-config.js +2 -2
  55. package/src/platforms/drivers/dsh/native-logs.js +16 -1
  56. package/src/platforms/drivers/dsh/plugin-manager.js +64 -6
  57. package/src/platforms/drivers/omp/channels-implementation.js +1 -1
  58. package/src/platforms/drivers/omp/gateway.js +28 -2
  59. package/src/platforms/drivers/omp/model-role-settings.js +235 -0
  60. package/src/platforms/drivers/opencode/conversion.js +60 -8
  61. package/src/platforms/manifest-schema.js +4 -0
  62. package/src/platforms/manifests/dsh.json +29 -4
  63. package/src/platforms/manifests/omp.json +5 -4
  64. package/src/platforms/registry.js +1 -0
  65. package/src/server/api/convert.js +26 -76
  66. package/src/server/api/platforms.js +1 -0
  67. package/src/server/api/settings.js +27 -0
  68. package/src/server/services/model-definition-schema.js +2 -2
  69. package/src/server/services/model-detector.js +13 -22
  70. package/src/server/services/native-log-observer.js +1 -0
  71. package/src/server/services/plugins-service.js +13 -14
  72. package/src/server/services/request-logger.js +95 -0
  73. package/src/server/services/usage-log-utils.js +2 -1
  74. package/dist/web/assets/AgentsDrawer-DooV5Qhx.js +0 -1
  75. package/dist/web/assets/AgentsDrawer-Pw2k-ZAa.css +0 -1
  76. package/dist/web/assets/GatewayConvertDrawer-Av3yzwhP.js +0 -1
  77. package/dist/web/assets/GatewayConvertDrawer-CuNlf7AC.css +0 -1
  78. package/dist/web/assets/Home-B01aL_SR.js +0 -1
  79. package/dist/web/assets/Home-CSuFuJ9Y.css +0 -1
  80. package/dist/web/assets/PluginsPanel-kX14E_DT.js +0 -1
  81. package/dist/web/assets/client-BgcNg4Bt.js +0 -1
  82. package/dist/web/assets/index-BFqnddoI.js +0 -2
  83. package/dist/web/assets/index-Bn8BUnwP.css +0 -1
  84. package/dist/web/assets/platforms-mqqe_82d.js +0 -1
@@ -3,7 +3,7 @@
3
3
  const { createDriver: createNativeConfigDriver } = require('./native-config');
4
4
  const { createDriver: createProjectsDriver } = require('./projects');
5
5
  const { createDriver: createSessionsDriver } = require('./sessions');
6
- const { listProfiles, listProfilePlugins, listProfileCapabilities, listProfileMcp, listProfilePrompts, listPlugins, upsertProfilePatchRow, deleteProfilePatchRow } = require('./common');
6
+ const { listProfiles, listManageableProfilePlugins, listProfileCapabilities, listProfileMcp, listProfilePrompts, listProfileAgentPresets, restoreAgentPresetRedactions, listPlugins, upsertProfilePatchRow, deleteProfilePatchRow } = require('./common');
7
7
  const { listSkills, getSkill, createSkill, updateSkill, deleteSkill } = require('./resources');
8
8
  const { installPlugin, uninstallPlugin, updatePlugin } = require('./plugin-manager');
9
9
  const { createDriver: createChannelsDriver } = require('./channels');
@@ -53,6 +53,25 @@ function promptConfigFromRequest(request = {}) {
53
53
  return config;
54
54
  }
55
55
 
56
+ function agentPresetConfigFromRequest(request = {}) {
57
+ const body = request.body || {};
58
+ const source = body.config && typeof body.config === 'object' && !Array.isArray(body.config) ? body.config : body;
59
+ const id = String(source.id || '').trim();
60
+ if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(id)) throw new Error('Agent preset id must use lowercase letters, numbers, and hyphens');
61
+ if (!Array.isArray(source.plugins) || source.plugins.some(plugin => !plugin || typeof plugin !== 'object' || Array.isArray(plugin))) {
62
+ throw new Error('Agent preset plugins must be an array of plugin declarations');
63
+ }
64
+ const config = {
65
+ id,
66
+ plugins: source.plugins
67
+ };
68
+ for (const key of ['name', 'description']) {
69
+ if (typeof source[key] === 'string' && source[key].trim()) config[key] = source[key].trim();
70
+ }
71
+ if (Number.isFinite(Number(source.order))) config.order = Number(source.order);
72
+ return config;
73
+ }
74
+
56
75
  function patchResult(context, request, profileName, operation, patch) {
57
76
  return {
58
77
  profile: profileName,
@@ -211,7 +230,7 @@ function createDriver(context = {}) {
211
230
  driver.listPlugins = request => resultFor(request, 'ok', listPlugins(context));
212
231
  driver.listProfilePlugins = async (request = {}) => {
213
232
  const profile = request.params?.profileName;
214
- const result = listProfilePlugins(context, profile);
233
+ const result = listManageableProfilePlugins(context, profile);
215
234
  return result ? resultFor(request, 'ok', result) : resultFor(request, 'unsupported');
216
235
  };
217
236
  driver.listProfileCapabilities = async (request = {}) => {
@@ -229,6 +248,45 @@ function createDriver(context = {}) {
229
248
  const result = listProfilePrompts(context, profile);
230
249
  return result ? resultFor(request, 'ok', result) : resultFor(request, 'unsupported');
231
250
  };
251
+ driver.listProfileAgentPresets = async (request = {}) => {
252
+ const result = listProfileAgentPresets(context, request.params?.profileName);
253
+ return result ? resultFor(request, 'ok', result) : resultFor(request, 'unsupported');
254
+ };
255
+ driver.upsertAgentPreset = async (request = {}) => {
256
+ const profileName = request.params?.profileName;
257
+ const requestedConfig = agentPresetConfigFromRequest(request);
258
+ const config = restoreAgentPresetRedactions(context, profileName, `preset-${requestedConfig.id}`, requestedConfig);
259
+ const id = `preset-${config.id}`;
260
+ const patch = requirePatchResult(upsertProfilePatchRow(context, profileName, {
261
+ id,
262
+ name: '@deepseek-ai/dsh-agent-preset',
263
+ config
264
+ }, profilePatchOptions(request)), profileName);
265
+ return resultFor(request, 'ok', {
266
+ profile: profileName,
267
+ operation: 'upsertAgentPreset',
268
+ patch,
269
+ ...listProfileAgentPresets(context, profileName)
270
+ });
271
+ };
272
+ driver.deleteAgentPreset = async (request = {}) => {
273
+ const profileName = request.params?.profileName;
274
+ const id = requiredId(request.params?.presetId, 'Agent preset id');
275
+ const current = listProfileAgentPresets(context, profileName);
276
+ if (!current) return resultFor(request, 'unsupported');
277
+ const preset = current.presets.find(item => item.id === id);
278
+ if (!preset) return resultFor(request, 'ok', current);
279
+ if (preset.builtIn) {
280
+ return resultFor(request, 'failed', undefined, 'Built-in Agent presets can be customized but cannot be deleted');
281
+ }
282
+ const patch = requirePatchResult(deleteProfilePatchRow(context, profileName, preset.patchId, profilePatchOptions(request)), profileName);
283
+ return resultFor(request, 'ok', {
284
+ profile: profileName,
285
+ operation: 'deleteAgentPreset',
286
+ patch,
287
+ ...listProfileAgentPresets(context, profileName)
288
+ });
289
+ };
232
290
  driver.installPlugin = async (request = {}) => {
233
291
  const result = await installPlugin(context, request);
234
292
  return resultFor(request, 'ok', result);
@@ -34,6 +34,45 @@ function clone(value) {
34
34
  return JSON.parse(JSON.stringify(value));
35
35
  }
36
36
 
37
+ function resolveDshInstallRoot(context = {}) {
38
+ const command = String(
39
+ context.dshCommand
40
+ || context.dependencies?.dshCommand
41
+ || process.env.DSH_COMMAND
42
+ || 'dsh'
43
+ ).trim();
44
+ if (!command) return null;
45
+ const commandHasPath = path.isAbsolute(command) || command.includes(path.sep);
46
+ if (!commandHasPath && /\s/.test(command)) return null;
47
+
48
+ const candidates = commandHasPath
49
+ ? [path.resolve(command)]
50
+ : String(context.dshEnv?.PATH || process.env.PATH || '')
51
+ .split(path.delimiter)
52
+ .filter(Boolean)
53
+ .map(directory => path.join(directory, command));
54
+
55
+ for (const candidate of candidates) {
56
+ let executablePath;
57
+ try {
58
+ fs.accessSync(candidate, fs.constants.X_OK);
59
+ executablePath = fs.realpathSync(candidate);
60
+ } catch (_) {
61
+ continue;
62
+ }
63
+
64
+ let directory = path.dirname(executablePath);
65
+ for (let depth = 0; depth < 8; depth += 1) {
66
+ const packageJson = readJsonFile(path.join(directory, 'package.json'), null);
67
+ if (packageJson?.name === '@deepseek-ai/dsh') return directory;
68
+ const parent = path.dirname(directory);
69
+ if (parent === directory) break;
70
+ directory = parent;
71
+ }
72
+ }
73
+ return null;
74
+ }
75
+
37
76
  function resolvePaths(context = {}) {
38
77
  const paths = context.paths || context.pathContext?.native || {};
39
78
  const home = path.resolve(paths.dir || paths.home || context.pathContext?.home || process.env.DSH_HOME || path.join(require('os').homedir(), '.dsh'));
@@ -46,7 +85,7 @@ function resolvePaths(context = {}) {
46
85
  patch: paths.patch || path.join(home, 'cordis.patch.yml')
47
86
  };
48
87
  const installAnchor = paths.installAnchor || context.installAnchor || process.env.DSH_INSTALL_ANCHOR;
49
- const installRoot = paths.installRoot || context.installRoot || process.env.DSH_INSTALL_ROOT;
88
+ const installRoot = paths.installRoot || context.installRoot || process.env.DSH_INSTALL_ROOT || resolveDshInstallRoot(context);
50
89
  if (installAnchor) resolved.installAnchor = installAnchor;
51
90
  if (installRoot) resolved.installRoot = installRoot;
52
91
  return resolved;
@@ -246,13 +285,19 @@ function listProfilePlugins(context = {}, profileName) {
246
285
  const bundleNames = Array.isArray(profileConfig.bundles) ? profileConfig.bundles : [];
247
286
  const dependencyNames = Object.keys(isObject(manifest.dependencies) ? manifest.dependencies : {});
248
287
  const names = [...new Set([...bundleNames, ...dependencyNames])];
249
- const plugins = names.map(name => {
288
+ const describePlugin = name => {
250
289
  const packageDir = resolvePackageDir(profile.profileDir, name, paths);
251
290
  const packageJsonPath = packageDir ? path.join(packageDir, 'package.json') : null;
252
291
  const packageJson = packageJsonPath ? readJsonFile(packageJsonPath, {}) : {};
253
292
  const bundle = isObject(packageJson.dsh?.bundle) ? packageJson.dsh.bundle : null;
254
- const patchPath = bundle?.patch && packageDir ? path.resolve(packageDir, bundle.patch) : null;
255
- const patchExists = Boolean(patchPath && fs.existsSync(patchPath));
293
+ const declaredPatches = Array.isArray(bundle?.patch)
294
+ ? bundle.patch
295
+ : bundle?.patch ? [bundle.patch] : [];
296
+ const patchPaths = packageDir
297
+ ? declaredPatches.filter(item => typeof item === 'string' && item.trim()).map(item => path.resolve(packageDir, item))
298
+ : [];
299
+ const patchPath = patchPaths[0] || null;
300
+ const patchExists = patchPaths.length > 0 && patchPaths.every(filePath => fs.existsSync(filePath));
256
301
  return {
257
302
  name,
258
303
  requestedVersion: manifest.dependencies?.[name] || manifest.devDependencies?.[name] || null,
@@ -261,11 +306,31 @@ function listProfilePlugins(context = {}, profileName) {
261
306
  bundle: Boolean(bundle),
262
307
  patch: bundle?.patch || null,
263
308
  patchPath,
309
+ patchPaths,
264
310
  patchExists,
311
+ description: packageJson.description || '',
265
312
  inBundleList: bundleNames.includes(name),
313
+ builtIn: isBuiltInProfilePackage(packageDir, name, profile.profileDir, paths),
266
314
  management: 'ctx+dsh-plugin'
267
315
  };
268
- });
316
+ };
317
+ const declaredPlugins = names.map(describePlugin);
318
+ const builtinNames = bundledPluginNames(declaredPlugins);
319
+ const plugins = [...declaredPlugins];
320
+ for (const name of builtinNames) {
321
+ if (names.includes(name)) continue;
322
+ const plugin = describePlugin(name);
323
+ plugin.installed = true;
324
+ if (!plugin.builtIn) {
325
+ const packageDir = resolvePackageDir(profile.profileDir, name, paths);
326
+ const profileModules = path.join(profile.profileDir, 'node_modules');
327
+ const sharedProfileModules = path.join(paths.profiles, 'node_modules');
328
+ if (!packageDir || (!isPathInside(profileModules, packageDir) && !isPathInside(sharedProfileModules, packageDir))) {
329
+ plugin.builtIn = true;
330
+ }
331
+ }
332
+ plugins.push(plugin);
333
+ }
269
334
  return {
270
335
  profile: profile.name,
271
336
  profilePath: profile.profileDir,
@@ -276,14 +341,57 @@ function listProfilePlugins(context = {}, profileName) {
276
341
  };
277
342
  }
278
343
 
344
+ function isPathInside(parentPath, targetPath) {
345
+ if (!parentPath || !targetPath) return false;
346
+ const parent = path.resolve(parentPath);
347
+ const target = path.resolve(targetPath);
348
+ return target === parent || target.startsWith(`${parent}${path.sep}`);
349
+ }
350
+
351
+ function isBuiltInProfilePackage(packageDir, packageName, profileDir, paths) {
352
+ const profileModules = path.join(profileDir, 'node_modules');
353
+ const sharedProfileModules = path.join(paths.profiles, 'node_modules');
354
+ if (packageDir && (isPathInside(profileModules, packageDir) || isPathInside(sharedProfileModules, packageDir))) return false;
355
+ if (/^@deepseek-ai\/dsh-/.test(packageName)) return true;
356
+ if (!packageDir) return false;
357
+ return true;
358
+ }
359
+
360
+ function bundledPluginNames(plugins) {
361
+ const names = new Set();
362
+ for (const plugin of plugins) {
363
+ if (!plugin.inBundleList || !plugin.builtIn) continue;
364
+ for (const filePath of plugin.patchPaths || []) {
365
+ if (!fs.existsSync(filePath)) continue;
366
+ const rows = collectPatchRows(readYamlFile(filePath, []), [], filePath);
367
+ for (const row of rows) {
368
+ const match = typeof row.name === 'string'
369
+ ? row.name.match(/^(@deepseek-ai\/[a-z0-9._-]+)(?:\/[a-z0-9._-]+)*$/i)
370
+ : null;
371
+ if (match) names.add(match[1]);
372
+ }
373
+ }
374
+ }
375
+ return names;
376
+ }
377
+
378
+ function listManageableProfilePlugins(context = {}, profileName) {
379
+ const result = listProfilePlugins(context, profileName);
380
+ if (!result) return null;
381
+ return {
382
+ ...result,
383
+ plugins: result.plugins.filter(plugin => !plugin.builtIn)
384
+ };
385
+ }
386
+
279
387
  function profilePatchFiles(context = {}, profileName) {
280
388
  const paths = resolvePaths(context);
281
389
  const pluginInfo = listProfilePlugins(context, profileName);
282
390
  if (!pluginInfo) return null;
283
391
  const profile = readProfileManifest(paths, profileName);
284
392
  const bundleFiles = pluginInfo.plugins
285
- .filter(plugin => plugin.inBundleList && plugin.patchPath)
286
- .map(plugin => plugin.patchPath);
393
+ .filter(plugin => plugin.inBundleList)
394
+ .flatMap(plugin => plugin.patchPaths || (plugin.patchPath ? [plugin.patchPath] : []));
287
395
  const files = [...new Set([
288
396
  ...bundleFiles,
289
397
  path.join(profile.profileDir, 'cordis.patch.yml'),
@@ -399,9 +507,11 @@ function upsertProfilePatchRow(context = {}, profileName, entry, options = {}) {
399
507
  const id = patchEntryId(entry);
400
508
  if (!id) throw new Error('DSH patch entry id is required');
401
509
  return updateProfilePatch(context, profileName, (document, meta) => {
402
- const hasComposedRow = meta.composedRows.some(row => patchEntryId(row.entry) === id);
510
+ const hasComposedRowOutsideProfile = meta.composedRows.some(row => (
511
+ patchEntryId(row.entry) === id && path.resolve(row.source) !== path.resolve(meta.patchPath)
512
+ ));
403
513
  const withoutOwn = removeManagedRows(document, row => patchEntryId(row) === id);
404
- if (hasComposedRow) withoutOwn.push(entry);
514
+ if (hasComposedRowOutsideProfile) withoutOwn.push(entry);
405
515
  else withoutOwn.push({ insert: [entry] });
406
516
  return withoutOwn;
407
517
  }, options);
@@ -428,6 +538,7 @@ function collectPatchRows(value, rows = [], source = null, options = {}) {
428
538
  id: entry.id || null,
429
539
  name: entry.name || null,
430
540
  disabled: entry.disabled === true,
541
+ hasDisabled: Object.prototype.hasOwnProperty.call(entry, 'disabled'),
431
542
  hasConfig: Object.prototype.hasOwnProperty.call(entry, 'config'),
432
543
  source
433
544
  };
@@ -462,7 +573,20 @@ function effectivePatchRows(info) {
462
573
  if (row.operation === 'delete') {
463
574
  byId.delete(row.id);
464
575
  } else {
465
- byId.set(row.id, row);
576
+ const current = byId.get(row.id);
577
+ if (row.operation === 'insert' || !current) {
578
+ byId.set(row.id, row);
579
+ } else {
580
+ byId.set(row.id, {
581
+ ...current,
582
+ ...row,
583
+ name: row.name || current.name,
584
+ disabled: row.hasDisabled ? row.disabled : current.disabled,
585
+ hasDisabled: row.hasDisabled || current.hasDisabled,
586
+ config: row.hasConfig ? row.config : current.config,
587
+ hasConfig: row.hasConfig || current.hasConfig
588
+ });
589
+ }
466
590
  }
467
591
  }
468
592
  }
@@ -545,10 +669,94 @@ function listPlugins(context = {}) {
545
669
  return {
546
670
  profiles: listProfiles(context).map(profile => listProfilePlugins(context, profile.name)).filter(Boolean),
547
671
  management: 'ctx+dsh-plugin',
548
- note: 'ctx 通过 dsh plugin 管理插件依赖;插件运行生命周期和 profile 重启仍由 dsh 负责。'
672
+ note: '内置插件只读展示;仅 Profile 安装的外置插件由此处管理。'
549
673
  };
550
674
  }
551
675
 
676
+ function listProfileAgentPresets(context = {}, profileName) {
677
+ const info = readProfilePatchContributions(context, profileName);
678
+ if (!info) return null;
679
+ const patchPath = path.join(info.profile.profileDir, 'cordis.patch.yml');
680
+ const bundledPatchPaths = new Set(info.pluginInfo.plugins
681
+ .filter(plugin => plugin.inBundleList && plugin.builtIn && plugin.patchPath)
682
+ .flatMap(plugin => plugin.patchPaths || [plugin.patchPath])
683
+ .map(filePath => path.resolve(filePath)));
684
+ const bundledPresetIds = new Set(info.contributions
685
+ .filter(contribution => bundledPatchPaths.has(path.resolve(contribution.path)))
686
+ .flatMap(contribution => contribution.rows)
687
+ .filter(row => row.name === '@deepseek-ai/dsh-agent-preset')
688
+ .map(row => row.id));
689
+ const presets = effectivePatchRows(info)
690
+ .filter(row => row.name === '@deepseek-ai/dsh-agent-preset' && isObject(row.config))
691
+ .map(row => ({
692
+ id: String(row.config.id || String(row.id || '').replace(/^preset-/, '')),
693
+ patchId: row.id,
694
+ name: typeof row.config.name === 'string' ? row.config.name : '',
695
+ description: typeof row.config.description === 'string' ? row.config.description : '',
696
+ order: Number.isFinite(row.config.order) ? row.config.order : 0,
697
+ plugins: Array.isArray(row.config.plugins) ? redactPatchConfig(row.config.plugins) : [],
698
+ builtIn: bundledPresetIds.has(row.id),
699
+ source: row.source
700
+ }))
701
+ .filter(preset => /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(preset.id));
702
+ return {
703
+ profile: info.pluginInfo.profile,
704
+ revision: fileRevision(patchPath),
705
+ presets,
706
+ management: 'ctx-profile-patch'
707
+ };
708
+ }
709
+
710
+ function effectiveRawPatchEntry(context = {}, profileName, entryId) {
711
+ const info = profilePatchFiles(context, profileName);
712
+ if (!info) return null;
713
+ const byId = new Map();
714
+ for (const filePath of info.files) {
715
+ if (!fs.existsSync(filePath)) continue;
716
+ for (const { operation, entry } of patchRowsFromDocument(readYamlFile(filePath, []))) {
717
+ const id = patchEntryId(entry);
718
+ if (!id) continue;
719
+ if (operation === 'delete') {
720
+ byId.delete(id);
721
+ continue;
722
+ }
723
+ const current = byId.get(id);
724
+ if (operation === 'insert' || !current) {
725
+ byId.set(id, { ...entry, source: filePath });
726
+ } else {
727
+ byId.set(id, { ...current, ...entry, id, source: filePath });
728
+ }
729
+ }
730
+ }
731
+ return byId.get(entryId) || null;
732
+ }
733
+
734
+ function restoreRedactedValues(value, original) {
735
+ if (value === '[REDACTED]' || value === '[EXPRESSION]') return clone(original);
736
+ if (Array.isArray(value)) {
737
+ const originalArray = Array.isArray(original) ? original : [];
738
+ return value.map((entry, index) => {
739
+ const entryId = isObject(entry) ? entry.id : null;
740
+ const originalEntry = entryId
741
+ ? originalArray.find(candidate => isObject(candidate) && candidate.id === entryId)
742
+ : originalArray[index];
743
+ return restoreRedactedValues(entry, originalEntry);
744
+ });
745
+ }
746
+ if (!isObject(value)) return value;
747
+ const originalObject = isObject(original) ? original : {};
748
+ return Object.fromEntries(Object.entries(value).map(([key, entry]) => [
749
+ key,
750
+ restoreRedactedValues(entry, originalObject[key])
751
+ ]));
752
+ }
753
+
754
+ function restoreAgentPresetRedactions(context = {}, profileName, presetId, config) {
755
+ const original = effectiveRawPatchEntry(context, profileName, presetId);
756
+ if (!original || !isObject(original.config)) return config;
757
+ return restoreRedactedValues(config, original.config);
758
+ }
759
+
552
760
  module.exports = {
553
761
  clone,
554
762
  resolvePaths,
@@ -562,9 +770,12 @@ module.exports = {
562
770
  credentialMetadata,
563
771
  listProfiles,
564
772
  listProfilePlugins,
773
+ listManageableProfilePlugins,
565
774
  listProfileCapabilities,
566
775
  listProfileMcp,
567
776
  listProfilePrompts,
777
+ listProfileAgentPresets,
778
+ restoreAgentPresetRedactions,
568
779
  listPlugins,
569
780
  profilePatchFiles,
570
781
  readProfilePatchContributions,
@@ -2,7 +2,7 @@
2
2
 
3
3
  const fs = require('fs');
4
4
  const { writeYamlFile } = require('../../../utils/native-config-patcher');
5
- const { resolvePaths, readYamlFile, mergeSettings, fileRevision, credentialMetadata, redactSecrets, listProfiles, listProfilePlugins, listProfileMcp, listProfilePrompts, DSH_YAML_SOURCE_TAGS } = require('./common');
5
+ const { resolvePaths, readYamlFile, mergeSettings, fileRevision, credentialMetadata, redactSecrets, listProfiles, listManageableProfilePlugins, listProfileMcp, listProfilePrompts, DSH_YAML_SOURCE_TAGS } = require('./common');
6
6
  const { resolveSkillRoots } = require('./resources');
7
7
 
8
8
  function createDriver(context = {}) {
@@ -62,7 +62,7 @@ function createDriver(context = {}) {
62
62
  cwd: request.query?.cwd
63
63
  });
64
64
  const profiles = listProfiles(context).map(profile => {
65
- const plugins = listProfilePlugins(context, profile.name);
65
+ const plugins = listManageableProfilePlugins(context, profile.name);
66
66
  const mcp = listProfileMcp(context, profile.name);
67
67
  const prompts = listProfilePrompts(context, profile.name);
68
68
  return {
@@ -37,6 +37,20 @@ function extractUsage(record = {}) {
37
37
  );
38
38
  }
39
39
 
40
+ function hasCacheUsage(usage = {}) {
41
+ const aliases = [
42
+ 'cached', 'cached_tokens', 'cachedTokens', 'cached_input_tokens', 'cachedInputTokens',
43
+ 'cache_read_input_tokens', 'cacheReadInputTokens', 'cacheRead', 'cache_read', 'cachedContentTokenCount'
44
+ ];
45
+ const hasField = source => aliases.some(key => (
46
+ source && Object.prototype.hasOwnProperty.call(source, key) && source[key] !== undefined && source[key] !== null
47
+ ));
48
+ if (hasField(usage)) return true;
49
+
50
+ return [usage.input_tokens_details, usage.inputTokensDetails, usage.prompt_tokens_details, usage.promptTokensDetails, usage.usageMetadata]
51
+ .some(hasField) || Boolean(usage.cache && (usage.cache.read !== undefined || usage.cache.readTokens !== undefined));
52
+ }
53
+
40
54
  function normalizeTimestamp(value) {
41
55
  if (typeof value === 'number' && Number.isFinite(value)) {
42
56
  if (value > 1e11) return value;
@@ -75,6 +89,7 @@ function normalizeDshEvent(record = {}, filePath = '', index = 0) {
75
89
  provider,
76
90
  model,
77
91
  tokens,
92
+ cacheUsageAvailable: hasCacheUsage(usage),
78
93
  cost,
79
94
  channelId: record.channelId || data.channelId || message.channelId || source.channelId,
80
95
  channel: record.channel || data.channel || message.channel || provider
@@ -113,4 +128,4 @@ function createDriver({ nativeRoot, pathContext, paths, fsImpl = fs } = {}) {
113
128
  };
114
129
  }
115
130
 
116
- module.exports = { createDriver, extractUsage, normalizeDshEvent };
131
+ module.exports = { createDriver, extractUsage, hasCacheUsage, normalizeDshEvent };
@@ -3,7 +3,7 @@
3
3
  const { execFile } = require('child_process');
4
4
  const path = require('path');
5
5
  const { promisify } = require('util');
6
- const { resolvePaths, listProfilePlugins, safeProfileName } = require('./common');
6
+ const { resolvePaths, listProfilePlugins, listManageableProfilePlugins, safeProfileName } = require('./common');
7
7
 
8
8
  const execFileAsync = promisify(execFile);
9
9
 
@@ -25,6 +25,40 @@ function normalizeSpecs(value) {
25
25
  return specs;
26
26
  }
27
27
 
28
+ function packageNameFromSpec(spec) {
29
+ const value = String(spec || '').trim();
30
+ if (!value || /^(?:https?:|git\+|github:|file:)/i.test(value)) return null;
31
+ const slash = value.lastIndexOf('/');
32
+ const versionSeparator = value.lastIndexOf('@');
33
+ if (versionSeparator > slash) return value.slice(0, versionSeparator);
34
+ return value;
35
+ }
36
+
37
+ function profilePluginInventory(context, profileName) {
38
+ const plugins = listProfilePlugins(context, profileName);
39
+ if (!plugins) {
40
+ const error = new Error(`DSH profile not found: ${profileName}`);
41
+ error.statusCode = 404;
42
+ throw error;
43
+ }
44
+ return plugins.plugins;
45
+ }
46
+
47
+ function requireExternalPluginSpecs(context, profileName, specs, { allowNew = false } = {}) {
48
+ const plugins = profilePluginInventory(context, profileName);
49
+ const byName = new Map(plugins.map(plugin => [plugin.name, plugin]));
50
+ const builtinNames = new Set(plugins.filter(plugin => plugin.builtIn).map(plugin => plugin.name));
51
+ for (const spec of specs) {
52
+ const name = packageNameFromSpec(spec);
53
+ if (name && builtinNames.has(name)) {
54
+ throw new Error(`DSH built-in plugin cannot be managed here: ${name}`);
55
+ }
56
+ if (!allowNew && (!name || !byName.has(name) || byName.get(name).builtIn)) {
57
+ throw new Error(`Only installed external DSH plugins can be managed here: ${spec}`);
58
+ }
59
+ }
60
+ }
61
+
28
62
  function resolveWorkingDirectory(paths, value) {
29
63
  if (value === undefined || value === null || value === '') return paths.dir;
30
64
  const cwd = path.resolve(String(value));
@@ -85,13 +119,16 @@ async function runDshPlugin(context = {}, profileName, args, options = {}) {
85
119
  exitCode: 0,
86
120
  stdout: redactCommandOutput(result.stdout),
87
121
  stderr: redactCommandOutput(result.stderr),
88
- plugins: listProfilePlugins(context, profile)
122
+ plugins: listManageableProfilePlugins(context, profile)
89
123
  };
90
124
  }
91
125
 
92
126
  async function installPlugin(context = {}, request = {}) {
93
127
  const body = request.body || {};
94
- return runDshPlugin(context, request.params?.profileName, ['add', ...normalizeSpecs(body.specs || body.spec || body.package)], {
128
+ const profileName = request.params?.profileName;
129
+ const specs = normalizeSpecs(body.specs || body.spec || body.package);
130
+ requireExternalPluginSpecs(context, profileName, specs, { allowNew: true });
131
+ return runDshPlugin(context, profileName, ['add', ...specs], {
95
132
  cwd: body.cwd
96
133
  });
97
134
  }
@@ -99,14 +136,35 @@ async function installPlugin(context = {}, request = {}) {
99
136
  async function uninstallPlugin(context = {}, request = {}) {
100
137
  const body = request.body || {};
101
138
  const specs = normalizeSpecs(body.specs || body.spec || request.params?.pluginName);
102
- return runDshPlugin(context, request.params?.profileName, ['remove', ...specs], { cwd: body.cwd });
139
+ const profileName = request.params?.profileName;
140
+ requireExternalPluginSpecs(context, profileName, specs);
141
+ return runDshPlugin(context, profileName, ['remove', ...specs], { cwd: body.cwd });
103
142
  }
104
143
 
105
144
  async function updatePlugin(context = {}, request = {}) {
106
145
  const body = request.body || {};
107
146
  const specs = body.specs || body.spec || body.package;
108
- const args = ['update', ...(specs === undefined ? [] : normalizeSpecs(specs))];
109
- return runDshPlugin(context, request.params?.profileName, args, { cwd: body.cwd });
147
+ const profileName = request.params?.profileName;
148
+ const inventory = profilePluginInventory(context, profileName);
149
+ let targets;
150
+ if (specs === undefined) {
151
+ targets = inventory.filter(plugin => !plugin.builtIn).map(plugin => plugin.name);
152
+ } else {
153
+ targets = normalizeSpecs(specs);
154
+ requireExternalPluginSpecs(context, profileName, targets);
155
+ }
156
+ if (!targets.length) {
157
+ const profile = safeProfileName(profileName);
158
+ return {
159
+ profile,
160
+ exitCode: 0,
161
+ stdout: '',
162
+ stderr: '',
163
+ skipped: 'No installed external DSH plugins to update',
164
+ plugins: listManageableProfilePlugins(context, profile)
165
+ };
166
+ }
167
+ return runDshPlugin(context, profileName, ['update', ...targets], { cwd: body.cwd });
110
168
  }
111
169
 
112
170
  module.exports = {
@@ -24,7 +24,7 @@ const {
24
24
  upsertSyncedChannels
25
25
  } = require('../../../server/services/channel-sync-utils');
26
26
 
27
- const OMP_THINKING_SUFFIX_RE = /:(minimal|low|medium|high|xhigh|off)$/;
27
+ const OMP_THINKING_SUFFIX_RE = /:(minimal|low|medium|high|xhigh|max|off)$/i;
28
28
 
29
29
  let configuredState = {
30
30
  channels: PATHS.channels?.omp,
@@ -9,6 +9,7 @@ const { allocateChannel, releaseChannel } = require('../../../server/services/ch
9
9
  const { recordSuccess, recordFailure } = require('../../../server/services/channel-health');
10
10
  const { attachServerShutdownHandling, expediteServerShutdown } = require('../../../server/services/server-shutdown');
11
11
  const { resolveOmpGatewayRoute } = require('./gateway-routing');
12
+ const { persistProxyRequestSnapshot } = require('../../../server/services/request-logger');
12
13
 
13
14
  const RETRYABLE_STATUS = new Set([401, 403, 429, 502, 503, 504]);
14
15
  const MAX_REQUEST_BYTES = 100 * 1024 * 1024;
@@ -157,7 +158,7 @@ function extractModelId(body, contentType = '') {
157
158
  }
158
159
 
159
160
  function normalizeModelId(value) {
160
- return String(value || '').trim().replace(/:(minimal|low|medium|high|xhigh|off)$/, '');
161
+ return String(value || '').trim().replace(/:(minimal|low|medium|high|xhigh|max|off)$/i, '');
161
162
  }
162
163
 
163
164
  function channelSupportsModel(channel, modelId) {
@@ -183,6 +184,17 @@ function writeJson(res, statusCode, payload) {
183
184
  res.end(body);
184
185
  }
185
186
 
187
+ function redactSensitiveQuery(path) {
188
+ const url = new URL(String(path || '/'), 'http://127.0.0.1');
189
+ for (const key of [...url.searchParams.keys()]) {
190
+ const normalizedKey = key.toLowerCase().replace(/_/g, '-');
191
+ if (/(^|[-])(api-?key|key|auth|authorization|oauth|access-token|token|secret|credential|password|signature|jwt|cookie|session)([-]|$)/i.test(normalizedKey)) {
192
+ url.searchParams.delete(key);
193
+ }
194
+ }
195
+ return `${url.pathname}${url.search}`;
196
+ }
197
+
186
198
  function constantTimeEqual(left, right) {
187
199
  const leftBuffer = Buffer.from(String(left || ''));
188
200
  const rightBuffer = Buffer.from(String(right || ''));
@@ -465,6 +477,19 @@ function createOmpGateway(options = {}) {
465
477
  }
466
478
  route.upstreamPath = `${route.upstreamPath}${parsed.search}`;
467
479
 
480
+ persistProxyRequestSnapshot('omp', {
481
+ timestamp: Date.now(),
482
+ source: 'omp',
483
+ request: {
484
+ method: req.method,
485
+ headers: req.headers
486
+ },
487
+ route: {
488
+ providerApi: route.providerApi,
489
+ path: redactSensitiveQuery(route.upstreamPath)
490
+ }
491
+ });
492
+
468
493
  inflightRequests++;
469
494
  const abortController = new AbortController();
470
495
  const abortDownstream = () => abortController.abort();
@@ -685,5 +710,6 @@ function createOmpGateway(options = {}) {
685
710
  }
686
711
 
687
712
  module.exports = {
688
- createOmpGateway
713
+ createOmpGateway,
714
+ redactSensitiveQuery
689
715
  };