在 Zoom 客户端内构建 In-Meeting App:基于 @zoom/appssdk 的完整开发实战指南
2026/9/13 6:53:09 网站建设 项目流程

在 Zoom 客户端内构建 In-Meeting App:基于 @zoom/appssdk 的完整开发实战指南

【免费下载链接】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

In-Meeting App 是运行在 Zoom 客户端嵌入式浏览器中的 Web 应用,可在会议进行中为与会者提供投票、游戏、协作工具等交互能力。本文以 in-meeting-apps.md 为核心骨架,结合本仓库 zoom-apps-sdk 技能包中的架构、配置与源码级实现细节,系统讲解从 App 类型选型、前后端架构、SDK 初始化到投票、白板、计时器、Layers API 沉浸式视觉等常见用例的完整开发路径。读完本文,你将掌握一套可直接落地复用的 In-Meeting App 开发方案,并理解其背后 SDK 的配置规则、授权流程与发布约束。

一、什么是 In-Meeting App

In-Meeting App 是运行在 Zoom 会议客户端界面内部的 Zoom App,与会者在会议过程中可以直接与之交互。典型形态包括投票、游戏、协作工具、议程管理等。这类应用与普通 Web 应用的区别在于:它运行在 Zoom 客户端的嵌入式浏览器中,通过@zoom/appssdk提供的 JavaScript API 访问会议上下文、参与者信息与渲染能力。

构建 In-Meeting App 需要组合以下技能:

  • zoom-apps-sdk(主要):提供运行在会议内所需的全部 SDK 能力;
  • oauth(认证):处理应用授权与令牌生命周期;
  • zoom-rest-api(可选):用于服务端侧的业务 API 调用。

本仓库中对应的技能入口分别是 zoom-apps-sdk/SKILL.md、oauth/SKILL.md,后者通过setup-zoom-oauth之后作为授权细节的参考。

二、App 类型选型:五种运行形态

In-Meeting App 并不是单一形态,根据交互方式可以划分为五种类型,每种类型对应不同的核心 API:

类型描述关键 API
Sidebar app会议侧边栏面板getMeetingContext,shareApp
Immersive app全屏 Layers API 渲染runRenderingContext,drawParticipant
Camera mode虚拟摄像头叠加层runRenderingContext({ view: 'camera' })
Collaborate共享状态应用startCollaborate,connect,postMessage
Background app无可见 UI 运行Events、REST API 调用

其中 Sidebar app 是最常见的形态(inMeeting上下文);Immersive 与 Camera 属于 Layers API 的高级渲染形态;Collaborate 依赖实时共享状态;Background app 则完全在后台运行,仅依赖事件与 REST API。更完整的运行上下文对照可参考 concepts/running-contexts.md。

三、总体架构:嵌入式浏览器 + 后端服务

In-Meeting App 的架构可以概括为一个“前端在 Zoom 内、后端在云上”的模式:

Frontend (Zoom embedded browser) Backend (Express/Node.js) ───────────────────────────────── ──────────────────────── @zoom/appssdk OAuth token exchange zoomSdk.config() REST API calls zoomSdk.getMeetingContext() Token storage (Redis) fetch('/api/data') ─────────────> Business logic

前端是加载在 Zoom 嵌入式浏览器(WebView)中的 HTML/CSS/JS 应用,@zoom/appssdk是连接前端与 Zoom 客户端的桥梁;后端(推荐 Node.js + Express)负责 OAuth 令牌交换、REST API 调用与业务逻辑,同时承担令牌存储(如 Redis)。前端通过fetch('/api/data')等常规 HTTP 请求与后端通信。

从 concepts/architecture.md 的架构细节可以看到,Zoom 在不同平台使用不同的浏览器引擎:Windows 为 WebView2(Chromium)、macOS/iOS 为 WKWebView(WebKit)、Android 为 WebView,而 Camera Mode 等部分场景使用 CEF(Chromium Embedded Framework)。这带来几个重要限制:

  • 不支持浏览器扩展;
  • window.open支持有限,应改用zoomSdk.openUrl()
  • 不同应用之间无法共享浏览器级存储;
  • CSP 必须允许frame-ancestors zoom.us *.zoom.us
  • Cookie 需要SameSite=None; Secure

