К основному содержанию
скилл CX Codex GP ChatGPT официальный · OpenAI

zoom-meeting-sdk-web-client-view — скилл для Codex: встроить Zoom

Скилл для Codex и ChatGPT, который помогает подключить Zoom Meeting SDK Web в режиме Client View — полноэкранной встречи с интерфейсом Zoom.

Установить в Codex · Через Skills CLI (Vercel) Все способы и платформы ↓
npx skills add openai/plugins --skill client-view -a codex
Открыть на GitHub

Что делает zoom-meeting-sdk-web-client-view

Client View рисует встречу на всю страницу через глобальный объект ZoomMtg с колбэками. Интерфейс совпадает с Zoom Web Client, поэтому кастомизировать почти ничего не нужно. Подходит, когда встречу надо встроить быстро и без своей вёрстки.

Внутри скилла — порядок инициализации: проверка браузера через checkSystemRequirements, предзагрузка WebAssembly (preLoadWasm, prepareWebSDK), загрузка локали, затем ZoomMtg.init и ZoomMtg.join. Для join нужен signature — SDK JWT, который выдаёт ваш бэкенд. Пароль передаётся в поле passWord с заглавной W, а события встречи слушаются через inMeetingServiceListener().

Осторожно с необязательными полями: если передать userEmail, tk или zak со значением undefined, вход может упасть с ошибкой. Добавляйте их в объект только когда есть реальное значение. Для установки нужен Node.js — команда идёт через npx.

Какие задачи решает

  • встроить полноэкранную Zoom-встречу в веб-приложение
  • поднять Client View без собственной вёрстки интерфейса
  • пройти шаги инициализации SDK в правильном порядке
  • подключить вход в встречу по SDK JWT с бэкенда
  • слушать события встречи через inMeetingServiceListener()

Примеры запросов агенту

  • Помоги подключить Zoom Meeting SDK Web в режиме Client View к моему приложению на React
  • Собери цепочку инициализации ZoomMtg: проверка браузера, preLoadWasm, prepareWebSDK, init и join
  • Почему ZoomMtg.join падает, если я передаю userEmail как undefined?

Как установить zoom-meeting-sdk-web-client-view

1Через Skills CLI (Vercel)

npx skills add openai/plugins --skill client-view -a codex

2Вручную: положить файлы

.agents/skills/client-view/SKILL.md

Для всех проектов — ~/.agents/skills/

По шагам, с командами для каждой платформы: Скиллы Codex: как установить, где лежат и как вызвать

Вопросы о zoom-meeting-sdk-web-client-view

Как установить zoom-meeting-sdk-web-client-view в Claude?

Выполните npx skills add openai/plugins --skill client-view -a codex, затем файл скилла появится по пути .agents/skills/client-view/SKILL.md.

Что делает zoom-meeting-sdk-web-client-view?

Помогает встроить Zoom Meeting SDK Web в режиме Client View: полноэкранная встреча с интерфейсом Zoom Web Client, управление через глобальный ZoomMtg и колбэки.

Что нужно для работы Client View?

Пакет @zoom/meetingsdk (или CDN-скрипты) и signature — SDK JWT, который формирует ваш бэкенд. Пароль встречи передаётся в поле passWord.

Исходный текст

SKILL.md · MIT
---
name: zoom-meeting-sdk-web-client-view
description: |
  Zoom Meeting SDK Web - Client View. Full-page Zoom meeting experience with the familiar Zoom interface.
  Uses ZoomMtg global singleton with callback-based API. Ideal for quick integration with minimal
  customization. Provides the same UI as Zoom Web Client.
---

# Zoom Meeting SDK Web - Client View

Full-page Zoom meeting experience embedded in your web application. Client View provides the familiar Zoom interface with minimal customization needed.

## Overview

Client View uses the `ZoomMtg` global singleton to render a full-page meeting experience identical to the Zoom Web Client.

| Aspect | Details |
|--------|---------|
| **API Object** | `ZoomMtg` (global singleton) |
| **API Style** | Callback-based |
| **UI** | Full-page takeover |
| **Password param** | `passWord` (capital W) |
| **Events** | `inMeetingServiceListener()` |
| **Best For** | Quick integration, standard Zoom UI |

## Installation

### NPM

