【VSCode插件】【p2p网络】为了硬写一个和MCP交互的日程表插件(Cursor/Trae),我学习了去中心化的libp2p

前言

我在之前的文章中实现了一个Electron版本的日程表,通过Trae/CursorChat中使用自然语言描述,通过大模型简化参数输入的操作,再通过MCP Server提供的功能和Electron端进行交互新增、删除、查询日程。

详情可见:

用MCP+Electron写了个服务,让Cursor/Trae提醒我不要忘记点外卖写一个日程表的electron项目,再 - 掘金

Eletron版本过于臃肿, 现在更改为VS Code插件版本,在通讯 这块踩了不少,本篇做记录交流学习。

代码仓库

插件仓库地址:

  1. Gitee: MCP日程表(VSCode-Cursor-Trae拓展)
  2. Github: Damon-law/schedules-extension-for-mcp

VSCode 拓展搜索 MCP日程表 或点击以下地址可查看:

MCP日程表 - Visual Studio Marketplace

Trae 安装可自行克隆仓库打包或者参考Trae 官方文档到VSCode插件商店下载.VSIX文件后自行安装:

管理插件 - 文档 - Trae CN

MCP Server地址:

  1. Gitee: MCP日程表的MCPServer: 与MCP日程表( VSCode/Trae/Cursor拓展) 交互的 MCP Server
  2. Github: Damon-law/mcp_server_for_schedules

功能预览

配置

MCP Server配置

  1. 克隆mcp server仓库
  2. 安装依赖:npm installpnpm install
  3. 打包项目:npm run buildpnpm build
  4. 配置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中,参考:

管理插件 - 文档 - Trae CN

或克隆插件仓库,自行打包为.VSIX文件,再安装到Trae/Cursor中。

简易使用(以Trae为样例)

Trae 中的 通知好像是默认设置为勿打扰模式,需要手动打开通知:

  1. 插件界面展示:
  1. 再对下中选择智能体MCP即可使用
  1. 新增循环日程
  1. 新增一次性日程
  1. 到时间显示提醒
  1. 查看提醒详情
  1. 查询日程
  1. 删除日程
  1. 清空所有日程

踩坑经历 和 libp2p 通讯部分实现 (主要)

踩坑经历: 中心化的思想采取了插件启动Express服务,MCP Server使用HTTP请求通讯的手段

在这个项目中踩得最大的坑是通讯方案。 实现MCP ServerVS 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 ServerVSCode插件的节点)发送typeadd-shecdule(新增日程)的消息,等待回调

VSCode插件端的节点接收到typeadd-shecdule(新增日程)的消息记为fromRes,处理完后,将处理结果作为typeadd-schedule-resolve(新增日程响应)根据fromRes中记录的来源节点id:fromPeerId,发送回来源的MCP Serer节点

对应的MCP Server节点在接收到typeadd-schedule-resolve(新增日程响应)的消息后,调用刚才保存的全局变量addScheduleResolve;至此,完成回调

此处,MCP Server节点发送typeadd-shecdule(新增日程)的消息时,是向所有的节点发送的。

  1. 日程的操作要同步到所有打开的VSCode插件端。
  2. 因为我们在定义的时候,只在VSCode插件端的节点处理typeadd-shecdule(新增日程)的消息。 只有在MCP Server端的节点处理typeadd-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, 而Libp2pESModule, 为了做一个兼容,不能直接写

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性能上不了解,不知道会不会占用很多的内存,但是不管怎么样,我终于是写出来了!

欢迎各位使用反馈,友好交流和学习,本项目所有代码都已开源至GiteeGithub

创作不易,点赞收藏评论!

参考资料

libp2p - libp2p Documentation Portal

VSCode插件开发全攻略(一)概览 - 我是小茗同学 - 博客园

相关推荐
每天吃饭的羊8 分钟前
面试题-函数类型的重载是啥意思
前端
迷途小码农么么哒9 分钟前
Element 分页表格跨页多选状态保持方案(十几行代码解决)
前端
木昆子11 分钟前
从能力到安全,AI编程工具怎么选
ai编程·trae
前端付豪15 分钟前
美团路径缓存淘汰策略全解析(性能 vs 精度 vs 成本的三难选择)
前端·后端·架构
abigale0328 分钟前
webpack+vite前端构建工具 -4webpack处理css & 5webpack处理资源文件
前端·css·webpack
500佰40 分钟前
如何开发Cursor
前端
InlaidHarp42 分钟前
Elpis DSL领域模型设计理念
前端
lichenyang45344 分钟前
react-route-dom@6
前端
番茄比较犟1 小时前
widget的同级移动
前端
每天吃饭的羊1 小时前
面试题-函数入参为interface类型进行约束
前端