在数据访问层面,In-Meeting App 拥有三条数据通路:Contextual(SDK API)仅需config()即可获取会议/用户/参与者信息;Server-side(REST API)通过后端以 OAuth 令牌调用 Zoom 全量 API;Header(X-Zoom-App-Context)是 Zoom 加载前端时注入的加密请求头,后端可用客户端密钥解密获得用户身份与会议上下文(uidmidaudissts等字段),解密实现可参考 concepts/architecture.md。

四、开发前置条件与环境变量

4.1 前置条件

根据 zoom-apps-sdk/SKILL.md 与 examples/quick-start.md,开始开发前需要准备:

  • 在 Zoom Marketplace 中创建"Zoom App"类型的应用;
  • 具备 OAuth 凭据(Client ID + Secret)及 Zoom Apps 相关 scope;
  • 一个 Web 应用(推荐 Node.js 18+ 与 Express);
  • 域名已加入 Marketplace 的 Domain Allowlist(否则客户端显示空白面板且无任何报错);
  • 本地开发使用 ngrok 或 HTTPS 隧道;
  • 在 Marketplace 中为所需能力启用对应的 OAuth scope。

4.2 环境变量

变量说明补充信息(来源)
ZOOM_APP_CLIENT_IDMarketplace App Credentials位于 Marketplace → 应用 → App Credentials
ZOOM_APP_CLIENT_SECRETMarketplace App Credentials仅限服务端使用,切勿写入前端或仓库
ZOOM_APP_REDIRECT_URI你的服务器 URL +/authOAuth 回调地址
SESSION_SECRET用于 Cookie 签名的随机字符串应使用密钥管理器生成并管理

ZOOM_APP_CLIENT_SECRET必须仅保留在服务端;建议开发与生产使用两套独立的应用凭据。OAuth 流程中产生的ZOOM_ACCESS_TOKENZOOM_REFRESH_TOKEN属于运行时值,不应硬编码在仓库文件中。

五、Quick Start:最小可运行的 In-Meeting App

5.1 SDK 初始化

所有 Zoom App 的第一步都是调用zoomSdk.config(),它声明应用将要使用的所有能力(capabilities)并返回运行上下文:

import zoomSdk from '@zoom/appssdk'; await zoomSdk.config({ capabilities: ['shareApp', 'getMeetingContext', 'getUserContext'], version: '0.16' }); const context = await zoomSdk.getMeetingContext(); console.log('Meeting ID:', context.meetingID); await zoomSdk.shareApp();

config()的返回值包含runningContext(当前运行上下文)、clientVersionunsupportedApis(当前客户端版本不支持的能力列表)。核心规则如下:

  1. config()必须在任何其他 SDK 方法之前调用;
  2. 只有列在capabilities中的能力才可用,调用未声明能力会抛错;
  3. 声明的能力必须与 Marketplace 中启用的 OAuth scope 匹配;
  4. 检查unsupportedApis以便优雅降级。

5.2 两种 SDK 引入方式与全局变量陷阱

方式 A:NPM(推荐用于框架项目)

npm install @zoom/appssdk
import zoomSdk from '@zoom/appssdk';

方式 B:CDN(原生 JS)

<script src="https://appssdk.zoom.us/sdk.js"></script>

关键陷阱:CDN 方式会在全局定义window.zoomSdk。如果你的代码里再写let zoomSdk = ...,会在 Zoom 嵌入式浏览器中抛出SyntaxError: redeclaration of non-configurable global property。正确做法是换个变量名,例如let sdk = window.zoomSdk;。NPM 导入是模块作用域变量,不存在此冲突。

5.3 运行上下文与能力/scope 对应关系

configResponse.runningContext决定你的应用当前运行在哪个界面,常见取值包括inMeeting(会议侧边栏,最常用,具备完整会议 API)、inMainClient(主客户端面板,无会议上下文 API)、inWebinar(网络研讨会侧边栏)、inImmersive(Layers API 全屏渲染)、inCamera(虚拟相机叠加)、inCollaborate(协作共享状态)等。

能力声明必须与 Marketplace 中启用的 OAuth scope 一一对应,常见映射如下:

能力所需 Scope
getMeetingContextzoomapp:inmeeting
getUserContextzoomapp:inmeeting
shareAppzoomapp:inmeeting
openUrlzoomapp:inmeeting
sendAppInvitationzoomapp:inmeeting
runRenderingContextzoomapp:inmeeting
authorizezoomapp:inmeeting
getMeetingParticipantszoomapp:inmeeting