```bash
npm install @zoom/meetingsdk --save
```

```javascript
import { ZoomMtg } from '@zoom/meetingsdk';
```

### CDN

```html
<script src="https://source.zoom.us/{VERSION}/lib/vendor/react.min.js"></script>
<script src="https://source.zoom.us/{VERSION}/lib/vendor/react-dom.min.js"></script>
<script src="https://source.zoom.us/{VERSION}/lib/vendor/redux.min.js"></script>
<script src="https://source.zoom.us/{VERSION}/lib/vendor/redux-thunk.min.js"></script>
<script src="https://source.zoom.us/{VERSION}/lib/vendor/lodash.min.js"></script>
<script src="https://source.zoom.us/zoom-meeting-{VERSION}.min.js"></script>
```

## Complete Initialization Flow

```javascript
// Step 1: Check browser compatibility
console.log('Requirements:', ZoomMtg.checkSystemRequirements());

// Step 2: Set CDN path (optional - for China or custom hosting)
// ZoomMtg.setZoomJSLib('https://source.zoom.us/{VERSION}/lib', '/av');

// Step 3: Preload WebAssembly modules
ZoomMtg.preLoadWasm();
ZoomMtg.prepareWebSDK();

// Step 4: Load language resources
ZoomMtg.i18n.load('en-US');
ZoomMtg.i18n.onLoad(() => {

  // Step 5: Initialize SDK
  ZoomMtg.init({
    leaveUrl: '/meeting-ended',
    patchJsMedia: true,
    disableCORP: !window.crossOriginIsolated,
    success: () => {
      console.log('SDK initialized');

      // Step 6: Join meeting
      const joinPayload = {
        signature: signature,
        meetingNumber: meetingNumber,
        userName: userName,
        passWord: passWord,
        success: (res) => {
          console.log('Joined meeting');

          // Step 7: Post-join operations
          ZoomMtg.getAttendeeslist({});
          ZoomMtg.getCurrentUser({
            success: (res) => console.log('Current user:', res.result.currentUser)
          });
        },
        error: (err) => console.error('Join failed:', err)
      };

      // IMPORTANT: only include optional fields when they have real values
      // Passing undefined for some optional fields can cause runtime join errors
      if (userEmail) joinPayload.userEmail = userEmail;
      if (tk) joinPayload.tk = tk;
      if (zak) joinPayload.zak = zak;

      ZoomMtg.join(joinPayload);
    },
    error: (err) => console.error('Init failed:', err)
  });
});
```

## ZoomMtg.init() - All Options

### Required

| Parameter | Type | Description |
|-----------|------|-------------|
| `leaveUrl` | `string` | URL to redirect after leaving meeting |

### UI Customization

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `showMeetingHeader` | `boolean` | `true` | Show meeting number and topic |
| `disableInvite` | `boolean` | `false` | Hide invite button |
| `disableCallOut` | `boolean` | `false` | Hide call out option |
| `disableRecord` | `boolean` | `false` | Hide record button |
| `disableJoinAudio` | `boolean` | `false` | Hide join audio option |
| `disablePreview` | `boolean` | `false` | Skip audio/video preview |
| `audioPanelAlwaysOpen` | `boolean` | `false` | Keep audio panel open |
| `showPureSharingContent` | `boolean` | `false` | Prevent overlays on shared content |
| `videoHeader` | `boolean` | `true` | Show video tile header |
| `isLockBottom` | `boolean` | `true` | Show/hide footer |
| `videoDrag` | `boolean` | `true` | Enable drag video tiles |
| `sharingMode` | `string` | `'both'` | `'both'` or `'fit'` |
| `screenShare` | `boolean` | `true` | Enable browser URL sharing |
| `hideShareAudioOption` | `boolean` | `false` | Hide "Share tab audio" checkbox |
| `disablePictureInPicture` | `boolean` | `false` | Disable PiP mode |
| `disableZoomLogo` | `boolean` | `false` | Remove Zoom logo (deprecated) |
| `defaultView` | `string` | `'speaker'` | `'gallery'`, `'speaker'`, `'multiSpeaker'` |

