# AI 助手 SDK

## 版本说明

**⚠️ 重要提示**：从 0.2.x 版本开始，`@ray-js/t-agent-plugin-assistant` 已被废弃，请使用 `@ray-js/t-agent-plugin-aistream` 替代。

## 安装（ray 小程序）

```shell
yarn add @ray-js/t-agent @ray-js/t-agent-plugin-aistream @ray-js/t-agent-ui-ray
```

> 确保 `@ray-js/t-agent` `@ray-js/t-agent-plugin-aistream` `@ray-js/t-agent-ui-ray` 版本一致

## 小程序 kit 要求

```json
{
  "dependencies": {
    "BaseKit": "3.12.0",
    "BizKit": "4.10.0",
    "DeviceKit": "4.6.1",
    "HomeKit": "3.4.0",
    "MiniKit": "3.12.1",
    "AIStreamKit": "1.3.2"
  },
  "baseversion": "2.21.10"
}
```

> 使用 MCP 功能时，`AIStreamKit` 需要 `2.2.1` 或以上版本。

## package.json 依赖要求

```json
{
  "dependencies": {
    "@ray-js/ray": ">=1.6.8"
  }
}
```

## 开发

```shell
# 开发 sdk
yarn run dev

# 开发模板小程序
yarn run miniapp
```

## 使用示例

使用 ray ui 实现一个对话页面