缺失 scope 会导致能力静默失败或抛错;新增 scope 后用户需要重新授权。配置路径:Marketplace → 你的应用 →Scopes标签页。

5.4 浏览器预览与 Demo 模式

SDK 只在 Zoom 客户端内部生效。在普通浏览器中打开时sdk.config()会抛错,因此必须实现 try/catch 回退 UI,并加上约 3 秒的超时兜底(防止 SDK 挂起),这与 examples/quick-start.md 中的 Hello World 实现一致。

5.5 完整后端骨架参考

一个最小可运行的 In-Meeting App 后端(Express)需要:Cookie 会话(SameSite=None+secure: true,嵌入式浏览器必需)、OWASP 安全响应头(Marketplace 审核必需)、/install安装入口(跳转 Zoom OAuth)、/auth回调(交换授权码、获取 deeplink 重定向回客户端)。完整可复制的代码位于 examples/quick-start.md,包括package.json.envserver.jspublic/index.html四份文件。本地运行流程为:npm installngrok http 3000→ 将 ngrok https 地址填入.envZOOM_APP_REDIRECT_URInpm run dev,并在 Marketplace 中配置 Home URL、Redirect URL 与 Domain Allow List。

六、常见用例实战

6.1 投票应用(Poll App)

投票应用需要声明分享、会议上下文、参与者列表与邀请能力:

import zoomSdk from '@zoom/appssdk'; // Initialize await zoomSdk.config({ capabilities: [ 'shareApp', 'getMeetingContext', 'getMeetingParticipants', 'sendAppInvitation' ] }); // Poll state let currentPoll = { question: '', options: [], votes: {} }; // Create poll function createPoll(question, options) { currentPoll = { question, options, votes: {} }; broadcastPollState(); } // Submit vote async function submitVote(optionIndex) { const context = await zoomSdk.getMeetingContext(); currentPoll.votes[context.participantId] = optionIndex; broadcastPollState(); } // Share results function getResults() { const counts = currentPoll.options.map((_, i) => Object.values(currentPoll.votes).filter(v => v === i).length ); return currentPoll.options.map((opt, i) => ({ option: opt, count: counts[i], percentage: (counts[i] / Object.keys(currentPoll.votes).length * 100).toFixed(1) })); } // Invite others to participate async function inviteParticipants() { await zoomSdk.sendAppInvitation({ action: 'open', message: 'Join the poll!' }); }

投票结果同步到所有与会者,属于典型的共享状态问题。本仓库的 examples/collaborate-mode.md 给出了三种状态同步模式:服务端中继(Socket.io)——以getMeetingUUID()的返回值作为房间 ID,服务端作为事实来源;SDK 消息传递——无需服务端,通过connect()+postMessage()直接向所有连接实例广播状态;CRDT(Y.js)——适合文本/白板等协作编辑场景,冲突自动消解,可用 meeting UUID 作为文档名。

6.2 协作白板(Collaborative Whiteboard)

白板的核心是"本地绘制 + 实时广播":

// Whiteboard with real-time sync const canvas = document.getElementById('whiteboard'); const ctx = canvas.getContext('2d'); // Drawing state let isDrawing = false; let lastX = 0; let lastY = 0; canvas.addEventListener('mousedown', (e) => { isDrawing = true; [lastX, lastY] = [e.offsetX, e.offsetY]; }); canvas.addEventListener('mousemove', (e) => { if (!isDrawing) return; const stroke = { from: { x: lastX, y: lastY }, to: { x: e.offsetX, y: e.offsetY }, color: currentColor, width: currentWidth }; drawStroke(stroke); broadcastStroke(stroke); // Sync with others [lastX, lastY] = [e.offsetX, e.offsetY]; }); function drawStroke(stroke) { ctx.beginPath(); ctx.moveTo(stroke.from.x, stroke.from.y); ctx.lineTo(stroke.to.x, stroke.to.y); ctx.strokeStyle = stroke.color; ctx.lineWidth = stroke.width; ctx.lineCap = 'round'; ctx.stroke(); } // Receive strokes from others onRemoteStroke((stroke) => { drawStroke(stroke); });

6.3 会议计时器/议程(Meeting Timer/Agenda)

通过 SDK 获取会议上下文,在前端维护议程状态与倒计时:

