# cloud.ai

> 支持端：云函数

`wx-server-sdk` 从 `4.0.1` 起提供 `cloud.ai()`。该接口返回底层 `@cloudbase/node-sdk` 的 AI 实例，用于调用大模型、混元生图和 Agent。

## 初始化

先调用 `cloud.init`，再取 AI 实例。

云函数中推荐使用当前环境：

```javascript
const cloud = require('wx-server-sdk')

cloud.init({
  env: cloud.DYNAMIC_CURRENT_ENV,
  timeout: 60000,
})
```

`timeout` 默认 `15000`（毫秒）。AI 请求会使用 `cloud.init` 传入的 `timeout`；单次 `generateText` / `streamText` / `sendMessage` 也可再传 `options.timeout` 覆盖。

本地 Node.js 可同时传入环境 ID 和密钥（会转交给 `@cloudbase/node-sdk`）：

```javascript
const cloud = require('wx-server-sdk')

cloud.init({
  env: '<你的环境ID>',
  secretId: '<SecretId>',
  secretKey: '<SecretKey>',
  timeout: 60000,
})
```

```javascript
const ai = cloud.ai()
```

`cloud.ai()` 需要 `wx-server-sdk` `4.0.1` 或更高版本。

## 大模型

### AI.createModel()

创建文生文模型。返回 `ReactModel`，提供 `generateText`、`streamText`。

#### 使用示例

```js
const ai = cloud.ai()
const model = ai.createModel('cloudbase')
```

内置模型名（命中后走对应实现，忽略 `defaultModelSubUrl`）：

`hunyuan`、`hunyuan-beta`、`hunyuan-exp`、`hunyuan-open`、`deepseek`、`moonshot`、`zhipu`、`dashscope`、`ark`、`01-ai`。

未命中内置名时（例如 `"cloudbase"`），走默认实现，请求路径为 `{model}{defaultModelSubUrl}`，即默认 `{model}/chat/completions`。

具体用哪个对话模型，在 `generateText` / `streamText` 的 `model` 字段里指定（以环境中已开通的为准）。

#### 类型声明

```js
function createModel(model: string, options?: { defaultModelSubUrl?: string }): ReactModel
```

#### 参数

| 参数名 | 必填 | 类型 | 说明 |
| ------ | ---- | ---- | ---- |
| model | 是 | `string` | 模型组名称。内置名见上；其他名称走默认路径 |
| options.defaultModelSubUrl | 否 | `string` | 仅非内置名生效。默认 `"/chat/completions"` |

### ReactModel.generateText()

非流式生成文本。

#### 使用示例

```js
const ai = cloud.ai()
const model = ai.createModel('cloudbase')
const res = await model.generateText({
  model: 'hy3-preview',
  messages: [{ role: 'user', content: '你好' }],
})

console.log(res.text)
console.log(res.usage)
```

#### 类型声明

```js
function generateText(input: ReactModelInput, options?: { timeout?: number }): Promise<{
  text: string
  messages: Array<ChatModelMessage>
  usage: Usage
  rawResponses: Array<unknown>
  error?: any
}>
```

`ReactModelInput` 在 `BaseChatModelInput` 上增加了 `topP`、`toolChoice`、`maxSteps`、`onStepFinish`，并把 `tools` 扩成 `ModelTool | FunctionTool`。

```js
interface BaseChatModelInput {
  model: string
  messages: Array<ChatModelMessage>
  temperature?: number
  top_p?: number
  tools?: Array<ModelTool>
  tool_choice?: 'none' | 'auto' | 'custom'
}
```

未出现在 `maxSteps` / `onStepFinish` / `topP` / `toolChoice` 中的其余字段会随请求体传给模型。

#### 参数

| 参数名 | 必填 | 类型 | 说明 |
| ------ | ---- | ---- | ---- |
| input.model | 是 | `string` | 对话模型名 |
| input.messages | 是 | `ChatModelMessage[]` | 消息列表 |
| input.temperature | 否 | `number` | 采样温度 |
| input.topP | 否 | `number` | 若传入，会写成请求里的 `top_p` |
| input.top_p | 否 | `number` | 与 `topP` 相同含义；`topP` 优先 |
| input.tools | 否 | `Array<ModelTool \| FunctionTool>` | 工具列表。带 `fn` 的项只会把 schema 发给模型，**不会**自动注册 `fn` |
| input.toolChoice | 否 | `"none" \| "auto" \| "custom"` | 若传入，会写成请求里的 `tool_choice` |
| input.tool_choice | 否 | `"none" \| "auto" \| "custom"` | 与 `toolChoice` 相同含义；`toolChoice` 优先 |
| input.maxSteps | 否 | `number` | 自动执行 tool call 的最大轮数，默认 `10`，必须 `>= 1` |
| input.onStepFinish | 否 | `(prop: IOnStepFinish) => unknown` | 每一轮模型请求结束时的回调 |
| options.timeout | 否 | `number` | 本次请求超时（毫秒） |

