@yancyyu/openhermit 1.6.36 → 1.6.38

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 (91) hide show
  1. package/dist-renderer/assets/{ProjectEditorOverlay-B_QAoeaA.js → ProjectEditorOverlay-lJZi-9Hp.js} +1 -1
  2. package/dist-renderer/assets/{TeamGraphOverlay-PB9luAZU.js → TeamGraphOverlay-ZEDfZyHb.js} +1 -1
  3. package/dist-renderer/assets/{_basePickBy-Dfzcp_Ry.js → _basePickBy-CIhniz70.js} +1 -1
  4. package/dist-renderer/assets/{_baseUniq-B5u2Yiq2.js → _baseUniq-cKAW4Q8I.js} +1 -1
  5. package/dist-renderer/assets/{arc-DElOI7qz.js → arc-YmNsoDXW.js} +1 -1
  6. package/dist-renderer/assets/{architectureDiagram-VXUJARFQ-Cf6f4tCu.js → architectureDiagram-VXUJARFQ-DHEls2sX.js} +1 -1
  7. package/dist-renderer/assets/{blockDiagram-VD42YOAC-FJUdo9Ry.js → blockDiagram-VD42YOAC-Bpwf1Sbg.js} +1 -1
  8. package/dist-renderer/assets/{c4Diagram-YG6GDRKO-BvJQS9lb.js → c4Diagram-YG6GDRKO-B0IaQ4w5.js} +1 -1
  9. package/dist-renderer/assets/channel-yIlSKy0e.js +1 -0
  10. package/dist-renderer/assets/{chunk-4BX2VUAB-n-SGLbin.js → chunk-4BX2VUAB-DLk-hcFc.js} +1 -1
  11. package/dist-renderer/assets/{chunk-55IACEB6-Dwle9tlA.js → chunk-55IACEB6-1XRmX_Zm.js} +1 -1
  12. package/dist-renderer/assets/{chunk-B4BG7PRW-Dic8YxQz.js → chunk-B4BG7PRW-1waH1DAD.js} +1 -1
  13. package/dist-renderer/assets/{chunk-DI55MBZ5-3n5jC1jk.js → chunk-DI55MBZ5-BqpZBtrN.js} +1 -1
  14. package/dist-renderer/assets/{chunk-FMBD7UC4-BqizUB3O.js → chunk-FMBD7UC4-Bly7vVym.js} +1 -1
  15. package/dist-renderer/assets/{chunk-QN33PNHL-JRDmD8o9.js → chunk-QN33PNHL-Ci2QWBAs.js} +1 -1
  16. package/dist-renderer/assets/{chunk-QZHKN3VN-BxFpQw92.js → chunk-QZHKN3VN-YCqFW7d-.js} +1 -1
  17. package/dist-renderer/assets/{chunk-TZMSLE5B-ByqPwtW9.js → chunk-TZMSLE5B-B0xGXInl.js} +1 -1
  18. package/dist-renderer/assets/classDiagram-2ON5EDUG-24fHez0s.js +1 -0
  19. package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-24fHez0s.js +1 -0
  20. package/dist-renderer/assets/clone-BTNuUva-.js +1 -0
  21. package/dist-renderer/assets/{cose-bilkent-S5V4N54A-CVztr86T.js → cose-bilkent-S5V4N54A-DxcFNQKT.js} +1 -1
  22. package/dist-renderer/assets/{dagre-6UL2VRFP-CIui920O.js → dagre-6UL2VRFP-DPo_RfZY.js} +1 -1
  23. package/dist-renderer/assets/{diagram-PSM6KHXK-CyL8-bgb.js → diagram-PSM6KHXK-U3hQsFe4.js} +1 -1
  24. package/dist-renderer/assets/{diagram-QEK2KX5R-CM_67YoY.js → diagram-QEK2KX5R-OrwrAy0V.js} +1 -1
  25. package/dist-renderer/assets/{diagram-S2PKOQOG-DtrtPSGg.js → diagram-S2PKOQOG-CXATPWVw.js} +1 -1
  26. package/dist-renderer/assets/{erDiagram-Q2GNP2WA-bOICzF9d.js → erDiagram-Q2GNP2WA-B0e8AfMF.js} +1 -1
  27. package/dist-renderer/assets/{flowDiagram-NV44I4VS-CJV1g9Hr.js → flowDiagram-NV44I4VS-CXfzA4jJ.js} +1 -1
  28. package/dist-renderer/assets/{ganttDiagram-JELNMOA3-CXbhDo09.js → ganttDiagram-JELNMOA3-CMr08qVl.js} +1 -1
  29. package/dist-renderer/assets/{gitGraphDiagram-V2S2FVAM-vbXopTpS.js → gitGraphDiagram-V2S2FVAM-vYFHpPmy.js} +1 -1
  30. package/dist-renderer/assets/{graph-CY2T-j4q.js → graph-DOe5j8dH.js} +1 -1
  31. package/dist-renderer/assets/{index-Dv7q-OB0.js → index-B2Dy7M2G.js} +1 -1
  32. package/dist-renderer/assets/index-Bi6nrZ4z.css +1 -0
  33. package/dist-renderer/assets/{index-CpyChjme.js → index-BySQS7AB.js} +1 -1
  34. package/dist-renderer/assets/{index-S-i9egm8.js → index-C_okzZXP.js} +1 -1
  35. package/dist-renderer/assets/{index-Mrh4pTHw.js → index-CzWxVCRL.js} +1 -1
  36. package/dist-renderer/assets/{index-Dn-BpzSm.js → index-V7dAKPqd.js} +571 -607
  37. package/dist-renderer/assets/{index-C9ONRXVI.js → index-VJ-MM9xa.js} +1 -1
  38. package/dist-renderer/assets/{infoDiagram-HS3SLOUP-BNg14AdU.js → infoDiagram-HS3SLOUP-D_WubR0B.js} +1 -1
  39. package/dist-renderer/assets/{journeyDiagram-XKPGCS4Q-2_PkPCiu.js → journeyDiagram-XKPGCS4Q-w9ca-1TI.js} +1 -1
  40. package/dist-renderer/assets/{kanban-definition-3W4ZIXB7-DjTx7qoU.js → kanban-definition-3W4ZIXB7-Jg9p6_pN.js} +1 -1
  41. package/dist-renderer/assets/{layout-DZlHGGN0.js → layout-B-z3y17c.js} +1 -1
  42. package/dist-renderer/assets/{linear-DnlOm48z.js → linear-D-RTX5UW.js} +1 -1
  43. package/dist-renderer/assets/{mindmap-definition-VGOIOE7T-B-nrgt7V.js → mindmap-definition-VGOIOE7T-CDQmHOYP.js} +1 -1
  44. package/dist-renderer/assets/{pieDiagram-ADFJNKIX-BToJFvWR.js → pieDiagram-ADFJNKIX-D_odsQL7.js} +1 -1
  45. package/dist-renderer/assets/{quadrantDiagram-AYHSOK5B-C0qoxXvH.js → quadrantDiagram-AYHSOK5B-BRsmYWSA.js} +1 -1
  46. package/dist-renderer/assets/{requirementDiagram-UZGBJVZJ-DCKGwsGN.js → requirementDiagram-UZGBJVZJ-ChNE_BOV.js} +1 -1
  47. package/dist-renderer/assets/{sankeyDiagram-TZEHDZUN-CS6JCcu7.js → sankeyDiagram-TZEHDZUN-C8FtpwKc.js} +1 -1
  48. package/dist-renderer/assets/{sequenceDiagram-WL72ISMW-C9pAWoSR.js → sequenceDiagram-WL72ISMW-DmLCzNcc.js} +1 -1
  49. package/dist-renderer/assets/{stateDiagram-FKZM4ZOC-BTTX_v1m.js → stateDiagram-FKZM4ZOC-WJBm4bhu.js} +1 -1
  50. package/dist-renderer/assets/{stateDiagram-v2-4FDKWEC3-BHk97lQJ.js → stateDiagram-v2-4FDKWEC3-_m6iPPUR.js} +1 -1
  51. package/dist-renderer/assets/{timeline-definition-IT6M3QCI-CSWCEzCQ.js → timeline-definition-IT6M3QCI-BXs_hOJs.js} +1 -1
  52. package/dist-renderer/assets/{treemap-GDKQZRPO-CmiIc68g.js → treemap-GDKQZRPO-o04MA0G9.js} +1 -1
  53. package/dist-renderer/assets/{xychartDiagram-PRI3JC2R-DhwSTphI.js → xychartDiagram-PRI3JC2R-Czj69XRd.js} +1 -1
  54. package/dist-renderer/index.html +2 -2
  55. package/package.json +2 -2
  56. package/src/main/ipc/extensions.ts +29 -50
  57. package/src/main/server.ts +17 -26
  58. package/src/main/services/extensions/ExtensionFacadeService.ts +2 -51
  59. package/src/main/services/extensions/library/McpLibraryService.ts +243 -0
  60. package/src/main/services/session-intelligence/UsageTelemetryService.ts +14 -1
  61. package/src/main/services/teams-mvp/TaskDispatchService.ts +32 -7
  62. package/src/renderer/api/httpClient.ts +108 -22
  63. package/src/renderer/components/extensions/ExtensionStoreView.tsx +6 -96
  64. package/src/renderer/components/extensions/plugins/PluginCard.tsx +8 -0
  65. package/src/renderer/components/extensions/plugins/PluginsPanel.tsx +13 -8
  66. package/src/renderer/components/team/TeamDetailView.tsx +15 -0
  67. package/src/renderer/components/team/tools/AddMcpInline.tsx +47 -0
  68. package/src/renderer/components/team/tools/AddSkillInline.tsx +61 -0
  69. package/src/renderer/components/team/tools/McpChip.tsx +42 -0
  70. package/src/renderer/components/team/tools/SkillChip.tsx +35 -0
  71. package/src/renderer/components/team/tools/ToolsSection.tsx +208 -0
  72. package/src/shared/types/extensions/api.ts +9 -0
  73. package/src/shared/types/extensions/index.ts +4 -0
  74. package/src/shared/types/extensions/mcp.ts +41 -0
  75. package/src/shared/utils/extensionNormalizers.ts +22 -0
  76. package/dist-renderer/assets/channel-DnbgZg0A.js +0 -1
  77. package/dist-renderer/assets/classDiagram-2ON5EDUG-BAD4p014.js +0 -1
  78. package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-BAD4p014.js +0 -1
  79. package/dist-renderer/assets/clone-CRX5ZTPd.js +0 -1
  80. package/dist-renderer/assets/index-B2z_IyRH.css +0 -1
  81. package/src/main/services/extensions/catalog/GlamaMcpEnrichmentService.ts +0 -190
  82. package/src/main/services/extensions/catalog/McpCatalogAggregator.ts +0 -150
  83. package/src/main/services/extensions/catalog/OfficialMcpRegistryService.ts +0 -381
  84. package/src/main/services/extensions/install/McpInstallService.ts +0 -407
  85. package/src/main/services/extensions/state/McpInstallationStateService.ts +0 -42
  86. package/src/renderer/components/extensions/mcp/McpServerCard.tsx +0 -314
  87. package/src/renderer/components/extensions/mcp/McpServerDetailDialog.tsx +0 -765
  88. package/src/renderer/components/extensions/mcp/McpServersPanel.tsx +0 -593
  89. package/src/renderer/components/extensions/skills/SkillDetailDialog.tsx +0 -372
  90. package/src/renderer/components/extensions/skills/SkillImportDialog.tsx +0 -343
  91. package/src/renderer/components/extensions/skills/SkillsPanel.tsx +0 -659
