> For the complete documentation index, see [llms.txt](https://docs.convai.com/api-docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.convai.com/api-docs/zh/cha-jian-yu-ji-cheng/web-plugins/convai-web-sdk/published-text-chat.md).

# 连接到已发布的文本聊天

通过一次性启动授权，从已发布的 Convai 角色开始一段无需凭证、仅文本的聊天，可在 React、原生 TypeScript 中使用，或作为可复制粘贴的嵌入内容。

让访客与已发布的角色聊天，而无需在浏览器中放入 API 密钥、角色 ID 或任何可重复使用的凭据。当你将已发布聊天嵌入自己的应用中，或当你正在把页面从已移除的 `<convai-chat>` 元素。

### 已发布聊天的工作原理

已发布聊天由 publication ID 标识。浏览器永远不会看到角色的配置；它通过 publication API 上的两个公开端点以及与 Core 的一次交互来工作：

1. 获取公开描述符：显示名称、描述、图片和模式（始终为 `"text"`).
2. 生成一个短期、一次性使用的启动授权。
3. 将授权传给 `connectWithPublicationGrant()`，它会与 Core 交换并加入会话。

该会话仅限文本。 `client.connectionType` 是 `"text"` 并且 `client.isPublishedChatSession` 是 `true`，而且 Core 签发的房间令牌不允许发布麦克风或摄像头轨道。授权只能使用一次，所以每个新会话都需要新的授权。

步骤 1 和 2 的辅助函数位于 `@convai/web-sdk/embed`。它们没有传输层依赖，可以安全地在服务器上导入。

### 先决条件

* `@convai/web-sdk` 已安装 1.9.0 或更高版本。
* 一个已发布角色的 publication ID（UUID）。
* 你的部署所对应的 publication API 源和 Core 源。默认值为 `https://api.convai.com` 并且 `https://realtime-api.convai.com`；对于预发布或本地部署，请显式传入两者。

### 连接并显示聊天

`loadPublishedChatSession()` 会同时获取描述符和一个授权。使用该授权连接，根据描述符为组件设置标签，并将 `requestPublicationGrant` 交给组件，这样它就能自行启动后续会话。

{% tabs %}
{% tab title="React" %}
{% code title="src/PublishedChat.tsx" %}

```tsx
import { useState } from 'react';
import { useConvaiClient } from '@convai/web-sdk/react';
import { ConvaiWidget } from '@convai/web-widget/react';
import {
  getPublishedChatEndpoints,
  loadPublishedChatSession,
  requestPublishedChatGrant,
} from '@convai/web-sdk/embed';

const publicationId = 'PUBLICATION_UUID';
const publicationApiUrl = 'https://api.convai.com';
const coreUrl = 'https://realtime-api.convai.com';

export default function PublishedChat() {
  const client = useConvaiClient();
  const [descriptor, setDescriptor] =
    useState<{ display_name: string; image_url: string | null }>();

  const connect = async () => {
    const session = await loadPublishedChatSession(publicationId, publicationApiUrl);
    setDescriptor(session.descriptor);
    await client.connectWithPublicationGrant(session.launchToken, { url: coreUrl });
  };

  const { launchGrantUrl } = getPublishedChatEndpoints(publicationId, publicationApiUrl);

  return (
    <>
      <button onClick={connect}>开始聊天</button>
      <ConvaiWidget
        convaiClient={client}
        defaultVoiceMode={false}
        publishedChatDescriptor={descriptor}
        publishedChatConnectOptions={{ url: coreUrl }}
        requestPublicationGrant={() => requestPublishedChatGrant(launchGrantUrl.toString())}
      />
    </>
  );
}
```

{% endcode %}
{% endtab %}

{% tab title="TypeScript" %}
{% code title="src/main.ts" %}