### Feature Toggles

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `isSupportAV` | `boolean` | `true` | Enable audio/video |
| `isSupportChat` | `boolean` | `true` | Enable in-meeting chat |
| `isSupportQA` | `boolean` | `true` | Enable webinar Q&A |
| `isSupportCC` | `boolean` | `true` | Enable closed captions |
| `isSupportPolling` | `boolean` | `true` | Enable polling |
| `isSupportBreakout` | `boolean` | `true` | Enable breakout rooms |
| `isSupportNonverbal` | `boolean` | `true` | Enable nonverbal feedback |
| `isSupportSimulive` | `boolean` | `false` | Enable Simulive |
| `disableVoIP` | `boolean` | `false` | Disable VoIP |
| `disableReport` | `boolean` | `false` | Disable report feature |

### Video Quality

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `enableHD` | `boolean` | `true` (≥2.8.0) | Enable 720p video |
| `enableFullHD` | `boolean` | `false` | Enable 1080p for webinar attendees |

### Advanced

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `debug` | `boolean` | `false` | Enable debug logging |
| `patchJsMedia` | `boolean` | `false` | Auto-apply media fixes |
| `disableCORP` | `boolean` | `false` | Disable web isolation |
| `helper` | `string` | `''` | Path to helper.html |
| `externalLinkPage` | `string` | - | Page for external links |
| `webEndpoint` | `string` | - | For ZFG environments |
| `leaveOnPageUnload` | `boolean` | `false` | Auto cleanup on page close |
| `isShowJoiningErrorDialog` | `boolean` | `true` | Show error dialog on join failure |
| `meetingInfo` | `Array<string>` | `[...]` | Meeting info to display |
| `inviteUrlFormat` | `string` | `''` | Custom invite URL format |
| `loginWindow` | `object` | `{width: 400, height: 380}` | Login popup size |

### Callbacks

| Parameter | Type | Description |
|-----------|------|-------------|
| `success` | `Function` | Called on successful init |
| `error` | `Function` | Called on init failure |

## ZoomMtg.join() - All Options

### Required

| Parameter | Type | Description |
|-----------|------|-------------|
| `signature` | `string` | SDK JWT from backend (v5.0+: must include appKey prefix) |
| `meetingNumber` | `string \| number` | Meeting or webinar number |
| `userName` | `string` | Display name |
| `passWord` | `string` | Meeting password (capital W!) |

### Authentication

| Parameter | Type | When Required | Description |
|-----------|------|---------------|-------------|
| `zak` | `string` | Starting as host | Host's Zoom Access Key |
| `tk` | `string` | Registration required | Registrant token |
| `userEmail` | `string` | Webinars | User email |
| `obfToken` | `string` | March 2026+ | App Privilege Token |

### Optional

| Parameter | Type | Description |
|-----------|------|-------------|
| `customerKey` | `string` | Custom ID (max 36 chars) |
| `recordingToken` | `string` | Local recording permission |

### Callbacks

| Parameter | Type | Description |
|-----------|------|-------------|
| `success` | `Function` | Called on successful join |
| `error` | `Function` | Called on join failure |

## Event Listeners

### User Events

```javascript
ZoomMtg.inMeetingServiceListener('onUserJoin', (data) => {
  console.log('User joined:', data);
  // { userId, userName, ... }
});

ZoomMtg.inMeetingServiceListener('onUserLeave', (data) => {
  console.log('User left:', data);
  // Reason codes:
  // 0: OTHER
  // 1: HOST_ENDED_MEETING
  // 2: SELF_LEAVE_FROM_IN_MEETING
  // 3: SELF_LEAVE_FROM_WAITING_ROOM
  // 4: SELF_LEAVE_FROM_WAITING_FOR_HOST_START
  // 5: MEETING_TRANSFER
  // 6: KICK_OUT_FROM_MEETING
  // 7: KICK_OUT_FROM_WAITING_ROOM
  // 8: LEAVE_FROM_DISCLAIMER
});

ZoomMtg.inMeetingServiceListener('onUserUpdate', (data) => {
  console.log('User updated:', data);
});

ZoomMtg.inMeetingServiceListener('onUserIsInWaitingRoom', (data) => {
  console.log('User in waiting room:', data);
});
```

### Meeting Status

```javascript
ZoomMtg.inMeetingServiceListener('onMeetingStatus', (data) => {
  // status: 1=connecting, 2=connected, 3=disconnected, 4=reconnecting
  console.log('Status:', data.status);
});
```