自动执行工具前必须先调用 `ai.registerFunctionTool`，把同名 `fn` 写入内部 `toolMap`。只把带 `fn` 的对象放进 `tools` 不够。

#### 返回值

| 属性名 | 类型 | 说明 |
| ------ | ---- | ---- |
| text | `string` | 最后一轮助手消息的 `content`，没有则为 `''` |
| messages | `ChatModelMessage[]` | 调用结束后的消息列表 |
| usage | `Usage` | 各轮 token 累加 |
| rawResponses | `unknown[]` | 各轮原始响应 |
| error | `any` | 工具执行抛错时返回，此时 `text` 为 `''` |

### ReactModel.streamText()

流式生成文本。SSE 会被封装成 `textStream` 和 `dataStream`。

#### 使用示例

```js
const ai = cloud.ai()
const model = ai.createModel('cloudbase')
const res = await model.streamText({
  model: 'hy3-preview',
  messages: [{ role: 'user', content: 'hi' }],
})

for await (const str of res.textStream) {
  console.log(str)
}

for await (const data of res.dataStream) {
  console.log(data)
}

const messages = await res.messages
const usage = await res.usage
```

`textStream` 的增量来自 `chunk.choices[0].delta.content`（仅当该值为 string 时入队）。

#### 类型声明

```js
function streamText(input: ReactModelInput, options?: { timeout?: number }): Promise<{
  dataStream: AsyncIterable<BaseDoStreamOutputChunk & { rawResponse?: any }>
  textStream: AsyncIterable<string>
  messages: Promise<Array<ChatModelMessage>>
  usage: Promise<Usage>
  error?: any
}>
```

#### 参数

与 `generateText()` 相同。

#### 返回值

| 属性名 | 类型 | 说明 |
| ------ | ---- | ---- |
| textStream | `AsyncIterable<string>` | 增量文本 |
| dataStream | `AsyncIterable` | 解析后的 SSE JSON 片段 |
| messages | `Promise<ChatModelMessage[]>` | 结束后的消息列表 |
| usage | `Promise<Usage>` | token 用量 |
| error | `any` | 工具执行抛错时存在 |

`dataStream` 元素的类型字段：

| 属性名 | 类型 | 说明 |
| ------ | ---- | ---- |
| choices | `Array` | 可选 |
| choices[n].finish_reason | `string` | 可选。工具调用时为 `"tool_calls"` |
| choices[n].delta | `ChatModelMessage` | 可选。本次增量 |
| usage | `Usage` | 可选 |
| rawResponse | `any` | 可选。该片段的原始对象 |

### AI.registerFunctionTool()

把工具函数登记到内部 `toolMap`。模型返回 tool call 后，SDK 用 `toolMap.get(name)(JSON.parse(arguments))` 执行。

#### 使用示例

```js
const ai = cloud.ai()

const getWeatherTool = {
  name: 'get_weather',
  description: "返回某个城市某天的温度。调用示例：get_weather({city: '北京', date: '03-19'})",
  fn: ({ city, date }) => `${city}在${date}的温度是：${20 + Math.random() * 10}`,
  parameters: {
    type: 'object',
    properties: {
      city: { type: 'string', description: '要查询的城市' },
      date: { type: 'string', description: '要查询的日期' },
    },
    required: ['city', 'date'],
  },
}

ai.registerFunctionTool(getWeatherTool)

const model = ai.createModel('cloudbase')
const result = await model.generateText({
  model: 'hy3-preview',
  tools: [getWeatherTool],
  messages: [{ role: 'user', content: '请告诉我北京今天的天气' }],
})

console.log(result.text)
```

同名工具重复注册会 `console.warn` 并覆盖。

#### 类型声明

```js
function registerFunctionTool(functionTool: FunctionTool): void

type FunctionTool = {
  name: string
  description: string
  fn: CallableFunction
  parameters: object
}

type ModelTool = {
  type: string
  function: {
    name: string
    description: string
    parameters: object
  }
}
```

#### 参数

| 参数名 | 必填 | 类型 | 说明 |
| ------ | ---- | ---- | ---- |
| functionTool.name | 是 | `string` | 工具名，对应 `toolMap` 的 key |
| functionTool.description | 是 | `string` | 工具描述 |
| functionTool.fn | 是 | `function` | 实际执行函数 |
| functionTool.parameters | 是 | `object` | 入参 JSON Schema |

## 生图

生图只在云函数 / Node.js 服务端通过 `wx-server-sdk` 调用。

当前可用模型：

