> ## Documentation Index
> Fetch the complete documentation index at: https://docs.stmlink.com/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> 对外开放的服务端接口有两组前缀，都用同一套鉴权：`/server/v1/...`（SRTC 与 SMeeting 的主接口）和 `/stm/srvapi/v1/...`（SMeeting 的用户体系，服务端极简对接会用到）。鉴权是 app_id + nonce + timestamp + signature 四个请求头，用 app_key 做 HMAC-SHA256 签名，只能从业务方自己的后端调用。除这两组前缀外的接口均为内部接口，不要建议客户调用。
> app_key 是服务端密钥，绝不能出现在客户端代码、前端配置或移动 App 里。客户端加入频道用的 token 必须由业务方后端签发后下发（SRTC 走 `/server/v1/channel/grant`，SMeeting 走 `/stm/srvapi/v1/member/grant`）。
> SRTC 与 SMeeting 是上下两层不同的产品，术语不通用：SRTC 是音视频底座，说「频道 channel」「加入 / 退出」；SMeeting 建在 SRTC 之上，说「房间 room」「会议 meeting」「进入 / 退出」。回答时按用户所在的层用对应术语，不要把「房间」「会议」安到 SRTC 的接口上。
> 同一能力在各端 SDK 里的包名、类名、方法名并不相同。写示例代码时请使用文档中该端自己的 API，不要把一个端的写法套到另一个端上。苹果平台每个产品都有两套 SDK（Swift 原生与 Objective-C），两套 API 不能混用。

# 聊天与自定义消息

> 群发与私聊、四种消息类型、自定义消息、禁言控制

### 发消息

```typescript theme={null}
sendRoomChatMessage(msg: string, type?: ChatMsgType, targetId?: string): Promise<void>
```

```typescript theme={null}
// 群发文本
await meeting.sendRoomChatMessage('大家好');

// 私聊
await meeting.sendRoomChatMessage('单独说一句', ChatMsgType.text, targetUid);

// 发图片（自己先上传，这里发的是元信息）
await meeting.sendRoomChatMessage(
  JSON.stringify({ url: uploadedUrl, w: 800, h: 600 }),
  ChatMsgType.pic
);
```

| 参数         | 说明                      |
| ---------- | ----------------------- |
| `msg`      | 消息内容，**始终是字符串**         |
| `type`     | `ChatMsgType`，默认 `text` |
| `targetId` | 传了就是私聊，不传是群发            |

`ChatMsgType`：`text = 1` / `file = 2` / `pic = 3` / `sound = 4`。

<Note>
  **SDK 不传文件。** `file` / `pic` / `sound` 类型只是给消息打个标记，
  实际内容要你自己上传（可以用 SDK 的预签名接口，见
  [资源与附件](/zh/meeting/harmony/advanced/resources)），然后把 URL 或元信息
  序列化成字符串发出去。

  也就是说 `msg` 的结构由你和自己的客户端约定，SDK 不解析它。
</Note>

***

### 收消息

```typescript theme={null}
onChatMessage: (m, data) => {
  // data.msgType: ChatMsgType
  // data.msg: string
  // data.uid?: string       发送者
  // data.isPrivate: boolean 是否私聊
  if (data.msgType === ChatMsgType.text) {
    this.messages = [...this.messages, data];
  } else if (data.msgType === ChatMsgType.pic) {
    const meta = JSON.parse(data.msg) as Record<string, Object>;
    // 按自己的约定解析
  }
}
```

<Warning>
  `data.uid` 是**可选**的 —— 系统消息可能没有发送者。渲染前判空。
</Warning>

***

### 自定义消息

跟聊天分开的一条通道，用于业务自己的信令（比如投票、白板同步指令）：

```typescript theme={null}
onCustomMessage: (m, data) => {
  // data.msg / data.uid? / data.isPrivate
  const payload = JSON.parse(data.msg) as Record<string, Object>;
}
```

<Note>
  自定义消息**只有接收事件，没有发送接口** —— 发送由业务后端负责。
  与 SRTC 的 `onCustomMessage` 是同一个设计。

  需要客户端之间互发业务信令时，用 `sendRoomChatMessage` 配一个自定的
  `ChatMsgType` 语义，或者走自己的后端。
</Note>

***

### 禁言

```typescript theme={null}
// 全体禁言
await meeting.adminUpdateRoomChatDisabled(true);

// 单独禁言某人
await meeting.adminUpdateUserChatDisabled(uid, true);
```

对应事件：

```typescript theme={null}
onRoomChatDisabledChange: (m, data) => {
  // data.chatDisabled / data.opUid
  this.chatEnabled = !data.chatDisabled;
},
onUserChatDisabledChange: (m, data) => {
  // data.uid / data.chatDisabled / data.opUid
  if (data.uid === this.myUid) {
    this.chatEnabled = !data.chatDisabled;
  }
}
```

<Note>
  输入框的可用性要**同时**看两个来源：房间级 `RoomInfo.chatDisabled` 和
  自己的 `MeetingUserInfo.chatDisabled`。任一为 `true` 就该禁用。

  ```typescript theme={null}
  const room = meeting.getRoomInfo();
  const me = meeting.getUserInfo(this.myUid);
  const canChat = room !== undefined && !room.chatDisabled && !me.chatDisabled;
  ```
</Note>

被禁言时也可以举手申请（`HandupType.chat`），见[举手](/zh/meeting/harmony/advanced/handup)。

***

### 一个最小的聊天面板

```typescript theme={null}
@State messages: RoomChatMsgEventData[] = [];
@State canChat: boolean = true;

private delegate: SMeetingDelegate = {
  onChatMessage: (m, data) => {
    this.messages = [...this.messages, data];      // 建新数组
  },
  onRoomChatDisabledChange: (m, data) => {
    this.refreshCanChat(m);
  },
  onUserChatDisabledChange: (m, data) => {
    if (data.uid === this.myUid) {
      this.refreshCanChat(m);
    }
  }
};
```

<Warning>
  ArkTS 的 `@State` 只观测第一层赋值 —— 消息列表永远用「建新数组再整体赋值」，
  `this.messages.push(...)` 不会刷新 UI。

  消息多了要注意截断（比如只留最近 200 条），否则每次都整体重建大数组会卡。
</Warning>

***

### 相关阅读

* [SMeetingEngine](/zh/meeting/harmony/api-reference/SMeetingEngine)
* [资源与附件](/zh/meeting/harmony/advanced/resources) —— 文件上传
* [事件参考](/zh/meeting/harmony/events)