import zoomSdk from '@zoom/appssdk'; // Timer app class MeetingTimer { constructor() { this.agenda = []; this.currentItem = 0; this.startTime = null; } async init() { await zoomSdk.config({ capabilities: ['getMeetingContext', 'shareApp'] }); } setAgenda(items) { // items: [{ title: 'Intro', duration: 5 }, ...] this.agenda = items.map(item => ({ ...item, elapsed: 0, status: 'pending' })); this.broadcastState(); } start() { this.startTime = Date.now(); this.agenda[this.currentItem].status = 'active'; this.tick(); } tick() { const item = this.agenda[this.currentItem]; const elapsed = Math.floor((Date.now() - this.startTime) / 1000 / 60); item.elapsed = elapsed; if (elapsed >= item.duration) { this.alertTimeUp(); } this.broadcastState(); setTimeout(() => this.tick(), 1000); } nextItem() { this.agenda[this.currentItem].status = 'completed'; this.currentItem++; if (this.currentItem < this.agenda.length) { this.startTime = Date.now(); this.agenda[this.currentItem].status = 'active'; } } alertTimeUp() { // Visual/audio alert document.getElementById('timer').classList.add('warning'); } }

6.4 Layers API:沉浸式视觉(Immersive / Camera)

Layers API(v1.5)提供两种渲染模式:immersive(全屏自定义视频布局)与camera(仅叠加到用户自己的摄像头画面),需要 Zoom 客户端 v5.10.6+。基础用法:

import zoomSdk from '@zoom/appssdk'; // Layers API for immersive experiences await zoomSdk.config({ capabilities: ['runRenderingContext', 'clearRenderingContext'] }); // Start Layers mode await zoomSdk.runRenderingContext({ view: 'immersive' }); // Draw on the video layer const canvas = document.getElementById('layers-canvas'); const ctx = canvas.getContext('2d'); // Example: Add participant name labels function drawNameLabel(participant, x, y) { ctx.fillStyle = 'rgba(0, 0, 0, 0.7)'; ctx.fillRect(x, y - 25, 150, 25); ctx.fillStyle = 'white'; ctx.font = '14px Arial'; ctx.fillText(participant.name, x + 5, y - 8); } // Example: Add virtual background effects function drawVirtualEffect() { // Draw confetti, borders, icons, etc. // These overlay on top of video } // Stop Layers mode async function exitLayers() { await zoomSdk.clearRenderingContext(); }

references/layers-api.md 提供了更完整的实现约束,值得注意的关键点包括:

  • 运行模式runRenderingContext({ view: 'immersive', defaultCutout: 'person' })对应 Team 模式(AI 抠除背景的人像剪影,v5.9.3+);defaultCutout: 'rectangle'对应 Presentation 模式(保留背景的全宽视频块);view: 'camera'为相机叠加。剪影形状还包括standardcirclesquareverticalRectangle(v5.11.0+)。
  • 约束:只有会议主持人可以将渲染上下文切换为 immersive;同一时间只允许一个 immersive 上下文;Camera Mode 可与 Presentation Mode 同时运行;Camera Mode 使用 CEF 渲染,初始化需要时间,过早调用绘制方法会失败,最佳做法是监听onRenderedAppOpened事件或对绘制调用实施指数退避重试。
  • 绘制方法drawParticipant(需participantUUID,旧的participantId已废弃;Immersive 可绘制任意与会者,Camera 只能绘制自己)、drawImage(接收的是标准ImageData对象而非 base64 字符串,注意 HiDPI 需要按devicePixelRatio缩放并可能分块平铺)、drawWebView(将应用自身 OSR webview 嵌入画布,每个渲染上下文只有一个 webview)。
  • 坐标系与层级:原点在左上角,支持"100px""50%"、原始数字三种单位;Immersive 使用 CSS 像素(相对会议画布自动缩放),Camera 使用相对renderTarget(默认 1280×720)的原始像素。z-index 建议:背景图0、参与者视频1、webview/交互叠加2+。移动已绘制元素时没有原位更新,必须"先 clear 再 draw"。

更完整的沉浸式与相机模式示例可参考 examples/layers-immersive.md 与 examples/layers-camera.md。

七、授权:In-Client OAuth with PKCE

In-Meeting App 的授权应优先使用In-Client OAuthzoomSdk.authorize()),用户在 Zoom 客户端内弹出授权窗口,无需跳转浏览器,是回头客体验最好的方案;Web 重定向方式仅在首次从 Marketplace 安装时使用。核心流程(前端):

