nimeet 文档中心
English

频道管理

频道列表、频道内用户、用户状态、踢人(kicking-rule)、禁言、角色切换与封禁规则。

版本 v1.1.0 · 更新于 2026-10-26 · 修订 1

频道管理覆盖两类操作:查询(频道、用户、用户状态)与治理(踢人、禁言、角色切换、封禁规则)。平台 API 使用 camelCase 字段;/dev/v1/* 提供语义等价的声网兼容入口,两者读写同一份业务事实。

能力总览

能力平台 API声网兼容
频道列表GET /v1/channelsGET /dev/v1/channel/{appid}
频道内用户GET /v1/channels/{channelName}/usersGET /dev/v1/channel/user/{appid}/{channelName}
用户状态GET /v1/channels/{channelName}/users/{uid}GET /dev/v1/channel/user/property/{appid}/{uid}/{channelName}
踢人DELETE /v1/channels/{channelName}/users/{uid}POST /dev/v1/kicking-rule(join_channel,并立即踢下线)
禁言 / 解禁POST /v1/channels/{channelName}/users/{uid}/mute · /unmute通过 kicking-rule 的 publish_audio / publish_video 表达
角色切换POST /v1/channels/{channelName}/users/{uid}/role客户端 setClientRole 对应语义,无 REST 等价物
封禁规则GET/POST/PATCH/DELETE /v1/ban-rulesGET/POST/PUT/DELETE /dev/v1/kicking-rule

查询频道

GET /v1/channels?page_no=0&page_size=100

参数位置说明
page_noquery0 起,默认 0
page_sizequery1–500,默认 100

响应 { total, pageNo, pageSize, channels: [{ channelName, userCount, profile, createdAt }] }。userCount 来自媒体面的实时在线人数,profile 取 communication / live,createdAt 在频道未经控制面创建时可能为 null。

声网兼容入口 GET /dev/v1/channel/{appid}?page_no=&page_size= 返回 { success: true, data: { channels: [{ channel_name, user_count }], total_size } }。

查询频道内用户

GET /v1/channels/{channelName}/users → { channel: { name, profile, createdAt }, users: [UserView] }

UserView 字段:uid、userId(可能为 null)、role、platform、joinedAt、audioMuted、videoMuted、publishing.{audio,video,screen}。

频道不存在时返回 404 channel_not_found(不是空列表)。声网兼容入口在频道不存在时返回 { success: true, data: { channel_exist: false } }——这是声网的表达习惯。

声网兼容接口的两种形态:

  • communication 频道:{ channel_exist: true, mode: 1, total, users: [uid…] }
  • live 频道:{ channel_exist: true, mode: 2, broadcasters: [uid…], audience: [uid…], audience_total },加 ?hosts_only=true 时省略观众列表。

查询单个用户

GET /v1/channels/{channelName}/users/{uid} → UserView;不在频道内返回 404 user_not_found。

声网兼容入口返回 { in_channel, uid, join, role, platform } 或 { in_channel: false };role 数值:1 communication 用户、2 主播、3 观众。

踢人

DELETE /v1/channels/{channelName}/users/{uid}

请求体可选:{ "durationSeconds": 600, "reason": "policy-violation" }(durationSeconds 0–86430,默认 600)。

动作序列(一次调用内完成):

  1. 建一条 join_channel 封禁规则;
  2. 从媒体面移除该参与者;
  3. 记录离开原因 3(被踢),并产生 user_leave / broadcaster_leave 回调。

响应:{ "kicked": true, "uid": 10001, "banRuleId": 7, "expireAt": "2026-10-26T03:00:00.000Z" }。

禁言与解除

POST /v1/channels/{channelName}/users/{uid}/mute

字段说明
kindaudio / video / all(默认 all)
banPublish默认 true:除静音当前已发布 track 外,额外禁止重新发布(建 publish_audio / publish_video 规则)
durationSeconds封禁窗口,默认 600

响应:{ muted: true, uid, kind, mutedTracks: ["TR_10001_a"], banRuleIds: [8] }。mutedTracks 是被服务端静音的 track sid。

POST .../unmute 做两件事:取消 track 静音,并删除对应的 publish_* 规则(只保留仍有需要的权限位)。

角色切换

POST /v1/channels/{channelName}/users/{uid}/role,请求体 { "role": "host" | "audience" }。

  • host:canPublish=true、canPublishData=true
  • audience:canPublish=false(仍可订阅)

成功返回 { uid, role },并产生回调:提升为 111 role_to_broadcaster,降级为 112 role_to_audience。

封禁规则(kicking-rule)

平台 API:

方法路径说明
GET/v1/ban-rules?channelName=&uid=列出未过期规则
POST/v1/ban-rules新建,成功 201
PATCH/v1/ban-rules/{id}更新时长 / 权限位
DELETE/v1/ban-rules/{id}删除,成功 204

创建请求体:channelName、uid、ip 至少给一个;privileges 必填且至少一项(join_channel / publish_audio / publish_video);durationSeconds 默认 3600。

语义:

  • join_channel:签发 Token 时直接 403 banned,且若在线立即踢下线(离开原因 3)。
  • publish_audio / publish_video:禁止对应媒体发布;可被 unmute 显式解除。
  • 规则的 expireAt 为 null 表示永久(durationSeconds: 0 目前走这条路径,见「已知限制」)。

声网兼容入口 POST /dev/v1/kicking-rule 额外接受 time(分钟,0–1440)或 time_in_seconds(0–86430);两者都缺省时按 3600 秒处理。PUT 用 appid + id 定位,DELETE 用 appid + id。

声网路径 ↔ 我们路径对照

声网路径我们的等价路径备注
GET /dev/v1/channel/{appid}GET /v1/channels声网返回 channel_name / user_count / total_size;平台返回 camelCase 且带 pageNo / pageSize
GET /dev/v1/channel/user/{appid}/{channelName}GET /v1/channels/{channelName}/users声网按频道 profile 返回 users 或 broadcasters/audience,并用 channel_exist 表达不存在
GET /dev/v1/channel/user/property/{appid}/{uid}/{channelName}GET /v1/channels/{channelName}/users/{uid}声网用 in_channel + 数值 role;我们返回完整 UserView
POST /dev/v1/kicking-rulePOST /v1/ban-rules声网 time(分钟)/time_in_seconds(秒);我们统一 durationSeconds。join_channel 会立即踢下线
GET /dev/v1/kicking-rule?appid=GET /v1/ban-rules声网按 appid(query)鉴权并列出;我们按项目凭证列出,支持 channelName / uid 过滤
PUT /dev/v1/kicking-rulePATCH /v1/ban-rules/{id}都支持改时长与权限位
DELETE /dev/v1/kicking-ruleDELETE /v1/ban-rules/{id}声网用 body 里的 appid+id(DELETE 带 body),我们用路径参数
(无 REST 等价物)POST /v1/channels/{c}/users/{uid}/mute /unmute声网仅能通过 publish_* 规则表达「禁止发布」;我们额外提供即时服务端静音
(无 REST 等价物,客户端 setClientRole)POST /v1/channels/{c}/users/{uid}/role服务端改角色,并产生 111/112 回调
(无,声网用 kicking-rule 的 join_channel 实现)DELETE /v1/channels/{c}/users/{uid}踢人 = 移除在线会话 + 建 join_channel 规则,一次完成

端到端示例

查询与声网兼容对照(安全,只读):

javascript
export const meta = {
  id: '06-channel-users',
  title: '频道内用户列表与用户详情',
  safe: true,
  needsApi: true,
};

/**
 * GET /v1/channels/{channelName}/users        → { channel: { name, profile, createdAt }, users: [UserView] }
 * GET /v1/channels/{channelName}/users/{uid}  → UserView
 *
 * 演示项目首启会 seed 三个频道(weekly-sync / design-review / all-hands)与 5 个用户;
 * 若以 NIMEET_SEED_DEMO=false 启动,这里会得到 404 channel_not_found —— 这也是文档化的行为。
 */
