Skip to content

Commit 283db4e

Browse files
committed
feat: v0.13.0 — MCP 桥接、类型系统治理、动效库与测试覆盖增强
MCP 桥接(Electron): - 新增 mcpBridge.ts MCP 协议桥接 - 新增 mcpManager.ts MCP 管理器 类型系统治理: - 新增 8 个领域类型文件(capture/common/feynman/flashcard/inspiration/note/pomodoro/sync) - 统一各模块类型定义 动效与氛围系统: - 新增 lib/animation/ 动效库 - 新增 lib/metaphors/ 隐喻系统 - 新增 immersive-bg.css 沉浸式背景样式 - useElasticPageTransition / useTypewriter / useInViewAnimation Hooks - atmosphereQuote 氛围文案系统 笔记模块增强: - 新增 capture/ 捕获子组件目录 - 新增 EditorToolbar 编辑器工具栏 - 新增 7 个 Hooks(useAnalyzeBridge/useAutoSave/useCaptureAnalysis 等) - 新增 templates.ts / todoTemplate.test.ts 费曼模块增强: - 新增 AIEvaluationPanel AI 评估面板 - 新增 FeynmanStepNavigation 步骤导航 - 新增 WeakPointPanel 薄弱点面板 - 新增 useFeynmanSession Hook 仪表盘深海主题: - 新增 DeepSeaDashboardLayout 布局组件 - breathing.test.ts / dashboardHelpers 工具函数 - 深海生物组件动效优化 AI 系统测试覆盖: - 新增 LocalFallback.test.ts / aiServiceFallback.test.ts / errorMessages.test.ts - 新增 env.test.ts / OfflineQueue.test.ts - 新增 sortPendingCalc.test.ts / todoTemplate.test.ts 其他: - UI 组件更新(AIButton/CommandPalette/ContextMenu/Modal) - 番茄钟/闪卡/灵感空间页面优化 - 设置页 hooks 子目录 - README 更新至 v0.13.0
1 parent 8b00cba commit 283db4e

91 files changed

Lines changed: 7372 additions & 115 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<p align="center"><strong>你的 AI 智能学习伙伴 — 让每一分钟学习都高效有价值</strong></p>
44
<p align="center">
55
<img src="https://img.shields.io/badge/status-beta-green.svg" alt="Status" />
6-
<img src="https://img.shields.io/badge/version-v0.12.0-blue.svg" alt="Version" />
6+
<img src="https://img.shields.io/badge/version-v0.13.0-blue.svg" alt="Version" />
77
<img src="https://img.shields.io/badge/license-Apache--2.0-blue.svg" alt="License" />
88
</p>
99
</p>