```typescript
import { ConvaiClient } from '@convai/web-sdk/core';
import { createConvaiWidget } from '@convai/web-widget/vanilla';
import {
  getPublishedChatEndpoints,
  loadPublishedChatSession,
  requestPublishedChatGrant,
} from '@convai/web-sdk/embed';

const publicationId = 'PUBLICATION_UUID';
const publicationApiUrl = 'https://api.convai.com';
const coreUrl = 'https://realtime-api.convai.com';

const client = new ConvaiClient();

const { launchGrantUrl } = getPublishedChatEndpoints(publicationId, publicationApiUrl);
const session = await loadPublishedChatSession(publicationId, publicationApiUrl);
await client.connectWithPublicationGrant(session.launchToken, { url: coreUrl });

createConvaiWidget(document.body, {
  convaiClient: client,
  defaultVoiceMode: false,
  publishedChatDescriptor: session.descriptor,
  publishedChatConnectOptions: { url: coreUrl },
  requestPublicationGrant: () => requestPublishedChatGrant(launchGrantUrl.toString()),
});
```

{% endcode %}
{% endtab %}
{% endtabs %}

`useConvaiClient()` 并且 `new ConvaiClient()` 这里无需任何配置：授权已包含会话所需的一切。

该描述符可以安全显示。角色配置、API 密钥和其他仅发布者可见的数据都不会返回到浏览器。

### 无需使用组件连接

使用 `requestPublishedChatGrant()` 并连接。等待 `botReady` 再发送第一条消息。

{% code title="src/headless.ts" %}

```typescript
import { ConvaiClient } from '@convai/web-sdk/core';
import { getPublishedChatEndpoints, requestPublishedChatGrant } from '@convai/web-sdk/embed';

const { launchGrantUrl } = getPublishedChatEndpoints('PUBLICATION_UUID', 'https://api.convai.com');
const launchToken = await requestPublishedChatGrant(launchGrantUrl.toString());

const client = new ConvaiClient();
const unsubscribe = client.on('botReady', () => {
  unsubscribe();
  client.sendUserTextMessage('Hello!');
});

await client.connectWithPublicationGrant(launchToken, { url: 'https://realtime-api.convai.com' });
```

{% endcode %}

### 开始新会话

该授权会被第一次连接消耗，不会保留用于重新连接。每个新会话都需要请求一个新的授权。

组件通过 `requestPublicationGrant`。重置、断开连接以及重新打开面板时都会调用它，以获取新的授权，而不是重用已用尽的授权。没有它，从组件中启动新会话会抛出 `已发布聊天组件需要 requestPublicationGrant 才能启动新会话`.

`requestPublicationGrant` 可以调用你自己的后端而不是 publication API，只要它返回的是 `launch_token` 字符串：

{% code title="src/PublishedChat.tsx" %}

```tsx
<ConvaiWidget
  convaiClient={client}
  publishedChatDescriptor={descriptor}
  requestPublicationGrant={async () => {
    const response = await fetch(`/api/chat/${publicationId}/grant`, { method: 'POST' });
    if (!response.ok) throw new Error('Unable to relaunch published chat');
    return (await response.json()).launch_token;
  }}
/>
```

{% endcode %}

如果你构建自己的界面并保留 `PublishedChatConnectOptions` 在不同会话之间传递它们，请通过 `freshPublicationGrantOptions()` 在每次新会话前。它会保留 `url`, `iceTransportPolicy` 并且 `signal`，并丢弃 `connectAttemptId`，它只能在重试同一个授权时复用。

```typescript
import { freshPublicationGrantOptions, requestPublishedChatGrant } from '@convai/web-sdk/embed';

const launchToken = await requestPublishedChatGrant(launchGrantUrl.toString());
await client.connectWithPublicationGrant(launchToken, freshPublicationGrantOptions(savedOptions));
```

### 重试、取消和错误

`connectWithPublicationGrant()` 会为每次交换发送一个幂等键， `connectAttemptId`。SDK 在你省略它时会生成一个。如果交换因网络错误、5xx 状态或格式错误的响应而失败，SDK 会使用相同的键重试一次，因此 Core 会重放同一条逻辑连接，而不会让授权悬空或创建第二个会话。

{% hint style="warning" %}
不要缓存授权、不要把它放进 URL 中，也不要用新生成的授权重试。如果你在 SDK 之外再加一层自己的重试，请使用相同的 `connectAttemptId` 并使用同一个授权。
{% endhint %}