export async function run(ctx) {
  const api = ctx.api;
  const channels = await api('GET', '/v1/channels', { query: { page_size: 100 } });
  const names = (channels.json.channels ?? []).map((entry) => entry.channelName);
  const channelName = names.includes('weekly-sync') ? 'weekly-sync' : names[0];

  if (!channelName) {
    return { summary: '当前项目没有活跃频道(NIMEET_SEED_DEMO=false 时属预期),跳过用户列表' };
  }

  const users = await api('GET', `/v1/channels/${encodeURIComponent(channelName)}/users`);
  const list = users.json.users ?? [];
  ctx.assert(Array.isArray(list), 'users 应为数组');
  ctx.assert(users.json.channel?.name === channelName, '响应应回显频道信息');

  let detail = null;
  if (list.length > 0) {
    const first = list[0];
    const one = await api('GET', `/v1/channels/${encodeURIComponent(channelName)}/users/${first.uid}`);
    detail = one.json;
    ctx.assert(detail.uid === first.uid, '用户详情应与列表中的 uid 一致');
  }

  // 404 分支:不存在的频道必须报 channel_not_found(而不是空列表)。
  const missing = await ctx.client('GET', '/v1/channels/no-such-channel/users');
  ctx.assert(missing.status === 404, '未知频道应返回 404');
  ctx.assert(missing.json.code === 'channel_not_found', `期望 channel_not_found,实际 ${missing.json?.code}`);

  return {
    summary: `${channelName}: ${list.length} 个用户${detail ? `,首个用户 uid=${detail.uid} role=${detail.role} publishing.video=${detail.publishing?.video}` : ''};未知频道 → 404 channel_not_found`,
    details: { channel: users.json.channel, users: list.map((entry) => ({ uid: entry.uid, role: entry.role, userId: entry.userId })) },
  };
}
javascript
export const meta = {
  id: '07-agora-compat-channels',
  title: '声网兼容:频道 / 用户 / 用户状态',
  safe: true,
  needsApi: true,
};

