
前言
我在之前的文章中实现了一个Electron
版本的日程表,通过Trae/Cursor
等Chat
中使用自然语言描述,通过大模型简化参数输入的操作,再通过MCP Server
提供的功能和Electron
端进行交互新增、删除、查询日程。
详情可见:
用MCP+Electron写了个服务,让Cursor/Trae提醒我不要忘记点外卖写一个日程表的electron项目,再 - 掘金
Eletron
版本过于臃肿, 现在更改为VS Code
插件版本,在通讯 这块踩了不少坑,本篇做记录交流学习。
代码仓库
插件仓库地址:
VSCode 拓展搜索 MCP日程表 或点击以下地址可查看:
MCP日程表 - Visual Studio Marketplace
Trae 安装可自行克隆仓库打包或者参考Trae 官方文档到VSCode
插件商店下载.VSIX
文件后自行安装:
MCP Server地址:
- Gitee: MCP日程表的MCPServer: 与MCP日程表( VSCode/Trae/Cursor拓展) 交互的 MCP Server
- Github: Damon-law/mcp_server_for_schedules
功能预览
配置
MCP Server配置
- 克隆
mcp server
仓库 - 安装依赖:
npm install
或pnpm install
- 打包项目:
npm run build
或pnpm build
- 配置MCP Server
js
{
"mcpServers": {
"schedules": {
// 配置了fnm的情况下,先指定你使用的node版本
"command": "fnm exec --using=20.10.0 node 你的路径\\mcp_server_for_schedules\\build\\index.js"
// 正常node
"command": "node 你的路径\\mcp_server_for_schedules\\build\\index.js"
}
}
}
VS Code插件
在VS Code 拓展商店中搜索 MCP日程表
安装再导入到Trae/Cursor中

或参考Trae官方文档,下载拓展的.VSIX
文件,再安装到Trae/Cursor中,参考:
或克隆插件仓库,自行打包为.VSIX
文件,再安装到Trae/Cursor中。
简易使用(以Trae为样例)
Trae
中的 通知好像是默认设置为勿打扰模式,需要手动打开通知:

- 插件界面展示:

- 再对下中选择智能体MCP即可使用

- 新增循环日程

- 新增一次性日程

- 到时间显示提醒

- 查看提醒详情

- 查询日程


- 删除日程

- 清空所有日程

踩坑经历 和 libp2p 通讯部分实现 (主要)
踩坑经历: 中心化的思想采取了插件启动Express服务,MCP Server使用HTTP请求通讯的手段
在这个项目中踩得最大的坑是通讯方案
。 实现MCP Server
和VS Code
插件的通讯的过程中踩了坑,比较有意思。分享一下:
最开始的设想是和Electron
版本的日程表一样启动一个Express Server
如下所示:

进展也进行的很顺利,测试也是正常。但是在打包发布,安装后发现存在非常严重的问题:
当VSCode
/Trae
/Cursor
只打开一个窗口的时候,插件是正常运行的,但是当存在多个窗口的时候就会有以下报错:

这是为什么呢? 这是因为 VS Code 的插件激活是每个窗口独立进程 ,onStartupFinished
会在每个窗口都激活一次extension
。
这就意味着这个方案是行不通的。 假设在插件中启动的Express
服务是在3001
端口。 打开多个窗口 的时候,每个窗口 都会激活插件重复启动这个Express
服务, 这意味着只有一个窗口的Express
服务是正常的,MCP Server
能交互的只有一个窗口。且只有一个窗口或者后面打开的窗口能正常进行提醒, 提醒的定时任务并不能实时地更新到多个窗口。
而在测试的时候,是只有一个窗口运行的,所以并没有任何问题,后面发布后,测试的时候发现炸了。
后续还尝试了在插件中使用net
模块的socket
实现,在MCP Server
中建立一个server
, 其余的进程都为Client
, 但是在关闭第一个窗体后就完全失效了。