| 能力 | model |
| ---- | ----- |
| 文生图 | `HY-Image-3.0-Plus-4090-Tob-v1.0` |
| 图生图 | `HY-Image-v3.0-I2I-ToB-v1.0.1` |

以下模型已下线，不要再传：`hunyuan-image`、`hunyuan-image-v3.0-v1.0.4`、`hunyuan-image-v3.0-v1.0.1`。对应的 `HunyuanGenerateImageInput` 也不再使用。

### AI.createImageModel()

创建指定的 AI 图像生成模型。返回 `DefaultImageModel` 实例（文档里也称 ImageModel），提供 `generateImage`。

#### 使用示例

```js
const ai = cloud.ai()
const imageModel = ai.createImageModel('hunyuan-image')
```

#### 类型声明

```js
function createImageModel<T extends 'hunyuan-image' | (string & {})>(provider: T): DefaultImageModel<T>
```

#### 参数

| 参数名 | 必填 | 类型 | 说明 |
| ------ | ---- | ---- | ---- |
| provider | 是 | `string` | 模型提供方标识。混元生图传 `"hunyuan-image"` |

### DefaultImageModel.generateImage()

根据 `input.model` 选择请求子路径，再把 `input` 作为请求体发送。

完整 URL 为 `{aiBaseUrl}/{provider}/{subUrl}`。`subUrl` 来自 `generateImageSubUrlConfig[provider][model]`，未命中则用默认值 `images/generations`。

#### 文生图

```js
const cloud = require('wx-server-sdk')

cloud.init({
  env: cloud.DYNAMIC_CURRENT_ENV,
  timeout: 150000,
})

exports.main = async (event) => {
  const ai = cloud.ai()
  const imageModel = ai.createImageModel('hunyuan-image')
  const res = await imageModel.generateImage({
    model: 'HY-Image-3.0-Plus-4090-Tob-v1.0',
    prompt: '一只胖胖的橘猫坐在窗台上打盹，水彩风格，温暖色调',
    size: '1024x1024',
    seed: 42,
    footnote: 'Generated by Cloudbase',
    revise: { value: true },
    enable_thinking: { value: false },
  })
  console.log(res.data[0].url)
  console.log(res.data[0].revised_prompt)
  return { url: res.data[0].url }
}
```

#### 图生图

`image_urls` 与 `images` 二选一，用于传入垫图。

```js
const res = await imageModel.generateImage({
  model: 'HY-Image-v3.0-I2I-ToB-v1.0.1',
  prompt: '把猫咪改成水彩风格',
  image_urls: ['https://example.com/cat.jpg'],
  revise: { value: true },
})

console.log(res.data[0].url)
console.log(res.data[0].revised_prompt)
```

也可以传 `images`（垫图 base64 列表）代替 `image_urls`。

#### 类型声明

```js
function generateImage(input: HunyuanARGenerateImageInput): Promise<HunyuanARGenerateImageOutput>
```

#### 参数

| 参数名 | 必填 | 类型 | 说明 |
| ------ | ---- | ---- | ---- |
| model | 是 | `string` | 文生图用 `"HY-Image-3.0-Plus-4090-Tob-v1.0"`，图生图用 `"HY-Image-v3.0-I2I-ToB-v1.0.1"` |
| prompt | 是 | `string` | 用于生成图片的文本 |
| size | 否 | `string` | 图片尺寸，格式 `"${宽}x${高}"` |
| seed | 否 | `number` | 生成种子 |
| footnote | 否 | `string` | 水印 |
| revise | 否 | `{ value: boolean }` | 是否改写 prompt |
| enable_thinking | 否 | `{ value: boolean }` | 改写是否开启 thinking 模式 |
| image_urls | 否 | `string[]` | 图生图垫图 URL 列表，与 `images` 二选一 |
| images | 否 | `string[]` | 图生图垫图 base64 列表，与 `image_urls` 二选一 |

#### 返回值

| 属性名 | 类型 | 说明 |
| ------ | ---- | ---- |
| id | `string` | 此次请求的 id |
| created | `number` | unix 时间戳 |
| data | `Array` | 返回的图片生成内容 |
| data[n].url | `string` | 生成的图片 url |
| data[n].revised_prompt | `string` | 可选。改写后的 prompt |

把生成结果上传到云存储时，`cloud.uploadFile` 的入参为 `cloudPath`、`fileContent`（`Buffer` 或 `ReadStream`），成功时返回 `fileID`：