/**
 * 三个 `/dev/v1` 接口与平台 API 一一对应:
 *   GET /dev/v1/channel/{appid}                              → 频道列表
 *   GET /dev/v1/channel/user/{appid}/{channelName}           → 频道内用户(communication 模式返回 users/total)
 *   GET /dev/v1/channel/user/property/{appid}/{uid}/{cname}  → 用户状态(in_channel / role / platform)
 *
 * 注意 mode/role 的数值映射:communication=1、LIVE_BROADCASTING=2;role 1=communication 用户、2=主播、3=观众。
 */
export async function run(ctx) {
  const api = ctx.api;
  const appId = ctx.credentials.appId;

  const channels = await api('GET', `/dev/v1/channel/${appId}`, { query: { page_size: 100 } });
  const list = channels.json.data?.channels ?? [];
  const channelName = list.find((entry) => entry.channel_name === 'weekly-sync')?.channel_name ?? list[0]?.channel_name;

  if (!channelName) {
    return { summary: '没有活跃频道(演示 seed 关闭时属预期),跳过声网用户接口' };
  }

  const users = await api('GET', `/dev/v1/channel/user/${appId}/${encodeURIComponent(channelName)}`);
  const data = users.json.data ?? {};
  const uids = data.users ?? data.broadcasters ?? [];
  ctx.assert(data.channel_exist === true, 'channel_exist 应为 true');

  let status = null;
  if (uids.length > 0) {
    const uid = uids[0];
    const property = await api('GET', `/dev/v1/channel/user/property/${appId}/${uid}/${encodeURIComponent(channelName)}`);
    status = property.json.data;
    ctx.assert(status.in_channel === true, 'in_channel 应为 true');
    ctx.assert(status.uid === uid, '应回显 uid');
  }

  const absent = await api('GET', `/dev/v1/channel/user/${appId}/definitely-no-such-channel`);
  ctx.assert(absent.json.data?.channel_exist === false, '不存在的频道应返回 channel_exist:false');

  return {
    summary:
      `${channelName}: mode=${data.mode} 用户数=${uids.length}` +
      `${status ? `;uid=${status.uid} role=${status.role} platform=${status.platform} join=${status.join}` : ''};` +
      '未知频道 → channel_exist:false',
    details: { channelData: data, userProperty: status },
  };
}

服务端治理全流程(会修改演示频道状态,运行器需加 --all):

javascript
import { seedChannel, uniqueChannel } from './_lib.mjs';

export const meta = {
  id: '15-moderation',
  title: '禁言 / 角色切换 / 封禁规则 / 踢人',
  safe: false, // 会修改演示频道内的用户状态
  needsApi: true,
};

