> 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/link-previews.md).

# 渲染链接和链接预览

将角色发送的链接转换为可点击链接、媒体卡片和 Open Graph 站点卡片，可在 Convai 小组件中或你自己的聊天界面中使用。

将字符发送的链接渲染为可点击链接，并以图片、视频和文档卡片显示，而不是原始 `[文档](https://…)` markdown。使用此页面开启链接，或通过 `linkPreviewResolver`，或在你自己构建的聊天 UI 中安全地渲染链接。

### 先决条件

* 已安装 Convai Web SDK，并且有一个可连接的客户端。参见 [Web SDK 快速入门](/api-docs/zh/cha-jian-yu-ji-cheng/web-plugins/convai-web-sdk/quickstart.md).

{% hint style="warning" %}
链接取决于服务器端设置。 `preserveLinksInOutput` 是一个按会话发送的标志，发送于 `/connect` 由实时和 WebSocket 传输，服务器默认关闭。Convai 托管的服务器会遵守它。若不启用，在文本到达客户端前服务器会移除每个 URL 的协议，因此指向 `https://docs.convai.com` 的 markdown 链接会变成指向 `docs.convai.com`，而小组件会将其显示为点状文本标签；裸 URL 则会作为纯文本到达，例如 `example.com/guide.pdf`。无论哪种情况都不会报错。
{% endhint %}

### 开启链接

设置 `preserveLinksInOutput: true` 在客户端配置中。 `ConvaiWidget` 并且 `createConvaiWidget` 无需再做任何额外接线即可渲染链接。

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

```tsx
import { useConvaiClient } from '@convai/web-sdk/react';
import { ConvaiWidget } from '@convai/web-widget/react';

export default function App() {
  const client = useConvaiClient({
    apiKey: 'YOUR_API_KEY',
    characterId: 'YOUR_CHARACTER_ID',
    preserveLinksInOutput: true,
  });

  return <ConvaiWidget convaiClient={client} />;
}
```

