@yancyyu/openhermit 1.6.29 → 1.6.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-renderer/assets/{ProjectEditorOverlay-CQm6jUR1.js → ProjectEditorOverlay-DkXfi2pg.js} +1 -1
- package/dist-renderer/assets/{TeamGraphOverlay-h0WDfifv.js → TeamGraphOverlay-CHNNVraw.js} +1 -1
- package/dist-renderer/assets/{_basePickBy-CgG_tjgX.js → _basePickBy-Do-Ff83V.js} +1 -1
- package/dist-renderer/assets/{_baseUniq-DwPTU9lP.js → _baseUniq-nDLhSuJI.js} +1 -1
- package/dist-renderer/assets/{arc-7nIrGRzY.js → arc-Bp7fA6sx.js} +1 -1
- package/dist-renderer/assets/{architectureDiagram-VXUJARFQ-BYhA6Ev2.js → architectureDiagram-VXUJARFQ-CPC1HdGy.js} +1 -1
- package/dist-renderer/assets/{blockDiagram-VD42YOAC-BVpZUGDg.js → blockDiagram-VD42YOAC-DTVKyNTO.js} +1 -1
- package/dist-renderer/assets/{c4Diagram-YG6GDRKO-DsdreMQ9.js → c4Diagram-YG6GDRKO-XVu-AN00.js} +1 -1
- package/dist-renderer/assets/channel-CIwbNcUO.js +1 -0
- package/dist-renderer/assets/{chunk-4BX2VUAB-CcoAs7Jd.js → chunk-4BX2VUAB-BcWmVyA-.js} +1 -1
- package/dist-renderer/assets/{chunk-55IACEB6-CGGAOoXd.js → chunk-55IACEB6-Co4Z2jsE.js} +1 -1
- package/dist-renderer/assets/{chunk-B4BG7PRW-FhpTEPvD.js → chunk-B4BG7PRW-C8q9gfDT.js} +1 -1
- package/dist-renderer/assets/{chunk-DI55MBZ5-DoYySbm1.js → chunk-DI55MBZ5-qDgb1gxO.js} +1 -1
- package/dist-renderer/assets/{chunk-FMBD7UC4-e9l2tGHG.js → chunk-FMBD7UC4-Cm8Gu2gu.js} +1 -1
- package/dist-renderer/assets/{chunk-QN33PNHL-DeiXVTCy.js → chunk-QN33PNHL-DYji1BRS.js} +1 -1
- package/dist-renderer/assets/{chunk-QZHKN3VN-DC2UJLJM.js → chunk-QZHKN3VN-DWAS568H.js} +1 -1
- package/dist-renderer/assets/{chunk-TZMSLE5B-BHFD9eZI.js → chunk-TZMSLE5B-CLFzXLA8.js} +1 -1
- package/dist-renderer/assets/classDiagram-2ON5EDUG-04A-pvql.js +1 -0
- package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-04A-pvql.js +1 -0
- package/dist-renderer/assets/clone-DQnvTIEM.js +1 -0
- package/dist-renderer/assets/{cose-bilkent-S5V4N54A-BdybQraU.js → cose-bilkent-S5V4N54A-CZdGhX_3.js} +1 -1
- package/dist-renderer/assets/{dagre-6UL2VRFP-DdF3pwM3.js → dagre-6UL2VRFP-BVY-G6nO.js} +1 -1
- package/dist-renderer/assets/{diagram-PSM6KHXK-B9Ldd3nh.js → diagram-PSM6KHXK-CUACvAwG.js} +1 -1
- package/dist-renderer/assets/{diagram-QEK2KX5R-XEqkrbpu.js → diagram-QEK2KX5R-3SfnesSG.js} +1 -1
- package/dist-renderer/assets/{diagram-S2PKOQOG-CipwtY59.js → diagram-S2PKOQOG-E3ksXClJ.js} +1 -1
- package/dist-renderer/assets/{erDiagram-Q2GNP2WA-BB-2ISGo.js → erDiagram-Q2GNP2WA-aYjGXss7.js} +1 -1
- package/dist-renderer/assets/{flowDiagram-NV44I4VS-B8XmJ0u2.js → flowDiagram-NV44I4VS-JMHrrTQs.js} +1 -1
- package/dist-renderer/assets/{ganttDiagram-JELNMOA3-D-8XglBb.js → ganttDiagram-JELNMOA3-CVQ-R5rN.js} +1 -1
- package/dist-renderer/assets/{gitGraphDiagram-V2S2FVAM-DL4ChakD.js → gitGraphDiagram-V2S2FVAM-OLn9jq61.js} +1 -1
- package/dist-renderer/assets/{graph-BiFNoBjP.js → graph-BAb2J0l8.js} +1 -1
- package/dist-renderer/assets/{index-qNBNjW4K.js → index-BSoCjBWn.js} +1 -1
- package/dist-renderer/assets/{index-6m1ZAymG.js → index-BtG3HbqP.js} +1 -1
- package/dist-renderer/assets/{index-BowUl0Jb.js → index-CH8e7g1f.js} +583 -573
- package/dist-renderer/assets/index-CSt8DTcn.css +1 -0
- package/dist-renderer/assets/{index-Dp3kJTEe.js → index-Ca4iNkRA.js} +1 -1
- package/dist-renderer/assets/{index-vAykq1H1.js → index-DU9PGgZJ.js} +1 -1
- package/dist-renderer/assets/{index-TOpt_T7A.js → index-DtMzIS9o.js} +1 -1
- package/dist-renderer/assets/{infoDiagram-HS3SLOUP-DRIBfHDi.js → infoDiagram-HS3SLOUP-CY_ptQNL.js} +1 -1
- package/dist-renderer/assets/{journeyDiagram-XKPGCS4Q-BOMiigU4.js → journeyDiagram-XKPGCS4Q-C2vuHEo_.js} +1 -1
- package/dist-renderer/assets/{kanban-definition-3W4ZIXB7-DDxeyjod.js → kanban-definition-3W4ZIXB7-mbdNfu8h.js} +1 -1
- package/dist-renderer/assets/{layout-DNANbrI4.js → layout-Do_ArEB1.js} +1 -1
- package/dist-renderer/assets/{linear-DxEJi1yT.js → linear-BMlMKyiq.js} +1 -1
- package/dist-renderer/assets/{mindmap-definition-VGOIOE7T-nBfGriW8.js → mindmap-definition-VGOIOE7T-Dfntn-o2.js} +1 -1
- package/dist-renderer/assets/{pieDiagram-ADFJNKIX-Din5j6sV.js → pieDiagram-ADFJNKIX-LiWHsGMV.js} +1 -1
- package/dist-renderer/assets/{quadrantDiagram-AYHSOK5B-DMVK2BEQ.js → quadrantDiagram-AYHSOK5B-D87St8AF.js} +1 -1
- package/dist-renderer/assets/{requirementDiagram-UZGBJVZJ-6SC94Gg_.js → requirementDiagram-UZGBJVZJ-DAa6lHBx.js} +1 -1
- package/dist-renderer/assets/{sankeyDiagram-TZEHDZUN-CD2gghhu.js → sankeyDiagram-TZEHDZUN-VOUngars.js} +1 -1
- package/dist-renderer/assets/{sequenceDiagram-WL72ISMW-BnhkN7nZ.js → sequenceDiagram-WL72ISMW-BzwzmFr2.js} +1 -1
- package/dist-renderer/assets/{stateDiagram-FKZM4ZOC-Bn8XdYX-.js → stateDiagram-FKZM4ZOC-BjAQEJ52.js} +1 -1
- package/dist-renderer/assets/{stateDiagram-v2-4FDKWEC3-1b6sI1_g.js → stateDiagram-v2-4FDKWEC3-BDwy4GJm.js} +1 -1
- package/dist-renderer/assets/{timeline-definition-IT6M3QCI-CNs3RPoa.js → timeline-definition-IT6M3QCI-Y5XBZt3W.js} +1 -1
- package/dist-renderer/assets/treemap-GDKQZRPO-DzkdUEow.js +162 -0
- package/dist-renderer/assets/{xychartDiagram-PRI3JC2R-B8o5J2f3.js → xychartDiagram-PRI3JC2R-D-zbvJOv.js} +1 -1
- package/dist-renderer/index.html +2 -2
- package/package.json +4 -1
- package/src/main/ipc/extensions.ts +353 -0
- package/src/main/server.ts +209 -6
- package/src/main/services/extensions/ExtensionFacadeService.ts +135 -0
- package/src/main/services/extensions/catalog/GlamaMcpEnrichmentService.ts +190 -0
- package/src/main/services/extensions/catalog/McpCatalogAggregator.ts +150 -0
- package/src/main/services/extensions/catalog/OfficialMcpRegistryService.ts +381 -0
- package/src/main/services/extensions/catalog/PluginCatalogService.ts +392 -0
- package/src/main/services/extensions/credentials/CredentialService.ts +343 -0
- package/src/main/services/extensions/install/McpInstallService.ts +407 -0
- package/src/main/services/extensions/install/PluginInstallService.ts +198 -0
- package/src/main/services/extensions/runtime/ClaudeCodeAdapter.ts +199 -0
- package/src/main/services/extensions/runtime/CodexAdapter.ts +100 -0
- package/src/main/services/extensions/runtime/CursorAdapter.ts +154 -0
- package/src/main/services/extensions/runtime/ExtensionsRuntimeAdapter.ts +172 -0
- package/src/main/services/extensions/runtime/GeminiAdapter.ts +91 -0
- package/src/main/services/extensions/runtime/HarnessInstallAdapter.ts +49 -0
- package/src/main/services/extensions/runtime/McpConfigStateReader.ts +209 -0
- package/src/main/services/extensions/runtime/OpenCodeAdapter.ts +91 -0
- package/src/main/services/extensions/runtime/adapterRegistry.ts +54 -0
- package/src/main/services/extensions/runtime/mcpDiagnosticsParser.ts +214 -0
- package/src/main/services/extensions/runtime/mcpRuntimeJson.ts +45 -0
- package/src/main/services/extensions/skills/SkillImportService.ts +155 -0
- package/src/main/services/extensions/skills/SkillMetadataParser.ts +323 -0
- package/src/main/services/extensions/skills/SkillPlanService.ts +411 -0
- package/src/main/services/extensions/skills/SkillReviewService.ts +73 -0
- package/src/main/services/extensions/skills/SkillRootsResolver.ts +49 -0
- package/src/main/services/extensions/skills/SkillScaffoldService.ts +89 -0
- package/src/main/services/extensions/skills/SkillScanner.ts +117 -0
- package/src/main/services/extensions/skills/SkillValidator.ts +69 -0
- package/src/main/services/extensions/skills/SkillsCatalogService.ts +92 -0
- package/src/main/services/extensions/skills/SkillsMutationService.ts +146 -0
- package/src/main/services/extensions/skills/SkillsWatcherService.ts +134 -0
- package/src/main/services/extensions/state/McpInstallationStateService.ts +42 -0
- package/src/main/services/extensions/state/PluginInstallationStateService.ts +281 -0
- package/src/main/services/identity/AgentTeamsIdentityStore.ts +218 -0
- package/src/main/services/runtime/providerAwareCliEnv.ts +60 -0
- package/src/main/services/team/ClaudeBinaryResolver.ts +469 -0
- package/src/main/services/team/ClaudeDoctorProbe.ts +0 -0
- package/src/main/services/team/cliFlavor.ts +54 -0
- package/src/main/services/teams-mvp/TaskDispatchService.ts +3 -0
- package/src/main/utils/atomicWrite.ts +72 -0
- package/src/main/utils/childProcess.ts +554 -0
- package/src/main/utils/cliEnv.ts +54 -0
- package/src/main/utils/cliPathMerge.ts +97 -0
- package/src/main/utils/pathDecoder.ts +664 -0
- package/src/main/utils/pathValidation.ts +432 -0
- package/src/main/utils/shellEnv.ts +331 -0
- package/src/renderer/api/httpClient.ts +61 -0
- package/src/renderer/components/extensions/ExtensionStoreView.tsx +63 -35
- package/src/renderer/components/extensions/ExtensionsSubTabTrigger.tsx +1 -1
- package/src/renderer/components/extensions/common/ExtensionToast.tsx +141 -0
- package/src/renderer/components/extensions/common/HarnessSelector.tsx +71 -0
- package/src/renderer/components/extensions/env/EnvVarPanel.tsx +335 -0
- package/src/renderer/components/extensions/env/ProjectEnvPanel.tsx +239 -0
- package/src/renderer/components/extensions/mcp/CustomMcpServerDialog.tsx +14 -223
- package/src/renderer/components/extensions/mcp/McpServerDetailDialog.tsx +111 -15
- package/src/renderer/components/extensions/mcp/McpServersPanel.tsx +51 -1
- package/src/renderer/components/extensions/skills/SkillsPanel.tsx +1 -126
- package/src/renderer/components/settings/sections/HarnessSection.tsx +2 -6
- package/src/renderer/components/settings/sections/TaskBusSection.tsx +17 -7
- package/src/renderer/components/sidebar/SidebarSessions.tsx +23 -0
- package/src/renderer/components/sidebar/WorkspaceBrowser.tsx +1 -7
- package/src/renderer/components/team/HarnessSelect.tsx +71 -0
- package/src/renderer/components/team/TeamDetailView.tsx +74 -123
- package/src/renderer/components/team/TeamListFilterPopover.tsx +0 -16
- package/src/renderer/components/team/TeamListView.tsx +7 -32
- package/src/renderer/components/team/dialogs/CreateTeamDialog.tsx +21 -12
- package/src/renderer/components/team/dialogs/EditTeamDialog.tsx +287 -418
- package/src/renderer/components/team/dialogs/useTeamEditForm.ts +283 -0
- package/src/renderer/components/team/kanban/KanbanBoard.tsx +26 -64
- package/src/renderer/components/team/messages/MessagesPanel.tsx +28 -24
- package/src/renderer/components/terminal/TerminalPanel.tsx +156 -0
- package/src/renderer/hooks/useExtensionsTabState.ts +2 -2
- package/src/renderer/store/slices/extensionsSlice.ts +42 -107
- package/src/renderer/store/slices/teamSlice.ts +8 -2
- package/src/renderer/utils/multimodelProviderVisibility.ts +17 -0
- package/src/renderer/utils/openCodeRuntimeDeliveryDiagnostics.ts +29 -9
- package/src/shared/types/api.ts +29 -0
- package/src/shared/types/extensions/index.ts +1 -0
- package/src/shared/types/extensions/mcp.ts +2 -0
- package/src/shared/types/extensions/plugin.ts +2 -1
- package/src/shared/types/extensions/skill.ts +7 -0
- package/src/shared/utils/providerExtensionCapabilities.ts +1 -1
- package/dist-renderer/assets/channel-C0SqeFU7.js +0 -1
- package/dist-renderer/assets/classDiagram-2ON5EDUG-DWew1HpM.js +0 -1
- package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-DWew1HpM.js +0 -1
- package/dist-renderer/assets/clone-Dm-k63Yr.js +0 -1
- package/dist-renderer/assets/index-BhellmRb.css +0 -1
- package/dist-renderer/assets/treemap-GDKQZRPO-DU_yr827.js +0 -162
- package/src/features/recent-projects/main/adapters/input/http/registerRecentProjectsHttp.ts +0 -30
- package/src/features/recent-projects/main/adapters/output/presenters/DashboardRecentProjectsPresenter.ts +0 -27
- package/src/features/recent-projects/main/adapters/output/sources/ClaudeRecentProjectsSourceAdapter.ts +0 -91
- package/src/features/recent-projects/main/adapters/output/sources/CodexRecentProjectsSourceAdapter.ts +0 -326
- package/src/features/recent-projects/main/composition/createRecentProjectsFeature.ts +0 -43
- package/src/features/recent-projects/main/index.ts +0 -3
- package/src/features/recent-projects/main/infrastructure/cache/InMemoryRecentProjectsCache.ts +0 -34
- package/src/features/recent-projects/main/infrastructure/codex/CodexAppServerClient.ts +0 -116
- package/src/features/recent-projects/main/infrastructure/identity/RecentProjectIdentityResolver.ts +0 -20
- package/src/features/recent-projects/main/infrastructure/identity/normalizeIdentityPath.ts +0 -10
- package/src/renderer/components/extensions/apikeys/ApiKeyCard.tsx +0 -143
- package/src/renderer/components/extensions/apikeys/ApiKeyFormDialog.tsx +0 -282
- package/src/renderer/components/extensions/apikeys/ApiKeysPanel.tsx +0 -280
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProjectEnvPanel — manage project-level environment variables for Skills/MCP.
|
|
3
|
+
* Scans required env vars from enabled MCP servers + Skills, shows fill status,
|
|
4
|
+
* allows editing and encrypted saving.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
8
|
+
|
|
9
|
+
import { api } from '@renderer/api';
|
|
10
|
+
import { Button } from '@renderer/components/ui/button';
|
|
11
|
+
import { Input } from '@renderer/components/ui/input';
|
|
12
|
+
import { Label } from '@renderer/components/ui/label';
|
|
13
|
+
import { Eye, EyeOff, Save, Shield } from 'lucide-react';
|
|
14
|
+
|
|
15
|
+
interface EnvVarEntry {
|
|
16
|
+
name: string;
|
|
17
|
+
isRequired: boolean;
|
|
18
|
+
description?: string;
|
|
19
|
+
source: string;
|
|
20
|
+
value?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface ProjectEnvPanelProps {
|
|
24
|
+
projectPath: string | null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const ProjectEnvPanel = ({ projectPath }: ProjectEnvPanelProps): React.JSX.Element => {
|
|
28
|
+
const [entries, setEntries] = useState<EnvVarEntry[]>([]);
|
|
29
|
+
const [editValues, setEditValues] = useState<Record<string, string>>({});
|
|
30
|
+
const [revealed, setRevealed] = useState<Set<string>>(new Set());
|
|
31
|
+
const [loading, setLoading] = useState(false);
|
|
32
|
+
const [saving, setSaving] = useState(false);
|
|
33
|
+
const [error, setError] = useState<string | null>(null);
|
|
34
|
+
const [successMsg, setSuccessMsg] = useState<string | null>(null);
|
|
35
|
+
|
|
36
|
+
const scanEnv = useCallback(async () => {
|
|
37
|
+
if (!projectPath || !api.credentials) return;
|
|
38
|
+
setLoading(true);
|
|
39
|
+
setError(null);
|
|
40
|
+
try {
|
|
41
|
+
// Installed MCP entries do not carry env-var requirements (InstalledMcpEntry
|
|
42
|
+
// only exposes name/scope/transport), so MCP env scanning is not available here.
|
|
43
|
+
// Required env vars are sourced from skill declarations below.
|
|
44
|
+
const mcpServers: {
|
|
45
|
+
name: string;
|
|
46
|
+
envVars?: { name: string; isRequired: boolean; description?: string };
|
|
47
|
+
}[] = [];
|
|
48
|
+
|
|
49
|
+
// Gather skills with required-env from catalog
|
|
50
|
+
const skillReqs: {
|
|
51
|
+
name: string;
|
|
52
|
+
envVars: { name: string; isRequired?: boolean; description?: string }[];
|
|
53
|
+
}[] = [];
|
|
54
|
+
if (api.skills) {
|
|
55
|
+
try {
|
|
56
|
+
const skills = await api.skills.list(projectPath);
|
|
57
|
+
for (const skill of skills) {
|
|
58
|
+
const reqEnv = skill.requiredEnv ?? [];
|
|
59
|
+
if (reqEnv.length > 0) {
|
|
60
|
+
skillReqs.push({
|
|
61
|
+
name: skill.name,
|
|
62
|
+
envVars: reqEnv.map((v) => ({
|
|
63
|
+
name: v.name,
|
|
64
|
+
isRequired: v.isRequired ?? true,
|
|
65
|
+
description: v.description,
|
|
66
|
+
})),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
} catch {
|
|
71
|
+
/* non-critical */
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const result = await api.credentials
|
|
76
|
+
.scanRequired(projectPath, mcpServers, skillReqs)
|
|
77
|
+
.catch(() => null);
|
|
78
|
+
const required = (result as any)?.required ?? [];
|
|
79
|
+
setEntries(required);
|
|
80
|
+
|
|
81
|
+
const saved = await api.credentials
|
|
82
|
+
.getProjectEnv(projectPath)
|
|
83
|
+
.catch(() => ({}) as Record<string, string>);
|
|
84
|
+
const initialValues: Record<string, string> = {};
|
|
85
|
+
for (const entry of required) {
|
|
86
|
+
initialValues[entry.name] = (saved as any)?.[entry.name] ?? entry.value ?? '';
|
|
87
|
+
}
|
|
88
|
+
setEditValues(initialValues);
|
|
89
|
+
} catch (err) {
|
|
90
|
+
setError(err instanceof Error ? err.message : '扫描环境变量失败');
|
|
91
|
+
} finally {
|
|
92
|
+
setLoading(false);
|
|
93
|
+
}
|
|
94
|
+
}, [projectPath]);
|
|
95
|
+
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
void scanEnv();
|
|
98
|
+
}, [scanEnv]);
|
|
99
|
+
|
|
100
|
+
const handleSave = async () => {
|
|
101
|
+
if (!projectPath || !api.credentials) return;
|
|
102
|
+
setSaving(true);
|
|
103
|
+
setError(null);
|
|
104
|
+
setSuccessMsg(null);
|
|
105
|
+
try {
|
|
106
|
+
await api.credentials.saveProjectEnv(projectPath, editValues);
|
|
107
|
+
setSuccessMsg('环境变量已加密保存');
|
|
108
|
+
setTimeout(() => setSuccessMsg(null), 3000);
|
|
109
|
+
await scanEnv();
|
|
110
|
+
} catch (err) {
|
|
111
|
+
setError(err instanceof Error ? err.message : '保存失败');
|
|
112
|
+
} finally {
|
|
113
|
+
setSaving(false);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const toggleReveal = (name: string) => {
|
|
118
|
+
setRevealed((prev) => {
|
|
119
|
+
const next = new Set(prev);
|
|
120
|
+
if (next.has(name)) next.delete(name);
|
|
121
|
+
else next.add(name);
|
|
122
|
+
return next;
|
|
123
|
+
});
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
if (!projectPath) {
|
|
127
|
+
return (
|
|
128
|
+
<div className="flex flex-col items-center justify-center py-12 text-text-muted">
|
|
129
|
+
<Shield className="mb-3 size-10 opacity-40" />
|
|
130
|
+
<p className="text-sm">请先选择一个项目以管理环境变量。</p>
|
|
131
|
+
</div>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (loading) {
|
|
136
|
+
return (
|
|
137
|
+
<div className="flex items-center justify-center py-12 text-text-muted">
|
|
138
|
+
<p className="text-sm">正在扫描项目所需的环境变量...</p>
|
|
139
|
+
</div>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const missingRequired = entries.filter((e) => e.isRequired && !editValues[e.name]?.trim());
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<div className="space-y-4 px-1">
|
|
147
|
+
<div className="flex items-center justify-between">
|
|
148
|
+
<div>
|
|
149
|
+
<h3 className="text-sm font-medium text-text">项目环境变量</h3>
|
|
150
|
+
<p className="text-xs text-text-muted">
|
|
151
|
+
管理当前项目所需的环境变量,供 Skills 和 CLI 工具使用。值已加密存储。
|
|
152
|
+
</p>
|
|
153
|
+
</div>
|
|
154
|
+
<Button size="sm" variant="outline" onClick={() => void scanEnv()} disabled={loading}>
|
|
155
|
+
刷新
|
|
156
|
+
</Button>
|
|
157
|
+
</div>
|
|
158
|
+
|
|
159
|
+
{error && (
|
|
160
|
+
<div className="rounded-md border border-red-500/30 bg-red-500/10 px-3 py-2 text-xs text-red-400">
|
|
161
|
+
{error}
|
|
162
|
+
</div>
|
|
163
|
+
)}
|
|
164
|
+
|
|
165
|
+
{successMsg && (
|
|
166
|
+
<div className="rounded-md border border-emerald-500/30 bg-emerald-500/10 px-3 py-2 text-xs text-emerald-400">
|
|
167
|
+
{successMsg}
|
|
168
|
+
</div>
|
|
169
|
+
)}
|
|
170
|
+
|
|
171
|
+
{missingRequired.length > 0 && (
|
|
172
|
+
<div className="rounded-md border border-amber-500/30 bg-amber-500/10 px-3 py-2 text-xs text-amber-400">
|
|
173
|
+
缺少必填变量:{missingRequired.map((e) => e.name).join(', ')}
|
|
174
|
+
</div>
|
|
175
|
+
)}
|
|
176
|
+
|
|
177
|
+
{entries.length === 0 && !loading && (
|
|
178
|
+
<div className="py-8 text-center text-xs text-text-muted">
|
|
179
|
+
未检测到所需的环境变量。启用 MCP 服务器或 Skills 后会自动扫描。
|
|
180
|
+
</div>
|
|
181
|
+
)}
|
|
182
|
+
|
|
183
|
+
{entries.length > 0 && (
|
|
184
|
+
<div className="space-y-3">
|
|
185
|
+
{entries.map((entry) => (
|
|
186
|
+
<div key={entry.name} className="space-y-1.5">
|
|
187
|
+
<div className="flex items-center gap-2">
|
|
188
|
+
<Label className="font-mono text-xs text-text">{entry.name}</Label>
|
|
189
|
+
<span
|
|
190
|
+
className={`rounded px-1.5 py-0.5 text-[10px] font-medium ${
|
|
191
|
+
entry.isRequired
|
|
192
|
+
? 'bg-red-500/10 text-red-400'
|
|
193
|
+
: 'bg-surface-raised text-text-muted'
|
|
194
|
+
}`}
|
|
195
|
+
>
|
|
196
|
+
{entry.isRequired ? '必填' : '可选'}
|
|
197
|
+
</span>
|
|
198
|
+
<span className="text-[10px] text-text-muted">来自 {entry.source}</span>
|
|
199
|
+
</div>
|
|
200
|
+
{entry.description && (
|
|
201
|
+
<p className="text-[11px] text-text-muted">{entry.description}</p>
|
|
202
|
+
)}
|
|
203
|
+
<div className="flex gap-2">
|
|
204
|
+
<Input
|
|
205
|
+
type={revealed.has(entry.name) ? 'text' : 'password'}
|
|
206
|
+
value={editValues[entry.name] ?? ''}
|
|
207
|
+
onChange={(e) =>
|
|
208
|
+
setEditValues((prev) => ({ ...prev, [entry.name]: e.target.value }))
|
|
209
|
+
}
|
|
210
|
+
placeholder={entry.isRequired ? '必填' : '可选'}
|
|
211
|
+
className="h-7 flex-1 font-mono text-xs"
|
|
212
|
+
/>
|
|
213
|
+
<Button
|
|
214
|
+
size="icon"
|
|
215
|
+
variant="ghost"
|
|
216
|
+
className="size-7 shrink-0"
|
|
217
|
+
onClick={() => toggleReveal(entry.name)}
|
|
218
|
+
>
|
|
219
|
+
{revealed.has(entry.name) ? (
|
|
220
|
+
<EyeOff className="size-3.5" />
|
|
221
|
+
) : (
|
|
222
|
+
<Eye className="size-3.5" />
|
|
223
|
+
)}
|
|
224
|
+
</Button>
|
|
225
|
+
</div>
|
|
226
|
+
</div>
|
|
227
|
+
))}
|
|
228
|
+
|
|
229
|
+
<div className="pt-2">
|
|
230
|
+
<Button size="sm" onClick={() => void handleSave()} disabled={saving}>
|
|
231
|
+
<Save className="mr-1.5 size-3.5" />
|
|
232
|
+
{saving ? '保存中...' : '加密保存'}
|
|
233
|
+
</Button>
|
|
234
|
+
</div>
|
|
235
|
+
</div>
|
|
236
|
+
)}
|
|
237
|
+
</div>
|
|
238
|
+
);
|
|
239
|
+
};
|
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
|
|
6
6
|
import { useEffect, useRef, useState } from 'react';
|
|
7
7
|
|
|
8
|
-
import { api } from '@renderer/api';
|
|
9
8
|
import { Button } from '@renderer/components/ui/button';
|
|
10
9
|
import {
|
|
11
10
|
Dialog,
|
|
@@ -24,16 +23,11 @@ import {
|
|
|
24
23
|
SelectValue,
|
|
25
24
|
} from '@renderer/components/ui/select';
|
|
26
25
|
import { useStore } from '@renderer/store';
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
getDefaultMcpSharedScope,
|
|
30
|
-
getMcpScopeLabel,
|
|
31
|
-
isProjectScopedMcpScope,
|
|
32
|
-
isSharedMcpScope,
|
|
33
|
-
} from '@shared/utils/mcpScopes';
|
|
26
|
+
import { getDefaultMcpSharedScope } from '@shared/utils/mcpScopes';
|
|
34
27
|
import { Plus, Server, Trash2 } from 'lucide-react';
|
|
35
28
|
|
|
36
|
-
import
|
|
29
|
+
import { HarnessSelector } from '../common/HarnessSelector';
|
|
30
|
+
|
|
37
31
|
import type {
|
|
38
32
|
McpCustomInstallRequest,
|
|
39
33
|
McpHeaderDef,
|
|
@@ -45,17 +39,10 @@ const SERVER_NAME_RE = /^[\w.-]{1,100}$/;
|
|
|
45
39
|
interface CustomMcpServerDialogProps {
|
|
46
40
|
open: boolean;
|
|
47
41
|
onClose: () => void;
|
|
48
|
-
projectPath: string | null;
|
|
49
|
-
cliStatus?: Pick<
|
|
50
|
-
CliInstallationStatus,
|
|
51
|
-
'installed' | 'authLoggedIn' | 'binaryPath' | 'launchError' | 'flavor' | 'providers'
|
|
52
|
-
> | null;
|
|
53
|
-
cliStatusLoading?: boolean;
|
|
54
42
|
}
|
|
55
43
|
|
|
56
44
|
type TransportMode = 'stdio' | 'http';
|
|
57
45
|
type HttpTransport = 'streamable-http' | 'sse' | 'http';
|
|
58
|
-
type Scope = 'local' | 'user' | 'project' | 'global';
|
|
59
46
|
|
|
60
47
|
const HTTP_TRANSPORT_OPTIONS: { value: HttpTransport; label: string }[] = [
|
|
61
48
|
{ value: 'streamable-http', label: 'Streamable HTTP' },
|
|
@@ -63,34 +50,19 @@ const HTTP_TRANSPORT_OPTIONS: { value: HttpTransport; label: string }[] = [
|
|
|
63
50
|
{ value: 'http', label: 'HTTP' },
|
|
64
51
|
];
|
|
65
52
|
|
|
66
|
-
interface EnvEntry {
|
|
67
|
-
key: string;
|
|
68
|
-
value: string;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
53
|
export const CustomMcpServerDialog = ({
|
|
72
54
|
open,
|
|
73
55
|
onClose,
|
|
74
|
-
projectPath,
|
|
75
|
-
cliStatus: cliStatusOverride,
|
|
76
|
-
cliStatusLoading: cliStatusLoadingOverride,
|
|
77
56
|
}: CustomMcpServerDialogProps): React.JSX.Element => {
|
|
78
57
|
const installCustomMcpServer = useStore((s) => s.installCustomMcpServer);
|
|
79
58
|
const storedCliStatus = useStore((s) => s.cliStatus);
|
|
80
|
-
const
|
|
81
|
-
const
|
|
82
|
-
const cliStatusLoading = cliStatusLoadingOverride ?? storedCliStatusLoading;
|
|
83
|
-
const defaultSharedScope = getDefaultMcpSharedScope(cliStatus?.flavor);
|
|
84
|
-
const scopeOptions: { value: Scope; label: string }[] = [
|
|
85
|
-
{ value: defaultSharedScope, label: getMcpScopeLabel(defaultSharedScope, cliStatus?.flavor) },
|
|
86
|
-
{ value: 'project', label: 'Project' },
|
|
87
|
-
{ value: 'local', label: 'Local' },
|
|
88
|
-
];
|
|
59
|
+
const defaultSharedScope = getDefaultMcpSharedScope(storedCliStatus?.flavor);
|
|
60
|
+
const installScope = defaultSharedScope === 'global' ? 'user' : defaultSharedScope;
|
|
89
61
|
|
|
90
62
|
// Form state
|
|
91
63
|
const [serverName, setServerName] = useState('');
|
|
92
64
|
const [transportMode, setTransportMode] = useState<TransportMode>('stdio');
|
|
93
|
-
const [
|
|
65
|
+
const [harnessType, setHarnessType] = useState('claudecode');
|
|
94
66
|
|
|
95
67
|
// Stdio fields
|
|
96
68
|
const [npmPackage, setNpmPackage] = useState('');
|
|
@@ -102,26 +74,9 @@ export const CustomMcpServerDialog = ({
|
|
|
102
74
|
const [headers, setHeaders] = useState<McpHeaderDef[]>([]);
|
|
103
75
|
|
|
104
76
|
// Shared
|
|
105
|
-
const [envVars, setEnvVars] = useState<EnvEntry[]>([]);
|
|
106
77
|
const [error, setError] = useState<string | null>(null);
|
|
107
78
|
const [installing, setInstalling] = useState(false);
|
|
108
|
-
const autoFilledValuesRef = useRef<Record<string, string>>({});
|
|
109
79
|
const wasOpenRef = useRef(false);
|
|
110
|
-
const previousDefaultSharedScopeRef = useRef<Scope>(defaultSharedScope);
|
|
111
|
-
const envVarLookupNames = envVars
|
|
112
|
-
.map((entry) => entry.key.trim())
|
|
113
|
-
.filter(Boolean)
|
|
114
|
-
.sort()
|
|
115
|
-
.join('\0');
|
|
116
|
-
const apiKeyLookupProjectPath = isProjectScopedMcpScope(scope)
|
|
117
|
-
? (projectPath ?? undefined)
|
|
118
|
-
: undefined;
|
|
119
|
-
const mutationDisableReason = getExtensionActionDisableReason({
|
|
120
|
-
isInstalled: false,
|
|
121
|
-
cliStatus,
|
|
122
|
-
cliStatusLoading,
|
|
123
|
-
section: 'mcp',
|
|
124
|
-
});
|
|
125
80
|
|
|
126
81
|
// Reset on open
|
|
127
82
|
useEffect(() => {
|
|
@@ -129,107 +84,20 @@ export const CustomMcpServerDialog = ({
|
|
|
129
84
|
if (justOpened) {
|
|
130
85
|
setServerName('');
|
|
131
86
|
setTransportMode('stdio');
|
|
132
|
-
setScope(defaultSharedScope);
|
|
133
87
|
setNpmPackage('');
|
|
134
88
|
setNpmVersion('');
|
|
135
89
|
setHttpUrl('');
|
|
136
90
|
setHttpTransport('streamable-http');
|
|
137
91
|
setHeaders([]);
|
|
138
|
-
setEnvVars([]);
|
|
139
92
|
setError(null);
|
|
140
93
|
setInstalling(false);
|
|
141
|
-
autoFilledValuesRef.current = {};
|
|
142
94
|
}
|
|
143
95
|
wasOpenRef.current = open;
|
|
144
|
-
|
|
145
|
-
previousDefaultSharedScopeRef.current = defaultSharedScope;
|
|
146
|
-
}
|
|
147
|
-
}, [defaultSharedScope, open]);
|
|
148
|
-
|
|
149
|
-
useEffect(() => {
|
|
150
|
-
if (!open) {
|
|
151
|
-
previousDefaultSharedScopeRef.current = defaultSharedScope;
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const previousDefaultSharedScope = previousDefaultSharedScopeRef.current;
|
|
156
|
-
if (
|
|
157
|
-
previousDefaultSharedScope !== defaultSharedScope &&
|
|
158
|
-
scope === previousDefaultSharedScope &&
|
|
159
|
-
isSharedMcpScope(scope)
|
|
160
|
-
) {
|
|
161
|
-
setScope(defaultSharedScope);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
previousDefaultSharedScopeRef.current = defaultSharedScope;
|
|
165
|
-
}, [defaultSharedScope, open, scope]);
|
|
166
|
-
|
|
167
|
-
useEffect(() => {
|
|
168
|
-
if (open && isProjectScopedMcpScope(scope) && !projectPath) {
|
|
169
|
-
setScope(defaultSharedScope);
|
|
170
|
-
}
|
|
171
|
-
}, [defaultSharedScope, open, projectPath, scope]);
|
|
172
|
-
|
|
173
|
-
// Auto-fill env vars from saved API keys
|
|
174
|
-
useEffect(() => {
|
|
175
|
-
if (!open || envVars.length === 0 || !api.apiKeys) return;
|
|
176
|
-
|
|
177
|
-
const envVarNames = envVars.map((e) => e.key.trim()).filter(Boolean);
|
|
178
|
-
if (envVarNames.length === 0) return;
|
|
179
|
-
|
|
180
|
-
void api.apiKeys.lookup(envVarNames, apiKeyLookupProjectPath).then(
|
|
181
|
-
(results) => {
|
|
182
|
-
const previousAutoFilledValues = autoFilledValuesRef.current;
|
|
183
|
-
const nextAutoFilledValues = Object.fromEntries(
|
|
184
|
-
results.map((result) => [result.envVarName, result.value])
|
|
185
|
-
);
|
|
186
|
-
setEnvVars((prev) => {
|
|
187
|
-
let changed = false;
|
|
188
|
-
const next = prev.map((entry) => {
|
|
189
|
-
const envVarName = entry.key.trim();
|
|
190
|
-
if (!envVarName) {
|
|
191
|
-
return entry;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
const previousValue = previousAutoFilledValues[envVarName];
|
|
195
|
-
const nextValue = nextAutoFilledValues[envVarName];
|
|
196
|
-
|
|
197
|
-
if (!nextValue) {
|
|
198
|
-
if (previousValue && entry.value === previousValue) {
|
|
199
|
-
changed = true;
|
|
200
|
-
return { ...entry, value: '' };
|
|
201
|
-
}
|
|
202
|
-
return entry;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
if (!entry.value || entry.value === previousValue) {
|
|
206
|
-
if (entry.value !== nextValue) {
|
|
207
|
-
changed = true;
|
|
208
|
-
return { ...entry, value: nextValue };
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
return entry;
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
return changed ? next : prev;
|
|
216
|
-
});
|
|
217
|
-
autoFilledValuesRef.current = nextAutoFilledValues;
|
|
218
|
-
},
|
|
219
|
-
() => {
|
|
220
|
-
// Silently fail
|
|
221
|
-
}
|
|
222
|
-
);
|
|
223
|
-
}, [apiKeyLookupProjectPath, envVarLookupNames, open]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
96
|
+
}, [open]);
|
|
224
97
|
|
|
225
98
|
const handleInstall = async () => {
|
|
226
99
|
setError(null);
|
|
227
100
|
|
|
228
|
-
if (mutationDisableReason) {
|
|
229
|
-
setError(mutationDisableReason);
|
|
230
|
-
return;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
101
|
if (!serverName.trim()) {
|
|
234
102
|
setError('Server name is required');
|
|
235
103
|
return;
|
|
@@ -263,20 +131,13 @@ export const CustomMcpServerDialog = ({
|
|
|
263
131
|
};
|
|
264
132
|
}
|
|
265
133
|
|
|
266
|
-
const envValues: Record<string, string> = {};
|
|
267
|
-
for (const entry of envVars) {
|
|
268
|
-
if (entry.key.trim() && entry.value) {
|
|
269
|
-
envValues[entry.key.trim()] = entry.value;
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
|
|
273
134
|
const request: McpCustomInstallRequest = {
|
|
274
135
|
serverName,
|
|
275
|
-
scope,
|
|
276
|
-
projectPath: isProjectScopedMcpScope(scope) ? (projectPath ?? undefined) : undefined,
|
|
136
|
+
scope: installScope,
|
|
277
137
|
installSpec,
|
|
278
|
-
envValues,
|
|
138
|
+
envValues: {},
|
|
279
139
|
headers: headers.filter((h) => h.key.trim() && h.value.trim()),
|
|
140
|
+
harnessType,
|
|
280
141
|
};
|
|
281
142
|
|
|
282
143
|
setInstalling(true);
|
|
@@ -290,11 +151,6 @@ export const CustomMcpServerDialog = ({
|
|
|
290
151
|
}
|
|
291
152
|
};
|
|
292
153
|
|
|
293
|
-
const addEnvVar = () => setEnvVars((prev) => [...prev, { key: '', value: '' }]);
|
|
294
|
-
const removeEnvVar = (i: number) => setEnvVars((prev) => prev.filter((_, idx) => idx !== i));
|
|
295
|
-
const updateEnvVar = (i: number, field: 'key' | 'value', val: string) =>
|
|
296
|
-
setEnvVars((prev) => prev.map((e, idx) => (idx === i ? { ...e, [field]: val } : e)));
|
|
297
|
-
|
|
298
154
|
const addHeader = () => setHeaders((prev) => [...prev, { key: '', value: '' }]);
|
|
299
155
|
const removeHeader = (i: number) => setHeaders((prev) => prev.filter((_, idx) => idx !== i));
|
|
300
156
|
const updateHeader = (i: number, field: 'key' | 'value', val: string) =>
|
|
@@ -303,13 +159,11 @@ export const CustomMcpServerDialog = ({
|
|
|
303
159
|
const canSubmit =
|
|
304
160
|
serverName.trim() &&
|
|
305
161
|
(transportMode === 'stdio' ? npmPackage.trim() : httpUrl.trim()) &&
|
|
306
|
-
!(isProjectScopedMcpScope(scope) && !projectPath) &&
|
|
307
|
-
!mutationDisableReason &&
|
|
308
162
|
!installing;
|
|
309
163
|
|
|
310
164
|
return (
|
|
311
165
|
<Dialog open={open} onOpenChange={(o) => !o && onClose()}>
|
|
312
|
-
<DialogContent className="max-h-[85vh] max-w-lg overflow-y-auto">
|
|
166
|
+
<DialogContent className="max-h-[85vh] w-full max-w-lg overflow-y-auto">
|
|
313
167
|
<DialogHeader>
|
|
314
168
|
<div className="flex items-center gap-2">
|
|
315
169
|
<div className="flex size-8 items-center justify-center rounded-lg border border-border bg-surface-raised">
|
|
@@ -440,7 +294,7 @@ export const CustomMcpServerDialog = ({
|
|
|
440
294
|
</Button>
|
|
441
295
|
</div>
|
|
442
296
|
{headers.length > 0 && (
|
|
443
|
-
<div className="space-y-2">
|
|
297
|
+
<div className="max-h-32 space-y-2 overflow-y-auto">
|
|
444
298
|
{headers.map((header, i) => (
|
|
445
299
|
<div key={i} className="flex items-center gap-2">
|
|
446
300
|
<Input
|
|
@@ -471,73 +325,10 @@ export const CustomMcpServerDialog = ({
|
|
|
471
325
|
</div>
|
|
472
326
|
)}
|
|
473
327
|
|
|
474
|
-
{/*
|
|
475
|
-
<
|
|
476
|
-
<Label className="text-xs">作用域</Label>
|
|
477
|
-
<Select value={scope} onValueChange={(v) => setScope(v as Scope)}>
|
|
478
|
-
<SelectTrigger className="h-8 text-sm">
|
|
479
|
-
<SelectValue />
|
|
480
|
-
</SelectTrigger>
|
|
481
|
-
<SelectContent>
|
|
482
|
-
{scopeOptions.map((opt) => (
|
|
483
|
-
<SelectItem
|
|
484
|
-
key={opt.value}
|
|
485
|
-
value={opt.value}
|
|
486
|
-
disabled={isProjectScopedMcpScope(opt.value) && !projectPath}
|
|
487
|
-
>
|
|
488
|
-
{opt.label}
|
|
489
|
-
</SelectItem>
|
|
490
|
-
))}
|
|
491
|
-
</SelectContent>
|
|
492
|
-
</Select>
|
|
493
|
-
</div>
|
|
494
|
-
|
|
495
|
-
{/* Environment variables */}
|
|
496
|
-
<div className="space-y-1.5">
|
|
497
|
-
<div className="flex items-center justify-between">
|
|
498
|
-
<Label className="text-xs">环境变量</Label>
|
|
499
|
-
<Button variant="ghost" size="sm" onClick={addEnvVar} className="h-6 px-1.5 text-xs">
|
|
500
|
-
<Plus className="mr-1 size-3" />
|
|
501
|
-
添加
|
|
502
|
-
</Button>
|
|
503
|
-
</div>
|
|
504
|
-
{envVars.length > 0 && (
|
|
505
|
-
<div className="space-y-2">
|
|
506
|
-
{envVars.map((entry, i) => (
|
|
507
|
-
<div key={i} className="flex items-center gap-2">
|
|
508
|
-
<Input
|
|
509
|
-
value={entry.key}
|
|
510
|
-
onChange={(e) => updateEnvVar(i, 'key', e.target.value)}
|
|
511
|
-
className="h-7 w-40 font-mono text-xs"
|
|
512
|
-
placeholder="ENV_VAR_NAME"
|
|
513
|
-
/>
|
|
514
|
-
<Input
|
|
515
|
-
type="password"
|
|
516
|
-
value={entry.value}
|
|
517
|
-
onChange={(e) => updateEnvVar(i, 'value', e.target.value)}
|
|
518
|
-
className="h-7 flex-1 text-xs"
|
|
519
|
-
placeholder="value"
|
|
520
|
-
/>
|
|
521
|
-
<Button
|
|
522
|
-
variant="ghost"
|
|
523
|
-
size="icon"
|
|
524
|
-
className="size-7 text-red-400 hover:bg-red-500/10"
|
|
525
|
-
onClick={() => removeEnvVar(i)}
|
|
526
|
-
>
|
|
527
|
-
<Trash2 className="size-3" />
|
|
528
|
-
</Button>
|
|
529
|
-
</div>
|
|
530
|
-
))}
|
|
531
|
-
</div>
|
|
532
|
-
)}
|
|
533
|
-
</div>
|
|
328
|
+
{/* Harness selector */}
|
|
329
|
+
<HarnessSelector capability="mcp" value={harnessType} onChange={setHarnessType} />
|
|
534
330
|
|
|
535
331
|
{/* Error */}
|
|
536
|
-
{mutationDisableReason && (
|
|
537
|
-
<div className="rounded-md border border-amber-500/30 bg-amber-500/5 px-3 py-2 text-xs text-amber-300">
|
|
538
|
-
{mutationDisableReason}
|
|
539
|
-
</div>
|
|
540
|
-
)}
|
|
541
332
|
{error && (
|
|
542
333
|
<div className="rounded-md border border-red-500/30 bg-red-500/5 px-3 py-2 text-xs text-red-400">
|
|
543
334
|
{error}
|