### Audio/Video Events

```javascript
ZoomMtg.inMeetingServiceListener('onActiveSpeaker', (data) => {
  // [{userId, userName}]
  console.log('Active speaker:', data);
});

ZoomMtg.inMeetingServiceListener('onNetworkQualityChange', (data) => {
  // {level: 0-5, userId, type: 'uplink'}
  // 0-1=bad, 2=normal, 3-5=good
  console.log('Network quality:', data);
});

ZoomMtg.inMeetingServiceListener('onAudioQos', (data) => {
  console.log('Audio QoS:', data);
});

ZoomMtg.inMeetingServiceListener('onVideoQos', (data) => {
  console.log('Video QoS:', data);
});
```

### Chat & Communication

```javascript
ZoomMtg.inMeetingServiceListener('onReceiveChatMsg', (data) => {
  console.log('Chat message:', data);
});

ZoomMtg.inMeetingServiceListener('onReceiveTranscriptionMsg', (data) => {
  console.log('Transcription:', data);
});

ZoomMtg.inMeetingServiceListener('onReceiveTranslateMsg', (data) => {
  console.log('Translation:', data);
});
```

### Recording & Sharing

```javascript
ZoomMtg.inMeetingServiceListener('onRecordingChange', (data) => {
  console.log('Recording status:', data);
});

ZoomMtg.inMeetingServiceListener('onShareContentChange', (data) => {
  console.log('Share content:', data);
});

ZoomMtg.inMeetingServiceListener('receiveSharingChannelReady', (data) => {
  console.log('Sharing channel ready:', data);
});
```

### Breakout Rooms

```javascript
ZoomMtg.inMeetingServiceListener('onRoomStatusChange', (data) => {
  // status: 2=InProgress, 3=Closing, 4=Closed
  console.log('Breakout room status:', data);
});
```

### Other Events

```javascript
ZoomMtg.inMeetingServiceListener('onJoinSpeed', (data) => {
  console.log('Join metrics:', data);
});

ZoomMtg.inMeetingServiceListener('onVbStatusChange', (data) => {
  console.log('Virtual background status:', data);
});

ZoomMtg.inMeetingServiceListener('onFocusModeStatusChange', (data) => {
  console.log('Focus mode:', data);
});

ZoomMtg.inMeetingServiceListener('onPictureInPicture', (data) => {
  console.log('PiP status:', data);
});

ZoomMtg.inMeetingServiceListener('onClaimStatus', (data) => {
  console.log('Host claim status:', data);
});
```

## Common Methods

### Meeting Info

```javascript
// Get current user
ZoomMtg.getCurrentUser({
  success: (res) => console.log(res.result.currentUser)
});

// Get all attendees
ZoomMtg.getAttendeeslist({});

// Get meeting info
ZoomMtg.getCurrentMeetingInfo({
  success: (res) => console.log(res)
});

// Get SDK version
ZoomMtg.getWebSDKVersion({
  success: (version) => console.log(version)
});
```

### Audio/Video Control

```javascript
// Mute/unmute specific user
ZoomMtg.mute({ userId, mute: true });

// Mute/unmute all
ZoomMtg.muteAll({ muteAll: true });

// Stop incoming audio
ZoomMtg.stopIncomingAudio({ stop: true });

// Mirror video
ZoomMtg.mirrorVideo({ mirror: true });
```

### Chat

```javascript
// Send chat message
ZoomMtg.sendChat({
  message: 'Hello!',
  userId: 0  // 0 = everyone
});
```

### Meeting Control

```javascript
// Leave meeting
ZoomMtg.leaveMeeting({});

// End meeting (host only)
ZoomMtg.endMeeting({});

// Lock meeting
ZoomMtg.lockMeeting({ lock: true });
```

### Host Controls

```javascript
// Make host
ZoomMtg.makeHost({ userId });

// Make co-host
ZoomMtg.makeCoHost({ userId });

// Remove co-host
ZoomMtg.withdrawCoHost({ userId });

// Remove participant
ZoomMtg.expel({ userId });

// Put on hold
ZoomMtg.putOnHold({ userId, bHold: true });

// Claim host with host key
ZoomMtg.claimHostWithHostKey({ hostKey: '123456' });

// Reclaim host
ZoomMtg.reclaimHost({});

// Admit all from waiting room
ZoomMtg.admitAll({});
```