在尝试两次均不可行,写了一堆代码发现行不通,我心态也是崩了,有种前功尽弃的感觉。
不过做人不要怕从头再来,自己前期调研不足,权当学习了。
经过仔细思考后,我想到了以下两种方案:
方案一: 通过本地启动一个Socket Server (放弃)
如果要用 Web仔 常用的 中心化 的思路, 在本地使用net
建立一个Socket Server
, 这样能保证服务的稳定。而VS Code插件
和MCP Server
中都启动一个Socket Client
, 这样窗口的开启和关闭都不会影响通讯服务的进行。
示图如下:

但是很快就否决了这个方案,本来使用这个插件就需要先配置一个MCP Server
了, 如果再要本地启动一个服务大大增加了用户的使用成本。这么麻烦的东西,谁想用呢?
方案二: libp2p 对等网络通讯 (最佳)
在这种情况下,中心化网络 的思想注定是无法实现的, 因为每个节点都可能关闭,所以不存在一个节点能作为稳定的中心,节点的数量也不可知,又要求彼此之间都能通讯。
只有通过建立局域网内的对等网络,每个服务都是一个节点,发现后自动链接,关闭后不影响其他节点的通讯广播。
因此选择使用 libp2p
建立对等网络。 并且指定mDNS
协议用于局域网的服务发现。
示图如下:

关于libp2p 和 mDNS 协议
Libp2p
: 是一个用于构建点对点(P2P)网络的模块化网络堆栈和库,支持多种传输层协议,如 TCP、UDP、QUIC 等,能够让不同环境、运行不同协议的设备实现互联, 可以发现 P2P 网络中的其他节点,维护节点在线状态,并根据节点状态调整网络连接,构建稳定的网络拓扑。
mDNS 即组播 DNS(Multicast DNS),是一种零配置网络协议,主要用于在局域网(LAN)中实现设备和服务发现,无需传统 DNS 服务器。
- 全称:Multicast Domain Name System(多播域名系统) 。
- 端口 :使用
5353
端口,当内网没有 DNS 服务器时,会出现此组播信息。 - 协议标准 :该协议发布为 RFC 6762,使用 IP 多播用户数据报协议(UDP)数据包。
- 相关技术 :可以与 DNS 服务发现(DNS - SD)结合使用,DNS 服务发现是 RFC 6763 中单独指定的配套零配置技术
在 libp2p
中,mDNS 通常用于在局域网 中查找其他节点,以建立对等网络 。 因此非常适合我们用于 多个窗体内的各个MCP Server
和插件实例
进行通讯, 在MCP Server
和插件实例
建立的节点会通过mDNS
协议自动发现,并链接至p2p
网络中。
通讯核心代码实现
以下以MCP Server
中的libp2p
使用为例:
js
// 新增日程回调
let addScheduleResolve: Function | null = null;
// 查询日程回调
let checkScheduleResolve: Function | null = null;
// 删除日程回调
let deleteScheduleResolve: Function | null = null;
// 清除所有日程回调
let clearAllSchedulesResolve: Function | null = null;
// 通讯通道
const chatProtocol = '/mcpSchedules/1.0.0'
// 新建p2p节点
async function createNode(port: number): Promise<Libp2p> {
const node = await createLibp2p({
addresses: {
listen: [`/ip4/127.0.0.1/tcp/${port}`]
},
transports: [tcp()],
streamMuxers: [yamux()], // 添加流多路复用器
connectionEncrypters: [noise()],
peerDiscovery: [
mdns({
interval: 2000, // 每2秒发送一次发现广播
serviceTag: 'mcp-shedules-local-libp2p-network' // 自定义服务标识,避免与其他mDNS服务冲突
})
],
services: {
// 添加ping服务依赖
ping: ping(),
identify: identify(), // Add
dht: kadDHT({
clientMode: true
}),
} //
});
// 监听节点启动事件
node.addEventListener('start', () => {
//console.log(`节点已启动,ID: ${node.peerId.toString()}`)
const addresses = node.getMultiaddrs().map(addr => addr.toString())
//console.log('监听地址:')
//addresses.forEach(addr => console.log(addr))
});
// 监听消息事件
node.handle(chatProtocol, async ({ stream }) => {
//streamToConsole(stream as any);
pipe(
// Read from the stream (the source)
stream.source,
// Decode length-prefixed data
(source) => lp.decode(source),
// Turn buffers into strings
(source) => map(source, (buf) => uint8ArrayToString(buf.subarray())),
// Sink function
async function (source) {
// Wait for all data to be received
// For each chunk of data
for await (const msg of source) {
// Output the data as a utf8 string
//console.log('> ' + msg.toString().replace('\n', ''))
try {
const res = JSON.parse(msg.toString().replace('\n', ''));
// 接收到新增日程响应消息
if (res.type === 'add-schedule-resolve') {
if (addScheduleResolve) {
addScheduleResolve(res.data);
addScheduleResolve = null;
}
}
// 查询日程的响应消息
else if (res.type === 'check-schedule-resolve') {
if (checkScheduleResolve) {
checkScheduleResolve(res.data);
checkScheduleResolve = null;
}
}
// 删除日程的响应消息
else if (res.type === 'delete-schedule-resolve') {
if (deleteScheduleResolve) {
deleteScheduleResolve(res.data);
deleteScheduleResolve = null;
}
}
// 清除日程的响应消息
else if(res.type === 'clear-all-schedules-resolve') {
if(clearAllSchedulesResolve) {
clearAllSchedulesResolve(res.data);
clearAllSchedulesResolve = null;
}
}
} catch (error) {
if(addScheduleResolve) {
addScheduleResolve( {
message: '序列化失败'
} )
}
}
}
}
)
});
// 监听节点发现事件
// 由于类型不兼容问题,可能需要使用更宽泛的类型或者检查导入的类型是否一致
// 这里尝试使用更宽泛的 CustomEvent 类型,暂时不指定具体泛型参数
node.addEventListener('peer:discovery', (event: CustomEvent<any>) => {
const peerInfo = event.detail
//console.log(`🔍 发现新节点: ${peerInfo.id.toString()}`)
const multiaddr = peerInfo.multiaddrs.find((addr: any) => addr.toString().includes('tcp'));
// 自动连接发现的节点
node.dialProtocol(multiaddr, chatProtocol).then((stream) => {
// console.log(`✅ 已自动连接到节点: ${peerInfo.id.toString()}`)
}).catch(err => {
//console.error(`❌ 连接节点失败: ${err.message}`)
})
})
// 节点断联事件
node.addEventListener('peer:disconnect', (evt: any) => {
//console.log(evt)
const peerId = peerIdFromPublicKey(evt?.detail?.publicKey)?.toString();
//console.log(`❌ 节点断开连接: ${peerId}`)
})
await node.start()
return node
}
createNode
是一个新建p2p
网络节点节点的函数, 在这个函数中定义了通讯协议为tcp
,使用mDNS
协议每两秒广播一次去发现局域网内的其他节点,并自定义了一个服务tag,不会导致连接到无关节点。还定义了一个通讯通道:
js
const chatProtocol = '/mcpSchedules/1.0.0'
用于和其他节点通讯,发送和接收消息。
通讯内容为JSON
字符串,内容比较简单:
js
{
type: '', // 消息或事件类型
fromPeerId: '', //来源节点id
data: {
...
}// 挟带的数据
}
下面以新增日程的内容为例展示流程:

在 MCP Server
中提供的新增日程的tool
如下:
js
server.tool('add-schedule', '添加日程或提醒,如果用户没有指定结束时间: end,则默认结束时间为开始时间: start或提醒时间: reminder加一小时', {
title: z.string().describe('日程标题'),
start: z.string().describe('开始时间,格式: YYYY-MM-DD HH:mm:ss'),
end: z.string().describe('结束时间,格式: YYYY-MM-DD HH:mm:ss。 用户没指定的时候默认值为开始时间加一小时'),
type: z.string().describe('日程类型,格式为:important: 重要, 日常:normal, 次要:minor, 用户不提及的时候默认为日常'),
reminder: z.string().describe('提醒时间,格式: YYYY-MM-DD HH:mm:ss'),
description: z.string().describe('日程描述'),
repeatType: z.string().describe('重复类型,格式为:daily: 每天, weekly: 每周, monthly: 每月, yearly: 每年 , none: 不重复'),
repeatInterval: z.number().describe('重复间隔,格式为:1, 2, 3, 4, 5, 6, 7, 8, 9, 10'),
repeatDays: z.array(z.number()).describe('重复天数,格式为:[1, 2, 3, 4, 5, 6, 7], 当repeatType为weekly时,该字段代表周的哪几天,从1开始,0代表周日。 当repeatType为monthly时,该字段代表月的哪几天,从1开始,0代表最后一天。 当repeatType为yearly时,该字段代表年的哪几天,从1开始,0代表最后一天。'),
repeatEnd: z.string().describe('重复结束时间,格式: YYYY-MM-DD HH:mm:ss')
}, async ({ title, start, end, type, reminder, description, repeatType, repeatInterval, repeatDays, repeatEnd }) => {
try {
const res = await new Promise((resolve, reject) => {
// 保存回调,收到回调消息后调用
addScheduleResolve = resolve;
// 组装body
const body = { title: title, start: start, end: end, type: type, reminder: reminder, description: description, repeatType: repeatType, repeatInterval: repeatInterval, repeatDays: repeatDays, repeatEnd: repeatEnd }
// 不存在活跃节点
if(node?.getPeers().length === 0) {
addScheduleResolve = null;
resolve({
message: '添加日程失败,没有链接节点'
})
}
// 遍历节点
node?.getPeers().forEach(async (peerId) => {
// 获取对应节点的tcp地址
const addr = (await node?.peerStore.getInfo(peerId))?.multiaddrs?.find((addr: any) => addr.toString().includes('tcp'));
if(!addr) {
return ;
}
// 拨通通讯通道获得传输流
const stream = await node?.dialProtocol(addr, chatProtocol);
if (stream) {
const json = {
type: 'add-schedule',
fromPeer: node?.peerId.toString(),
data: body
}
// 传输
pipe(
[JSON.stringify(json)],
// Turn strings into buffers
(source) => map(source, (string) => uint8ArrayFromString(string)),
// Encode with length prefix (so receiving side knows how much data is coming)
(source) => lp.encode(source),
// Write to the stream (the sink)
stream.sink
)
}
})
}) as any;
return {
content: [{
type: 'text',
text: res?.id ? '日程添加成功' : '日程添加失败'
}]
};
}
catch (error: any) {
return {
content: [{
type: 'text',
text: '日程添加失败:' + error.message
}]
};
}
});
在这个tool
中采用了一个Promise
异步的形式,先把resolve
函数作为一个全局变量
:addScheduleResolve
保存起来,然后再组装消息,向所有已连接的节点(包括MCP Server
和VSCode插件
的节点)发送type
为add-shecdule(新增日程)
的消息,等待回调。
在VSCode插件
端的节点接收到type
为add-shecdule(新增日程)
的消息记为fromRes
,处理完后,将处理结果作为type
为add-schedule-resolve(新增日程响应)
根据fromRes
中记录的来源节点id
:fromPeerId
,发送回来源的MCP Serer节点
。
对应的MCP Server节点
在接收到type
为add-schedule-resolve(新增日程响应)
的消息后,调用刚才保存的全局变量
:addScheduleResolve
;至此,完成回调;
此处,MCP Server节点
发送type
为add-shecdule(新增日程)
的消息时,是向所有的节点发送的。
- 日程的操作要同步到所有打开的
VSCode
插件端。 - 因为我们在定义的时候,只在
VSCode插件
端的节点处理type
为add-shecdule(新增日程)
的消息。 只有在MCP Server
端的节点处理type
为add-schedule-resolve(新增日程响应)
, 并不影响。
VSCode插件实现
功能 Features
- 通过MCP新增日程 (一次性日程, 循环性日程: 每日、 每周、 每年)
- 通过MCP查询日程
- 通过MCP删除日程
- 通过MCP清空日程
- 通过MCP更改日程内容 (目前可以删了重建)
- 编辑器内定时提醒日程
- ...
主要实现
侧边栏
定义了一个侧边栏查看日程表的侧边TreeView
:
js
// 日程表侧边栏树数据
class ScheduleTreeProvider implements vscode.TreeDataProvider<ScheduleTreeItem> {
private _onDidChangeTreeData: vscode.EventEmitter<ScheduleTreeItem | undefined | void> = new vscode.EventEmitter<ScheduleTreeItem | undefined | void>();
readonly onDidChangeTreeData: vscode.Event<ScheduleTreeItem | undefined | void> = this._onDidChangeTreeData.event;
// 对齐时间到周一
alignToMonday(date: Date | string) {
const now = dayjs(date);
const day = now.day(); // 0=周日, 1=周一, ..., 6=周六
const diffToMonday = day === 0 ? -6 : 1 - day; // 计算到周一的天数差
return now.add(diffToMonday, 'day');
}
getTreeItem(element: ScheduleTreeItem): vscode.TreeItem {
return element;
}
getChildren(element?: ScheduleTreeItem): Thenable<ScheduleTreeItem[]> {
if (!element) {
// 根节点
return Promise.resolve([
new ScheduleTreeItem('今日日程', vscode.TreeItemCollapsibleState.Expanded),
new ScheduleTreeItem('明日日程', vscode.TreeItemCollapsibleState.Collapsed),
new ScheduleTreeItem('本周日程', vscode.TreeItemCollapsibleState.Collapsed)
]);
} else if (element.label === '今日日程') {
return Promise.resolve(this.getTodaySchedules());
} else if (element.label === '明日日程') {
return Promise.resolve(this.getTomorrowSchedules());
} else if (element.label === '本周日程') {
return Promise.resolve(this.getWeekSchedules());
} else if (element.schedule) {
// 展开单个日程,显示详情
return Promise.resolve(this.getScheduleDetailItems(element.schedule));
} else {
return Promise.resolve([]);
}
}
// 获取今日日程详情
getTodaySchedules(): ScheduleTreeItem[] {
const schedules: Schedule[] = store.get(SCHEDULE_KEY) as Schedule[] || [];
const today = dayjs();
const todayStart = dayjs().startOf('day').format('YYYY-MM-DD HH:mm:ss');
const todayEnd = dayjs().endOf('day').format('YYYY-MM-DD HH:mm:ss');
const items = expandRecurringSchedules(schedules, todayStart, todayEnd)
.filter(s => s.start && s.start.startsWith(today.format('YYYY-MM-DD')))
.map(s => new ScheduleTreeItem(`${s.processStatus} ${s.title} - ${s.start ? dayjs(s.start).format('HH:mm') : ''} ~ ${s.end ? dayjs(s.end).format('HH:mm') : ''}`, vscode.TreeItemCollapsibleState.Collapsed, s))
.sort((a, b) => {
// 根据优先级排序, 进行中, 未开始, 已过期
const aPriority = a.schedule?.processStatusPriority || 4;
const bPriority = b.schedule?.processStatusPriority || 4;
if (aPriority !== bPriority) {
return aPriority - bPriority;
}
const aStart = dayjs(a.schedule?.start);
const bStart = dayjs(b.schedule?.start);
return aStart.diff(bStart);
});
if (items.length === 0) {
items.push(new ScheduleTreeItem('暂无日程', vscode.TreeItemCollapsibleState.None));
}
return items;
}
// 获取明日日程详情
getTomorrowSchedules(): ScheduleTreeItem[] {
const schedules: Schedule[] = store.get(SCHEDULE_KEY) as Schedule[] || [];
const tomorrow = dayjs().add(1, 'day');
const tomorrowStart = tomorrow.clone().startOf('day').toISOString();
const tomorrowEnd = tomorrow.clone().endOf('day').toISOString();
const items = expandRecurringSchedules(schedules, tomorrowStart, tomorrowEnd)
.filter(s => s.start && s.start.startsWith(tomorrow.format('YYYY-MM-DD')))
.map(s => new ScheduleTreeItem(`${s.processStatus} ${s.title} - ${s.start ? dayjs(s.start).format('HH:mm') : ''} ~ ${s.end ? dayjs(s.end).format('HH:mm') : ''}`, vscode.TreeItemCollapsibleState.Collapsed, s))
.sort((a, b) => {
const aPriority = a.schedule?.processStatusPriority || 4;
const bPriority = b.schedule?.processStatusPriority || 4;
if (aPriority !== bPriority) {
return aPriority - bPriority;
}
const aStart = dayjs(a.schedule?.start);
const bStart = dayjs(b.schedule?.start);
return aStart.diff(bStart);
});
if (items.length === 0) {
items.push(new ScheduleTreeItem('暂无日程', vscode.TreeItemCollapsibleState.None));
}
return items;
}
// 获取本周日程详情
getWeekSchedules(): ScheduleTreeItem[] {
const schedules: Schedule[] = store.get(SCHEDULE_KEY) as Schedule[] || [];
const monday = this.alignToMonday(new Date()); // 对齐到周一
const sundayEnd = monday.clone().add(6, 'day').endOf('day');
const items = expandRecurringSchedules(schedules, monday.toISOString(), sundayEnd.toISOString())
.filter(s => {
const start = dayjs(s.start);
return s.start && start.isSameOrBefore(sundayEnd) && start.isSameOrAfter(monday);
})
.map(s => new ScheduleTreeItem(`${s.processStatus} ${s.title} - ${s.start ? dayjs(s.start).format('YYYY-MM-DD HH:mm') : ''} ~ ${s.end ? dayjs(s.end).format('YYYY-MM-DD HH:mm') : ''}`, vscode.TreeItemCollapsibleState.Collapsed, s))
.sort((a, b) => {
const aPriority = a.schedule?.processStatusPriority || 4;
const bPriority = b.schedule?.processStatusPriority || 4;
if (aPriority !== bPriority) {
return aPriority - bPriority;
}
const aStart = dayjs(a.schedule?.start);
const bStart = dayjs(b.schedule?.start);
return aStart.diff(bStart);
});
if (items.length === 0) {
items.push(new ScheduleTreeItem('暂无日程', vscode.TreeItemCollapsibleState.None));
}
return items;
}
// 获取日程详情
getScheduleDetailItems(schedule: Schedule): ScheduleTreeItem[] {
const items: ScheduleTreeItem[] = [];
// 起止时间
const start = schedule.start ? dayjs(schedule.start).format('YYYY-MM-DD HH:mm:ss') : '';
const end = schedule.end ? dayjs(schedule.end).format('YYYY-MM-DD HH:mm:ss') : '';
items.push(new ScheduleTreeItem(`日程状态:${schedule.processStatus}`, vscode.TreeItemCollapsibleState.None));
items.push(new ScheduleTreeItem(`开始时间: ${start}`, vscode.TreeItemCollapsibleState.None));
items.push(new ScheduleTreeItem(`结束时间: ${end}`, vscode.TreeItemCollapsibleState.None));
// 提醒时间
const reminder = schedule.reminder ? dayjs(schedule.reminder).format('YYYY-MM-DD HH:mm:ss') : '';
if (reminder) {
let label = `提醒时间: ${reminder}`;
if (schedule.hasNotified || dayjs().isAfter(dayjs(reminder))) {
// 用 Unicode 删除线
label = `提醒时间: ${strikethrough(reminder)}`;
}
items.push(new ScheduleTreeItem(label, vscode.TreeItemCollapsibleState.None));
}
// 详情
if (schedule.description) {
items.push(new ScheduleTreeItem(`详情: ${schedule.description}`, vscode.TreeItemCollapsibleState.None));
}
// 事件类型
let typeText = schedule.type || 'normal';
let typeInfo = Object.values(EVENT_TYPES).find(t => t.value === typeText);
if (typeInfo) {
const typeItem = new ScheduleTreeItem(`事件类型: ${typeInfo.text}`, vscode.TreeItemCollapsibleState.None);
// 颜色处理(VSCode 只支持内置颜色,不能直接用 hex,可以用 ThemeColor 或 iconPath 自定义 SVG)
typeItem.iconPath = new ThemeIcon('circle-filled', new ThemeColor(TYPE_ICON_COLOR_MAP[typeText] || 'charts.blue'));
items.push(typeItem);
} else {
items.push(new ScheduleTreeItem(`事件类型: ${typeText}`, vscode.TreeItemCollapsibleState.None));
}
return items;
}
// 刷新
refresh(): void {
this._onDidChangeTreeData.fire();
}
}