/**
 * 平台接口:
 *   POST /v1/channels/{c}/users/{uid}/mute | unmute | role
 *   GET/POST/PATCH/DELETE /v1/ban-rules
 *   DELETE /v1/channels/{c}/users/{uid}            (踢人 + 建 join_channel 规则)
 * 声网兼容:POST/GET/PUT/DELETE /dev/v1/kicking-rule
 *
 * 语义要点:
 *  - mute 先静音已发布 track,再按 banPublish 建 publish_audio/publish_video 规则阻止重发;
 *  - join_channel 封禁在“签发 Token 时”即被拒绝(403 banned),并在在线时立刻踢下线(离开原因 3);
 *  - 踢人默认建 600s 的 join_channel 规则。
 */
export async function run(ctx) {
  const api = ctx.api;
  const appId = ctx.credentials.appId;
  const channelName = uniqueChannel('docs-mod');
  const uid = 94001;
  await seedChannel(api, appId, channelName, uid);

  // ------------------------------------------------------------------ mute
  const mute = await api('POST', `/v1/channels/${channelName}/users/${uid}/mute`, {
    body: { kind: 'audio', banPublish: true, durationSeconds: 600 },
  });
  ctx.assert(mute.json.muted === true, 'mute 应返回 muted:true');
  ctx.assert(Array.isArray(mute.json.mutedTracks) && mute.json.mutedTracks.length > 0, '应返回被静音的 track sid');
  ctx.assert(Array.isArray(mute.json.banRuleIds) && mute.json.banRuleIds.length > 0, 'banPublish 应创建封禁规则');

  const unmute = await api('POST', `/v1/channels/${channelName}/users/${uid}/unmute`, { body: { kind: 'audio' } });
  ctx.assert(unmute.json.unmuted === true, 'unmute 应返回 unmuted:true');

  // ------------------------------------------------------------ role change
  const demoted = await api('POST', `/v1/channels/${channelName}/users/${uid}/role`, { body: { role: 'audience' } });
  ctx.assert(demoted.json.role === 'audience', '角色应变为 audience');
  const promoted = await api('POST', `/v1/channels/${channelName}/users/${uid}/role`, { body: { role: 'host' } });
  ctx.assert(promoted.json.role === 'host', '角色应恢复为 host');

  // ----------------------------------------------------------- ban rules
  const created = await api('POST', '/v1/ban-rules', {
    body: { channelName, uid, privileges: ['join_channel'], durationSeconds: 600 },
  });
  ctx.assert(created.status === 201, `创建封禁规则应返回 201,实际 ${created.status}`);
  const ruleId = created.json.id;

  const listed = await api('GET', '/v1/ban-rules', { query: { channelName } });
  ctx.assert((listed.json.rules ?? []).some((rule) => rule.id === ruleId), '规则应出现在列表中');

  const patched = await api('PATCH', `/v1/ban-rules/${ruleId}`, { body: { durationSeconds: 120 } });
  ctx.assert(patched.json.expireAt, '更新时长后应返回 expireAt');

  const denied = await ctx.client('POST', '/v1/tokens', { body: { channelName, uid } });
  ctx.assert(denied.status === 403 && denied.json.code === 'banned', '被 join_channel 封禁的 uid 无法获得 Token');

  const removed = await api('DELETE', `/v1/ban-rules/${ruleId}`);
  ctx.assert(removed.status === 204, '删除规则应返回 204');

  const allowed = await api('POST', '/v1/tokens', { body: { channelName, uid } });
  ctx.assert(allowed.status === 200, '解除封禁后应能重新签发 Token');

  // ------------------------------------------------------------------ kick
  const kick = await api('DELETE', `/v1/channels/${channelName}/users/${uid}`, {
    body: { durationSeconds: 600, reason: 'docs-example' },
  });
  ctx.assert(kick.json.kicked === true, '踢人应返回 kicked:true');
  ctx.assert(typeof kick.json.banRuleId === 'number', '踢人应返回 banRuleId');

  const gone = await ctx.client('GET', `/v1/channels/${channelName}/users/${uid}`);
  ctx.assert(gone.status === 404 && gone.json.code === 'user_not_found', '被踢用户应不在频道内');

  // ------------------------------------------------- 声网兼容 kicking-rule
  const agoraCreate = await api('POST', '/dev/v1/kicking-rule', {
    body: { appid: appId, cname: channelName, uid, time: 10, privileges: ['publish_video'] },
  });
  ctx.assert(agoraCreate.json.status === 'success', '声网 kicking-rule 创建应返回 status:success');
  const agoraId = agoraCreate.json.id;

  const agoraList = await api('GET', '/dev/v1/kicking-rule', { query: { appid: appId } });
  ctx.assert((agoraList.json.rules ?? []).some((rule) => rule.id === agoraId), '声网规则列表应包含新规则');

  const agoraUpdate = await api('PUT', '/dev/v1/kicking-rule', {
    body: { appid: appId, id: agoraId, time_in_seconds: 120 },
  });
  ctx.assert(agoraUpdate.json.status === 'success', '声网规则更新应成功');

  const agoraDelete = await api('DELETE', '/dev/v1/kicking-rule', { body: { appid: appId, id: agoraId } });
  ctx.assert(agoraDelete.json.status === 'success', '声网规则删除应成功');

  return {
    summary:
      `${channelName}: mute→unmute 成功;role audience↔host;封禁 uid 时 Token 403 banned,解禁后 200;` +
      `踢人得到 banRuleId=${kick.json.banRuleId};声网 kicking-rule 创建/更新/删除闭环`,
    details: {
      mutedTracks: mute.json.mutedTracks,
      kick: kick.json,
      agoraRuleId: agoraId,
    },
  };
}