### Raise Hand

```javascript
// Raise hand
ZoomMtg.raiseHand({ userId });

// Lower hand
ZoomMtg.lowerHand({ oderId });

// Lower all hands
ZoomMtg.lowerAllHands({});
```

### Spotlight & Pin

```javascript
// Spotlight video
ZoomMtg.operateSpotlight({ oderId, action: 'add' }); // or 'remove'

// Pin video
ZoomMtg.operatePin({ oderId, action: 'add' }); // or 'remove'

// Allow multi-pin
ZoomMtg.allowMultiPin({ allow: true });
```

### Screen Share

```javascript
// Start screen share
ZoomMtg.startScreenShare({});

// Share specific source (Electron)
ZoomMtg.shareSource({ source });
```

### Recording

```javascript
// Start/stop recording
ZoomMtg.record({ record: true }); // or false

// Show/hide record button
ZoomMtg.showRecordFunction({ show: true });
```

### Breakout Rooms

```javascript
// Create breakout room
ZoomMtg.createBreakoutRoom({
  rooms: [{ name: 'Room 1' }, { name: 'Room 2' }]
});

// Open breakout rooms
ZoomMtg.openBreakoutRooms({});

// Close breakout rooms
ZoomMtg.closeBreakoutRooms({});

// Join breakout room
ZoomMtg.joinBreakoutRoom({ roomId });

// Leave breakout room
ZoomMtg.leaveBreakoutRoom({});

// Move user to breakout room
ZoomMtg.moveUserToBreakoutRoom({ oderId, roomId });

// Get breakout room status
ZoomMtg.getBreakoutRoomStatus({
  success: (res) => console.log(res)
});
```

### Virtual Background

```javascript
// Check support
ZoomMtg.isSupportVirtualBackground({
  success: (data) => console.log(data.result.isSupport)
});

// Set virtual background
ZoomMtg.setVirtualBackground({ imageUrl: '...' });

// Get VB status
ZoomMtg.getVirtualBackgroundStatus({
  success: (data) => console.log(data)
});

// Lock virtual background (host)
ZoomMtg.lockVirtualBackground({ lock: true });
```

### UI Control

```javascript
// Show/hide meeting header
ZoomMtg.showMeetingHeader({ show: true });

// Show/hide invite button
ZoomMtg.showInviteFunction({ show: true });

// Show/hide join audio button
ZoomMtg.showJoinAudioFunction({ show: true });

// Show/hide callout button
ZoomMtg.showCalloutFunction({ show: true });

// Re-render with new options
ZoomMtg.reRender({ lang: 'de-DE' });
```

### Language

```javascript
// Load language
ZoomMtg.i18n.load('de-DE');

// Reload language
ZoomMtg.i18n.reload('de-DE');

// Get current language
ZoomMtg.i18n.getCurrentLang();

// Get all translations
ZoomMtg.i18n.getAll();
```

## Rate Limits

| Method | Limit |
|--------|-------|
| `join()` | 10 seconds |
| `callOut()` | 10 seconds |
| `mute()` | 1 second |
| `muteAll()` | 5 seconds |

## DOM Elements

The SDK automatically adds these elements:
- `#zmmtg-root` - Main meeting container
- `#aria-notify-area` - Accessibility announcements

Do NOT manually create or remove these.

### SPA (React/Next) Overlay Gotcha

If you "join" but see a blank/black area or your app shell instead of the meeting UI, the Zoom UI can be rendering **behind** your app layout. Ensure `#zmmtg-root` occupies the viewport and is above other fixed elements:

```css
#zmmtg-root {
  position: fixed !important;
  inset: 0 !important;
  z-index: 9999 !important;
}
```

### Join Payload Sanitization Gotcha

If `ZoomMtg.join()` succeeds partially but the screen turns black and console shows errors like `Cannot read properties of undefined (reading 'toString')`, sanitize optional fields before calling join.

Do not pass optional keys with `undefined` values (`userEmail`, `tk`, `zak`, etc.). Build a payload and only attach those keys when they are non-empty strings.

Also prefer `defaultView: 'speaker'` during `ZoomMtg.init()` unless you have SharedArrayBuffer/gallery-view prerequisites fully configured.