可查看今日明日和本周的日程安排,展开可以查看详细信息,通过前方圆点颜色 区分事件的类型(重要程度),并在前方标明日程的进行状态,提供了一个方法refresh
当日程交互导致日程发生变化的时候,也刷新对应的TreeView
;
status bar
在左下角定义了一个Status Bar
, 用于展示今日日程数量:

实现代码如下:
js
const todaySchedulesCount = getTodaySchedulesCount();
let statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 100);
if (todaySchedulesCount > 0) {
statusBarItem.text = `$(calendar) ${todaySchedulesCount}个日程`;
statusBarItem.command = 'schedules-for-mcp.showScheduleDetail';
statusBarItem.show();
}
else {
statusBarItem.text = `$(calendar) 今日无日程`;
statusBarItem.command = 'schedules-for-mcp.showScheduleDetail';
statusBarItem.show();
}
context.subscriptions.push(statusBarItem);
定时提醒
使用了轻量型的node-schedule
作为定时任务处理库,和Electron
版本没什么区别,详细可以查看顶部文章链接,不再详叙。
其他注意事项
由于VSCode插件
的开发是使用Commonjs
, 而Libp2p
是ESModule
, 为了做一个兼容,不能直接写
js
import { xx } from 'esmodule';
我采取了await import(...)
的方法导入,代码如下:
js
// 以下是libp2p的库
/* import { mdns } from '@libp2p/mdns';
import { createLibp2p } from 'libp2p';
import { tcp } from '@libp2p/tcp';
import { yamux } from '@chainsafe/libp2p-yamux';
import { noise } from '@chainsafe/libp2p-noise';
import { kadDHT } from '@libp2p/kad-dht';
import type { Libp2p } from 'libp2p';
import { ping } from '@libp2p/ping';
import { identify } from '@libp2p/identify';
import { pipe } from 'it-pipe';
//import { streamToConsole } from './stream.js';
import * as lp from 'it-length-prefixed';
import map from 'it-map';
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string';
import { toString as uint8ArrayToString } from 'uint8arrays/to-string';
import { peerIdFromPublicKey } from '@libp2p/peer-id'; */
// 由于插件是commonjs, 而libp2p是ESModule, 所以需要用以下这种方式引用
let mdns: any = null;
let createLibp2p: any = null;
let tcp: any = null;
let yamux: any = null;
let noise: any = null;
let kadDHT: any = null;
let ping: any = null;
let identify: any = null;
let pipe: any = null;
let lp: any = null;
let map: any = null;
let uint8ArrayFromString: any = null;
let uint8ArrayToString: any = null;
let peerIdFromPublicKey: any = null;
let peerIdFromString: any = null;
type Libp2p = any;
// commonjs 引用ESModule的兼容写法
async function importAll() {
if (!mdns) {
mdns = (await import('@libp2p/mdns')).mdns;
}
if (!createLibp2p) {
createLibp2p = (await import('libp2p')).createLibp2p;
}
if (!tcp) {
tcp = (await import('@libp2p/tcp')).tcp;
}
if (!yamux) {
yamux = (await import('@chainsafe/libp2p-yamux')).yamux;
}
if (!noise) {
noise = (await import('@chainsafe/libp2p-noise')).noise;
}
if (!kadDHT) {
kadDHT = (await import('@libp2p/kad-dht')).kadDHT;
}
if (!ping) {
ping = (await import('@libp2p/ping')).ping;
}
if (!identify) {
identify = (await import('@libp2p/identify')).identify;
}
if (!pipe) {
pipe = (await import('it-pipe')).pipe;
}
if (!lp) {
lp = (await import('it-length-prefixed'));
}
if (!map) {
map = (await import('it-map')).default;
}
if (!uint8ArrayFromString) {
uint8ArrayFromString = (await import('uint8arrays/from-string')).fromString;
}
if (!uint8ArrayToString) {
uint8ArrayToString = (await import('uint8arrays/to-string')).toString;
}
if (!peerIdFromPublicKey || !peerIdFromString) {
peerIdFromPublicKey = (await import('@libp2p/peer-id')).peerIdFromPublicKey;
peerIdFromString = (await import('@libp2p/peer-id')).peerIdFromString;
}
}
感觉十分地臃肿,不知道有没有更好的兼容办法。
MCP Server实现
Tool 工具
目前提供了以下tool
:
-
add-schedule
: 添加日程或提醒,如果用户没有指定结束时间: end,则默认结束时间为开始时间: start或提醒时间: reminder加一小时 -
get-current-date
: 获取当前日期,进行日程操作时先执行这个更新日期 -
get-schedules
: 根据时间区间获取当前日程 -
delete-schedule
: 删除日程 -
clear-all-schedules
: 清空所有日程 - ...
配置
js
{
"mcpServers": {
"schedules": {
// 配置了fnm的情况下 --using指定node版本
"command": "fnm exec --using=20.10.0 node 你的路径\\mcp_server_for_schedules\\build\\index.js"
// 正常node
"command": "npx 你的路径\\mcp_server_for_schedules\\build\\index.js"
}
}
}
其余实现
MCP Server
主要是做了一个libp2p
发送和接收消息的处理。 比较有意思的点可能就是内部使用Promise
实现了一个等待消息回复的异步回调(详细实现可以看上面的libp2p
通讯实现)。 其余的都是挺简单的。
总结和拓展
这个实现最大的难点其实是在于通讯的方法上,web仔写多web了,一开始想着只是简单的把Electron
版本的日程表迁移过来,中心化的思想在插件端启动了一个Express
服务,调试阶段也正常。但是到发布阶段遇到多窗口的时候,完全崩了。可能在技术上我也比较执拗,本着无论如何都要实现的想法,通过一番调研后,最终通过使用libp2p
实现局域网内对等网络的方式实现了,对于我这个web仔来说也是学习和重温的机会。
在后续的拓展和维护中,可能会增加一些配置,例如可能会把mDNS
发现节点的tag
作为插件和MCP Server
的配置,这样可以避免不同编辑器之间的污染(或许不需要避免), 目前你在Cursor/VS Code/ Trae/ Lingma
中安装了这个插件和MCP Server
, 你会发现他们之间是可以相互通讯的,并不影响。
目前插件只是发布了初版,libp2p
性能上不了解,不知道会不会占用很多的内存,但是不管怎么样,我终于是写出来了!
欢迎各位使用反馈,友好交流和学习,本项目所有代码都已开源至Gitee
和Github
。