ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Zoom WebSockets 事件订阅接入实战:knowledge-work-plugins 中 Zoom 插件技能的技术指南

Zoom WebSockets 事件订阅接入实战:knowledge-work-plugins 中 Zoom 插件技能的技术指南 Zoom WebSockets 事件订阅接入实战knowledge-work-plugins 中 Zoom 插件技能的技术指南【免费下载链接】knowledge-work-pluginsOpen source repository of plugins primarily intended for knowledge workers to use in Claude Cowork项目地址: https://gitcode.com/GitHub_Trending/kn/knowledge-work-plugins本指南以 knowledge-work-plugins 仓库中 websockets 技能 为核心系统讲解如何基于 Zoom WebSockets 建立持久化、低延迟的 Zoom 事件流从 Server-to-Server OAuth 认证、Marketplace 事件订阅配置到 WebSocket 连接建立、心跳保活、令牌刷新与断线重连并给出完整的事件格式、常见事件类型与 5 分钟预检 Runbook。读完本文你将能独立搭建一个稳定的 Zoom 实时事件消费端并正确区分 WebSockets、Webhooks 与 RTMS 三种事件/媒体交付方式的适用边界。为什么需要 Zoom WebSockets与 Webhooks 的选型对比Zoom 平台向开发者交付事件的方式主要有两类Webhooks一次性 HTTP POST与 WebSockets持久化、双向连接。原技能文档SKILL.md强调在路由到低延迟事件工作流时当持久连接、更快的事件投递或安全约束使 WebSockets 优于 Webhooks 时应优先使用 WebSockets。两者的核心差异如下维度WebSocketsWebhooks连接持久化、双向一次性 HTTP POST延迟更低无 HTTP 开销更高每个事件新建连接安全性主动直连 Zoom不暴露公网端点需要端点校验、IP 白名单模型拉取由你主动连接 Zoom推送由 Zoom 连接你的服务状态有状态维护连接无状态每个事件相互独立配置较复杂需要 access token 与连接管理较简单只需端点 URL选择 WebSockets 的场景实时、低延迟的更新至关重要例如会议开始/结束状态机、参会人进出统计安全要求极高银行、医疗、金融等行业不允许开放公网回调端点不希望暴露任何公网 HTTP 端点需要双向通信能力。选择 Webhooks 的场景更偏好简单配置事件通知数量少已有现成的 HTTP 基础设施。在该仓库的插件体系内start 技能 的路由表将 websockets 列为“路由确定之后再引入”的支撑性参考文档之一属于事件交付链路与 webhooks、rest-api 并列的专门技能而在事件订阅场景中webhooks 技能 与本文所讲的 websockets 技能互为对照可结合使用。前置条件与环境变量接入 Zoom WebSockets 前需要准备在 Zoom Marketplace 中创建Server-to-ServerS2SOAuth 应用获取Account ID、Client ID、Client Secret三项凭证创建WebSocket 订阅subscription并启用所需事件。仓库为这些凭证定义了标准化的.env键详见 references/environment-variables.md变量是否必需用途获取位置ZOOM_CLIENT_ID是OAuth 应用身份标识Zoom Marketplace → OAuth 应用 → App CredentialsZOOM_CLIENT_SECRET是OAuth 应用密钥Zoom Marketplace → OAuth 应用 → App CredentialsZOOM_ACCOUNT_IDS2S OAuth 模式下需要账户级令牌授权服务型应用Zoom Marketplace → Server-to-Server OAuth 应用凭证ZOOM_SUBSCRIPTION_ID建立订阅后持久化的订阅标识用于重连/恢复订阅创建 API 的响应返回值ZOOM_ACCESS_TOKEN运行时临时值连接 WebSocket 所需的访问令牌运行时由 OAuth 换取不持久化存储需要注意ZOOM_SUBSCRIPTION_ID并非来自 Marketplace 界面而是你的应用调用订阅创建 API 后自行持久化保存的返回值ZOOM_ACCESS_TOKEN属于运行时值只在内存中短暂存在。三步快速接入第 1 步创建 Server-to-Server OAuth 应用进入 Zoom Marketplace 的应用创建页面选择创建Server-to-Server OAuth类型应用复制生成的 Account ID、Client ID 与 Client Secret。S2S OAuth 属于“账户授权”Account Authorization模式采用grant_typeaccount_credentials即行业所称的 Client Credentials Grant / 两脚 OAuth / 机器对机器认证专为无用户交互的后端自动化而设计。完整的认证流程与错误码4700–4741 区间可参考仓库中的 oauth 技能。第 2 步启用 WebSocket 事件订阅在应用的Feature → Event Subscriptions中新建事件订阅选择WebSockets作为投递方式method type勾选需要订阅的事件例如meeting.created、meeting.started保存后系统会生成一个与该订阅绑定的连接参数订阅 ID。第 3 步通过 WebSocket 建立连接下面的 Node.js 示例完整展示了“先换取 S2S 访问令牌再携带subscriptionId与access_token连接 Zoom WebSocket 端点”的流程摘自 SKILL.mdconst WebSocket require(ws); const axios require(axios); // Step 1: Get access token async function getAccessToken() { const credentials Buffer.from(${CLIENT_ID}:${CLIENT_SECRET}).toString(base64); const response await axios.post( https://zoom.us/oauth/token, new URLSearchParams({ grant_type: account_credentials, account_id: ACCOUNT_ID }), { headers: { Authorization: Basic ${credentials}, Content-Type: application/x-www-form-urlencoded } } ); return response.data.access_token; } // Step 2: Connect to WebSocket async function connectWebSocket() { const accessToken await getAccessToken(); // WebSocket URL from your subscription settings const wsUrl wss://ws.zoom.us/ws?subscriptionId${SUBSCRIPTION_ID}access_token${accessToken}; const ws new WebSocket(wsUrl); ws.on(open, () { console.log(WebSocket connection established); }); ws.on(message, (data) { const event JSON.parse(data); console.log(Event received:, event.event); // Handle different event types switch (event.event) { case meeting.started: console.log(Meeting started: ${event.payload.object.topic}); break; case meeting.ended: console.log(Meeting ended: ${event.payload.object.uuid}); break; case meeting.participant_joined: console.log(Participant joined: ${event.payload.object.participant.user_name}); break; } }); ws.on(close, (code, reason) { console.log(Connection closed: ${code} - ${reason}); // Implement reconnection logic }); ws.on(error, (error) { console.error(WebSocket error:, error); }); return ws; } connectWebSocket();连接 URL 与参数WebSocket 连接端点为见 references/connection.mdwss://ws.zoom.us/ws?subscriptionId{SUBSCRIPTION_ID}access_token{ACCESS_TOKEN}参数说明subscriptionId来自 Marketplace 的 WebSocket 订阅 IDaccess_token有效的 S2S OAuth 访问令牌一个常见的误区是去寻找一个“对所有人都通用的wss://...端点”——实际上连接完全由你的订阅 ID 与访问令牌参数化不存在固定的通用 URL这正是 troubleshooting/common-issues.md 中列出的头号问题。事件格式与常见事件通过 WebSocket 收到的事件与 Webhook 事件具有完全相同的结构。单个事件负载如下摘自 SKILL.md{ event: meeting.started, event_ts: 1706123456789, payload: { account_id: abcD3ojkdbjfg, object: { id: 1234567890, uuid: abcdefgh-1234-5678-abcd-1234567890ab, host_id: xyz789, topic: Team Standup, type: 2, start_time: 2024-01-25T10:00:00Z, timezone: America/Los_Angeles } } }references/events.md 对事件结构给出了更细的字段说明字段类型说明eventstring事件类型标识符event_tsnumberUnix 时间戳毫秒payload.account_idstringZoom 账户 IDpayload.objectobject事件专属数据常用事件一览事件描述meeting.created会议被创建meeting.updated会议设置被修改meeting.deleted会议被删除meeting.started会议开始meeting.ended会议结束meeting.participant_joined参会人加入会议meeting.participant_left参会人离开会议recording.completed云录制处理完成可下载user.created新增用户user.updated用户信息变更事件负载细节按类别展开会议类事件meeting.updated会在payload中同时携带object更新后与old_object更新前便于做变更对比meeting.ended的object中会出现end_time与累计durationmeeting.participant_joined的object.participant内含id、user_id、user_name、email、join_timemeeting.participant_left则包含leave_time与leave_reason。此外还有屏幕共享类事件meeting.sharing_started/meeting.sharing_ended其object.sharing_details.content标识共享内容类型如screen。录制类事件recording.started/recording.stopped记录录制起止时刻recording.completed在云录制处理完成后触发其object内含total_size、recording_count以及recording_files数组每个文件条目带有file_type如MP4、TRANSCRIPT、file_size、download_url、status等字段另有recording.trashed移入回收站、recording.deleted永久删除、recording.recovered从回收站恢复。用户类事件user.created/user.updated/user.deleted/user.deactivated/user.activated覆盖账户成员生命周期user.updated同样附带old_object便于对比。网络研讨会类事件webinar.created/webinar.updated/webinar.deleted/webinar.started/webinar.ended与会议类事件对称webinar.registration_created在有人报名网络研讨会时触发object.registrant内含报名人email、姓名与join_url。事件处理与过滤references/events.md 给出了基于事件名分发的处理模式用一张事件名到处理函数的映射表在message回调中按event.event分发未匹配的事件统一记录为Unhandled event避免静默丢弃const eventHandlers { meeting.created: (payload) { console.log(New meeting: ${payload.object.topic}); notifyCalendarService(payload.object); }, meeting.started: (payload) { console.log(Meeting started: ${payload.object.topic}); updateMeetingStatus(payload.object.id, in_progress); }, meeting.ended: (payload) { console.log(Meeting ended: ${payload.object.uuid}); updateMeetingStatus(payload.object.id, completed); calculateAttendance(payload.object); }, meeting.participant_joined: (payload) { const { participant } payload.object; console.log(${participant.user_name} joined); trackAttendance(payload.object.id, participant); }, recording.completed: (payload) { console.log(Recording ready: ${payload.object.topic}); downloadRecordings(payload.object.recording_files); }, user.created: (payload) { console.log(New user: ${payload.object.email}); sendWelcomeEmail(payload.object); } }; ws.on(message, (data) { const event JSON.parse(data); const handler eventHandlers[event.event]; if (handler) { handler(event.payload); } else { console.log(Unhandled event: ${event.event}); } });当事件量过大时可依次考虑三种降噪策略订阅时精挑事件只勾选需要的、处理函数内过滤丢弃不符合条件的事件、使用多个订阅把不同事件路由到不同处理器。例如只处理特定主持人相关的事件可在消息回调中先做过滤再进入处理流程。连接管理保活、重连与单连接限制WebSocket 是有状态连接必须由客户端主动维护其生命周期这是与 Webhook“每个事件独立”最大的运维差异。心跳保活Keep-AliveZoom 会关闭空闲连接因此需要周期性发送心跳。原文档建议每 30 秒发送一次 ping// Send ping every 30 seconds setInterval(() { if (ws.readyState WebSocket.OPEN) { ws.ping(); } }, 30000);references/connection.md 中的WebSocketManager进一步将心跳封装为可启停的组件并在open时启动、在close时停止同时监听pong确认连接存活class WebSocketManager { constructor() { this.ws null; this.pingInterval null; } startHeartbeat() { // Ping every 30 seconds this.pingInterval setInterval(() { if (this.ws this.ws.readyState WebSocket.OPEN) { this.ws.ping(); console.log(Ping sent); } }, 30000); } stopHeartbeat() { if (this.pingInterval) { clearInterval(this.pingInterval); this.pingInterval null; } } connect(url) { this.ws new WebSocket(url); this.ws.on(open, () { console.log(Connected); this.startHeartbeat(); }); this.ws.on(pong, () { console.log(Pong received - connection alive); }); this.ws.on(close, () { this.stopHeartbeat(); }); } }断线重连可靠性策略要求实现自动重连。原文档给出的基础版是固定 5 秒延迟重连references/connection.md 则推荐带抖动的指数退避基础延迟 1 秒、每次翻倍、上限 30 秒最大重试 10 次连接成功后将重试计数清零class ReconnectingWebSocket { constructor(config) { this.config config; this.ws null; this.reconnectAttempts 0; this.maxReconnectAttempts 10; this.baseDelay 1000; // 1 second this.maxDelay 30000; // 30 seconds } async connect() { try { const token await getAccessToken( this.config.accountId, this.config.clientId, this.config.clientSecret ); const url wss://ws.zoom.us/ws?subscriptionId${this.config.subscriptionId}access_token${token.accessToken}; this.ws new WebSocket(url); this.ws.on(open, () { console.log(Connected successfully); this.reconnectAttempts 0; // Reset on successful connection }); this.ws.on(close, (code, reason) { console.log(Disconnected: ${code} - ${reason}); this.scheduleReconnect(); }); this.ws.on(error, (error) { console.error(WebSocket error:, error.message); }); this.ws.on(message, (data) { this.handleMessage(JSON.parse(data)); }); } catch (error) { console.error(Connection failed:, error.message); this.scheduleReconnect(); } } scheduleReconnect() { if (this.reconnectAttempts this.maxReconnectAttempts) { console.error(Max reconnection attempts reached); return; } // Exponential backoff with jitter const delay Math.min( this.baseDelay * Math.pow(2, this.reconnectAttempts) Math.random() * 1000, this.maxDelay ); console.log(Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts 1})); setTimeout(() { this.reconnectAttempts; this.connect(); }, delay); } handleMessage(event) { // Override this method to handle events console.log(Event:, event.event, event.payload); } close() { if (this.ws) { this.ws.close(); this.ws null; } } }单连接限制关键约束每个订阅同一时刻只能存在一条 WebSocket 连接。打开新连接会立即关闭已存在的旧连接。这意味着多实例部署时必须防止重复消费者——若多个 worker 各自建连后建的连接会踢掉先建的形成连接抖动循环在 RUNBOOK.md 的可靠性策略中明确要求“每个环境中的每个订阅流只允许一个活跃消费者”。连接限制汇总限制项取值每个订阅的连接数1新建连接会关闭现有连接连接超时因环境而异必须实现心跳保活消息大小以 Zoom 官方文档当前限制为准认证与令牌生命周期连接稳定性的核心获取访问令牌WebSocket 连接要求携带有效的 S2S OAuth 访问令牌。令牌获取方式与快速接入中的示例一致将clientId:clientSecret做 Base64 编码放入Authorization: Basic头向https://zoom.us/oauth/token提交grant_typeaccount_credentials与account_id。references/connection.md 的版本将返回值封装为{ accessToken, expiresIn }其中expiresIn通常为 3600 秒1 小时。令牌刷新策略S2S OAuth 访问令牌 1 小时后过期且没有独立的 refresh token 机制——到期后直接重新请求一个新令牌即可。与用户授权流存在约 90 天有效期的 refresh token不同S2S 的令牌管理要简单得多但必须在客户端显式处理否则会出现周期性断连。references/connection.md 给出一个带令牌预刷新的ZoomWebSocketClient在距过期还有 5 分钟时提前刷新令牌并通过“关闭旧连接 → 用新令牌重连”完成无缝切换class ZoomWebSocketClient { constructor(accountId, clientId, clientSecret, subscriptionId) { this.accountId accountId; this.clientId clientId; this.clientSecret clientSecret; this.subscriptionId subscriptionId; this.ws null; this.tokenExpiry null; } async refreshTokenIfNeeded() { const now Date.now(); const bufferTime 5 * 60 * 1000; // 5 minutes before expiry if (!this.tokenExpiry || now this.tokenExpiry - bufferTime) { const { accessToken, expiresIn } await getAccessToken( this.accountId, this.clientId, this.clientSecret ); this.accessToken accessToken; this.tokenExpiry now (expiresIn * 1000); // Reconnect with new token if (this.ws) { this.ws.close(); await this.connect(); } } } async connect() { await this.refreshTokenIfNeeded(); const wsUrl wss://ws.zoom.us/ws?subscriptionId${this.subscriptionId}access_token${this.accessToken}; this.ws new WebSocket(wsUrl); // Set up event handlers... } }错误处理关闭码与应对动作references/connection.md 提供了 WebSocket 关闭码的语义与推荐动作关闭码含义应对动作1000正常关闭干净退出clean shutdown1001正在离开服务端关停重连1006异常关闭网络问题重连1008策略违规检查令牌有效性1011内部错误服务端错误稍后重试对应的处理逻辑示例ws.on(close, (code, reason) { switch (code) { case 1000: console.log(Connection closed normally); break; case 1001: case 1006: console.log(Connection lost, reconnecting...); scheduleReconnect(); break; case 1008: console.log(Auth error - refreshing token); refreshTokenAndReconnect(); break; default: console.log(Unexpected close: ${code} - ${reason}); scheduleReconnect(); } }); ws.on(error, (error) { console.error(WebSocket error:, error); // The close event will follow, handle reconnection there });注意error事件之后通常会跟随close事件因此重连逻辑统一放在close分支处理即可避免重复重连。完整客户端一个生产可用的整合示例references/connection.md 最后给出一个将“令牌获取、心跳、令牌预刷新、指数退避重连、事件分发”全部整合的完整类可作为接入骨架直接使用const WebSocket require(ws); const axios require(axios); class ZoomWebSocketClient { constructor(config) { this.config config; this.ws null; this.accessToken null; this.tokenExpiry null; this.pingInterval null; this.reconnectAttempts 0; this.handlers new Map(); } on(event, handler) { this.handlers.set(event, handler); } async getAccessToken() { const credentials Buffer.from( ${this.config.clientId}:${this.config.clientSecret} ).toString(base64); const response await axios.post( https://zoom.us/oauth/token, new URLSearchParams({ grant_type: account_credentials, account_id: this.config.accountId }), { headers: { Authorization: Basic ${credentials}, Content-Type: application/x-www-form-urlencoded } } ); this.accessToken response.data.access_token; this.tokenExpiry Date.now() (response.data.expires_in * 1000); return this.accessToken; } async connect() { await this.getAccessToken(); const url wss://ws.zoom.us/ws?subscriptionId${this.config.subscriptionId}access_token${this.accessToken}; this.ws new WebSocket(url); this.ws.on(open, () { console.log(WebSocket connected); this.reconnectAttempts 0; this.startPing(); this.scheduleTokenRefresh(); }); this.ws.on(message, (data) { const event JSON.parse(data); const handler this.handlers.get(event.event); if (handler) { handler(event.payload); } }); this.ws.on(close, (code, reason) { console.log(Disconnected: ${code}); this.stopPing(); if (code ! 1000) { this.reconnect(); } }); this.ws.on(error, (error) { console.error(Error:, error.message); }); } startPing() { this.pingInterval setInterval(() { if (this.ws?.readyState WebSocket.OPEN) { this.ws.ping(); } }, 30000); } stopPing() { if (this.pingInterval) { clearInterval(this.pingInterval); } } scheduleTokenRefresh() { const refreshIn this.tokenExpiry - Date.now() - 300000; // 5 min before expiry setTimeout(() this.refreshToken(), refreshIn); } async refreshToken() { await this.getAccessToken(); // Close and reconnect with new token this.ws?.close(1000); await this.connect(); } reconnect() { const delay Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000); this.reconnectAttempts; console.log(Reconnecting in ${delay}ms...); setTimeout(() this.connect(), delay); } disconnect() { this.stopPing(); this.ws?.close(1000); } } // Usage const client new ZoomWebSocketClient({ accountId: process.env.ZOOM_ACCOUNT_ID, clientId: process.env.ZOOM_CLIENT_ID, clientSecret: process.env.ZOOM_CLIENT_SECRET, subscriptionId: process.env.ZOOM_SUBSCRIPTION_ID }); client.on(meeting.started, (payload) { console.log(Meeting started: ${payload.object.topic}); }); client.on(meeting.ended, (payload) { console.log(Meeting ended: ${payload.object.uuid}); }); client.on(meeting.participant_joined, (payload) { console.log(Participant joined: ${payload.object.participant.user_name}); }); client.connect();5 分钟预检 Runbook快速定位问题仓库为 WebSockets 技能提供了专门的 RUNBOOK.md在深入排查前按以下顺序预检可快速覆盖绝大多数常见故障确认 OAuth 令牌生成使用 S2S 凭证与 Account ID 向https://zoom.us/oauth/token换取令牌验证响应是 JSON 且包含access_token记录过期时间并主动刷新若认证间歇性失败检查时钟偏移与缓存了过期令牌的可能。确认订阅配置事件订阅已创建且投递方式为 WebSockets所需事件类型已勾选并保存。确认连接 URL 与认证使用订阅配置中的精确连接参数并按协议要求附加访问令牌。确认运行时可靠性实现带退避的重连处理心跳/ping-pong 与连接生命周期事件多 worker 运行时防止重复消费者。确认事件处理语义谨慎处理事件顺序假设事件处理器必须幂等记录事件 ID 与投递时间戳。快速探针令牌请求成功且返回 JSONWebSocket 能连上并至少收到一个已订阅事件强制断连后重连路径可用。Runbook 还提供了可直接复制执行的验证命令# 1) Validate S2S token request curl -X POST https://zoom.us/oauth/token \ -H Authorization: Basic $(printf %s:%s $ZOOM_CLIENT_ID $ZOOM_CLIENT_SECRET | base64) \ -H Content-Type: application/x-www-form-urlencoded \ -d grant_typeaccount_credentialsaccount_id$ZOOM_ACCOUNT_ID # 2) Basic Zoom API probe with token curl -X GET https://api.zoom.us/v2/users/me \ -H Authorization: Bearer $ZOOM_ACCESS_TOKEN # 3) Tail app logs while forcing reconnect tests pm2 logs your-websocket-service --lines 120预期的健康状态是令牌/API 探针返回 JSONWebSocket 服务日志呈现出 connect → receive → reconnect 的完整序列。快速决策树连接被拒/被关闭→ 令牌无效、URL 错误或订阅配置有问题已连接但收不到事件→ 事件选择错误或根本没有触发对应活动事件风暴/重复事件→ 缺少去重/幂等逻辑。常见问题诊断troubleshooting/common-issues.md 汇总了三类高频问题的根因与修复“WebSocket URL 在哪里”—— 不存在对所有人都通用的wss://...端点连接由你的订阅subscriptionId与访问令牌参数化。详见 references/connection.md。断连/重连循环—— 常见原因访问令牌过期通常约 1 小时每个订阅的单连接限制新连接会关闭旧连接客户端没有心跳保活。修复主动刷新令牌并用新令牌重连实现带抖动的指数退避确保每个订阅只有一条活跃连接。收不到事件—— 常见原因订阅的事件主题与你测试的行为不匹配应用/订阅未启用或账户设置要求的部署方式不满足。修复在 Marketplace 核对主题并触发一个你确实订阅过的事件记录原始入站消息并校验解析逻辑。WebSockets 与 RTMS不要把事件流和媒体流混淆WebSockets 事件订阅只承载事件通知不承载音视频媒体数据。仓库中另一个相关技能是 rtms 技能用于实时媒体流Realtime Media Streams。两者对比如下特性WebSocketsRTMS用途事件通知媒体流数据会议事件、用户事件音频、视频、转录文本使用场景对 Zoom 事件作出反应AI/ML、实时转录对应技能本文所讲的 websockets 技能rtms 技能需要实时音频/视频/转录文本数据时应改用 rtms 技能而非 WebSockets 事件订阅反过来只需要感知“会议开始了/结束了、有人加入了”这类状态变化时WebSockets 事件订阅是更轻量、更合适的选择。进阶阅读路径本文所依据的 websockets 技能 还配套了以下仓库内文档可深入阅读references/connection.md——连接生命周期、认证、错误处理与完整客户端实现references/events.md——完整事件类型参考与处理示例references/environment-variables.md——标准化.env键与取值位置troubleshooting/common-issues.md——订阅 URL 困惑、断连、收不到事件的诊断RUNBOOK.md——5 分钟预检清单与验证命令oauth 技能——完整的 S2S OAuth 认证流程与 4700–4741 错误码表rtms 技能——实时媒体流接入指南与事件订阅互补。小结在 knowledge-work-plugins 的 Zoom 插件体系中websockets 技能解决的是“如何以持久化、低延迟、不暴露公网端点的方式消费 Zoom 事件”这一核心问题。落地一个稳定可靠的 Zoom WebSocket 消费者需要同时把握四条主线认证S2S OAuth 换取 1 小时有效的访问令牌并预刷新、连接wss://ws.zoom.us/ws?subscriptionId...access_token...且每订阅仅一条活跃连接、存活30 秒心跳 带抖动指数退避重连 关闭码分类处理与消费事件结构解析、按事件名分发、处理器幂等与事件过滤。遵循上述流程与 Runbook 预检即可快速完成从零到可运行的 Zoom 实时事件管道搭建。【免费下载链接】knowledge-work-pluginsOpen source repository of plugins primarily intended for knowledge workers to use in Claude Cowork项目地址: https://gitcode.com/GitHub_Trending/kn/knowledge-work-plugins创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表