| 情况                         | 会发生什么                                                                                                        |
| -------------------------- | ------------------------------------------------------------------------------------------------------------ |
| 该授权不是一个 32 到 512 个字符的字符串   | 抛出 `需要有效的 publication 启动令牌` 在任何请求之前。                                                                         |
| 客户端已连接或正在连接                | 抛出 `在重新连接前请先断开当前会话`。调用 `disconnect()` 首先。                                                                    |
| `signal` 中止                | 交换或房间连接会被取消并拆除，Promise 将以一个 `AbortError`.                                                                    |
| Core 拒绝该授权                 | Promise 会以服务器消息拒绝， `error` 事件会携带相同错误触发，并且 `client.requestTraceId` 在服务器返回 trace ID 时保存该 ID。                   |
| publication API 拒绝描述符或授权请求 | `loadPublishedChatSession()` 或 `requestPublishedChatGrant()` 会以服务器消息拒绝，或者 `无法加载已发布聊天（状态）` / `无法启动已发布聊天（状态）`. |

引述 `client.requestTraceId` 在报告连接失败时。

### 网络与部署

已发布聊天通过 WebRTC 连接，并使用 `iceTransportPolicy: 'relay'` 作为默认值，因此流量会通过 TURN 服务器。Convai 托管的 Core 已提供此服务。对于没有 TURN 的部署，请传入 `iceTransportPolicy: 'all'`.

```typescript
await client.connectWithPublicationGrant(launchToken, {
  url: 'https://realtime.example.com',
  iceTransportPolicy: 'all',
});
```

### 从服务器调用授权辅助函数

在浏览器中， `requestPublishedChatGrant()` 并且 `loadPublishedChatSession()` 会为每个浏览器创建一个 requester ID，并随每次授权请求一起发送。在服务器上或进行服务端渲染时，没有可供其作用域限定的浏览器，因此除非你为所服务的访客传入 requester ID，否则此调用会抛出错误：16 到 128 个字母、数字、 `_` 或 `-`.

```typescript
const launchToken = await requestPublishedChatGrant(
  launchGrantUrl.toString(),
  fetch,
  undefined,        // signal
  visitorScopedId,  // 每个访客一个 ID
);
```

{% hint style="warning" %}
不要为每个访客重用同一个进程级 requester ID。
{% endhint %}

### 无需构建步骤即可嵌入