## Resources

- [Main Web SDK Skill](../SKILL.md)
- [Reference Index](references/index.md)
- [Error Codes](../troubleshooting/error-codes.md)
- [Common Issues](../troubleshooting/common-issues.md)
- [SharedArrayBuffer Setup](../concepts/sharedarraybuffer.md)
- [Official API Reference](https://marketplacefront.zoom.us/sdk/meeting/web/index.html)

## Operations

- [RUNBOOK.md](RUNBOOK.md) - 5-minute preflight and debugging checklist.

© Zoom · MIT · openai/plugins

Похожие скиллы

Всё по теме «Фронтенд и UI» →
build-zoom-meeting-sdk-app официальный anthropics Инструкции и шаблоны для интеграции Zoom Meeting SDK в веб и нативные клиенты. Помогает настроить JWT-подписи и устранить баги вёрстки. CX Codex +1 2 100установок zoom-apps-sdk официальный anthropics Справочный скилл по Zoom Apps SDK. Помогает агенту писать веб-приложения, которые работают внутри клиента Zoom: встречи, вебинары, Zoom Phone. CX Codex +1 2 000установок web-development Mindrally Скилл с правилами веб-разработки: сетка Bootstrap, вьюхи и модели Django, атрибуты HTMX. Пригодится, когда агент пишет шаблоны и бэкенд в одном проекте. CX Codex +1 815установок domain-web actionbook Скилл задаёт правила проектирования веб-сервисов на Rust: от async-обработчиков до состояния, ошибок и middleware. Нужен при работе с Cargo-проектом. CX Codex +1 3 100установок web-performance-audit aj-geddes Скилл разбирает скорость загрузки страниц, находит узкие места и подсказывает, что чинить. Пригодится после крупных правок или жалоб на медленную загрузку. CX Codex +1 543установок view-transitions patterns.dev Учит агента работать с View Transitions API: переходы между страницами и состояниями интерфейса браузер анимирует сам. CX Codex +1 678установок

Из того же набора304

Весь набор →
ui-toolkit Скилл подсказывает агенту, как подключить предсобранный веб-интерфейс Zoom Video SDK и настроить сессию. Пригодится, если нужна кастомная сессия, а не обычная встреча Zoom. CX Codex GP ChatGPT ★ 7 596звёзд репо setup-zoom-oauth Скилл разбирает авторизацию Zoom по шагам: тип приложения, grant flow, scopes, обмен и обновление токенов. Пригодится, когда интеграция встала на аутентификации. CX Codex GP ChatGPT ★ 7 596звёзд репо rivet-sdk Скилл ведёт агента по сборке серверной интеграции с Zoom на Rivet: модули, OAuth, вебхуки, деплой. Пригодится, когда не хочется вручную писать обвязку API. CX Codex GP ChatGPT ★ 7 596звёзд репо zoom-oauth Скилл для Codex и ChatGPT, который разбирает потоки авторизации Zoom: выбор grant flow, области доступа, ротация refresh-токенов и разбор ошибок. CX Codex GP ChatGPT ★ 7 596звёзд репо zoom-meeting-sdk-react-native Скилл для ИИ-агента, который помогает встроить встречи Zoom в приложение на React Native: авторизация по JWT, вход и старт встречи, настройка iOS и Android. CX Codex GP ChatGPT ★ 7 596звёзд репо zoom-meeting-sdk-ios Скилл для Codex и ChatGPT, который ведёт агента по встраиванию встреч Zoom в нативные iOS-приложения: авторизация, запуск как хост, жизненный цикл. CX Codex GP ChatGPT ★ 7 596звёзд репо build-zoom-meeting-sdk-app Скилл ведёт агента по шагам сборки приложения, которое подключается к настоящим встречам Zoom: от выбора платформы до отладки подписи SDK. CX Codex GP ChatGPT ★ 7 596звёзд репо zoom-contact-center-android Скилл для работы с Zoom Contact Center SDK на Android: чат, видео, ZVA, обратный звонок и кампании. Пригодится, когда встраиваете поддержку в нативное приложение. CX Codex GP ChatGPT ★ 7 596звёзд репо

Спонсоры проекта

Компании, которые помогают держать аналитику открытой