@ai-matrx/meet

中文文档
0.11.130发布于 2026/10/30MITTypeScriptESM + CJS近 30 天下载 5.6万

Enterprise, AI-native calls and meetings for Matrx clients: LiveKit Cloud rooms with server-minted tokens, instant ring/accept/decline, Zoom-grade meetings with durable links, lobby admission and guest join, host controls, Egress recording with enforced c

原始使用文档
文档目录17 节
  1. Why this package exists
  2. The doctrine
  3. 1. The client NEVER holds a LiveKit secret.
  4. 2. A token is refreshed before it bites, and minted ONCE.
  5. 3. The phase never lies. There is no infinite spinner.
  6. 4. A call reaches you on TWO legs, and rings ONCE.
  7. 5. A guest is a first-class identity, not a degraded user.
  8. 6. Admission is a server decision. Roles come from the toke…
  9. 7. A durable link is an identity, not a URL for one occurre…
  10. 8. Consent is a property of the package, not a prop.
  11. 9. The AI is a participant, and it looks like one.
  12. 10. The stage is arithmetic, and it never shuffles.
  13. 11. Nothing fails silently, and no control is a lie.
  14. The one-hour integration (R7)
  15. What the host owns, and what it must never do
  16. Server prerequisites
  17. Gates

@ai-matrx/meet

Enterprise, AI-native calls and meetings for every Matrx client: tap a person and be in a call in two seconds, or run a meeting that rivals Zoom — lobby, host controls, screen share, recording — where the AI half is not an add-on but the reason ours is better.

One provider, one component, under an hour to full production conferencing including the AI.

pnpm add @ai-matrx/meet