![ChatPage1](https://static1.tuyacn.com/static/txp-ray-TAgent/ChatPage1.png)

```tsx
// ChatPage.tsx
import React from 'react';
import { View } from '@ray-js/components';
import { createChatAgent, withDebug, withUI } from '@ray-js/t-agent';
import {
  ChatContainer,
  defaultRenderOptions,
  MessageInput,
  MessageList,
  MessageActionBar,
} from '@ray-js/t-agent-ui-ray';
import { withAIStream, withBuildIn } from '@ray-js/t-agent-plugin-aistream';

const createAgent = () => {
  const agent = createChatAgent(
    withUI(),
    withAIStream({
      earlyStart: true,
      agentId: 'your-agent-id',
      tokenOptions: {
        api: 'm.life.ai.agent.token.get',
        version: '1.0',
        extParams: {
          dialogueMode: 1,
        },
      },
    }),
    withDebug(),
    withBuildIn()
  );

  agent.plugins.aiStream.onUserDataRead((type, data, result) => {
    if (type === 'start-event') {
      result.userData = {
        sessionAttributes: {
          'custom.param': {
            'custom.app.scene': {
              value: 'chat-page',
            },
          },
        },
      };
    }
  });

  const { onChatStart, createMessage } = agent;

  onChatStart(async result => {
    const hello = createMessage({
      role: 'assistant',
    });

    hello.bubble.setText('Hello, world!');
    result.messages.push(hello);
    await hello.persist();
  });

  return agent;
};

const renderOptions = {
  ...defaultRenderOptions,
};

export default function ChatPage() {
  return (
    <View style={{ height: '100vh' }}>
      <ChatContainer createAgent={createAgent} renderOptions={renderOptions}>
        <MessageList />
        <MessageInput />
        <MessageActionBar />
      </ChatContainer>
    </View>
  );
}
```

# t-agent

t-agent 包是使用 TypeScript 编写的对话助手 SDK，用于构建对话智能体，支持插件机制，可以扩展对话智能体的功能。
该包是纯 SDK 包，不包含任何 UI 组件，可以和任何 UI 框架搭配使用。

## 基本概念

![flow.png](https://static1.tuyacn.com/static/txp-ray-TAgent/flow.png)

### ChatAgent 对话智能体 Agent

对话智能体的核心类，负责管理对话的生命周期，消息的创建，消息的持久化等，支持插件和 hook 机制，可以扩展对话智能体的功能。

使用 `createChatAgent` 创建一个 ChatAgent 实例，如下：

```tsx
import { createChatAgent } from '@ray-js/t-agent';
const createAgent = () => {
  /* 在 createChatAgent 参数里应用插件，注意插件是有顺序的 */
  const agent = createChatAgent();

  return agent;
};
```

主要属性：

- `agent.session` ChatSession 会话容器，用于存储会话相关的数据
- `agent.plugins` 应用插件后，会在这里存储插件的相关方法和 Hooks

主要方法：

- `agent.start()` 启动
- `agent.dispose()` 释放
- `agent.pushInputBlocks(blocks, signal)` 从外部将消息 block 推到 ChatAgent 里，用于用户向 AI 发送消息
- `agent.createMessage(data)` 创建一个与当前 Agent 绑定的消息
- `agent.emitTileEvent(tileId: string, payload: any)` tile 发送事件
- `agent.removeMessage(messageId: string)` 删除消息
- `agent.flushStreamToShow(message: ChatMessage, response: StreamResponse, composer: ComposeHandler)` 流式更新消息

### Hooks 机制

ChatAgent 仅定义了一个运行框架和数据结构，具体行为是由 Hook 机制实现的，Hook 机制是一种事件驱动的编程模型，通过注册回调函数来实现对话智能体的行为。

```tsx
import { createChatAgent } from '@ray-js/t-agent';
const createAgent = () => {
  const agent = createChatAgent();

  const { onChatStart } = agent;

  // 在对话开始时触发 onChatStart Hook
  onChatStart(result => {
    console.log('Chat start', result);
  });
  return agent;
};
```

#### Hook 执行顺序

Hook 按照以下顺序执行：

```
onAgentStart → onChatStart/onChatResume → onMessageListInit → onInputBlocksPush
```

ChatAgent 主要 Hook 和参数：

- `agent.onAgentStart` 初始化 Agent
- `agent.onChatStart` 对话开始时触发
  - `result.messages` 用于初始化消息列表
- `agent.onChatResume` 对话恢复时触发
  - `result.messages` 已经恢复好了的消息列表
- `agent.onMessageListInit` 对话开始、对话恢复后，消息列表初始化时触发
  - `result.messages` 用于渲染的消息列表，和前面两个 hooks 的 message 是同一个列表
- `agent.onInputBlocksPush` 推送消息 block 时触发
  - `blocks` 输入的消息块 blocks
  - `signal` 中断信号
- `agent.onMessageChange` 消息变化时触发
  - `type` 变化的类型，`show`、`update`、`remove`
  - `message` 变化的消息
- `agent.onMessagePersist` 消息持久化时触发
  - `payload` 持久化相关参数
  - `message` 持久化的目标消息
- `agent.onTileEvent` 消息持久化完成时触发
  - `tile` 触发事件的 tile
  - `payload` 事件的 payload
- `agent.onAgentDispose` 释放 Agent 时触发
- `agent.onUserAbort` 用户中断时触发
  - `reason` 中断原因
- `agent.onError` 出错时触发
  - `error` 错误对象

这些 hooks 都接收一个回调函数作为参数，当触发时会调用这个回调函数，回调函数可以是同步也可以是异步，回调函数的返回不影响结果，
如果需要改变结果，可以通过修改回调函数的 result 参数或修改 tile、message 对象来修改。

### ChatSession 会话容器

ChatSession 存储和智能体聊天的消息列表，上下文数据等内容，在 ChatAgent 创建时一同创建

主要属性：

- `session.messages` 消息列表
- `session.sessionId` 会话 id
- `session.isNewChat` 是否是新会话，用于区分新会话 `onChatStart` 和恢复会话 `onChatResume`

主要方法：

- `session.set` 设置会话数据，可以是任意类型
- `session.get` 获取会话数据
- `session.getData` 用对象的方式获取会话数据
- `session.getLatestMessage` 获取最后一条消息

Hooks:

- `session.onChange` 注册会话数据变化的回调

### ChatMessage 对话消息

ChatMessage 是对话消息的抽象，用于存储消息的内容，状态等信息，也提供了一系列方便的方法用于操作消息。
一条消息 ChatMessage 下，会有多个 ChatTile，用于展示不同的内容。

创建一条消息使用 `createMessage` 方法，如下：

```tsx
const createAgent = () => {
  const agent = createChatAgent();

  const { createMessage, onChatStart } = agent;

  // 初始化聊天时，发送一条消息
  onChatStart(async result => {
    // 创建一条由智能体助手发送的消息
    const message = createMessage({
      role: 'assistant',
    });

    // 访问 message.bubble 可以快速创建一个文本气泡
    message.bubble.setText('Hello!');
    result.messages.push(message);
  });

  return agent;
};
```

主要属性：

- `message.id` 消息 id
- `message.role` 消息角色，可以是 assistant 或者 user
- `message.tiles` 消息的 tile 列表
- `message.status` 消息状态，`ChatMessageStatus` 枚举，可以是 `START`、`UPDATING`、`FINISH` 等
- `message.meta` 消息附带的额外数据
- `message.isShow` 是否已经展示到界面上
- `message.bubble` 消息中，气泡 tile 的快捷方式

主要方法

- `message.show` 消息展示到界面上
- `message.update` 将当前消息状态更新到界面上
- `message.remove` 将当前消息从界面上移除
- `message.persist` 持久化消息
- `message.addTile` 添加一个 tile
- `message.removeTile` 移除一个 tile
- `message.setTilesLocked` 设置所有的 tile 的锁定状态
- `message.set` 设置消息的属性
- `message.setMetaValue` 按 key-value 设置 meta 的属性
- `message.deleteMetaValue` 删除 meta 的属性
- `message.setMeta` 直接设置 meta 对象
- `message.findTileByType` 通过 tile 类型查找 tile

#### ChatTile 对话消息块

ChatTile 是对话消息的块，用于展示消息内的不同内容，例如文本、图片、卡片等。

为消息添加一个 tile 使用 `addTile` 方法，如下：

```tsx
const message = createMessage({
  role: 'assistant',
});

// 添加一个图片 tile
message.addTile('image', {
  src: '/image.jpg',
});

await message.show();
```

ChatTile 的主要属性：

- `tile.id` tile 的 id
- `tile.type` tile 类型
- `tile.data` tile 数据
- `tile.children` tile 的子 tile
- `tile.locked` tile 是否被锁定
- `tile.fallback` 当 tile 无法展示时的回退内容
- `tile.message` tile 所属的消息

ChatTile 的主要方法：

- `tile.update` 是 `tile.message.update` 的快捷方式
- `tile.show` 是 `tile.message.show` 的快捷方式
- `tile.setLocked` 设置 tile 的锁定状态
- `tile.addTile` 添加一个子 tile
- `tile.setData` 设置 tile 的数据
- `tile.setFallback` 设置 tile 的回退内容
- `tile.findByType` 通过 tile 类型查找子 tile

`message.bubble` 是一个快捷方式，只要访问它，就会在当前的消息中快速添加一个气泡 tile，如下：

```tsx
const message = createMessage({
  role: 'assistant',
});

message.bubble.setText('Hello, world!');
// 等价于
message.addTile('bubble', {}).addTile('text', { text: 'Hello, world!' });

await message.show();
```

针对气泡消息，除了支持 tile 基本方法外，还额外提供了一些属性和方法：

- `message.bubble.text` 属性，读取气泡文本
- `message.bubble.setText` 方法，设置气泡文本
- `message.bubble.isMarkdown` 属性，是否是 markdown 格式
- `message.bubble.setIsMarkdown` 方法，设置是否是 markdown 格式
- `message.bubble.status` 属性，气泡状态 BubbleTileStatus
- `message.bubble.setStatus` 方法，设置气泡状态
- `message.bubble.info` 属性，气泡信息
- `message.bubble.setInfo` 方法，设置气泡信息
- `message.bubble.initWithInputBlocks` 方法，用输入块初始化气泡

**气泡消息支持长按操作**，可以进行复制、删除等操作，同时在消息加载和更新过程中会有动画显示。

气泡消息支持以下长按操作：

- 复制：复制消息文本内容
- 删除：删除当前消息

### 生命周期

ChatAgent 在不同的阶段会触发不同的 Hook，开发者可以通过注册 Hook 来实现自定义行为，下面的时序图展示了 ChatAgent 的生命周期。

```mermaid
sequenceDiagram
  participant AI as Backend(AI)
  participant A as ChatAgent
  participant UI as UI界面
  actor User as 用户

  User ->> UI: 打开聊天界面
  UI ->> UI: 创建 Agent 对象 agent
  UI ->> A: agent.start() 启动 Agent
  rect rgba(255,255,0,0.1)
    note over A: Hook: onAgentStart
    A ->> AI: 获取是否新会话
    AI ->> A: 返回
  end
  alt 是新会话
    A ->> AI: 开始新会话
    note over A: Hook: onChatStart
  else 是恢复会话
    A ->> AI: 读取会话历史
    AI ->> A: 返回历史消息
    note over A: Hook: onChatResume
  end
  rect rgba(255,255,0,0.1)
    note over A: Hook: onMessageListInit
    A ->> UI: 组装要展示的消息
    UI ->> User: 展示消息
  end
  opt 对话
    User ->> UI: 输入并发送消息
    UI ->> A: agent.pushInputBlocks()
    rect rgba(255,255,0,0.1)
      note over A: Hook: onInputBlocksPush
      A ->> A: 创建 Message 对象 msg<br/>msg 设置为用户输入的文本<br/>msg.show()
      note over A: Hook: onMessageChange show
      A -->> UI: 更新界面
      UI -->> User: 展示新消息
      A ->> A: 创建 Message 对象 respMsg<br/>respMsg 设置为 loading<br/>respMsg.show()
      note over A: Hook: onMessageChange show
      A -->> UI: 更新界面
      UI -->> User: 展示响应消息loading
      A ->> AI: 调用 AI
      AI ->> A: 返回消息流
      loop 流式消息
        AI ->> A: 消息包
        A ->> A: 更新respMsg数据<br/>respMsg.update()
        note over A: Hook: onMessageChange update
        A -->> UI: 更新界面
        UI -->> User: 蹦字/展示卡片
      end
    end
   end

    opt 操作消息行动点（以单选为例）
       User ->> UI: 选中选项
       UI ->> A: agent.emitTileEvent()
       rect rgba(255,255,0,0.1)
          note over A: Hook: onTileEvent
          A ->> A: 设置 msg 状态<br/>msg.persist()
          note over A: Hook: onMessagePersist
          A ->> AI: 持久化消息
          AI ->> A: 返回结果
          A ->> A: msg.update()
          note over A: Hook: onMessageChange update
          A ->> UI: 更新界面
          UI ->> User: 消息置灰，高亮选中
       end
    end
    opt 删除消息
       User ->> UI: 删除消息
       UI ->> A: agent.removeMessage()
       rect rgba(255,255,0,0.1)
          A ->> A: msg.remove()
          A ->> AI: 标记消息删除
          note over A: Hook: onMessageChange remove
          A ->> UI: 更新界面
          UI ->> User: 消息消失
       end
    end
```

### Plugin 插件机制

插件是基于以上的 Hook 机制实现的，插件可以实现对话智能体的功能，例如对接 AI 平台，提供 UI 界面等。
插件也可以暴露一些方法和属性，供开发者使用。

```tsx
import { createChatAgent, withUI } from '@ray-js/t-agent';
const createAgent = () => {
  const agent = createChatAgent(
    withUI() // withUI 插件提供了一些渲染 UI 的方法
  );

  return agent;
};

// ScrollToBottom.tsx
import React from 'react';
import { Button } from '@ray-js/components';
import { useChatAgent } from '@ray-js/t-agent-ui-ray';
const ScrollToBottom = () => {
  const agent = useChatAgent();

  const scroll = () => {
    // 使用插件暴露的方法
    agent.plugins.ui.emitEvent('scrollToBottom', { animation: false });
  };

  return <Button onClick={scroll}>Scroll to bottom</Button>;
};
```

#### 编写一个插件

插件是一个高阶函数，接收一个选项，返回一个函数，这个函数接收一个 ChatAgent 对象，可以在这个函数中注册 Hook。

```tsx
import { ChatAgent, createHooks, Hookable } from '@ray-js/t-agent';

// 试下一个 MyPlugin 插件
export type MyPlugin = GetChatPluginHandler<typeof withMyPlugin>;

export const withMyPlugin = (options: any) => {
  // 创建一个 Hookable 对象
  const hooks = createHooks();

  return (agent: ChatAgent) => {
    const { onChatStart } = agent;

    onChatStart(async () => {
      console.log('Chat start');
      // 触发插件的 Hook
      await hooks.callHook('onMyHook', 'Hello, world!');
    });

    // 暴露插件的方法和 Hook
    return {
      hooks,
      myPlugin: {
        // 暴露一个方法
        myMethod() {
          console.log('My method');
        },
        // 暴露一个 Hook
        onMyHook: fn => {
          return hooks.hook('onMyHook', fn);
        },
      },
    };
  };
};
```

```tsx
// 使用插件
import { createChatAgent } from '@ray-js/t-agent';

const createAgent = () => {
  const agent = createChatAgent(
    // 使用插件
    withMyPlugin({})
  );

  // 调用插件暴露的方法
  agent.plugins.myPlugin.myMethod();

  // 注册插件的 Hook
  agent.plugins.myPlugin.onMyHook(msg => {
    console.log(msg);
  });

  return agent;
};
```

## 内置插件

### withDebug

withDebug 插件会在 console 里打印日志，方便调试

```tsx
const agent = createChatAgent(
  withDebug({
    autoStart: true, // 是否自动启动，默认为 true
  })
);

// 启动
agent.plugins.debug.start();

// 停止
agent.plugins.debug.stop();
```

### withUI

withUI 插件提供了一些默认的 UI 行为，例如消息的展示，消息的删除等，消息总线

```tsx
const agent = createChatAgent(withUI());

agent.plugins.ui.emitter; // 消息总线

// 滚动到底部
agent.plugins.ui.emitEvent('scrollToBottom', { animation: false });

// 监听事件
const off = agent.plugins.ui.onEvent('scrollToBottom', payload => {
  console.log('scroll to bottom', payload.animation);
});

// 取消监听
off();
```

ui 插件的主要事件如下，你还可以自由地注册需要的事件：

- `messageListInit` 初始化消息列表
  - `payload.messages: ChatMessageObject[]` 消息列表
- `messageChange` 消息变化
  - `payload.type: 'show' | 'update' | 'remove'` 变化类型
  - `payload.message: ChatMessageObject` 消息
- `scrollToBottom` 滚动到底部
  - `payload.animation: boolean` 是否使用动画
- `sendMessage` 发送消息，触发 UI 界面更新
  - `payload.blocks: InputBlock[]` 输入块
- `setInputBlocks` 设置输入块到 MessageInput 输入框
  - `payload.blocks: InputBlock[]` 输入块
- `sessionChange`: 会话数据变化
  - `payload.key: string`: 会话数据 key
  - `payload.value: any`: 会话数据 value
  - `payload.oldValue: any`: 会话数据旧值

UI 插件增强功能（0.2.x 新增）：

withUI 插件在 0.2.x 版本中新增了 Hook 机制，可以让开发者自定义消息反馈和历史清理的行为：

**消息反馈 Hook**：

- `agent.plugins.ui.hook('onMessageFeedback', async context => {})` 注册消息反馈 Hook
  - `context.payload.messageId: string` 消息 ID
  - `context.payload.rate: 'like' | 'unlike'` 反馈类型
  - `context.payload.content?: string` 反馈内容（可选）
  - `context.result: { success: boolean }` 返回结果，需要设置是否成功

**清空历史 Hook**：

- `agent.plugins.ui.hook('onClearHistory', async context => {})` 注册清空历史 Hook
  - `context.payload: any` 清空历史的参数
  - `context.result: { success: boolean }` 返回结果，需要设置是否成功

**调用 Hook**：

- `agent.plugins.ui.callHook('onMessageFeedback', payload)` 调用消息反馈 Hook
- `agent.plugins.ui.callHook('onClearHistory', payload)` 调用清空历史 Hook

使用示例：

```tsx
const agent = createChatAgent(withUI(), withAIStream({ agentId: 'your-agent-id' }));

// 注册消息反馈处理
agent.plugins.ui.hook('onMessageFeedback', async context => {
  const { messageId, rate, content } = context.payload;
  try {
    // 调用你的 API 提交反馈
    await submitFeedback({ messageId, rate, content });
    context.result = { success: true };
  } catch (error) {
    context.result = { success: false };
  }
});

// 注册清空历史处理
agent.plugins.ui.hook('onClearHistory', async context => {
  try {
    // 调用你的 API 清空历史
    await clearChatHistory();
    context.result = { success: true };
  } catch (error) {
    context.result = { success: false };
  }
});
```

> 注意，这里的 ChatMessageObject 是一个消息对象，不是 ChatMessage 类型，
> 它包含了消息的一些属性和方法，这是为了避免在 UI 层修改消息对象，导致 ChatAgent 中的消息对象不一致。
> 对消息对象的修改应该始终在 ChatAgent 中进行。

## 附带 utils 工具

### getLogger(prefix: string): Logger

创建一个 logger，用于打印日志

```tsx
import { getLogger } from '@ray-js/t-agent';
const logger = getLogger('MyPlugin');
logger.debug('Hello, world!');
```

### Emitter 事件总线

Emitter 是一个事件总线，用于注册和触发事件

```tsx
import { Emitter, EmitterEvent } from '@ray-js/t-agent';
const emitter = new Emitter();

// 注册事件
const cb = event => console.log('detail', event.detail);
emitter.addEventListener('event', cb);

// 触发事件
emitter.dispatchEvent(new EmitterEvent('event', { detail: 'Hello, world!' }));

// 移除事件
emitter.removeEventListener('event', cb);
```

### StreamResponse

StreamResponse 是一个流式响应对象，用于处理流式消息

```tsx
import { StreamResponse } from '@ray-js/t-agent';

const partStream = await getPartStream(); // 获取流数据
const response = new StreamResponse(partStream);

const parts = response.parts();
for await (const part of parts) {
  console.log('part', part);
}
```

### createHooks、Hookable

参见 `hookable` npm 包

### isAbortError

判断是否是中断错误

### safeParseJSON

安全地解析 JSON 字符串，解析失败返回 `undefined`

```tsx
import { safeParseJSON } from '@ray-js/t-agent';

const obj = safeParseJSON<{ a: number }>('{"a": 1}');

console.log(obj.a); // 1
```

# t-agent-plugin-aistream

t-agent-plugin-aistream 是一个对接小程序 AI 智能体平台的插件，提供了对接小程序 AI 智能体平台的能力。

## 安装

```shell
yarn add @ray-js/t-agent-plugin-aistream
```

## 使用

```tsx
import { createChatAgent, withUI } from '@ray-js/t-agent';
import { withAIStream, withBuildIn } from '@ray-js/t-agent-plugin-aistream';

const createAgent = () => {
  const agent = createChatAgent(
    withUI(), // 一般都需要应用 withUI 插件
    withAIStream({
      agentId: 'your-agent-id', // 输入你的智能体ID
    }),
    withBuildIn()
  );

  return agent;
};
```

## 包含的插件

### withAIStream 插件

提供了对接小程序 AI 智能体平台的能力

参数：

- `agentId` 智能体 ID（必填）
- `clientType` 客户端类型，默认为 APP (2)
- `deviceId` 设备 ID，当 clientType 为 DEVICE (1) 时必填
- `wireInput` 是否将输入块传递给智能体，默认为 true，设置为 false 时，需要你自己编写 onInputBlocksPush Hook 来处理输入块
- `historySize` 历史消息大小，默认为 1000
- `indexId` 索引 ID，默认为 'default'
- `homeId` 家庭 ID，不填默认当前家庭
- `earlyStart` 是否在 onAgentStart 阶段就建立连接
- `eventIdPrefix` eventId 前缀，用于方便云端调试和日志追踪
- `tokenOptions` 获取 agent token 的参数
  - `api` API 接口名
  - `version` 接口版本
  - `extParams` 额外参数
- `createChatHistoryStore` 自定义消息存储函数

方法：

- `agent.plugins.aiStream.send(blocks, signal, userData)` 向智能体发送一条消息
  - `blocks` 输入块数组
  - `signal` 可选的 AbortSignal，用于中断请求
  - `userData` 可选的用户数据，会附带在发送的消息中
- `agent.plugins.aiStream.chat(blocks, signal, options)` 向智能体发送一条消息，并生成提问 ChatMessage 对象和 AI 回答 ChatMessage 对象，流式更新
  - `blocks` 输入块数组
  - `signal` 可选的 AbortSignal
  - `options.sendBy` 发送者角色，默认为 'user'
  - `options.responseBy` 响应者角色，默认为 'assistant'
  - `options.userData` 可选的用户数据
- `agent.plugins.aiStream.removeMessage(message)` 删除一条历史消息
- `agent.plugins.aiStream.clearAllMessages()` 清空当前会话的所有消息和本地历史
- `agent.plugins.aiStream.getChatId()` 获取当前会话的 chatId，返回 Promise<string>

Hooks：

- `onMessageParse` 当读取历史消息，解析消息时触发，可以在这个 Hook 里修改消息
  - `msgItem` 存储的消息对象
  - `result.messages` 解析后的消息列表
- `onChatMessageSent` 当用户消息和响应消息创建后触发
  - `userMessage` 用户发送的消息
  - `respMessage` AI 的响应消息
- `onTextCompose` 当收到文本数据时触发，用于处理文本的渲染
  - `respMsg` 响应消息
  - `status` 消息状态
  - `result.text` 文本内容，可以修改
- `onSkillCompose` 当收到技能数据时触发，用于处理技能的渲染
  - `skill` 当前技能数据 (ReceivedTextSkillPacketBody)
  - `respMsg` 响应消息
  - `result.messages` 消息列表
- `onSkillsEnd` 当所有技能处理完成时触发
  - `skills` 技能数据列表 (ReceivedTextSkillPacketBody[])
  - `respMsg` 响应消息
  - `result.messages` 消息列表
- `onTTTAction` tile 使用 `sendAction` 时触发
  - `tile` 触发的 tile
  - `result.action` TTTAction，可以修改要执行的动作
- `onCardsReceived` 当收到卡片数据时触发
  - `skills` 技能数据列表 (ReceivedTextSkillPacketBody[])
  - `result.cards` 卡片列表
- `onUserDataRead` **(0.2.x 新增)** 当需要读取用户自定义数据时触发，用于向 AI 平台传递额外的上下文信息
  - `type` 触发类型，可以是 'create-session' 或 'start-event'
    - `'create-session'` 创建会话时触发，只触发一次
    - `'start-event'` 每次发送消息时触发
  - `data` 上下文数据
    - `data.blocks` 当 type 为 'start-event' 时，包含本次发送的输入块
  - `result.userData` 返回的用户数据对象，会被合并后发送给 AI 平台

### withMCP 插件

提供 MCP 服务暴露能力，可以实现让 Agent 调用小程序里面提供的 tool，实现操作蓝牙设备，调用自定义 api，读取本地文件等功能，使用时先需要在智能体节点上开启“设备 MCP”，此外，MCP tool 的名字必须以 `device.` 开头。

![mcp-config.jpg](https://static1.tuyacn.com/static/txp-ray-TAgent/mcp-config.jpg)

**使用前提**：

- 必须在 `withAIStream` 之后使用
- 使用 MCP 功能时，`AIStreamKit` 需要 `2.2.1` 或以上版本
- `createServer` 必须同步返回 `McpServer`

参数：

- `createServer(agent)` 创建 MCP 服务实例
- `createContext(event, agent)` 可选，为每次 MCP 请求补充上下文

**使用示例**：

```tsx
import { createChatAgent, withUI } from '@ray-js/t-agent';
import { McpServer, withAIStream, withMCP } from '@ray-js/t-agent-plugin-aistream';

const createAgent = () => {
  const server = new McpServer({
    name: 'demo-mcp',
    version: '1.0.0',
  });

  server.registerTool(
    'device.echo',
    {
      description: '回显输入参数',
      inputSchema: {
        type: 'object',
        properties: {
          text: { type: 'string' },
        },
      },
    },
    async (args, context) => {
      return {
        content: [
          {
            type: 'text',
            text: JSON.stringify({
              text: args.text,
              sessionId: context.sessionId,
            }),
          },
        ],
      };
    }
  );

  return createChatAgent(
    withUI(),
    withAIStream({
      agentId: 'your-agent-id',
    }),
    withMCP({
      createServer: () => server,
      createContext: event => ({
        traceId: event.eventId,
      }),
    })
  );
};
```

启用后，插件会：

- 在创建 session 时注入 `sessionAttributes.deviceMcp`
- 在收到 `MCP_CMD` 事件时调用对应 tool
- 将 tool 执行结果按 JSON-RPC 响应格式回传给 AIStream

### 自定义变量传入

某些情况下，你可能需要自定义变量到 agent 作为流程中的变量，你需要在工作流里配置自定义变量的引用处，然后再在小程序里传入自定义变量：

![custom-var-config.jpg](https://static1.tuyacn.com/static/txp-ray-TAgent/custom-var-config.jpg)

**小程序中传入自定义变量**：

```tsx
const agent = createChatAgent(
  withUI(),
  withAIStream({
    agentId: 'your-agent-id',
  })
);

agent.plugins.aiStream.onUserDataRead((type, data, result) => {
  // 在创建会话时传递用户信息
  if (type === 'create-session') {
    result.userData = {
      sessionAttributes: {
        'custom.param': {
          'custom.app.test_me': {
            // 此处为平台上配置接收的参数名
            value: `with value test me ${type} ${Date.now()}`,
          },
        },
      },
    };
    return;
  }
  // 在每次发送消息时传递动态上下文
  if (type === 'start-event') {
    result.userData = {
      sessionAttributes: {
        'custom.param': {
          'custom.app.test_me': {
            // 此处为平台上配置接收的参数名
            value: `with value test me ${type} ${Date.now()}`,
          },
        },
      },
    };
    return;
  }
});
```

### withBuildIn 插件

提供了一些内置的功能，比如智能家居、知识库搜索等。

**支持的技能**：

- **智能家居**：设备控制、场景管理
- **知识库搜索**：关联文档展示

#### 配置项

```tsx
withBuildIn({
  // 是否默认开启 TTS 自动播放，默认 false
  audioAutoPlay: true,
});
```

#### TTS 语音播放

当 `withAIStream({ enableTts: true })` 开启后，服务端会随消息下发 TTS 音频包。`withBuildIn` 会消费这些音频包并提供完整的播放能力：

- **自动播放**：音频流开始（`StreamFlag.START`）时，若 `AIStream.audioAutoPlay` 为 `true`，则自动播放。
- **气泡播放按钮**：音频流结束后，会在对应气泡里插入一个 `bubbleTool` tile（语音播放 / 复制按钮）。播放参数（`path` / `codecType` / `sampleRate` / `channels` / `bitDepth` / `pts`）会按音频流 id 聚合后写入 tile，点击即可重新播放。
- **单条互斥**：同一时刻只播放一条，开始新播放或主动停止时会先停掉当前播放。
- **播放生命周期**：监听底层 `onAudioPlayChanged`，在播放完成 / 异常时自动复位播放状态。

**对外暴露的 session 状态**（均通过 `sessionChange` 事件广播，UI 侧可用 `useAgentSessionValue` 订阅并派生组件状态）：

| key                         | 类型             | 说明                  |
| --------------------------- | ---------------- | --------------------- |
| `AIStream.audioAutoPlay`    | `boolean`        | 是否开启 TTS 自动播放 |
| `AIStream.audioPlaying`     | `boolean`        | 当前是否正在播放      |
| `AIStream.playingMessageId` | `string \| null` | 正在播放的消息 id     |

**对外暴露的 UI 事件**：

| 事件名          | 说明                                     |
| --------------- | ---------------------------------------- |
| `audioPlayStop` | 主动停止当前播放（不论是否开启自动播放） |

示例：在页面顶部做一个三态（播放中 / 自动播放 / 静音）切换按钮：

```tsx
import { useAgentSessionValue, useEmitEvent } from '@ray-js/t-agent-ui-ray';

function AudioToggle() {
  const [audioAutoPlay, setAudioAutoPlay] = useAgentSessionValue<boolean>('AIStream.audioAutoPlay');
  const [audioPlaying] = useAgentSessionValue<boolean>('AIStream.audioPlaying');
  const emitEvent = useEmitEvent();

  const onClick = () => {
    if (audioPlaying) {
      // 播放中：点击立刻停止播放，不论是否静音
      emitEvent('audioPlayStop', undefined);
      return;
    }
    // 否则切换自动播放 / 静音
    setAudioAutoPlay(prev => !prev);
  };

  const label = audioPlaying ? '🎵 播放中' : audioAutoPlay ? '🔊 自动播放' : '🔇 静音';
  return <View onClick={onClick}>{label}</View>;
}
```

> 组件内的 `playing` 状态应从 session 派生（如 `audioPlaying && playingMessageId === message.id`），不要在 tile 内部维护本地播放状态，避免被持久化或与全局状态不一致。

## mock 机制

为了方便开发，我们提供了一个 mock 机制，可以在开发时不用连接小程序 AI 智能体平台，直接使用 mock 数据进行开发。

### mock AI Stream 响应

```tsx
import { mock } from '@ray-js/t-agent-plugin-aistream';

mock.hooks.hook('sendToAIStream', context => {
  if (context.options.blocks?.some(block => block.text?.includes('hello'))) {
    context.responseText = 'hello, who are you?';
  }

  if (context.options.blocks?.some(block => block.text?.includes('智能家居'))) {
    context.responseText = '正在为您控制智能设备...';
    context.responseSkills = [
      {
        code: 'smart_home',
        general: {
          action: 'control_device',
          data: {
            devices: [
              {
                deviceId: 'vdevo174796589841019',
                icon: '',
                dps: { range: '0', toggle: 'ON' },
                name: '毛巾架',
              },
            ],
          },
        },
        custom: {},
      },
    ];
  }
});
```

### mock ASR 语音识别

```tsx
import { mock } from '@ray-js/t-agent-plugin-aistream';

mock.hooks.hook('asrDetection', context => {
  context.responseText = 'Hello world!, I am a virtual assistant.';
});
```

### mock MCP

推荐做法是在 mock 的 `sendToAIStream` 钩子里，根据输入内容决定要调用哪个 MCP tool，再通过 `context.callMCPTool()` 直接走一遍完整的 MCP 调用链。

```tsx
import { mock } from '@ray-js/t-agent-plugin-aistream';

const getToolCall = (text: string) => {
  if (text.includes('报错') || text.includes('失败') || text.includes('异常')) {
    return {
      name: 'device.home.energy.summary.fail',
      arguments: {
        reason: 'mock-error-case',
      },
    };
  }

  if (text.includes('能耗') || text.includes('电量') || text.includes('数据')) {
    return {
      name: 'device.home.energy.summary.get',
      arguments: {
        period: 'today',
      },
    };
  }

  return {
    name: 'device.app.open',
    arguments: {
      category: 'music',
    },
  };
};

export const setupMockMCP = () => {
  mock.hooks.hook('sendToAIStream', async context => {
    const text = context.data
      .filter(item => item.type === 'text')
      .map(item => item.text)
      .join(' ');

    if (!/mcp|应用|能耗|电量|数据|报错|失败|异常/i.test(text)) {
      return;
    }

    const toolCall = getToolCall(text);

    await context.writeText(`MCP mock 正在调用 ${toolCall.name}...`);

    try {
      const result = await context.callMCPTool(toolCall.name, toolCall.arguments, {
        delayMs: 80,
      });
      await context.writeText(
        `工具 ${toolCall.name} 已返回：${
          typeof result === 'string' ? result : JSON.stringify(result == null ? {} : result)
        }`
      );
    } catch (error) {
      const message = error instanceof Error ? error.message : String(error);
      await context.writeText(`MCP mock 已收到工具报错：${message}`);
    }

    await context.end();
  });
};
```

使用时在创建 agent 前注册一次即可：

```tsx
const createAgent = () => {
  setupMockMCP();

  return createChatAgent(
    withUI(),
    withAIStream({
      agentId: 'your-agent-id',
    }),
    withMCP({
      createServer: () => server,
    })
  );
};
```

## 附带的 utils 工具（现在不稳定，还在开发中）

### AbortController

这个是小程序里的 AbortController ponyfill，参见 mdn

### runTTTAction

运行一个 TTTAction，用于处理用户的操作行为，目前支持以下动作

- `openRoute` 打开一个路由
- `openMiniApp` 打开一个小程序
- `openH5` 打开一个 H5 页面
- `sendMessage` 发送一条消息
- `buildIn` 内置行动

### AsrAgent

ASR 语音识别代理，用于识别用户的语音输入

使用方法：

```tsx
import { createAsrAgent } from '@ray-js/t-agent-plugin-aistream';

async function startAsr() {
  const asrAgent = createAsrAgent({
    agentId: 'your-agent-id',
    onMessage: message => {
      if (message.type === 'text') {
        console.log('识别结果：', message.text);
      } else if (message.type === 'file') {
        console.log('音频文件：', message.file);
      }
    },
    onFinish: () => {
      console.log('识别完成');
    },
    onError: error => {
      console.error('识别出错：', error);
    },
    recordingOptions: {
      saveFile: false,
      sampleRate: 16000,
      maxDuration: 60000, // 最长60秒
    },
  });

  // 开始识别
  await asrAgent.start();

  // 结束识别
  await asrAgent.stop();
}
```

### promisify TTT

内置大量 TTT API 的 promisify 方法，用于将 TTT API 转换为 Promise，同时支持 mock

使用方法：

```tsx
import { promisify } from '@ray-js/t-agent-plugin-aistream';

interface RouterParams {
  /** 路由链接 */
  url: string;
  complete?: () => void;
  success?: (params: null) => void;
  fail?: (params: {
    errorMsg: string;
    errorCode: string | number;
    innerError: {
      errorCode: string | number;
      errorMsg: string;
    };
  }) => void;
}
const router = promisify<RouterParams>(ty.router);

// mock，只在 IDE 下生效
mock.hooks.hook('router', context => {
  console.log('call router', context.options);
});

// 调用
await router({ url: '/pages/index/index' });
```

### sendBlocksToAIStream

**注意：此函数仅供内部使用，一般开发者不需要直接调用**

给 AIStream 发送消息块，这是一个底层函数，通常应该使用 `agent.plugins.aiStream.send` 或 `agent.plugins.aiStream.chat` 方法。

#### 适用场景

- ✅ **适用**：需要直接控制流式响应的处理时
- ✅ **适用**：实现自定义的消息发送逻辑
- ❌ **不适用**：一般的对话场景，应使用 `agent.plugins.aiStream.chat`
- ❌ **不适用**：简单的消息发送，应使用 `agent.plugins.aiStream.send`

#### 函数签名

```tsx
import { sendBlocksToAIStream } from '@ray-js/t-agent-plugin-aistream';

export interface SendBlocksToAIStreamParams {
  blocks: InputBlock[];
  session: AIStreamSession;
  attribute?: AIStreamChatAttribute;
  signal?: AbortSignal;
}

export function sendBlocksToAIStream(params: SendBlocksToAIStreamParams): {
  response: StreamResponse;
  metaPromise: Promise<Record<string, any>>;
};
```

#### 使用示例

```tsx
const send = async () => {
  try {
    // 需要先获取 AIStreamSession 对象
    const streamSession = agent.session.get('AIStream.streamSession');

    const result = sendBlocksToAIStream({
      blocks: [{ type: 'text', text: 'hello' }],
      session: streamSession,
      signal: new AbortController().signal,
    });

    // 获取发送后的元数据
    const meta = await result.metaPromise;

    // 获取流式消息
    const parts = result.response.parts();
    for await (const part of parts) {
      console.log('part', part);
    }
  } catch (error) {
    console.error('Send message failed:', error);
    // 错误处理逻辑
  }
};
```

### 媒体文件相关函数

`uploadMedia`、`uploadVideo`、`uploadImage` 用于上传媒体文件，带缓存

`isFullLink` 用于判断是否是 http(s) 协议开头的链接

`parseCloudKey` 用于解析 URL 中云存储的 key

`isLinkExpired` 用于判断链接是否过期

`getUrlByCloudKey` 从缓存中获取云存储的下载签名 URL，如果没有则返回 `undefined`

`setUrlByCloudKey` 设置云存储的下载签名 URL 到缓存中

`resetUrlByCloudKey` 重置云存储的下载签名 URL 缓存

`chooseImage`、`chooseVideo` 选择媒体文件

```tsx
import { useEffect } from 'react';

async function getPictureList() {
  // 从云端拉取用户私有的图片列表
  const list = await pictureListRequest();
  // 这样设置到缓存里，下次就不用再请求了
  for (const item of list) {
    setUrlByCloudKey(item.path, item.displayUrl);
  }
}
```

# t-agent-ui-ray

t-agent-ui-ray 是一个基于 ray 的 UI 组件库，包含了一些常用的对话界面组件，例如消息列表、消息输入框等。

## 安装

```shell
yarn add @ray-js/t-agent-ui-ray
```

## 使用

```tsx
import React from 'react';
import { View } from '@ray-js/components';
import {
  ChatContainer,
  defaultRenderOptions,
  MessageActionBar,
  MessageInput,
  MessageList,
} from '@ray-js/t-agent-ui-ray';
// createAgent 实现参见 t-agent 的使用示例
import { createAgent } from './createAgent';

const renderOptions = {
  ...defaultRenderOptions,
};

export default function ChatPage() {
  // createAgent 必须返回一个 ChatAgent 应用过 withUI、withAIStream 插件的实例
  return (
    <View style={{ height: '100vh' }}>
      <ChatContainer createAgent={createAgent} renderOptions={renderOptions}>
        <MessageList />
        <MessageInput />
        <MessageActionBar />
      </ChatContainer>
    </View>
  );
}
```

## 组件

### ChatContainer

对话容器，用于包裹消息列表和消息输入框，提供了 `ChatAgent` 的上下文

props:

- `className` 容器的类名
- `createAgent` 创建 `ChatAgent` 的函数，在 `ChatContainer` 挂载后会调用这个函数创建 `ChatAgent` 实例
- `agentRef` 用于获取 `ChatAgent` 实例的 ref
- `renderOptions` 渲染选项，用于决定 `MessageList` 中各个元素的渲染方式，具体参照下面的 renderOptions 自定义渲染 部分
  - `renderTileAs` 该函数决定如何在消息中渲染 tile
  - `customBlockTypes` 自定义 block 类型，只有在这里注册的 block 类型才会被 `renderCustomBlockAs` 渲染
  - `renderCustomBlockAs` 该函数决定如何在 markdown 气泡消息中渲染自定义 block，默认支持 `echarts`
  - `renderCardAs` 该函数决定如何在消息中渲染卡片，一般不需要自定义此项
  - `renderLongPressAs` **(0.2.x 新增)** 该函数决定如何渲染长按菜单，可以自定义长按菜单的样式和行为
  - `formatErrorMessageAs` **(0.2.x 新增)** 该函数决定如何格式化错误消息，可以根据错误代码返回用户友好的错误信息
  - `customCardMap` 自定义卡片映射，无需修改 `renderCardAs` 函数，只需要在这里注册卡片类型和对应的组件
  - `getStaticResourceBizType` 获取静态资源 `bizType`，用于获取静态资源

### MessageList

消息列表，用于展示消息。在 0.2.x 版本中集成了 LazyScrollView 组件，提供了更好的性能优化。

**Props**：

- `className` 列表的类名
- `roleSide` 消息角色的对齐方式，默认 `{ user: 'end', assistant: 'start' }`

**LazyScrollView 集成（0.2.x 新增）**：

MessageList 内部使用了 LazyScrollView 组件来优化大量消息的渲染性能：

- **懒加载渲染**：只渲染可见区域内的消息，大幅提升性能
- **高度自适应**：自动计算消息高度，支持动态内容
- **notifyHeightChanged()**：当消息内容发生变化时，自动通知高度更新

组件会自动处理以下场景：

- 保证最下面 10 条消息始终渲染，避免滚动到底部时出现白屏
- 消息高度变化时自动更新滚动位置
- 支持滚动到底部的动画效果

### MessageInput

消息输入框，用于输入消息、上传附件、ASR 语音识别

props:

- `className` 输入框的类名
- `placeholder` 输入框的占位符
- `placeholderStyle` 输入框 placeholder 的样式
- `renderTop` 用于渲染输入框上方的内容
- `style` 输入框容器样式
- `attachment` 是否启用附件上传，或传入 `{ image, video, imageCount, videoCount }` 精细控制
- `maxTextLength` 文本最大长度，默认 200
- `maxAudioMs` 录音最大时长
- `onStateChange` 输入框状态变化回调，状态值见 `MessageInputState`
- `amplitudeCount` 语音波形振幅采样数量

`MessageInput` 当前默认导出的是 `MessageInputAIStream`，适用于已接入 `withAIStream` 的场景。

### MessageActionBar（0.2.x 新增）

消息操作栏组件，用于多选消息时显示操作按钮，支持删除选中消息和清空历史记录。

**Props**：

无需传入任何 props，组件会自动根据多选状态显示和隐藏

**功能**：

- **返回按钮**：退出多选模式
- **清空历史按钮**：清空所有历史消息，会调用 `onClearHistory` Hook
- **删除选中按钮**：删除当前选中的消息，当没有选中消息时按钮会被禁用

**工作原理**：

MessageActionBar 组件会监听会话数据中的 `UIRay.multiSelect.show` 状态来决定是否显示。当用户长按消息选择"多选"时，该组件会自动显示。

**使用示例**：

```tsx
export default function ChatPage() {
  return (
    <View style={{ height: '100vh' }}>
      <ChatContainer createAgent={createAgent}>
        <MessageList />
        <MessageInput />
        <MessageActionBar />
      </ChatContainer>
    </View>
  );
}
```

### PrivateImage

私有图片组件，用于展示私有图片，props 同 Image，增加 bizType 参数

### LazyScrollView

懒加载滚动视图组件，用于优化长列表性能，自动管理可见区域的渲染

主要特性：

- 虚拟滚动：只渲染可见区域的元素
- 高度缓存：自动缓存元素高度，提升滚动性能
- 动态加载：根据滚动位置动态显示/隐藏元素
- `notifyHeightChanged()` 功能：当元素高度发生变化时，可以调用此方法通知滚动视图更新

使用说明：

LazyScrollView 主要在 MessageList 内部使用，开发者一般不需要直接使用。如果需要在消息中动态改变高度，可以通过 `notifyHeightChanged` 参数来通知高度变化：

```tsx
// 在 tile 组件中使用
const MyTile = ({ notifyHeightChanged }) => {
  const [expanded, setExpanded] = useState(false);

  const handleToggle = () => {
    setExpanded(!expanded);
    // 通知高度变化
    notifyHeightChanged();
  };

  return (
    <View>
      <Button onClick={handleToggle}>展开/收起</Button>
      {expanded && <View>详细内容...</View>}
    </View>
  );
};
```

### 内置 tile 组件

- bubble 气泡
- buttons 按钮组
- card 卡片
- divider 分隔线
- documents 关联文档
- executeCard 执行结果卡片
- file 文件
- image 图片
- operateCard 操作结果卡片
- recommendations 推荐行动
- text 文本，包含 markdown 支持
- time 时间标识
- tip 提示
- video 视频
- workflow 工作选项

## React Hooks

### useChatAgent

在 `ChatContainer` 上下文里取得 `ChatAgent` 实例

### useAgentMessage

在 `ChatContainer` 上下文里取得 `messages: ChatMessageObject[]` 列表

### useRenderOptions

在 `ChatContainer` 上下文里取得 `renderOptions` 对象

### useOnEvent

在 `ChatContainer` 上下文里注册 ui 事件，在组件卸载时自动取消注册

```tsx
import { useOnEvent } from '@ray-js/t-agent-ui-ray';

const MyComponent = () => {
  useOnEvent('scrollToBottom', payload => {
    console.log('scroll to bottom', payload.animation);
  });

  return <div>My Component</div>;
};
```

### useEmitEvent

在 `ChatContainer` 上下文里触发 ui 事件

```tsx
import { useEmitEvent } from '@ray-js/t-agent-ui-ray';

const MyComponent = () => {
  const emitEvent = useEmitEvent();

  const scroll = () => {
    emitEvent('scrollToBottom', { animation: false });
  };

  return <button onClick={scroll}>Scroll to bottom</button>;
};
```

### useTileProps

在 tile 组件里取得 `TileProps` 对象，如果是 tile，可以直接从 props 中获取

```tsx
import { useTileProps } from '@ray-js/t-agent-ui-ray';

const MyTilePart = () => {
  const { message, agent, tile, emitTileEvent } = useTileProps();

  return <div>My Tile</div>;
};
```

### useSendAction

发送一个 TTTAction。

如果当前组件位于 tile 或 card 内部，会优先走 `emitTileEvent`。
如果当前组件只是 `ChatContainer` 下的普通子组件，则会退化为触发 UI 事件 `runTTTAction`。

```tsx
import { useSendAction } from '@ray-js/t-agent-ui-ray';

const ActionPanel = () => {
  const sendAction = useSendAction();

  const handleClick = () => {
    sendAction({
      type: 'sendMessage',
      blocks: [{ type: 'text', text: 'hello' }],
      sendImmediately: true,
    });
  };

  return <button onClick={handleClick}>Send Message</button>;
};
```

### useTranslate

获取国际化翻译函数，用于翻译界面文本，提供了完整的多语言支持。

```tsx
import { useTranslate } from '@ray-js/t-agent-ui-ray';

const MyComponent = () => {
  const t = useTranslate();

  return (
    <div>
      {t('t-agent.message.action.copy')} {/* 输出: "复制消息" */}
      {t('t-agent.message.delete.title')} {/* 输出: "删除消息" */}
      {t('t-agent.message.clear-history.title')} {/* 输出: "清空历史" */}
    </div>
  );
};
```

**支持的语言**：

内置的多语言支持包括：

- **中文简体** (`zh-Hans`)：简体中文
- **中文繁体** (`zh-Hant`)：繁体中文
- **英文** (`en`)：英语
- **日文** (`ja`)：日语
- **德文** (`de`)：德语
- **法文** (`fr`)：法语
- **西班牙文** (`es`)：西班牙语
- **意大利文** (`it`)：意大利语

系统会根据用户的系统语言自动选择对应的翻译，如果不支持当前语言则回退到英文。

## renderOptions 自定义渲染

### 替换或新增 tile

如果你需要将某个 tile 替换成自己的实现，或者新增一个 tile，可以覆写 `renderTileAs`，例如：

```tsx
import { ImageTileData } from '@ray-js/t-agent';
import { Image } from '@ray-js/ray';
import { defaultRenderOptions, TileProps } from '@ray-js/t-agent-ui-ray';

function MyImageTile(props: TileProps<ImageTileData>) {
  // 实现自己的 ImageTile
  return <Image src={props.tile.data.src}></Image>;
}

const renderOptions = {
  ...defaultRenderOptions,
  renderTileAs: (props: TileProps) => {
    if (props.tile.type === 'image') {
      return <MyImageTile {...props} />;
    }
    // 保持默认行为
    return defaultRenderOptions.renderTileAs(props);
  },
};
```

### 自定义长按菜单（0.2.x 新增）

如果你需要自定义长按菜单的样式或行为，可以覆写 `renderLongPressAs` 函数，例如：

```tsx
import { defaultRenderOptions, LongPressResult } from '@ray-js/t-agent-ui-ray';
import { View, Button } from '@ray-js/ray';

const renderOptions = {
  ...defaultRenderOptions,
  renderLongPressAs: (res: LongPressResult) => {
    if (!res.menuProps.showActionMenu) {
      return null;
    }

    return (
      <View className="my-custom-menu">
        {res.menuProps.menuItems.map(item => (
          <Button key={item.key} onClick={() => res.menuProps.handleMenuItemClick(item)}>
            {item.displayLabel}
          </Button>
        ))}
      </View>
    );
  },
};
```

长按菜单功能包括：

- **复制消息**：复制文本内容到剪贴板
- **删除消息**：删除单条消息
- **多选**：进入多选模式，配合 MessageActionBar 使用
- **喜欢/不喜欢**：对助手消息进行反馈（仅对 assistant 角色消息可用）

### 自定义错误消息格式化（0.2.x 新增）

如果你需要自定义错误消息的显示格式，可以覆写 `formatErrorMessageAs` 函数，例如：

```tsx
import { defaultRenderOptions } from '@ray-js/t-agent-ui-ray';

const renderOptions = {
  ...defaultRenderOptions,
  formatErrorMessageAs: (message: string, code: string | undefined) => {
    // 根据错误代码返回自定义的错误消息
    if (code === 'network-offline') {
      return '网络连接异常，请检查您的网络设置';
    }
    if (code === 'timeout') {
      return '请求超时，请稍后重试';
    }
    // 使用默认的错误消息
    return message;
  },
};
```

内置支持的错误代码包括：

- `network-offline`：网络已断开
- `timeout`：发送超时
- `invalid-params`：无效参数
- `session-create-failed`：连接失败
- `connection-closed`：连接已关闭
- 等等

### 自定义卡片

卡片分为三类：内置卡片（buildIn）、自定义卡片（custom）、低代码卡片（lowCode），目前低代码卡片还在开发中，自定义卡片可以通过 `customCardMap` 注册自己的卡片组件。

卡片的数据结构如下：

```tsx
enum ChatCardType {
  CUSTOM = 'custom', // 可供业务方使用的自定义卡片
  BUILD_IN = 'buildIn', // 平台内置的卡片
  LOW_CODE = 'lowCode', // 低代码卡片
}

interface ChatCardObject<T = any> {
  cardCode: string; // 唯一标识卡片的 code
  cardType: ChatCardType; // 卡片类型
  cardData: T; // 卡片携带的数据
}
```

注册一个自定义卡片：

```tsx
import {
  ChatCardObject,
  ChatCardType,
  defaultRenderOptions,
  useTileProps,
  useSendAction,
} from '@ray-js/t-agent-ui-ray';
import { View, Text, Button } from '@ray-js/ray';

const MyCard: ChatCardComponent<{ title: string }, { clicked: boolean }> = props => {
  // 如果你需要拿到 agent、message、tile、emitTileEvent 等属性，可以使用 useTileProps
  const { message, agent, tile, emitTileEvent } = useTileProps();
  const { card, setCardState } = props;
  const { cardData, cardState, cardCode } = card as ChatCardObject<{ title: string }>;

  // 如果你需要发送一个 TTTAction，可以使用 useSendAction
  const sendAction = useSendAction();

  return (
    <View>
      <Text>My Card</Text>
      <Button
        onClick={() => {
          sendAction({ type: 'sendMessage', blocks: [{ type: 'text', text: 'hello' }] });
          // 如果需要更新卡片状态，可以使用 setCardState
          // 第二个参数表示是否持久化状态
          setCardState({ clicked: true }, { persist: true });
        }}
      >
        填充文本到输入框
      </Button>
    </View>
  );
};

const renderOptions = {
  ...defaultRenderOptions,
  customCardMap: {
    myCard: MyCard,
  },
};
```

### 自定义 block

在 `TextTile` 里的 markdown 渲染器是支持自定义 block 的，你可以通过 `customBlockTypes` 和 `renderCustomBlockAs` 来注册和渲染自定义 block。

block 的数据结构如下：

```tsx
export interface MarkdownBlock {
  id: string;
  type: string;
  children: string; // block 里的内容
}
```

注册一个自定义 block：

```tsx
import { defaultRenderOptions, MarkdownBlock } from '@ray-js/t-agent-ui-ray';
import { View, Text } from '@ray-js/ray';

const renderOptions = {
  ...defaultRenderOptions,
  customBlockTypes: ['my-block'],
  renderCustomBlockAs: (block: MarkdownBlock) => {
    if (block.type === 'my-block') {
      return (
        <View>
          <View>This is My Block</View>
          <View>{block.children}</View>
        </View>
      );
    }
    return defaultRenderOptions.renderCustomBlockAs(props);
  },
};
```

假设 AI 给你发了 markdown 文本，里面有一个类型是 `my-block` 的 `fence`，在上面我们注册了 `my-block`，那这个 `fence` 可以当做自定义 block。

````markdown
这是我的自定义 block！

```my-block
Hello, world!
```
````

以下 fence 没有注册过，不会被渲染成自定义 block，仅当做普通的代码块渲染。

```javascript
console.log('Hello, world!');
```

渲染结果如下：

这是我的自定义 block！
This is My Block
Hello, world!
以下 fence 没有注册过，不会被渲染成自定义 block，仅当做普通的代码块渲染。

console.log('Hello, world!');

### getStaticResourceBizType

如果你的静态资源需要带上 `bizType`，可以通过 `getStaticResourceBizType` 来获取 `bizType`。

```tsx
import { defaultRenderOptions } from '@ray-js/t-agent-ui-ray';

const renderOptions = {
  ...defaultRenderOptions,
  getStaticResourceBizType: (src: string, scene: string) => 'bizType',
};
```

针对不同的场景，你可能需要不同的 `bizType`，你可以根据 `src` 和 `scene` 来返回不同的 `bizType`。

内置的 `scene` 有以下几种：

- `image:view` 图片查看
- `image:upload` 图片上传
- `video:view` 视频查看
- `video:upload` 视频上传
- `videoThumb:view` 视频缩略图查看
- `videoThumb:upload` 视频缩略图上传

### 自定义多语言

如果你需要自定义 t-agent 里的多语言，可以覆写 `i18nTranslate` 函数，例如：

```tsx
import { defaultRenderOptions } from '@ray-js/t-agent-ui-ray';

const renderOptions = {
  ...defaultRenderOptions,
  i18nTranslate: (key: string) => {
    if (key === 'hello') {
      return '你好';
    }
    // 使用默认的多语言
    return I18n.t(key);
  },
};
```

以下是内置的多语言 key：

| key                                                        | 使用场景                    | 含义                                     |
| ---------------------------------------------------------- | --------------------------- | ---------------------------------------- |
| t-agent.build-in.button.create_scene_manually              | ButtonTile 内置按钮         | 手动创建场景                             |
| t-agent.build-in.button.enter_home_manage                  | ButtonTile 内置按钮         | 进入"家庭管理"                           |
| t-agent.build-in.button.enter_room_manage                  | ButtonTile 内置按钮         | 进入"房间管理"                           |
| t-agent.build-in.button.enter_alarm_message                | ButtonTile 内置按钮         | 进入"告警消息列表"                       |
| t-agent.build-in.button.enter_home_message                 | ButtonTile 内置按钮         | 进入"家庭消息列表"                       |
| t-agent.build-in.button.enter_bulletin                     | ButtonTile 内置按钮         | 进入"通知消息列表"                       |
| t-agent.build-in.button.enter_notification_setting         | ButtonTile 内置按钮         | 进入"消息推送设置"                       |
| t-agent.build-in.button.enter_personal_information         | ButtonTile 内置按钮         | 进入"个人资料"                           |
| t-agent.build-in.button.enter_account_security             | ButtonTile 内置按钮         | 进入"账号与安全"                         |
| t-agent.build-in.button.enter_setting                      | ButtonTile 内置按钮         | 进入"通用设置"                           |
| t-agent.build-in.button.enter_paring                       | ButtonTile 内置按钮         | 进入"设备配网"                           |
| t-agent.build-in.button.enter_share_device                 | ButtonTile 内置按钮         | 进入"设备分享"                           |
| t-agent.build-in.button.enter_faq_feedback                 | ButtonTile 内置按钮         | 进入"常见问题与反馈"                     |
| t-agent.build-in.button.questionnaire_take                 | ButtonTile 内置按钮         | 填写问卷                                 |
| t-agent.build-in.button.set_home_location                  | ButtonTile 内置按钮         | 设置家庭位置                             |
| t-agent.input.voice.require-permission                     | MessageInput 切换语音输入   | 需要授权录音权限                         |
| t-agent.input.upload.failed                                | MessageInput 上传文件       | 文件上传失败                             |
| t-agent.input.asr.oninput.text.top                         | MessageInput ASR 语音输入   | 我在听，请说话                           |
| t-agent.input.asr.oninput.text.center                      | MessageInput ASR 语音输入   | 松开发送，上划取消                       |
| t-agent.input.asr.ptt                                      | MessageInput ASR 语音输入   | 按住说话                                 |
| t-agent.input.asr.error.too-short                          | MessageInput ASR 错误       | 说话时间太短                             |
| t-agent.input.asr.error.empty                              | MessageInput ASR 错误       | 未能从语音中识别到文字                   |
| t-agent.input.asr.error.unknown                            | MessageInput ASR 错误       | 语音识别失败                             |
| t-agent.input.asr.error.timeout                            | MessageInput ASR 错误       | 语音识别已达时长限制，将直接发送         |
| t-agent.input.upload.source-type.camera                    | MessageInput 上传文件       | 拍照                                     |
| t-agent.input.upload.source-type.camera.require-permission | MessageInput 上传文件       | 拍照需要摄像头权限，请在设置中开启       |
| t-agent.input.upload.source-type.album                     | MessageInput 上传文件       | 从相册中选择                             |
| t-agent.input.upload.source-type.album.require-permission  | MessageInput 上传文件       | 从相册中选择需要相册权限，请在设置中开启 |
| t-agent.input.upload.image.max-reached                     | MessageInput 上传文件       | 已达到图片上传上限                       |
| t-agent.input.upload.video.max-reached                     | MessageInput 上传文件       | 已达到视频上传上限                       |
| t-agent.file-tile.unknown-filename                         | FileTile 文件显示           | 文件                                     |
| t-agent.message.feedback.success                           | BubbleTile 消息评价         | 反馈成功                                 |
| t-agent.message.bubble.aborted                             | BubbleTile 消息             | 用户中断                                 |
| t-agent.message.action.copy                                | BubbleTile 长按菜单         | 复制消息                                 |
| t-agent.message.action.delete                              | BubbleTile 长按菜单         | 删除消息                                 |
| t-agent.message.action.multi-select                        | BubbleTile 长按菜单         | 多选                                     |
| t-agent.message.action.like                                | BubbleTile 长按菜单         | 喜欢消息                                 |
| t-agent.message.action.unlike                              | BubbleTile 长按菜单         | 不喜欢消息                               |
| t-agent.message.copy.success                               | BubbleTile 复制成功         | 复制成功                                 |
| t-agent.message.delete.success                             | BubbleTile 删除成功         | 删除成功                                 |
| t-agent.message.like.success                               | BubbleTile 反馈             | 点赞成功                                 |
| t-agent.message.unlike.success                             | BubbleTile 反馈             | 取消点赞成功                             |
| t-agent.message.delete.title                               | BubbleTile 删除消息弹窗标题 | 删除消息                                 |
| t-agent.message.delete.content                             | BubbleTile 删除消息弹窗内容 | 确定要删除这条消息吗？                   |
| t-agent.message.delete.confirm                             | BubbleTile 删除消息弹窗确认 | 确认                                     |
| t-agent.message.delete.cancel                              | BubbleTile 删除消息弹窗取消 | 取消                                     |
| t-agent.message.clear-history.title                        | MessageActionBar 清空历史   | 清空历史                                 |
| t-agent.message.clear-history.content                      | MessageActionBar 清空历史   | 确定要清空历史吗？                       |
| t-agent.message.clear-history.button                       | MessageActionBar 清空历史   | 清空历史                                 |
| t-agent.message.multi-select-delete.title                  | MessageActionBar 多选删除   | 删除选中消息                             |
| t-agent.message.multi-select-delete.content                | MessageActionBar 多选删除   | 确定要删除这些选中的消息吗？             |
| t-agent.execute-card-tile.execution.success                | ExecuteCardTile 执行结果    | 执行成功                                 |
| t-agent.execute-card-tile.execution.failed                 | ExecuteCardTile 执行结果    | 执行失败                                 |
| t-agent.execute-card-tile.scene.invalid                    | ExecuteCardTile 场景状态    | 场景失效                                 |
| t-agent.execute-card-tile.delete                           | ExecuteCardTile 按钮        | 删除                                     |
| t-agent.execute-card-tile.execute                          | ExecuteCardTile 按钮        | 执行                                     |
| t-agent.execute-card-tile.switch.scene.state               | ExecuteCardTile 操作        | 切换场景状态                             |
| t-agent.operate-card-tile.open.device.failed               | OperateCardTile 操作结果    | 打开设备失败                             |
| t-agent.operate-card-tile.open.scene.failed                | OperateCardTile 操作结果    | 打开场景失败                             |
| t-agent.operate-card-tile.operation.impact                 | OperateCardTile 标题        | 本次操作影响：                           |
| t-agent.operate-card-tile.hide.details                     | OperateCardTile 按钮        | 收起详情                                 |
| t-agent.operate-card-tile.view.details                     | OperateCardTile 按钮        | 查看详情                                 |
| t-agent.operate-card-tile.device.move.desc                 | OperateCardTile 设备移动    | 设备"{device}"移动到"{room}"             |
| t-agent.operate-card-tile.device.rename.desc               | OperateCardTile 设备重命名  | 设备"{oldName}"改名"{newName}"           |
| t-agent.operate-card-tile.device.count                     | OperateCardTile 设备计数    | {count}个设备                            |
| t-agent.operate-card-tile.scene.count                      | OperateCardTile 场景计数    | {count}个场景                            |
| t-agent.operate-card-tile.home.count                       | OperateCardTile 家庭计数    | {count}个家庭                            |
| t-agent.operate-card-tile.room.count                       | OperateCardTile 房间计数    | {count}个房间                            |
| t-agent.operate-card-tile.group.count                      | OperateCardTile 群组计数    | {count}个群组                            |
| t-agent.operate-card-tile.description.format               | OperateCardTile 描述格式    | {items}。                                |
| t-agent.operate-card-tile.description.separator            | OperateCardTile 描述分隔符  | ，                                       |
| t-agent.expand.tab.device                                  | ExpandTile 标签页           | 设备                                     |
| t-agent.expand.tab.scene                                   | ExpandTile 标签页           | 场景                                     |
| t-agent.expand.tab.more                                    | ExpandTile 标签页           | 其他                                     |
| t-agent.expand.execution.success                           | ExpandTile 执行结果         | 执行成功                                 |
| t-agent.expand.execution.failed                            | ExpandTile 执行结果         | 执行失败                                 |
| t-agent.expand.device.rename                               | ExpandTile 设备重命名       | {oldName}改名成{newName}                 |
| t-agent.expand.scene.rename                                | ExpandTile 场景重命名       | {oldName}改名成{newName}                 |
| t-agent.expand.scene.one-click                             | ExpandTile 场景类型         | 一键执行                                 |
| t-agent.expand.scene.auto                                  | ExpandTile 场景类型         | 自动执行                                 |
| t-agent.expand.no.details                                  | ExpandTile 详情显示         | 没有可显示的详情内容                     |
| t-agent.error.unknown-error                                | 错误提示                    | 未知错误                                 |
| t-agent.error.network-offline                              | 错误提示                    | 网络已断开，请检查网络连接               |
| t-agent.error.invalid-params                               | 错误提示                    | 无效参数，请重试                         |
| t-agent.error.session-create-failed                        | 错误提示                    | 连接失败，请重试                         |
| t-agent.error.connection-closed                            | 错误提示                    | 连接已关闭，请重试                       |
| t-agent.error.event-exists                                 | 错误提示                    | 消息发送异常，请稍后再试                 |
| t-agent.error.event-disposed                               | 错误提示                    | 消息发送异常，请稍后再试                 |
| t-agent.error.event-closed                                 | 错误提示                    | 消息发送异常，请稍后再试                 |
| t-agent.error.event-aborted                                | 错误提示                    | 消息已中断                               |
| t-agent.error.event-write-failed                           | 错误提示                    | 消息发送异常，请稍后再试                 |
| t-agent.error.event-no-data-code                           | 错误提示                    | 消息发送异常，请稍后再试                 |
| t-agent.error.stream-exists                                | 错误提示                    | 消息发送异常，请稍后再试                 |
| t-agent.error.timeout                                      | 错误提示                    | 发送超时                                 |
| t-agent.error.asr-empty                                    | 错误提示                    | 语音识别结果为空                         |