对于只需要直接嵌入聊天的页面，请使用 `<convai-widget>` 来自 [`@convai/web-widget`](https://www.npmjs.com/package/@convai/web-widget)的元素。它会为每个会话生成自己的授权，并根据公开描述符为其标题栏设置标签，因此宿主页面永远不会持有角色 ID 或凭据。

{% code title="index.html" %}

```html
<script src="https://cdn.jsdelivr.net/npm/@convai/web-widget/v1.js" async></script>

<convai-widget
  publication-id="PUBLICATION_UUID"
  publication-api-url="https://api.convai.com"
  core-url="https://realtime-api.convai.com"
  placement="inline"
  style="display:block;width:100%;height:640px">
</convai-widget>
```

{% endcode %}

有两个细节很重要，任一出错都会导致无法渲染：

* 不要添加 `type="module"` 到 script 标签中。加载器会读取 `document.currentScript`，它在 `null` 模块脚本中是。
* 设置 `placement="inline"` 用于由 `style`定义尺寸的面板。否则，组件会以右下角悬浮启动器的形式显示。

主题、事件以及该元素的其他属性记录在 `@convai/web-widget` 包中。

### 从 `<convai-chat>`

1.9.0 版本移除了 `<convai-chat>` 元素以及 `@convai/web-sdk/embed/browser` 入口点（`dist/embed/chat-embed-v1.js`）。请替换 script 标签和该元素；publication 属性保持不变。

{% code title="index.html" %}

```html
<!-- before -->
<script
  type="module"
  src="https://cdn.jsdelivr.net/npm/@convai/web-sdk@X/dist/embed/chat-embed-v1.js"
</script>
<convai-chat
  publication-id="…"
  publication-api-url="…"
  core-url="…"
</convai-chat>

<!-- after -->
<script
  src="https://cdn.jsdelivr.net/npm/@convai/web-widget@X/v1.js"
  async
</script>
<convai-widget
  publication-id="…"
  publication-api-url="…"
  core-url="…"
  placement="inline"
</convai-widget>
```

{% endcode %}

请注意 `type="module"` 已移除，并且 `placement="inline"` 已添加。 `@convai/web-sdk/embed` 并且 `connectWithPublicationGrant()` 中的辅助函数

### API 参考

| 属性 / 方法                                          | 类型                                                                                                    | 描述                                                                          |
| ------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
| `connectWithPublicationGrant`                    | `(launchToken: string, options?: PublishedChatConnectOptions) => Promise<void>`                       | 与 Core 交换一次性授权，并打开一个仅文本会话。                                                  |
| `PublishedChatConnectOptions.url`                | `string`                                                                                              | Core 源。默认为 `https://realtime-api.convai.com`.                               |
| `PublishedChatConnectOptions.iceTransportPolicy` | `RTCIceTransportPolicy`                                                                               | 默认为 `'relay'`。仅在部署没有 TURN 服务器时使用 `'all'` 。                                  |
| `PublishedChatConnectOptions.signal`             | `AbortSignal`                                                                                         | 取消正在进行的授权交换或房间连接，并将其拆除。                                                     |
| `PublishedChatConnectOptions.connectAttemptId`   | `string`                                                                                              | 幂等键。若未提供则自动生成；仅在重试同一授权时复用。                                                  |
| `client.connectionType`                          | `'audio' \| 'video' \| 'text' \| null`                                                                | `'text'` 适用于已发布聊天会话。                                                        |
| `client.isPublishedChatSession`                  | `boolean`                                                                                             | 客户端上次是否通过 publication 授权连接。                                                 |
| `client.requestTraceId`                          | `string \| null`                                                                                      | 最近一次连接请求的服务器 trace ID（如有提供）。                                                |
| `loadPublishedChatSession`                       | `(publicationId, publicationApiUrl?, fetch?, signal?, requesterId?) => Promise<PublishedChatSession>` | 获取公开描述符和一个授权。解析为 `{ descriptor, launchToken }`.                             |
| `requestPublishedChatGrant`                      | `(launchGrantUrl, fetch?, signal?, requesterId?) => Promise<string>`                                  | 为新会话生成授权。服务器调用方必须传入 `requesterId`.                                          |
| `getPublishedChatEndpoints`                      | `(publicationId, publicationApiUrl?) => PublishedChatEndpoints`                                       | 返回已验证的 `descriptorUrl` 并且 `launchGrantUrl`。如果 publication ID 不是 UUID，则抛出错误。 |
| `fetchPublishedChatDescriptor`                   | `(descriptorUrl, fetch?, signal?) => Promise<PublishedChatDescriptor>`                                | 仅获取公开描述符。                                                                   |
| `freshPublicationGrantOptions`                   | `(options?: PublishedChatConnectOptions) => PublishedChatConnectOptions \| undefined`                 | 复制选项但不包含 `connectAttemptId`，用于新会话的开始。                                       |
| `PublishedChatDescriptor`                        | `{ publication_id, display_name, description, image_url, mode: 'text' }`                              | 某个发布内容的公开展示数据。                                                              |
| `requestPublicationGrant` （组件选项）                 | `() => Promise<string>`                                                                               | 当组件启动后续会话时生成新的授权。已发布聊天必需。                                                   |
| `publishedChatConnectOptions` （组件选项）             | `PublishedChatConnectOptions`                                                                         | 组件传递给 `connectWithPublicationGrant()`.                                      |
| `publishedChatDescriptor` （组件选项）                 | `{ display_name, image_url }`                                                                         | 无需查询角色即可为组件标题栏设置标签。                                                         |


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.convai.com/api-docs/zh/cha-jian-yu-ji-cheng/web-plugins/convai-web-sdk/published-text-chat.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