{% 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';

const client = new ConvaiClient({
  apiKey: 'YOUR_API_KEY',
  characterId: 'YOUR_CHARACTER_ID',
  preserveLinksInOutput: true,
});

createConvaiWidget(document.body, { convaiClient: client });
```

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

* 该标志在会话连接时只读取一次。若要更改它，请重新连接；不重新连接的 UI 切换不会影响当前会话。
* 该标志以 `preserve_links_in_output` 发送，未设置时则省略，因此采用服务器默认值。如果你在服务器上为 `/connect` 构建 `connectWithConnectionData()`请求，请在那里添加该字段。
* 如果你自己渲染 `chatMessages` ，在 `content` 中启用该标志后应预期看到 markdown 链接。语音不受影响；URL 绝不会被朗读。
* SSE 传输不会发送该标志，Interaction API 也没有按请求对应项：通过 SSE 的链接遵循部署的 `PRESERVE_LINKS_IN_LLM_OUTPUT` 设置，该设置默认关闭。在 SSE 客户端上设置 `preserveLinksInOutput` 不会产生任何效果。若要在 SSE 聊天中使用链接，请让你的 Convai 联系人针对你的部署启用该设置。

### 每个链接的渲染方式

两个小组件都以相同方式渲染链接。预览仅出现在字符的气泡上。用户自己的气泡保持链接可点击，带有相同的悬停标题，但不会嵌入媒体，因此粘贴的 URL 不会将远程媒体拉入转录文本。

| 链接                                                  | 字符的气泡                                    | 点击                                                     | 如果失败                                                             |
| --------------------------------------------------- | ---------------------------------------- | ------------------------------------------------------ | ---------------------------------------------------------------- |
| 网页                                                  | 带下划线的链接。长裸 URL 会被缩短以便显示；悬停时显示完整 URL。     | 新标签页                                                   | —                                                                |
| 图片（`png jpg gif webp avif`)                         | 带有加载占位骨架的卡片，然后是图片和一个 `主机 · 文件` 说明。       | 会打开灯箱。按 Escape、背景层或 **×** 可将其关闭； **打开原始内容** 会在新标签页中打开。 | `预览不可用 — 主机 · 文件` 在出错或 12 秒未加载后。 `http:` 在 `https:` 页面上的图片从不被请求。 |
| 视频（`mp4 webm mov ogg`)                              | 带原生控件的卡片。                                | 播放、全屏                                                  | 与图片相同。                                                           |
| 文档（`pdf doc docx xls xlsx csv ppt pptx txt md zip`) | 带扩展名瓷片、文件名和 `主机 · 在新标签页中打开`.             | 新标签页                                                   | 不会获取任何内容，因此也不会失败。                                                |
| `mailto:`, `tel:`, `sms:`                           | 带图标和地址的徽标。悬停可解释会发生什么。                    | 交由操作系统处理                                               | 根据浏览器设计，缺少处理程序不会有任何提示。                                           |
| `ftp:`                                              | 带有一个 **FTP** 徽标的链接。悬停会警告大多数浏览器已不再打开这些链接。 | 新标签页                                                   | 浏览器会静默拒绝。                                                        |
| `javascript:`, `data:` 以及其他协议                       | 以点状文本显示的标签，绝不是链接。                        | 无                                                      | —                                                                |

一条消息最多显示 6 张卡片。后续媒体或文档链接会以纯链接形式渲染。

骨架是一个半透明块，基于 `currentColor`，因此它与小组件所在的背景相匹配。其闪烁效果和灯箱进入动画会在 `prefers-reduced-motion`.

#### 在原生小组件中为卡片设置样式

`createConvaiWidget` 提供 `part` 属性，可从宿主页对其进行样式设置：

| 元素   | 部件                                                                                                                                          |
| ---- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| 链接   | `message-link`, `message-link-badge`, `message-link-blocked`, `message-contact`                                                             |
| 媒体卡片 | `message-media-card`, `message-media`, `message-media-zoom`, `message-media-skeleton`, `message-media-caption`, `message-media-unavailable` |
| 文档卡片 | `message-file-card`, `message-file-icon`, `message-file-name`, `message-file-meta`                                                          |
| 图片灯箱 | `lightbox`, `lightbox-backdrop`, `lightbox-image`, `lightbox-caption`, `lightbox-close`, `lightbox-open`                                    |
| 站点卡片 | `message-site-card`, `message-site-favicon`, `message-site-name`, `message-site-title`, `message-site-description`, `message-site-image`    |

### 为网页添加站点卡片

普通网页链接可以显示页面自己的卡片：favicon、站点名称、标题、描述和图片，这些信息读取自其 Open Graph 和 Twitter 标签。浏览器无法获取其他来源的页面，因此你需要提供一个解析器，让后端为你获取元数据。

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

```tsx
import { useConvaiClient, type LinkPreviewResolver } from '@convai/web-sdk/react';
import { ConvaiWidget } from '@convai/web-widget/react';

const resolveLinkPreview: LinkPreviewResolver = async (url, signal) => {
  const res = await fetch(`/api/link-preview?url=${encodeURIComponent(url)}`, { signal });
  return res.status === 200 ? res.json() : null;
};

export default function App() {
  const client = useConvaiClient({
    apiKey: 'YOUR_API_KEY',
    characterId: 'YOUR_CHARACTER_ID',
    preserveLinksInOutput: true,
  });

  return <ConvaiWidget convaiClient={client} linkPreviewResolver={resolveLinkPreview} />;
}
```

{% endcode %}
{% endtab %}

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

```typescript
import { ConvaiClient } from '@convai/web-sdk/core';
import { type LinkPreviewResolver } from '@convai/web-sdk/vanilla';
import { createConvaiWidget } from '@convai/web-widget/vanilla';

const resolveLinkPreview: LinkPreviewResolver = async (url, signal) => {
  const res = await fetch(`/api/link-preview?url=${encodeURIComponent(url)}`, { signal });
  return res.status === 200 ? res.json() : null;
};

const client = new ConvaiClient({
  apiKey: 'YOUR_API_KEY',
  characterId: 'YOUR_CHARACTER_ID',
  preserveLinksInOutput: true,
});

createConvaiWidget(document.body, {
  convaiClient: client,
  linkPreviewResolver: resolveLinkPreview,
});
```

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

`MarkdownRenderer` 将相同的函数作为其 `resolveLinkPreview` 属性。

* 解析器返回 `{ url, title?, description?, image?, siteName?, favicon? }` 或 `null`。仅当结果具有 `标题` 或 `图片`.
* 时才会绘制卡片。解析器运行时不会显示任何内容，失败或返回 `null`时也不会显示任何内容。链接本身不受影响。
* 每个 URL 的结果会在页面加载期间缓存，因此同一个 URL 被引用两次只需一次请求。
* 站点卡片仅显示在字符的气泡上，并计入 6 张卡片的限制。媒体和文档链接保持各自的卡片，不会被展开。
* `图片` 并且 `favicon` 仅在它们是绝对 `http(s)` URL 时才使用。所有其他字段都会以文本形式渲染，而不是标记。

你的解析器端点代表用户抓取页面。需要会话，DNS 解析后拒绝私人和回环地址，对每次重定向重新检查，限制时间和响应大小，并且绝不转发 cookie。

### 在你自己的聊天 UI 中渲染链接

如果你使用 `useConvaiClient` 或 `ConvaiClient` 进行连接，但自己绘制消息列表，字符的文本会以纯字符串形式到达你这里。可使用 SDK 的气泡渲染器或其 tokenizer 来渲染。

#### 在 React 中使用气泡渲染器

`MarkdownRenderer` 是 React 小组件绘制的组件，因此你无需额外代码即可获得卡片、骨架屏和协议白名单。向用户自己的气泡传入 `media={false}` 以保留链接但移除嵌入内容。

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

```tsx
import { MarkdownRenderer } from '@convai/web-widget/react';

export function Bubble({ text, isMe }: { text: string; isMe: boolean }) {
  return <MarkdownRenderer text={text} media={!isMe} />;
}
```

{% endcode %}

#### 基于 tokenizer 构建渲染器

`parseInlineMarkdown` 将一行文本转换为你映射到自己标记上的节点。它导出自 `@convai/web-sdk/core`, `@convai/web-sdk/vanilla` 并且 `@convai/web-sdk/react`.

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

```typescript
import { parseInlineMarkdown } from '@convai/web-sdk/core';

for (const node of parseInlineMarkdown(line)) {
  switch (node.kind) {
    case 'link':
      // node.media 是 'image' | 'video' | null：用于提示是否嵌入。
      renderAnchor(node.href, node.label, node.media);
      break;
    case 'image':
      renderImage(node.src, node.alt);
      break;
    case 'blocked':
      // 不安全的协议。将标签作为纯文本显示，绝不作为 href。
      renderPlainText(node.label);
      break;
    default:
      renderText(node);
  }
}
```

{% endcode %}

* 一个其关闭 `)` 尚未流入的链接仍保持为文本，因此气泡不会在响应过程中在不同形态之间闪烁。
* `compactUrl` 会将较长的裸 URL 缩短以便显示，例如 `a.example.com/…/file.mp4`，而不改变 href。绝不要将其结果用作 href。
* `linkKindForUrl` 并且 `describeUrl` 会给你相同的分类和 `主机 · 文件` 说明，这与小组件使用的一致，且 `resolveLinkPreviewCached` 如果你自己绘制站点卡片，它会为你提供小组件共享的解析器缓存。

### 协议白名单

仅 `http`, `https`, `ftp`, `ftps`, `mailto`, `tel` 并且 `sms` 会变成链接。其他所有内容，包括 `javascript:` 并且 `data:`，都会变成一个 `blocked` 节点，并且小组件会将其渲染为文本。

{% hint style="warning" %}
绝不要把 `blocked` 节点的 `href` 放到锚点上。服务器不会净化协议：一个目标为 `javascript:alert(1)` 的 markdown 链接会原样到达客户端，因此客户端是唯一的防护。若你自己编写 tokenizer 而不是 `parseInlineMarkdown`，在渲染前用 `isSafeHref` 检查每个 href。
{% endhint %}

### API 参考

| 属性 / 方法                    | 类型                                                                                      | 描述                                                                                                                                   |
| -------------------------- | --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `preserveLinksInOutput`    | `boolean`                                                                               | 客户端选项。发送于 `/connect` 作为 `preserve_links_in_output`。服务器默认关闭。连接时读取一次。                                                                  |
| `linkPreviewResolver`      | `LinkPreviewResolver`                                                                   | `ConvaiWidget` 属性和 `createConvaiWidget` 选项。用于普通网页链接的 Open Graph 查找；若不需要站点卡片则省略。                                                      |
| `MarkdownRenderer`         | `React.FC<{ text: string; media?: boolean; resolveLinkPreview?: LinkPreviewResolver }>` | React 小组件的气泡渲染器。 `media` 默认为 `true`；将 `false` 传给用户自己的气泡。                                                                             |
| `resolveLinkPreview`       | `LinkPreviewResolver`                                                                   | `MarkdownRenderer` 用于与 `linkPreviewResolver`.                                                                                        |
| `LinkPreviewResolver`      | `(url: string, signal: AbortSignal) => Promise<LinkPreviewMeta \| null>`                | 你的后端调用。                                                                                                                              |
| `LinkPreviewMeta`          | `{ url, title?, description?, image?, siteName?, favicon? }`                            | 解析器返回的内容。                                                                                                                            |
| `resolveLinkPreviewCached` | `(resolver, url) => Promise<LinkPreviewMeta \| null>`                                   | 渲染器共享的按 URL 缓存。                                                                                                                      |
| `parseInlineMarkdown`      | `(line: string) => InlineNode[]`                                                        | 将一行文本转换为 `文本`, `strong`, `em`, `link`, `图片` 并且 `blocked` 节点。                                                                         |
| `InlineNode`               | 联合类型                                                                                    | `{ kind: 'link', href, label, media }`, `{ kind: 'image', src, alt }`, `{ kind: 'blocked', href, label }`，以及 `文本`, `strong` 并且 `em`. |
| `isSafeHref`               | `(url: string) => boolean`                                                              | `true` 当 URL 的协议在白名单中且可用作 href 时。                                                                                                    |
| `mediaKindForUrl`          | `(url: string) => 'image' \| 'video' \| null`                                           | 按文件扩展名对 URL 分类； `null` 表示不可预览。                                                                                                       |
| `linkKindForUrl`           | `(url: string) => LinkKind`                                                             | 安全 URL 的展示类别： `网页`, `图片`, `视频`, `文档`, `电子邮件`, `电话`, `sms` 或 `ftp`.                                                                   |
| `LinkKind`                 | 联合类型                                                                                    | 这些值 `linkKindForUrl` 返回。                                                                                                             |
| `describeUrl`              | `(url: string) => string`                                                               | `主机 · 文件`，用于说明。                                                                                                                      |
| `compactUrl`               | `(url: string, max?: number) => string`                                                 | 仅用于显示的长 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/link-previews.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.