@@ -4768,38 +4768,12 @@ app.post('/api/extensions/plugins/uninstall', async (request) => {
4768
4768
  return result;
4769
4769
  });
4770
4770
 
4771
- app.get('/api/extensions/mcp/search', async (request) => {
4772
- const query = (request.query as Record<string, string>).q ?? '';
4773
- const limit = Number((request.query as Record<string, string>).limit) || 20;
4774
- const result = await ext.mcpSearch(query, limit);
4775
- return result;
4776
- });
4777
-
4778
- app.get('/api/extensions/mcp/browse', async (request) => {
4779
- const cursor = (request.query as Record<string, string>).cursor;
4780
- const limit = Number((request.query as Record<string, string>).limit) || 20;
4781
- const result = await ext.mcpBrowse(cursor || undefined, limit);
4782
- return result;
4783
- });
4784
-
4785
4771
  app.get('/api/extensions/mcp/installed', async (request) => {
4786
4772
  const projectPath = (request.query as Record<string, string>).projectPath;
4787
4773
  const result = await ext.mcpGetInstalled(projectPath);
4788
4774
  return result;
4789
4775
  });
4790
4776
 
4791
- app.get('/api/extensions/mcp/:registryId', async (request) => {
4792
- const { registryId } = request.params as { registryId: string };
4793
- const result = await ext.mcpGetById(registryId);
4794
- return result;
4795
- });
4796
-
4797
- app.post('/api/extensions/mcp/install', async (request) => {
4798
- const body = request.body as Record<string, unknown>;
4799
- const result = await ext.mcpInstall(body as any);
4800
- return result;
4801
- });
4802
-
4803
4777
  app.post('/api/extensions/mcp/install-custom', async (request) => {
4804
4778
  const body = request.body as Record<string, unknown>;
4805
4779
  const result = await ext.mcpInstallCustom(body as any);
@@ -4817,6 +4791,23 @@ app.post('/api/extensions/mcp/uninstall', async (request) => {
4817
4791
  return result;
4818
4792
  });
4819
4793
 
4794
+ app.get('/api/extensions/mcp/library', async () => {
4795
+ return ext.mcpLibraryList();
4796
+ });
4797
+
4798
+ app.post('/api/extensions/mcp/library', async (request) => {
4799
+ return ext.mcpLibraryUpsert(request.body as any);
4800
+ });
4801
+
4802
+ app.delete('/api/extensions/mcp/library/:id', async (request) => {
4803
+ const { id } = request.params as { id: string };
4804
+ return ext.mcpLibraryDelete(id);
4805
+ });
4806
+
4807
+ app.post('/api/extensions/mcp/library/import', async (request) => {
4808
+ return ext.mcpLibraryImport((request.body ?? {}) as any);
4809
+ });
4810
+
4820
4811
  app.get('/api/extensions/skills', async (request) => {
4821
4812
  const projectPath = (request.query as Record<string, string>).projectPath;
4822
4813
  const result = await ext.skillsList(projectPath);
@@ -1,23 +1,18 @@
1
1
  /**
2
- * Facade service that combines plugin catalog + MCP catalog + installation state
2
+ * Facade service that combines plugin catalog + installation state
3
3
  * into enriched data ready for the renderer.
4
4
  *
5
5
  * Also provides install target resolution for the security model
6
- * (main-side re-resolution: renderer sends pluginId/registryId, main resolves from catalog).
6
+ * (main-side re-resolution: renderer sends pluginId, main resolves from catalog).
7
7
  */
8
8
 
9
9
  import { createLogger } from '@shared/utils/logger';
10
10
 
11
- import { type McpCatalogAggregator } from './catalog/McpCatalogAggregator';
12
11
  import { type PluginCatalogService } from './catalog/PluginCatalogService';
13
- import { type McpInstallationStateService } from './state/McpInstallationStateService';
14
12
  import { type PluginInstallationStateService } from './state/PluginInstallationStateService';
15
13
 
16
14
  import type {
17
15
  EnrichedPlugin,
18
- InstalledMcpEntry,
19
- McpCatalogItem,
20
- McpSearchResult,
21
16
  PluginCatalogItem,
22
17
  } from '@shared/types/extensions';
23
18
 
@@ -27,8 +22,6 @@ export class ExtensionFacadeService {
27
22
  constructor(
28
23
  private readonly pluginCatalog: PluginCatalogService,
29
24
  private readonly pluginState: PluginInstallationStateService,
30
- private readonly mcpAggregator: McpCatalogAggregator | null = null,
31
- private readonly mcpState: McpInstallationStateService | null = null
32
25
  ) {}
33
26
 
34
27
  // ── Plugin methods ───────────────────────────────────────────────────
@@ -85,51 +78,9 @@ export class ExtensionFacadeService {
85
78
  return { qualifiedName: plugin.qualifiedName, plugin };
86
79
  }
87
80
 
88
- // ── MCP methods ──────────────────────────────────────────────────────
89
-
90
- /**
91
- * Search MCP servers across both registries.
92
- */
93
- async searchMcp(query: string, limit?: number): Promise<McpSearchResult> {
94
- if (!this.mcpAggregator) {
95
- return { servers: [], warnings: ['MCP catalog not configured'] };
96
- }
97
- return this.mcpAggregator.search(query, limit);
98
- }
99
-
100
- /**
101
- * Browse MCP catalog with pagination.
102
- */
103
- async browseMcp(
104
- cursor?: string,
105
- limit?: number
106
- ): Promise<{ servers: McpCatalogItem[]; nextCursor?: string }> {
107
- if (!this.mcpAggregator) {
108
- return { servers: [] };
109
- }
110
- return this.mcpAggregator.browse(cursor, limit);
111
- }
112
-
113
- /**
114
- * Get a single MCP server by registry ID (for install flow).
115
- */
116
- async getMcpById(registryId: string): Promise<McpCatalogItem | null> {
117
- if (!this.mcpAggregator) return null;
118
- return this.mcpAggregator.getById(registryId);
119
- }
120
-
121
- /**
122
- * Get installed MCP servers.
123
- */
124
- async getInstalledMcp(projectPath?: string): Promise<InstalledMcpEntry[]> {
125
- if (!this.mcpState) return [];
126
- return this.mcpState.getInstalled(projectPath);
127
- }
128
-
129
81
  // ── Cache invalidation ───────────────────────────────────────────────
130
82
 
131
83
  invalidateInstalledCache(): void {
132
84
  this.pluginState.invalidateCache();
133
- this.mcpState?.invalidateCache();
134
85
  }
135
86
  }
@@ -0,0 +1,243 @@
1
+ /**
2
+ * McpLibraryService — a reusable, global library of MCP server definitions.
3
+ *
4
+ * cc-switch model: a server is defined once and can be enabled for any worker
5
+ * (= installed into that worker's project config) without re-entering the
6
+ * command / URL / env each time. The "enable/disable for a worker" action is
7
+ * handled by the existing install/uninstall path; this service only owns the
8
+ * persisted library of definitions.
9
+ *
10
+ * Storage: ~/.hermit/mcp-library.json (HERMIT_HOME override respected).
11
+ */
12
+
13
+ import * as fs from 'node:fs';
14
+ import * as os from 'node:os';
15
+ import * as path from 'node:path';
16
+ import { randomUUID } from 'node:crypto';
17
+
18
+ import { createLogger } from '@shared/utils/logger';
19
+ import { getErrorMessage } from '@shared/utils/errorHandling';
20
+
21
+ import { McpConfigStateReader } from '../runtime/McpConfigStateReader';
22
+
23
+ import type {
24
+ McpHeaderDef,
25
+ McpInstallSpec,
26
+ McpLibraryEntry,
27
+ McpLibraryImportRequest,
28
+ McpLibraryImportResult,
29
+ McpLibraryUpsertRequest,
30
+ } from '@shared/types/extensions';
31
+
32
+ const logger = createLogger('Extensions:McpLibrary');
33
+
34
+ function getHermitHome(): string {
35
+ return process.env.HERMIT_HOME ?? path.join(os.homedir(), '.hermit');
36
+ }
37
+
38
+ /**
39
+ * Best-effort mapping of a raw MCP server config (from ~/.claude.json /
40
+ * .mcp.json) into the install spec the current install path understands.
41
+ * Returns null when the config cannot be represented (e.g. an arbitrary stdio
42
+ * command rather than an npm package) — the caller skips those.
43
+ */
44
+ function rawConfigToInstallSpec(config: Record<string, unknown>): McpInstallSpec | null {
45
+ const url = typeof config.url === 'string' ? config.url : null;
46
+ if (url) {
47
+ const rawType = typeof config.type === 'string' ? config.type : 'http';
48
+ const transportType =
49
+ rawType === 'sse' ? 'sse' : rawType === 'streamable-http' ? 'streamable-http' : 'http';
50
+ return { type: 'http', url, transportType };
51
+ }
52
+
53
+ const command = typeof config.command === 'string' ? config.command : null;
54
+ if (command) {
55
+ const args = Array.isArray(config.args) ? config.args.map(String) : [];
56
+ const npmPackage = extractNpmPackage(command, args);
57
+ if (npmPackage) {
58
+ return { type: 'stdio', npmPackage };
59
+ }
60
+ }
61
+
62
+ return null;
63
+ }
64
+
65
+ /** Pull the package spec out of `npx -y <pkg>` / `npm exec <pkg>` style commands. */
66
+ function extractNpmPackage(command: string, args: string[]): string | null {
67
+ const base = path.basename(command);
68
+ if (base !== 'npx' && base !== 'npm' && base !== 'pnpm' && base !== 'bunx') {
69
+ return null;
70
+ }
71
+ // Skip flags (-y, --yes, exec, ...) and take the first package-looking token.
72
+ for (const arg of args) {
73
+ if (arg.startsWith('-')) continue;
74
+ if (arg === 'exec' || arg === 'dlx') continue;
75
+ return arg;
76
+ }
77
+ return null;
78
+ }
79
+
80
+ function extractEnvValues(config: Record<string, unknown>): Record<string, string> | undefined {
81
+ const env = config.env;
82
+ if (!env || typeof env !== 'object' || Array.isArray(env)) return undefined;
83
+ const out: Record<string, string> = {};
84
+ for (const [key, value] of Object.entries(env as Record<string, unknown>)) {
85
+ if (typeof value === 'string') out[key] = value;
86
+ }
87
+ return Object.keys(out).length > 0 ? out : undefined;
88
+ }
89
+
90
+ function extractHeaders(config: Record<string, unknown>): McpHeaderDef[] | undefined {
91
+ const headers = config.headers;
92
+ if (!headers || typeof headers !== 'object' || Array.isArray(headers)) return undefined;
93
+ const out: McpHeaderDef[] = [];
94
+ for (const [key, value] of Object.entries(headers as Record<string, unknown>)) {
95
+ if (typeof value === 'string') out.push({ key, value });
96
+ }
97
+ return out.length > 0 ? out : undefined;
98
+ }
99
+
100
+ export class McpLibraryService {
101
+ private readonly filePath: string;
102
+ private entries: McpLibraryEntry[] | null = null;
103
+
104
+ constructor(
105
+ dataDir: string = getHermitHome(),
106
+ private readonly stateReader = new McpConfigStateReader()
107
+ ) {
108
+ this.filePath = path.join(dataDir, 'mcp-library.json');
109
+ }
110
+
111
+ // ── Read ─────────────────────────────────────────────────────────────────
112
+
113
+ list(): McpLibraryEntry[] {
114
+ return [...this.load()].sort((a, b) => a.name.localeCompare(b.name));
115
+ }
116
+
117
+ // ── Write ──────────────────────────────────────────────────────────────────
118
+
119
+ upsert(request: McpLibraryUpsertRequest): McpLibraryEntry {
120
+ const name = request.name.trim();
121
+ if (!name) throw new Error('MCP 名称不能为空');
122
+
123
+ const entries = this.load();
124
+ const now = Date.now();
125
+
126
+ if (request.id) {
127
+ const existing = entries.find((e) => e.id === request.id);
128
+ if (!existing) throw new Error(`未找到库条目: ${request.id}`);
129
+ this.assertNameAvailable(entries, name, request.id);
130
+ existing.name = name;
131
+ existing.description = request.description?.trim() || undefined;
132
+ existing.installSpec = request.installSpec;
133
+ existing.envValues = request.envValues;
134
+ existing.headers = request.headers;
135
+ existing.updatedAt = now;
136
+ this.save(entries);
137
+ return existing;
138
+ }
139
+
140
+ this.assertNameAvailable(entries, name, null);
141
+ const entry: McpLibraryEntry = {
142
+ id: randomUUID(),
143
+ name,
144
+ description: request.description?.trim() || undefined,
145
+ installSpec: request.installSpec,
146
+ envValues: request.envValues,
147
+ headers: request.headers,
148
+ createdAt: now,
149
+ updatedAt: now,
150
+ };
151
+ entries.push(entry);
152
+ this.save(entries);
153
+ return entry;
154
+ }
155
+
156
+ remove(id: string): void {
157
+ const entries = this.load();
158
+ const next = entries.filter((e) => e.id !== id);
159
+ if (next.length !== entries.length) this.save(next);
160
+ }
161
+
162
+ /**
163
+ * Import MCP servers already present in live config into the library.
164
+ * Skips entries whose name already exists, and ones whose config can't be
165
+ * represented by the current install spec (e.g. arbitrary stdio commands).
166
+ */
167
+ async importFromLive(request: McpLibraryImportRequest): Promise<McpLibraryImportResult> {
168
+ const configured = await this.stateReader.readConfigured(request.projectPath);
169
+ const entries = this.load();
170
+ const existingNames = new Set(entries.map((e) => e.name.toLowerCase()));
171
+
172
+ const imported: string[] = [];
173
+ const skipped: string[] = [];
174
+ const now = Date.now();
175
+
176
+ for (const entry of configured) {
177
+ if (existingNames.has(entry.name.toLowerCase())) {
178
+ skipped.push(entry.name);
179
+ continue;
180
+ }
181
+ const installSpec = rawConfigToInstallSpec(entry.config);
182
+ if (!installSpec) {
183
+ skipped.push(entry.name);
184
+ continue;
185
+ }
186
+ entries.push({
187
+ id: randomUUID(),
188
+ name: entry.name,
189
+ installSpec,
190
+ envValues: extractEnvValues(entry.config),
191
+ headers: extractHeaders(entry.config),
192
+ createdAt: now,
193
+ updatedAt: now,
194
+ });
195
+ existingNames.add(entry.name.toLowerCase());
196
+ imported.push(entry.name);
197
+ }
198
+
199
+ if (imported.length > 0) this.save(entries);
200
+ return { imported, skipped };
201
+ }
202
+
203
+ // ── Private ──────────────────────────────────────────────────────────────
204
+
205
+ private assertNameAvailable(
206
+ entries: McpLibraryEntry[],
207
+ name: string,
208
+ ignoreId: string | null
209
+ ): void {
210
+ const clash = entries.find(
211
+ (e) => e.name.toLowerCase() === name.toLowerCase() && e.id !== ignoreId
212
+ );
213
+ if (clash) throw new Error(`库中已存在同名 MCP: ${name}`);
214
+ }
215
+
216
+ private load(): McpLibraryEntry[] {
217
+ if (this.entries) return this.entries;
218
+ try {
219
+ if (fs.existsSync(this.filePath)) {
220
+ const raw = fs.readFileSync(this.filePath, 'utf-8');
221
+ const parsed = JSON.parse(raw) as McpLibraryEntry[];
222
+ this.entries = Array.isArray(parsed) ? parsed : [];
223
+ } else {
224
+ this.entries = [];
225
+ }
226
+ } catch (error) {
227
+ logger.warn(`Failed to load MCP library from ${this.filePath}: ${getErrorMessage(error)}`);
228
+ this.entries = [];
229
+ }
230
+ return this.entries;
231
+ }
232
+
233
+ private save(entries: McpLibraryEntry[]): void {
234
+ this.entries = entries;
235
+ try {
236
+ const dir = path.dirname(this.filePath);
237
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
238
+ fs.writeFileSync(this.filePath, JSON.stringify(entries, null, 2), 'utf-8');
239
+ } catch (error) {
240
+ logger.warn(`Failed to save MCP library to ${this.filePath}: ${getErrorMessage(error)}`);
241
+ }
242
+ }
243
+ }
@@ -26,6 +26,8 @@ function redisConfig(cfg: TaskBusConfig) {
26
26
  password: cfg.redis.password,
27
27
  db: cfg.redis.db,
28
28
  lazyConnect: true,
29
+ maxRetriesPerRequest: 0,
30
+ retryStrategy: () => null,
29
31
  };
30
32
  }
31
33
 
@@ -39,6 +41,9 @@ async function getRedis(cfg: TaskBusConfig): Promise<Redis | null> {
39
41
  }
40
42
 
41
43
  const r = new Redis(redisConfig(cfg));
44
+ r.on('error', () => {
45
+ /* handled by connect/ping fallback */
46
+ });
42
47
  try {
43
48
  await r.connect();
44
49
  await r.ping();
@@ -107,7 +112,7 @@ async function doScan(cfg: TaskBusConfig): Promise<ParseResult | null> {
107
112
  const result = await scanSessions();
108
113
  lastLocalScan = statusFromParseResult(result, false);
109
114
 
110
- if (!cfg.telemetry.uploadEnabled) {
115
+ if (!cfg.enabled || !cfg.telemetry.uploadEnabled) {
111
116
  return result;
112
117
  }
113
118
 
@@ -212,10 +217,18 @@ export async function getTelemetryStatus(
212
217
 
213
218
  const cfg = { redis: redisCfg };
214
219
  const client = new Redis(redisConfig(cfg as TaskBusConfig));
220
+ client.on('error', () => {
221
+ /* handled by connect/ping fallback */
222
+ });
215
223
  try {
216
224
  await client.connect();
217
225
  await client.ping();
218
226
  } catch {
227
+ try {
228
+ client.disconnect();
229
+ } catch {
230
+ /* ignore */
231
+ }
219
232
  return lastLocalScan;
220
233
  }
221
234
 
@@ -79,6 +79,7 @@ export class TaskDispatchService {
79
79
  this.stopHeartbeat();
80
80
  this.stopConsumers();
81
81
  this.stopResponseConsumers();
82
+ this.collabBoard.setRedis(null);
82
83
  this.redis?.disconnect();
83
84
  this.redisSub?.disconnect();
84
85
  this.redis = null;
@@ -757,27 +758,51 @@ export class TaskDispatchService {
757
758
 
758
759
  private async connectRedis(): Promise<void> {
759
760
  if (!this.config?.redis) return;
761
+ let redis: Redis | null = null;
762
+ let redisSub: Redis | null = null;
760
763
  try {
761
764
  const ioredis = await import('ioredis');
762
765
  const { host, port, password, db } = this.config.redis;
763
- const opts = { host, port, password: password || undefined, db: db ?? 0 };
766
+ const opts = {
767
+ host,
768
+ port,
769
+ password: password || undefined,
770
+ db: db ?? 0,
771
+ lazyConnect: true,
772
+ maxRetriesPerRequest: 0,
773
+ retryStrategy: () => null,
774
+ };
764
775
 
765
- this.redis = new ioredis.default(opts);
766
- this.redisSub = new ioredis.default(opts);
776
+ redis = new ioredis.default(opts);
777
+ redisSub = new ioredis.default(opts);
767
778
 
768
- this.redis.on('error', (err: Error) => {
769
- console.error('[TaskDispatchService] Redis error:', err.message);
779
+ redis.on('error', (err: Error) => {
780
+ console.warn('[TaskDispatchService] Redis error:', err.message);
781
+ });
782
+ redisSub.on('error', (err: Error) => {
783
+ console.warn('[TaskDispatchService] Redis subscriber error:', err.message);
770
784
  });
771
785
 
772
- await this.redis.ping();
786
+ await redis.connect();
787
+ await redisSub.connect();
788
+ await redis.ping();
773
789
 
790
+ this.redis = redis;
791
+ this.redisSub = redisSub;
792
+ redis = null;
793
+ redisSub = null;
774
794
  this.collabBoard.setRedis(this.redis);
775
795
  this.startHeartbeat();
776
796
  this.startConsumers();
777
797
  this.startResponseConsumers();
778
798
  this.subscribeStatus();
779
799
  } catch (err) {
780
- console.error('[TaskDispatchService] Redis connect failed:', err);
800
+ console.warn('[TaskDispatchService] Redis connect failed:', err);
801
+ this.collabBoard.setRedis(null);
802
+ redis?.disconnect();
803
+ redisSub?.disconnect();
804
+ this.redis?.disconnect();
805
+ this.redisSub?.disconnect();
781
806
  this.redis = null;
782
807
  this.redisSub = null;
783
808
  }