```js
const https = require('https')

function downloadImage(url) {
  return new Promise((resolve, reject) => {
    https.get(url, (response) => {
      const chunks = []
      response.on('data', (chunk) => chunks.push(chunk))
      response.on('end', () => resolve(Buffer.concat(chunks)))
      response.on('error', reject)
    })
  })
}

exports.main = async (event) => {
  const imageModel = cloud.ai().createImageModel('hunyuan-image')
  const res = await imageModel.generateImage({
    model: 'HY-Image-3.0-Plus-4090-Tob-v1.0',
    prompt: event.prompt,
  })
  const imageBuffer = await downloadImage(res.data[0].url)
  const uploadRes = await cloud.uploadFile({
    cloudPath: `ai-images/${Date.now()}.jpg`,
    fileContent: imageBuffer,
  })
  return { fileID: uploadRes.fileID }
}
```

## Agent

### AI.bot.sendMessage()

与 Agent 对话。请求 POST 到 `{aiBotBaseUrl}/bots/{botId}/send-message`，body 为 `props`，`stream: true`。

本 SDK 类型里，`sendMessage` 只声明了 `botId`、`msg`、`history`。

#### 使用示例

```js
const ai = cloud.ai()
const res = await ai.bot.sendMessage({
  botId: 'botId-xxx',
  msg: '你好',
  history: [{ role: 'user', content: '你是李白。' }],
})

for await (const str of res.textStream) {
  console.log(str)
}

for await (const data of res.dataStream) {
  console.log(data)
}
```

`textStream` 的增量来自 `dataStream` 各项的 `content`（没有则为 `''`）。遇到 SSE 数据为 `"[DONE]"` 时结束 `dataStream`。

#### 类型声明

```js
function sendMessage(props: IBotSendMessage, options?: { timeout?: number }): Promise<StreamResult>

interface IBotSendMessage {
  botId: string
  msg: string
  history: Array<{
    role: string
    content: string
  }>
}

// dataStream 元素
type BotEventStreamData = {
  content: string
}

// eventSourceStream 元素
interface ParsedEvent {
  type: 'event'
  event?: string
  id?: string
  data: string
}
```

`StreamResult` 提供：

| 属性名 | 类型 | 说明 |
| ------ | ---- | ---- |
| eventSourceStream | `AsyncIterable<ParsedEvent>` | 解析后的 SSE 事件 |
| dataStream | `AsyncIterable<{ content: string }>` | 将 `ParsedEvent.data` JSON.parse 后的对象 |
| textStream | `AsyncIterable<string>` | `content` 字段组成的文本流 |

#### 参数

| 参数 | 类型 | 必填 | 说明 |
| ---- | ---- | ---- | ---- |
| props.botId | `string` | 是 | Agent ID，用于拼 URL |
| props.msg | `string` | 是 | 本次消息 |
| props.history | `Array<{ role: string, content: string }>` | 是 | 历史消息 |
| props.history[n].role | `string` | 是 | 角色 |
| props.history[n].content | `string` | 是 | 内容 |
| options.timeout | `number` | 否 | 本次请求超时（毫秒） |

## 类型定义

### ChatModelMessage

```js
type UserMessage = { role: 'user'; content: string }
type SystemMessage = { role: 'system'; content: string }
type PlainAssistantMessage = { role: 'assistant'; content: string }
type ToolCallAssistantMessage = {
  role: 'assistant'
  tool_calls: Array<ToolCall>
  content?: string
}
type AssistantMessage = PlainAssistantMessage | ToolCallAssistantMessage
type ToolMessage = { role: 'tool'; tool_call_id: string; content: string }
type ChatModelMessage = UserMessage | SystemMessage | AssistantMessage | ToolMessage
```

### ToolCall

```js
type ToolCall = {
  id: string
  type: string
  function: {
    name: string
    arguments: string
  }
}
```

### Usage

```js
type Usage = {
  completion_tokens: number
  prompt_tokens: number
  total_tokens: number
}
```

### IOnStepFinish

```js
interface IOnStepFinish {
  messages: Array<ChatModelMessage>
  text?: string
  toolCall?: ToolCall
  toolResult?: unknown
  finishReason?: string
  stepUsage?: Usage
  totalUsage?: Usage
}
```

## 完整云函数示例

```js
const cloud = require('wx-server-sdk')

cloud.init({
  env: cloud.DYNAMIC_CURRENT_ENV,
  timeout: 60000,
})

exports.main = async (event) => {
  const ai = cloud.ai()
  const { type, messages, prompt } = event

  if (type === 'image') {
    const imageModel = ai.createImageModel('hunyuan-image')
    const res = await imageModel.generateImage({
      model: 'HY-Image-3.0-Plus-4090-Tob-v1.0',
      prompt: prompt || '一只可爱的猫咪在草地上玩耍',
    })
    return { url: res.data[0].url }
  }

  const model = ai.createModel('cloudbase')
  const result = await model.generateText({
    model: 'hy3-preview',
    messages: messages || [{ role: 'user', content: '你好' }],
  })
  return { text: result.text, usage: result.usage }
}
```