‎client/electron/mcpBridge.ts‎

Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
/**
2+
* MCP Bridge — 纯 Node.js 子进程
3+
*
4+
* 由主进程的 mcpManager.ts 通过 child_process.fork() 启动,
5+
* 在独立的 Node.js 运行环境中加载 MCP SDK 并管理 server 连接。
6+
*
7+
* 为什么需要 Bridge:
8+
* Electron 主进程是 CJS 环境,而 MCP SDK 的 StdioClientTransport
9+
* 仅提供 ESM 导出。Electron 的模块系统不支持 new Function + import() 技巧。
10+
* Bridge 运行在纯 Node.js 中,原生支持 ESM dynamic import()。
11+
*
12+
* 通信协议(JSON over IPC):
13+
* 主进程 → Bridge:{ id, method, params }
14+
* Bridge → 主进程:{ id, result?, error? }
15+
*
16+
* 注意:此文件不应 import 任何 Electron 模块(app, BrowserWindow 等)。
17+
*
18+
* @ai-context Electron MCP 集成 — Bridge 子进程
19+
*/
20+
21+
// ================================================================
22+
// IPC 消息类型
23+
// ================================================================
24+
25+
interface BridgeRequest {
26+
id: string;
27+
method: 'init' | 'listTools' | 'callTool' | 'shutdown';
28+
params?: Record<string, unknown>;
29+
}
30+
31+
interface BridgeResponse {
32+
id: string;
33+
result?: unknown;
34+
error?: string;
35+
}
36+
37+
// ================================================================
38+
// MCP SDK 类型(运行时)
39+
// ================================================================
40+
41+
interface McpClient {
42+
connect(transport: unknown): Promise<void>;
43+
listTools(): Promise<{ tools: McpToolInfo[] }>;
44+
callTool(params: { name: string; arguments?: Record<string, unknown> }): Promise<unknown>;
45+
close(): Promise<void>;
46+
}
47+
48+
interface McpTransport {
49+
close(): Promise<void>;
50+
}
51+
52+
interface McpToolInfo {
53+
name: string;
54+
description?: string;
55+
inputSchema?: Record<string, unknown>;
56+
}
57+
58+
interface StdioServerParameters {
59+
command: string;
60+
args?: string[];
61+
env?: Record<string, string>;
62+
stderr?: 'pipe' | 'overlapped' | 'inherit' | 'ignore';
63+
cwd?: string;
64+
}
65+
66+
// ================================================================
67+
// SDK 构造器(init 时加载)
68+
// ================================================================
69+
70+
let ClientCtor: (new (opts: { name: string; version: string }) => McpClient) | null = null;
71+
let StdioTransportCtor: (new (params: StdioServerParameters) => McpTransport) | null = null;
72+
73+
// ================================================================
74+
// Server 管理
75+
// ================================================================
76+
77+
interface ServerEntry {
78+
name: string;
79+
client: McpClient | null;
80+
transport: McpTransport | null;
81+
connected: boolean;
82+
}
83+
84+
const servers = new Map<string, ServerEntry>();
85+
86+
// ================================================================
87+
// 核心方法
88+
// ================================================================
89+
90+
async function handleInit(params: Record<string, unknown>): Promise<Record<string, boolean>> {
91+
// 1a. CJS 加载 Client
92+
const clientMod = require('@modelcontextprotocol/sdk/client') as {
93+
Client: new (opts: { name: string; version: string }) => McpClient;
94+
};
95+
ClientCtor = clientMod.Client;
96+
log('Client loaded via CJS require');
97+
98+
// 1b. 原生 import() 加载 StdioClientTransport
99+
// 在纯 Node.js (v22+) 中,new Function 保留原生 ESM dynamic import
100+
const nativeImport = new Function('specifier', 'return import(specifier)') as (s: string) => Promise<unknown>;
101+
const stdioMod = await nativeImport('@modelcontextprotocol/sdk/client/stdio.js') as {
102+
StdioClientTransport: new (params: StdioServerParameters) => McpTransport;
103+
};
104+
StdioTransportCtor = stdioMod.StdioClientTransport;
105+
log('StdioClientTransport loaded via native dynamic import');
106+
107+
// 2. 启动各 server
108+
const serverConfigs = params.servers as Array<{
109+
name: string;
110+
command: string;
111+
args: string[];
112+
}>;
113+
114+
for (const config of serverConfigs) {
115+
await startServer(config);
116+
}
117+
118+
// 3. 返回状态
119+
const status: Record<string, boolean> = {};
120+
for (const [name, entry] of servers) {
121+
status[name] = entry.connected;
122+
}
123+
124+
const connected = Object.values(status).filter(Boolean).length;
125+
log(`Init complete: ${connected}/${serverConfigs.length} servers connected`);
126+
return status;
127+
}
128+
129+
async function startServer(config: { name: string; command: string; args: string[] }): Promise<void> {
130+
if (!ClientCtor || !StdioTransportCtor) return;
131+
132+
const entry: ServerEntry = {
133+
name: config.name,
134+
client: null,
135+
transport: null,
136+
connected: false,
137+
};
138+
servers.set(config.name, entry);
139+
140+
try {
141+
log(`Starting server "${config.name}": ${config.command} ${config.args.join(' ')}`);
142+
143+
const transport = new StdioTransportCtor({
144+
command: config.command,
145+
args: config.args,
146+
env: process.env as Record<string, string>,
147+
stderr: 'pipe',
148+
});
149+
150+
entry.transport = transport;
151+
152+
const client = new ClientCtor({
153+
name: `keban-${config.name}`,
154+
version: '1.0.0',
155+
});
156+
157+
await client.connect(transport);
158+
entry.client = client;
159+
entry.connected = true;
160+
161+
log(`Server "${config.name}" connected successfully`);
162+
} catch (err) {
163+
const errMsg = err instanceof Error ? err.message : String(err);
164+
log(`Failed to start server "${config.name}": ${errMsg}`);
165+
if (entry.transport) {
166+
entry.transport.close().catch(() => {});
167+
}
168+
entry.transport = null;
169+
entry.client = null;
170+
entry.connected = false;
171+
}
172+
}
173+
174+
async function handleListTools(params: Record<string, unknown>): Promise<McpToolInfo[]> {
175+
const serverName = params.serverName as string;
176+
const entry = servers.get(serverName);
177+
if (!entry?.connected || !entry.client) {
178+
throw new Error(`MCP server "${serverName}" is not connected`);
179+
}
180+
const result = await entry.client.listTools();
181+
return result.tools;
182+
}
183+
184+
async function handleCallTool(params: Record<string, unknown>): Promise<unknown> {
185+
const serverName = params.serverName as string;
186+
const toolName = params.toolName as string;
187+
const args = params.args as Record<string, unknown> | undefined;
188+
189+
const entry = servers.get(serverName);
190+
if (!entry?.connected || !entry.client) {
191+
throw new Error(`MCP server "${serverName}" is not connected`);
192+
}
193+
194+
log(`Calling tool "${toolName}" on server "${serverName}"`);
195+
return await entry.client.callTool({ name: toolName, arguments: args });
196+
}
197+
198+
async function handleShutdown(): Promise<void> {
199+
log('Shutting down all MCP servers...');
200+
201+
const promises: Promise<void>[] = [];
202+
203+
for (const entry of servers.values()) {
204+
if (entry.client) {
205+
promises.push(entry.client.close().catch(() => {}));
206+
}
207+
if (entry.transport) {
208+
promises.push(entry.transport.close().catch(() => {}));
209+
}
210+
entry.connected = false;
211+
entry.client = null;
212+
entry.transport = null;
213+
}
214+
215+
await Promise.allSettled(promises);
216+
servers.clear();
217+
log('All MCP servers shut down');
218+
}
219+
220+
// ================================================================
221+
// IPC 消息处理
222+
// ================================================================
223+
224+
function log(msg: string): void {
225+
// 通过 IPC 发送日志到主进程(stderr 保留给错误)
226+
process.stdout.write(`[MCP-Bridge] ${msg}\n`);
227+
}
228+
229+
process.on('message', async (msg: BridgeRequest) => {
230+
const response: BridgeResponse = { id: msg.id };
231+
232+
try {
233+
switch (msg.method) {
234+
case 'init':
235+
response.result = await handleInit(msg.params ?? {});
236+
break;
237+
case 'listTools':
238+
response.result = await handleListTools(msg.params ?? {});
239+
break;
240+
case 'callTool':
241+
response.result = await handleCallTool(msg.params ?? {});
242+
break;
243+
case 'shutdown':
244+
await handleShutdown();
245+
response.result = { ok: true };
246+
break;
247+
default:
248+
throw new Error(`Unknown method: ${msg.method}`);
249+
}
250+
} catch (err) {
251+
response.error = err instanceof Error ? err.message : String(err);
252+
}
253+
254+
process.send!(response);
255+
});
256+
257+
// 优雅退出:父进程断开时自行退出
258+
process.on('disconnect', () => {
259+
log('Parent disconnected, exiting');
260+
handleShutdown().finally(() => process.exit(0));
261+
});
262+
263+
// 未捕获异常不要崩溃,发日志后继续
264+
process.on('uncaughtException', (err) => {
265+
log(`Uncaught exception: ${err.message}`);
266+
});
267+
268+
log(`Bridge process started, env keys: ${Object.keys(process.env).length}, PATH: ${(process.env.PATH || process.env.Path || '').slice(0, 80)}`);
269+
log('Waiting for commands...');

0 commit comments

Comments
 (0)