curl 版本:

bash
#!/usr/bin/env bash
# 频道与用户查询:平台 API 与声网兼容 API 对照
set -euo pipefail

: "${NIMEET_API_BASE:=http://127.0.0.1:8787}"
CRED_FILE=$([ -f 控制面 class="tok-flag">-credentials.json ] && echo 控制面 class="tok-flag">-credentials.json || echo data/demo-credentials.json)
CRED=$(node -e "const c=JSON.parse(require('fs').readFileSync(process.argv[1],'utf8'));process.stdout.write(Buffer.from(c.customerId+':'+c.customerSecret).toString('base64'))" "$CRED_FILE")
APP_ID=$(node -e "process.stdout.write(JSON.parse(require('fs').readFileSync(process.argv[1],'utf8')).appId)" "$CRED_FILE")

# 平台 API:camelCase 字段 + 统一分页
curl -s "$NIMEET_API_BASE/v1/channels?page_no=0&page_size=100" \
  -H "Authorization: Basic $CRED"

# 频道内用户(channelName 路径参数)
curl -s "$NIMEET_API_BASE/v1/channels/weekly-sync/users" \
  -H "Authorization: Basic $CRED"

# 声网兼容:频道列表(success/data 信封,snake_case 字段)
curl -s "$NIMEET_API_BASE/dev/v1/channel/$APP_ID?page_no=0&page_size=100" \
  -H "Authorization: Basic $CRED"

# 声网兼容:频道内用户(communication 模式返回 users/total,LIVE 模式返回 broadcasters/audience)
curl -s "$NIMEET_API_BASE/dev/v1/channel/user/$APP_ID/weekly-sync" \
  -H "Authorization: Basic $CRED"

# 声网兼容:用户状态(in_channel / role / platform)
curl -s "$NIMEET_API_BASE/dev/v1/channel/user/property/$APP_ID/10001/weekly-sync" \
  -H "Authorization: Basic $CRED"

已知限制

  • ip 规则当前只记录不参与判定:join_channel 判定按 (uid, channelName) 进行(签发 Token 时没有客户端 IP 可信来源)。需要按 IP 治理时请在网关层完成。
  • mute / role / kick 的目标必须是当前在线的参与者,否则返回 404 user_not_found / channel_not_found。
  • GET /v1/ban-rules 只返回未过期规则;管理「已过期历史」暂不提供。
  • durationSeconds: 0 在实现里落为 expireAt: null(永久),与「仅本次踢下线、可立即重进」的常见预期不同,接入时请显式传正数。

本页由文档构建流水线生成 · 2026-10-31 01:24:02 UTC · 文档中心 v1.1.0.

内容依据公开 API 定义.

ni-meet 是自建 RTC PaaS,媒体面基于 LiveKit(Apache-2.0),并与声网 API 兼容。