// 1. Get code challenge from your backend const { codeChallenge, state } = await fetch('/api/auth/challenge').then(r => r.json()); // 2. Trigger in-client authorization await zoomSdk.authorize({ codeChallenge, state }); // 3. Listen for authorization result zoomSdk.addEventListener('onAuthorized', async (event) => { const { code, state } = event; // 4. Send code to backend for token exchange await fetch('/api/auth/token', { method: 'POST', body: JSON.stringify({ code, state }) }); });

后端侧需要实现三个端点:GET /api/auth/challenge(生成 32 字节随机code_verifierstate,将code_challenge返回前端并仅存于服务端会话);POST /api/auth/token必须先校验state防止 CSRF,再用code + code_verifier调用 Zoom 令牌端点交换 access_token/refresh_token,交换后清理 PKCE 数据);GET /api/auth/status(检查令牌是否过期)。同时提供基于refresh_token的自动刷新中间件,在令牌距过期不足 5 分钟时提前刷新。完整可运行的前后端实现见 examples/in-client-oauth.md。promptAuthorize()可用于 Guest 模式下引导用户提升授权级别。

八、发布清单与 Marketplace 配置

应用上线前,需要逐项核对 in-meeting-apps.md 中的发布清单:

  • 所有响应带上 OWASP 安全头
  • 强制 HTTPS,并持有有效 SSL 证书
  • 实现 PKCE OAuth
  • 对全部 SDK 调用做错误处理
  • 提供浏览器预览回退 UI
  • 配置域名白名单(Domain Allowlist)
  • 在多种屏幕尺寸下测试
  • 提交至 Zoom Marketplace

其中 OWASP 安全头(Strict-Transport-SecurityX-Content-Type-OptionsContent-Security-Policy中的frame-ancestors zoom.us *.zoom.usReferrer-Policy)是 Marketplace 审核的硬性要求,标准配置可直接参考 examples/quick-start.md 与 concepts/security.md。关于域名白名单,除应用自身域名外,使用 CDN 时还需将appssdk.zoom.us及其他 CDN 域名加入白名单;ngrok 免费版 URL 每次重启都会变化,需要在 Marketplace 的 Home URL、Redirect URL、OAuth Allow List、Domain Allow List 四处同步更新。

九、技能链路与深入路径

In-Meeting App 开发的完整技能链路为:

zoom-apps-sdk --> oauth --> zoom-rest-api (optional)

即先掌握 SDK 能力,再实现授权,最后按需接入 REST API。根据 zoom-apps-sdk/SKILL.md 的集成索引,推荐的深入路径如下:

  1. 通读架构:concepts/architecture.md(嵌入式浏览器、深链、X-Zoom-App-Context 解密);
  2. 跑通 Hello World:examples/quick-start.md;
  3. 理解运行上下文:concepts/running-contexts.md;
  4. 实现 In-Client OAuth:examples/in-client-oauth.md;
  5. 按需扩展能力:references/apis.md(100+ SDK 方法分类参考);
  6. 排查问题:troubleshooting/common-issues.md 与 troubleshooting/debugging.md。

对于沉浸式体验可继续阅读 use-cases/immersive-experiences.md(自定义视频布局),实时共享状态应用可参考 use-cases/collaborative-apps.md。在动手前,也可以先通过choose-zoom-approach技能确认 Zoom Apps 与 Meeting SDK 的适用边界(见 concepts/meeting-sdk-vs-zoom-apps.md),避免选错技术路线。

十、高频问题速查

  • 应用显示空白面板→ 检查 Domain Allowlist:在 Marketplace → 应用 → Feature → Zoom App → Add Allow List 中添加域名;
  • SyntaxError: redeclaration→ CDN 模式下不要用let zoomSdk,改用let sdk = window.zoomSdk
  • config()抛错→ SDK 仅在 Zoom 客户端内生效,普通浏览器请走 try/catch 预览 UI;
  • SDK 调用静默失败→ 检查 Marketplace Scopes 中是否启用了对应 scope;
  • 新增 scope 后仍失败→ 用户需要重新授权;
  • Camera Mode 绘制失败→ CEF 未就绪,监听onRenderedAppOpened或指数退避重试。

这些诊断要点均来自 troubleshooting/common-issues.md,该文件覆盖了实际开发中 90% 的常见问题。

【免费下载链接】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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询