Built on @ai-matrx/data/realtime for signaling and @ai-matrx/messaging for invitations. There is zero hand-rolled .channel( code in this package, and there must be none in yours.


Why this package exists

Video calling is the feature every team estimates at two weeks and ships in three months, because the hard parts are not the tiles. They are: who is allowed in this room, what happens when the token expires mid-meeting, why the camera light stays on after you leave, why the phone in the other room is still ringing after you answered on your laptop, why the headset you unplugged is still "selected", and what a guest who has no account is allowed to do. Every one of those has a name in this README and a regression test beside it.

We are deliberately inflexible. LiveKit Cloud is the media infrastructure, hardcoded (D1/D2). There is no provider abstraction and never will be. The package does exactly what we want it to do, and that is the product.


The doctrine

1. The client NEVER holds a LiveKit secret.

Not in an env var, not in a build-time constant, not "just for local dev". A LiveKit API secret in a browser bundle mints a token for any room in the project, including meetings the holder was never invited to — it is a full compromise of every conversation in the organization.

So the package has no code path that could accept one. It asks aidream; aidream mints. That is the only authorization path into a room (R1), and a guest's entire capability is the token they were given: exactly one room, nothing else (R5).

2. A token is refreshed before it bites, and minted ONCE.

Refresh happens at 80% of the token's life, not when a reconnect fails at 100% — the classic two-hour-meeting bug is a session that outlives its token and cannot come back. And every concurrent asker shares ONE mint: a React strict-mode double effect that mints twice gives one identity two LiveKit sessions, and LiveKit disconnects the older one. That looks exactly like "my call randomly drops when I switch tabs".

A token with no expires_at is refused rather than used, because a token the package cannot schedule a refresh for is a meeting that will silently fail to reconnect.

3. The phase never lies. There is no infinite spinner.

requesting-token → lobby | connecting → connected ⇄ reconnecting → left, and any failure becomes failed with the remedy attached to the snapshot. Reconnecting is a visible state, because a frozen tile with no explanation is indistinguishable from a crashed app.

Device failures get four different sentences, not one shrug: permission denied, no such device, device already in use by another app, and insecure context (getUserMedia does not exist on http). A single "media error" would tell the user the wrong thing three times out of four.

4. A call reaches you on TWO legs, and rings ONCE.

The durable leg is the invite row and its Postgres Changes event: it survives a reload, a late-joining tab, and a dropped socket, and it is what makes a missed call a fact rather than a lost packet. The instant leg is a broadcast on your user channel, with no database round trip. Both are deduplicated on the invite id.

Neither is a fallback for the other. Remove the durable leg and calls get lost; remove the instant leg and ringing feels slow.

Around that: every ring ends (an unanswered call writes missed), answering on one device stops every other (the settled signal on your own channel), being in a call answers busy rather than ringing out, and the state machine refuses to resurrect a settled call when a late accept arrives.

5. A guest is a first-class identity, not a degraded user.

A meeting link works for anyone (D6). Guests enter a name, wait in the lobby, and the host admits them. Authenticated users skip the prompt; host powers require authentication.

This is enforced by TYPES, not by discipline: MeetIdentity.userId is UserId | null, and every operation that needs an account takes a UserId. A guest provider has no call center at all — so useCalls().canCall is false and no call control renders rather than a disabled button (no dead ends).

6. Admission is a server decision. Roles come from the token.

admit is an auth-checked RPC; the broadcast that follows only tells the knocker, who has no database read of their own. Two hosts clicking "admit" is the ordinary case, and the RPC is what makes it idempotent.

Every participant's role is read from the server-minted token metadata. A client that could decide it was host could mute anyone. And a co-host cannot remove the host — without that rank check, "host controls" is a mutual-eviction button.

A meeting slug resolves before, during, and after its scheduled time, and a recurring meeting keeps ONE link. A link that dies when the meeting ends breaks every "here are the notes" follow-up, which is when people actually go looking for it.

Slugs deliberately exclude the characters people mishear reading them aloud: no 0/O, no 1/l.

There is no showRecordingIndicator={false}. Recording someone without telling them is illegal in a two-party-consent jurisdiction and indefensible everywhere else, so the indicator and the join-time notice ship with the package. The only knob is the org's recordingPolicy — disabled / host-controlled / always-on — which decides whether recording may happen at all, never whether people are told.

The indicator turns solid when the server says the egress is running, never on a client's hope; a refused start reverts it rather than leaving a red dot over a recording that is not running. And a recording lands in the platform files system as a file id, never a provider URL — a LiveKit URL is an orphan the day the room is garbage collected.

9. The AI is a participant, and it looks like one.

The note-taker joins the room as a real LiveKit participant with is_agent in its token metadata, so it renders as a labeled tile. An invisible listener in a meeting is the thing nobody should ever ship.

Everything AI executes on the platform agent system through @ai-matrx/agents. The package never talks to a model, never holds a prompt, and never carries an agent definition — it takes agent IDs as injected identity, and an unconfigured capability reports unavailable with the remedy while the UI hides that action entirely.

🚨 THE USER-INPUT LAW. user_input is what a human typed. Transcripts, rosters, and agendas travel as named variables. The server stores and frames user_input as the turn's human utterance, so smuggling a transcript through it corrupts every conversation it touches. Exactly one call in this package sends a user_input: the in-meeting question a person actually asked.

Interim transcript lines never reach an agent, and a truncated transcript says so on the wire.

10. The stage is arithmetic, and it never shuffles.

Screen share (or an explicit pin) owns the stage; then active speakers, then raised hands, then cameras-on, then stable by join time. Tiles that reshuffle on every roster event are the most disorienting thing a meeting UI can do.

A large meeting pages and says +N more people — it does not render 200 videos and die, and it does not silently drop the people it cannot fit (R5).

11. Nothing fails silently, and no control is a lie.

Every error is a MeetError with a stable code and a remedy sentence. Every stand-in announces itself: an unplugged headset says "your remembered microphone is no longer connected — using the system default" rather than quietly switching you to the laptop mic.

A control an actor cannot use is absent, never disabled-looking and never a button the server will refuse.


The one-hour integration (R7)

Four subpaths (CORE-DESIGN §5.1): @ai-matrx/meet/core (framework-free; @ai-matrx/meet re-exports it), @ai-matrx/meet/react (provider, hooks, <MeetingSkinRoot>, the record and ringing surfaces), and one per skin — @ai-matrx/meet/skins/meet holds the Meet skin's parts (ControlBar, ParticipantTile, Captions, …) for a host layout that composes them.

import "@ai-matrx/meet/tokens.css";   // defaults — override these with your brand
import "./brand.css";
import "@ai-matrx/meet/styles.css";   // structural, last

import { MeetProvider, IncomingCallHost, MeetingSkinRoot, useCalls } from "@ai-matrx/meet/react";

export function App({ children }) {
  return (
    <MeetProvider
      client={supabase}
      baseUrl={AIDREAM_URL}
      displayName={user.name}
      organizationId={org.id}
      accessToken={async () => (await supabase.auth.getSession()).data.session?.access_token ?? null}
      transport={matrxTransport}
      agents={{ noteTaker: NOTE_TAKER_AGENT_ID, liveIntelligence: LIVE_AGENT_ID }}
    >
      {/* Mount ONCE. Calls now ring on whatever surface the user is on. */}
      <IncomingCallHost />
      {children}
    </MeetProvider>
  );
}

// Anywhere a person appears: your own control, calling through the hook.
const calls = useCalls();
calls.canCall && <button onClick={() => calls.call([userId], "video")}>Call</button>

// Your meeting route — the skin root chooses the viewer's skin (Meet until S9):
<MeetingSkinRoot roomName={roomName} meetingId={meetingId} />

The stage fills the viewport (100dvh). To run a meeting inside a box — a board tile, a split pane — give an ancestor a definite height and set --mx-meet-height: 100% on it; every screen (pre-join, lobby, room, record) then fills that box.

🚨 You do not tell Meet who the user is, and there is no prop for it. The acting user is read from client's own session — the same client that mints the JWT on every request — so the id in a p_user_id argument and the id in the token cannot disagree. Through 0.5.x this WAS a prop, every host filled it from a copy in its own state, and a rotated auth cookie left that copy naming an account the tab could no longer prove it was; meet_pending_call_invites refused those reads at 403 with "the acting user … is not the authenticated user". Pass displayName and avatarUrl — they are chrome — and nothing else about identity. A userId prop is ignored and says so.

A guest join is the same provider with guestName, which is the one way to declare the guest lane. A guest has no account, so that mount reads no session at all:

<MeetProvider client={supabase} baseUrl={AIDREAM_URL} organizationId={orgId}
              guestName={typedName} accessToken={async () => null}>
  <MeetingSkinRoot roomName={meeting.roomName} slug={slug} />
</MeetProvider>

Invitations ride the messaging seam — register the handlers once:

<MessagingProvider
  actions={[
    createCallInviteHandler({ calls, repository, onJoin: (invite) => router.push(`/meet/${invite.roomName}`) }),
    createMeetingInviteHandler({ onOpen: (payload) => router.push(`/meet/${payload.slug}`) }),
  ]}
>

What the host owns, and what it must never do

Inject: the Supabase client, aidream's base URL, who the user is (or a guest name), the org, an access-token source, the Matrx transport, agent ids, brand token values, and a router.

Never: catch a package error to reinterpret it, retry a package call, validate a package input, or branch on a package quirk. That logic belongs in here (C22) — open a PR against this package, release it, and delete the massage.


Server prerequisites

This is the client half. Two server-side pieces are required and are not built yet:

  • The aidream meet router — token minting (identity, grants, TTL, guest + lobby gating), Egress start/stop, LiveKit webhooks that keep the database TRUE-current, and the LiveKit Agents note-taker. The exact routes this package calls are MEET_ROUTES in src/core/tokens.ts.
  • The communication.meet_* schema in Matrx Main — the tables and RPCs named in src/core/repository.ts, under the platform's canonical DB conventions.

Until both exist the package fails loudly, with a remedy naming the missing piece. It never fabricates a room, a token, or a meeting record. See FEATURE.md.


Gates

pnpm --filter @ai-matrx/meet typecheck
pnpm --filter @ai-matrx/meet test
pnpm --filter @ai-matrx/meet check:package   # build + publint + packed-tarball canary

MIT licensed. Source: aidream/apps/shared/meet.

版本与发布记录

@ai-matrx/meet 共发布 272 个版本,首次发布于 2026/09/08;过去 12 个月发布了 272 个版本。

版本发布日期距今
0.11.130latest2026/10/306 天前
0.11.1292026/10/288 天前
0.11.1282026/10/2512 天前
0.11.1272026/10/2512 天前
0.11.1262026/10/2412 天前
0.11.1252026/10/2413 天前

关于 @ai-matrx/meet 的常见问题

@ai-matrx/meet 是什么?

Enterprise, AI-native calls and meetings for Matrx clients: LiveKit Cloud rooms with server-minted tokens, instant ring/accept/decline, Zoom-grade meetings with durable links, lobby admission and guest join, host controls, Egress recording with enforced c

如何安装 @ai-matrx/meet?

在项目目录运行 npm install @ai-matrx/meet,或使用 pnpm add / yarn add / bun add。

@ai-matrx/meet 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@ai-matrx/meet 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@ai-matrx/meet 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=20。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@ai-matrx/meet 可以商用吗?

@ai-matrx/meet 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@ai-matrx/meet 的下载量有多少?

根据 npm 官方下载统计,@ai-matrx/meet 在 2026/10/05 – 2026/11/03 这 30 天内被下载 56,210 次,日均约 1,874 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@ai-matrx/meet 还在维护吗?

最新版本 0.11.130 发布于 2026/10/30(6 天前);至今共发布 272 个版本,其中过去 12 个月发布了 272 个;首次发布于 2026/09/08。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。