# 接入方式

小程序 AI 开发模式（以下简称此模式）提供了一套智能化的运行环境和开发框架，开发者可根据自身业务要求，参考本文档，将小程序的功能抽象为原子接口和原子组件，并封装成 `SKILL`，供小程序 AI 调用。

## 一、全局提示词

全局提示词可以用来整体说明此模式下提供的服务范围，提供相关的背景知识、行为逻辑和回答风格，引导小程序 AI 生成猜你想问的内容等等。

另一方面，一个小程序可能存在多个 `SKILLs`，多个 `SKILLs` 的关联关系及相关介绍也可以在全局提示词上说明，以帮助模型选择合适的 `SKILL` 和原子接口来为用户提供服务。

通常，全局提示词是一个名为 `AGENTS.md`  的文件，在小程序 AI 中，你可以这样指定它的路径：

``` json
// app.json
{
  "agent": {
    "skills": [],
    "instruction": "path/to/AGENTS.md",
  }
}
```

> instruction 字段目前非必填，文件大小为最大 10000 字节。

## 二、SKILL 声明

一个小程序可声明多个 `SKILLs`，`SKILL` 必须封装在一个独立分包里，而一个独立分包可以放置多个 `SKILLs`，且需要全局开启按需注入 `lazyCodeLoading`。

app.json 中增加 `agent` 字段，声明 `SKILL` 列表（`agent.skills` 字段），每个 `SKILL` 的字段说明如下：

| 字段 | 必填 | 说明 |
| - | - | - |
| name | 是 | 名称 |
| description | 是 | SKILL 简要说明 |
| path | 是 | SKILL 分包路径（绝对路径） |

``` json
// app.json
{
  "lazyCodeLoading": "requiredComponents",
  "subPackages": [
    {
      "root": "path/to/pkg",
      "independent": true,
      "pages": []
    }
  ],
  "agent": {
    "skills": [
      {
        "name": "weatherSkill",
        "description": "查询天气业务",
        "path": "path/to/pkg/weatherSkill"
      },
      {
        "name": "shoppingSkill",
        "description": "购物业务",
        "path": "path/to/pkg/shoppingSkill"
      }
    ]
  }
}
```

> 目前一个小程序最多可以声明 30 个 SKILL。

## 三、SKILL 封装

`SKILL` 包含业务说明（`SKILL.md`）、模型可调用能力的声明（`mcp.json`）、原子接口（apis）与原子组件（components）的实现，其目录结构如下：

| 文件 |	说明 | 固定目录结构 |
| - | - | - |
| SKILL.md |	SKILL 的详细说明，只支持单文件，不支持引用其他 md 文件 |	是，最大长度 16000 字节 |
| index.js	| 注册当前 SKILL 所涉及的所有原子接口 |	是 |
| mcp.json |	模型可调用能力的声明（当前 SKILL 所涉及的所有原子接口声明） |	是，最大长度 24000 字节 |
| components/ | 	当前 SKILL 所涉及的原子组件代码实现的所在目录 |	否，仅作为建议 |
| apis/	|  当前 SKILL 所涉及的原子接口代码实现的所在目录 |	否，仅作为建议|

> 目前计算 mcp.json 的长度时，会除去所有的 outputSchema 字段及空格、换行符后再计算。

``` bash
|-- path/to/pkg/weatherSkill
| |-- SKILL.md
| |-- mcp.json
| |-- components/
| |-- apis/
| |-- index.js
```

### 3.1 模型可调用能力的声明

`mcp.json` 包含所有原子接口的 Schema 声明，其中 `componentPath` 表示该原子接口返回的结果可由原子组件展示卡片。`inputSchema` 和 `outputSchema` 需遵循 JSON Schema 规范。

| 属性 | 是否必填 | 说明 |
|------|----------|------|
| name | 是 | 标识符，跟 `index.js` 中导出的原子接口函数名一致 |
| description | 是 | 原子接口的功能描述 |
| inputSchema | 是 | 原子接口的入参，需为对象格式 |
| outputSchema | 建议填 | 原子接口返回的 `structuredContent `对应的 schema |
| _meta | 否 | 元信息配置：`ui.componentPath` 指定渲染的原子组件（相对于 `SKILL` 目录）；`context` 声明由平台注入函数第二个参数的只读上下文，如本轮用户原文，详见 3.9 用户 query 透传。 |

下面是只有一个 getWeather 的原子接口的声明示例：

```json
{
  "apis": [
    {
      "name": "getWeather",
      "description": "查询当前位置或某地的未来一段时间的天气",
      "_meta": {
        "ui": {
          "componentPath": "path/to/comp",
          "pagePath": "path/to/page"
        }
      },
      "inputSchema": {
        "type": "object",
        "properties": {
          "location": {
            "type": "string",
            "description": "要查询天气的地点，如城市名、行政区名或经纬度。不传则默认取当前位置",
          },
          "days": {
            "type": "number",
            "description": "预报天数，范围1-15，默认7",
          }
        },
        "required": [
          "days"
        ]
      },
      "outputSchema": {}
    }
  ]
}
```

#### 处理图片/文件内容

小程序 AI 的输入框支持了多模态上传，用户可以发送图片/文件给小程序 AI。如果需要处理用户上传的图片/文件，在原子接口的声明中，在 `inputSchema` 定义的字段中填写 "format": "image" | "file"`，小程序 AI 会调用相应的原子接口，并将接收到的图片/文件传给对应字段。

```json
{
  "name": "EditPhoto",
  "description": "帮用户 P 图",
  "inputSchema": {
    "type": "object",
    "properties": {
      "imagePath": {
        "type": "string",
        "description": "本地图片路径",
        "format": "image"
      },
      "query": { "type": "string", "description": "用户的 P 图需求" }
    },
    "required": [ "imagePath", "query" ]
  }
}
```

#### 用户 query 透传

需要获取本轮用户原文时，可通过 `_meta.context.query` 声明由平台直接透传，无需模型复述，详见 3.9 用户 query 透传。

#### 回复正文中的页面链接

若希望模型回复的正文里出现可点击的小程序页面链接，在原子接口 `outputSchema` 中，将页面路径对应的 string 字段标记为 `"format": "page-link"`，并用 `x-link-text-field` 指向同级描述字段。小程序 AI 会从 `structuredContent` 抽取路径，在回复中渲染为 `#小程序：名称/描述` 蓝链。完整字段约定与示例见 3.7 文本混排链接。

### 3.2 原子接口实现

以原子接口 getWeather 为例，其返回值说明如下：

| 属性 | 类型 | 必填 |
|------|------|------|
| isError | boolean | 否，默认为 false |
| content | ContentBlock[] | 是，返回给 LLM 的文本内容，不超过 200 KB |
| structuredContent | Record | 否，返回给 LLM 的结构化数据，不超过 200 KB。若 outputSchema 中标记了 page-link，将从该对象抽取页面路径用于正文混排，详见 3.7 |
| handoff | HandoffResolver | 否，接力到小程序页面的数据，详情见下一节「小程序接力页」 |
| _meta | Record | 否，对 LLM 不可见，可携带元数据，不超过 200 KB |
| apiCalls | { name: string; arguments: Record }[] | 否，显式指定下一步调用的原子接口 |

其中 `isError`、`structuredContent`、`content` 三个字段的数据是会作为 LLM 的上下文供 LLM 理解，而 `_meta` 内容对 LLM 不可见，用于携带私有数据，传递给原子组件。

当 `isError: true` 时，将不进行 GUI 卡片渲染，`structuredContent` 将被忽略（包括不抽取文本混排链接），`content` 常用于异常流程的引导。

当 `isError: false` 时，且该原子接口有绑定原子组件，小程序 AI 将倾向于渲染  GUI 卡片，`structuredContent` 数据会传给原子组件用来渲染（便于模型理解 GUI 卡片的内容），`content` 用于后续流程引导。比如，如需强制出 GUI 卡片时，可在 `content` 里追加引导 ：「接下来为用户展示 XXX UI 卡片」。

ContentBlock[] 目前只支持 TextContent，其定义如下：

```typescript
interface TextContent {
  type: "text";
  text: string;
}
```

HandoffResolver 定义如下：

```typescript
type HandoffResolver = (data: {
  result: {
    isError: boolean
    content: ContentBlock[]
    structuredContent: Record<string, unknown>
    _meta: Record<string, unknown>
  },
}) => { query: string; payload: Record<string, unknown> }
```

在原子接口的内部实现中，可以通过 `wx.request` 从服务器请求数据，也可以通过[微信云开发](/miniprogram/dev/wxcloudservice/wxcloud/basis/getting-started.md)调用云函数或数据库获取数据。

下面是原子接口 getWeather 的代码实现示例：

```javascript
async function getWeather({ location, days }) {
  // 原子接口内部实现，此处省略
  // wx.request({ url: 'https://www.example.com/get_weather' })
  // wx.cloud.callFunction({ name: 'get_weather' })

  return {
    isError: false,
    content: [{ type: "text", text: "已为您查询到北京未来3天天气" }],
    structuredContent: {
      location: "北京",
      forecasts: [
        { date: "2026-04-23", dayWeather: "晴", nightWeather: "晴", tempHigh: 28, tempLow: 14, wind: "北风2级" },
        { date: "2026-04-24", dayWeather: "多云", nightWeather: "阴", tempHigh: 25, tempLow: 13, wind: "东南风3级" },
        { date: "2026-04-25", dayWeather: "小雨", nightWeather: "中雨", tempHigh: 20, tempLow: 12, wind: "东风4级" }
      ]
    },
  }
}
```

#### 显式指定下一步调用的原子接口

在某些较固定流程的场景中，需要在一次原子接口调用之后，明确要触发下一个原子接口的调用。此时，可显式指定要调用的原子接口，以减少模型推理的耗时。

```javascript
wx.modelContext.registerAPI('placeOrder', async ({ skuId, count }) => {
  const loggedIn = await checkUserLogin()

  // 未登录：指示下一步调用 login
  if (!loggedIn) {
    return {
      isError: false,
      content: [{ type: 'text', text: '用户未登录' }],
      apiCalls: [{ name: 'login', arguments: {} }],
    }
  }

  // 已登录：正常下单
  const order = await createOrder({ skuId, count })
  return {
    isError: false,
    content: [{ type: 'text', text: `订单已创建` }],
  }
})
```

### 3.3 注册原子接口

在 `path/to/pkg/weatherSkill/index.js` 进行注册，当小程序 AI 发起原子接口调用时，就会在注册列表中找到原子接口的函数入口并执行：

```javascript
const getWeather = require('./apis/getWeather')

const skill = wx.modelContext.createSkill('/path/to/pkg/weatherSkill')
// name 需与定义文件中的一致
skill.registerAPI('getWeather', getWeather)
```

#### 中间件机制

为了方便在多个原子接口同时处理一些公共逻辑，我们提供了原子接口中间件的机制，可用于统一登录态，统一上报和错误监听等场景。

- wx.modelContext.createSkill(skillPath: string)  // 创建 skill，传入的 skillPath 参数与 app.json 上的 `agent.skills[].path` 一致
  - skill.use(Middleware) // 注册中间件
  - skill.registerAPI(name, handler)  // 注册原子接口，等同于 wx.modelContext.registerAPI('name', handler)

```typescript
interface MiddlewareContext {
  name: string                  // 当前执行的原子接口名
  skillPath: string             // 当前中间件执行时所在的skill
  arguments: Record<string, any>  // 原子接口的入参
}
type Middleware = (ctx: MiddlewareContext, next: () => Promise<void>) => Promise<void>
```

示例：

```javascript
const skill = wx.modelContext.createSkill('pages/weather')
skill.registerAPI('getWeather', getWeather)
skill.use(async (ctx, next) => {
  // 1. 统一登录（仅首次）
  const token = wx.getStorageSync('token')
  if (!token) {
    const { code } = await wx.login()
    const res = await wx.request({ url: '...', data: { code } })
    wx.setStorageSync('token', res.data.token)
    // 也可以直接抛出错误，让Agent进行其他操作
    // throw new Error('登录过期，接下来自动执行xxx skill 下的 login 工具')
  }
  // 2. 上报：记录开始时间
  const start = Date.now()
  try {
    await next()  // 执行实际 handler
    // 上报成功
    reportLog({
      name: ctx.name,
      cost: Date.now() - start,
    })
  } catch (err) {
    // 3. 统一错误监听
    reportError({
      name: ctx.name,
      error: err,
      cost: Date.now() - start,
    })
    throw e
}) 
```

核心语义：
- 每次原子接口调用都会执行中间件
- next() 前原子接口调用的前置逻辑，next() 后是后置逻辑，try/catch 包裹可捕获错误
- 整个中间件链与原子接口执行时间共享超时上限（当前为 300s）
- 支持多个中间件：按注册顺序形成链，外层 -> 内层 -> handler -> 内层 -> 外层
- 中间件函数为 async 类型，运行时 await 整个链路

### 3.4 接力到小程序页面

> 此特性的版本支持范围：微信 iOS 8.0.75，基础库 3.16.2，开发者工具 nightly 2.02.2607032

原子接口返回的结构化数据有两种呈现方式，其中一个是小程序页面，也就是在对话流程中，原子接口被调用后所返回的数据会传入小程序页面，将对话流程接力到小程序页面内，以无缝衔接当前流程状态。

若只需在回复文字中间给出详情入口，而不把当前对话流程整段接力进页面，应使用 outputSchema 的 `format: "page-link"`（见 3.7），不要返回 `handoff`。两种能力可同时存在，互不替代。

**实现上，分以下三步：**

#### 1、在 mcp.json 声明原子接口对应的 pagePath

> pagePath 暂不支持动态改变

```json
{
  "apis": [
    {
      "name": "getWeather",
      "_meta": {
        "ui": {
          "pagePath": "/path/to/page"
        }
      }
    }
  ],
  "components": []
}

```

#### 2、原子接口返回  handoff 对象

在原子接口返回 `handoff` 对象，与 `content` / `structuredContent` 字段同级。`handoff` 对象包含 `query` 与 `payload` 字段，其中 `query` 与前面的 `pagePath` 字段构造出打开小程序页面的链接，而 `payload` 字段通过 `wx.onAgentHandoff` 接口传递给小程序。

| 字段 | 必填 | 说明 |
| - | - | - |
| path  | 否 | 允许动态指定页面路径 |
| query | 否 | 页面 query 字符串，小程序页面通过 Page.onLoad 和 wx.onAgentHandoff 获取 |
| payload | 否  | 可传递原子接口返回的业务数据，用于小程序页面同步对应的流程状态，小程序页面通过 wx.onAgentHandoff 获取 |

下面是原子接口 getWeather 的代码实现示例

```javascript
async function getWeather({ location, days }) {
  // 原子接口内部实现，此处省略
  // wx.request({ url: 'https://www.example.com/get_weather' })
  // wx.cloud.callFunction({ name: 'get_weather' })

  return {
    isError: false,
    content: [{ type: "text", text: "已为您查询到北京未来3天天气" }],
    structuredContent: {},
    handoff: (data) => {
      // data.result.structuredContent 模型根据用户意图过滤后数据
      // query 包含特殊字符时，开发者需进行 encodeURIComponent
      return {
        query: `poi=${encodeURIComponent('北京')}`,
        payload: {
          location: '北京',
          forecasts: [
            { date: "2026-04-23", dayWeather: "晴", nightWeather: "晴", tempHigh: 28, tempLow: 14, wind: "北风2级" },
            { date: "2026-04-24", dayWeather: "多云", nightWeather: "阴", tempHigh: 25, tempLow: 13, wind: "东南风3级" },
            { date: "2026-04-25", dayWeather: "小雨", nightWeather: "中雨", tempHigh: 20, tempLow: 12, wind: "东风4级" }
          ]
        },
      }
    },
  }
}
```

#### 3、小程序监听 wx.onAgentHandoff 获取 handoff 数据

小程序通过 `wx.onAgentHandoff ` 获取原子接口传过来的 `handoff` 数据，用于将页面同步至对应的流程状态，必要时，也需将业务流程对应的页面栈同步成合适的状态。

| API | 说明|
| - | - |
| wx.onAgentHandoff(callback) | 点击小程序卡片进入时触发 |
| wx.offAgentHandoff(callback) |  取消监听 |

回调参数：

```javascript
{
  pageId: number    // 目标页实例 ID，用于关联跳转的页面，精确投递 payload
  path: string      // 当前打开的页面的 path（不含 query）
  query: string     // 页面 query 字符串，与原子接口返回的 handoff.query 相同
  payload: unknown  // 原子接口返回的 handoff.payload
}
```

代码示例：

```javascript
App({
  onLaunch() {
    wx.onAgentHandoff(({ pageId, path, query, payload }) => {
      this.globalData.agentHandoffs = this.globalData.agentHandoffs || {}
      this.globalData.agentHandoffs[pageId] = { path, query, payload }
    })
  },
})

Page({
  onLoad(query) {
    const handoffs = getApp().globalData.agentHandoffs || {}
    const handoff = handoffs[this.getPageId()]
    delete handoffs[this.getPageId()]

    // 使用原子接口返回的数据更新页面状态
    this.setData({ forecasts: handoff.payload.forecasts })

    // 使用 query 参数拉取数据，注意若 handoff 提交的 query 有 encode ，这里需要自行 decode
    const parsedPoi = decodeURIComponent(query.poi)
    this.loadPoiDetail(parsedPoi).then(forecasts => this.setData({ forecasts }))
  },
})

```

总体上，可参考[示例 demo](https://github.com/wechat-miniprogram/ai-mode-demo)

### 3.5 原子组件实现

> 暂不支持调试

#### 3.5.1 原子组件定义

原子组件用于承接原子接口返回的结构化数据的展示。在编码上，其与小程序的自定义组件一致，不过需要注意的是，原子组件不可声明为虚拟组件。

通过 wx.modelContext.getContext / getViewContext 获取原子组件渲染时需要的尺寸、数据等信息。

```javascript
Component({
  lifetimes: {
    created() {
      const modelCtx = wx.modelContext.getContext(this);
      const { NotificationType } = wx.modelContext;
      modelCtx.on(NotificationType.Input, (data) => {
        // 原子接口的入参数据
      });
      modelCtx.on(NotificationType.Result, (data) => {
        // 原子接口的出参数据
      });

      const viewCtx = wx.modelContext.getViewContext(this);
      const { minHeight, maxHeight, width } = viewCtx.getDimensions();
      viewCtx.on(NotificationType.Overflow, (data) => {
        // data.contentHeight: 内容总高
        // data.overflowHeight: 溢出高度
        // data.maxHeihgt: 当前卡片容器的硬性阈值
        // 如果给原子组件的 root element 设置 maxHeight 和 overflow: hidden 则无法触发
      });
    }
  }
})
```

#### 3.5.2 关联小程序页面（必须）

原子组件渲染出来的 GUI 卡片上方有个标题栏，其右上方提供进入小程序的入口，该入口需配置与原子组件关联的小程序页面，是必填项。通过此方式进入小程序的场景值为 1442 或 1443。

![](https://res8.wxqcloud.qq.com.cn/wxdoc/c8904191-99cc-4142-8b16-d11d138f475e.png)

实现上，分为两步：

**1、在 mcp.json 声明原子组件所对应的小程序页面的完整 path（固定 path）：**

```json
{
  "apis": [],
  "components": [
    {
      "path": "path/to/comp",
      "relatedPage": "/path/to/related-page"
    }
  ]
}
```

**2、在原子组件渲染时，动态设置页面的 query 参数：**

```javascript
Component({
  lifetimes: {
    created() {
      const modelCtx = wx.modelContext.getContext(this)
      this._viewCtx = wx.modelContext.getViewContext(this)

      modelCtx.on('result', (data) => {
        const { orderId } = data.result.structuredContent
        this.setData({ orderId })
        // 若只传 query，路径由 mcp.json 中的 components[].relatedPage 决定
        this._viewCtx.setRelatedPage({ query: `orderId=${orderId}` })
        // 若原子组件可能与多个小程序页面关联，也可传入 path 参数
        this._viewCtx.setRelatedPage({ path: '', query: `orderId=${orderId}` })
      })
    }
  },
  methods: {
    onSpecChange(e) {
      const spec = e.detail.value
      this.setData({ selectedSpec: spec })
      // 状态变化后更新 query 参数
      this._viewCtx.setRelatedPage({
        query: `orderId=${this.data.orderId}&spec=${spec}`
      })
    }
  }
})
```

#### 3.5.3 原子组件的约束

**原子组件的运行环境与原子接口隔离，处于不同的上下文中**，其可调用的接口能力也与原子接口有所不同。

原子组件存在以下特性与限制：

- 原子组件的渲染区域有限，其宽度随屏幕宽度变化，其最小高度是宽高比 4:1，最大高度是宽高比 1:1，不超出最小最大高度时，随内容自动撑高
- 初始化时决定卡片高度，后续不可再改变高度
- 仅支持 tap 点击、Image load、image error 事件，不支持其他交互事件
- 默认不支持网络请求和云开发接口，需声明为「实时动态组件」
- 默认不支持定时器接口，如 setTimeout、setInterval，需声明为「实时动态组件」
- 不支持打开小程序接口
- 不支持动画
- 禁止上下滚动（即禁止使用 overflow-y）
- 支持点击后帮用户上行一条文本消息（ModelContext.sendFollowUpMessage），效果等同于用户语音发送一条文本消息
- 支持点击后打开半屏页面（见下节）

#### 3.5.4 实时动态组件

对于需要展示实时动态内容的合理的场景，可在 `mcp.json` 声明需要实时动态能力（支持 `wx.request`、定时器等接口）

注意：此能力需要单独审核（正常提审即可），请声明使用场景，非必要场景不建议使用

```json
{
  "apis": [],
  "components": [
    {
      "path": "path/to/comp",
      "permissions": {
        "scope.dynamic": {
          "desc": "声明使用场景"
        }
      }
    }
  ]
}
```

#### 3.5.5 原子组件过期态

支持通过接口形式，将已经渲染出来的原子组件置为过期态，防止用户再次点击。

![](https://res8.wxqcloud.qq.com.cn/wxdoc/f89e7541-d052-4792-af38-2fdb128e568f.png)

**1、组件过期声明**

在 `mcp.json` 的 `components` 字段下声明：

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| path | string | 是 | 组件路径（相对于 skill 目录） |
| expirable | boolean | 否 | 声明该组件渲染的卡片是否可被过期，默认 false |
| expiredText | string | 否 | 过期后显示的文案，默认 “服务已过期” |

```json
{
  "apis": [
    {
      "name": "searchRides",
      "description": "搜索打车方案",
      "_meta": { "ui": { "componentPath": "components/ride-card/index" } },
      "inputSchema": { ... }
    }
  ],
  "components": [
    {
      "path": "components/ride-card/index",
      "expirable": true,
      "expiredText": "店铺已关闭"
    }
  ]
}
```

**2、组件过期接口**

提供两种粒度的过期 API：

**方式一：wx.modelContext.expireAllCards()**

```javascript
wx.modelContext.expireAllCards(): Promise<{ errMsg: string }>
```

可在原子接口/组件里调用。所有声明了 `expirable: true` 的组件（包括自身），将被标记为过期并渲染蒙层。

同时支持按 `componentPath` 过滤，使所有相同的 `componentPath` 的原子组件或最近一个特定的 `componentPath` 的原子组件过期：

```javascript
// 过期特定组件的卡片（绝对路径）
wx.modelContext.expireAllCards({
  componentPaths: ['packageA/weatherSkill/components/weather-card/index']
})

// 只过期最近一张匹配的卡片
wx.modelContext.expireAllCards({
  componentPaths: ['packageA/weatherSkill/components/weather-card/index'],
  match: 'latest'
})
```

**方式二：wx.modelContext.getViewContext(this).expirePreviousCards()**

```javascript
const viewCtx = wx.modelContext.getViewContext(this)
viewCtx.expirePreviousCards(): Promise<{ errMsg: string }>
```

只能在原子组件里调用，之前渲染过的原子组件，声明了 `expirable: true` 的将被标记为过期并渲染蒙层。

同时支持按 `componentPath` 过滤，使所有相同的 `componentPath` 的原子组件或最近一个特定的 `componentPath` 的原子组件过期：

```javascript
// 过期特定组件的卡片（绝对路径）
viewCtx.expirePreviousCards({
  componentPaths: ['packageA/weatherSkill/components/weather-card/index']
})

// 只过期最近一张匹配的卡片
viewCtx.expirePreviousCards({
  componentPaths: ['packageA/weatherSkill/components/weather-card/index'],
  match: 'latest'
})
```

**3、组件过期事件**

原子组件被置为过期时，会收到过期事件，此时原子组件内部可以做一些必要的清理。

```javascript
Component({
  lifetimes: {
    created() {
      const { NotificationType } = wx.modelContext;
      const viewCtx = wx.modelContext.getViewContext(this);

      viewCtx.on(NotificationType.Expire, (event) => {
        console.log('卡片已过期', event.expiredText)
        // 停止轮询、禁用按钮等清理逻辑
      })
    }
  }
})
```

#### 3.5.6 半屏原子组件

对于列表场景，在卡片里面可能难以承载全部列表项，这时可以使用半屏原子组件，对卡片进行半屏化，呈现更多的内容。

**使用方式**

提供 `collapsible-view` 组件：

- 自定义内容：可以放置任意内容。
  - 在原子组件下：会根据卡片可用空间计算可展示的内容，超过的内容直接隐藏。
  - 在半屏原子组件下：全部展示。
- 自定义展开按钮：提供 `slot: button` 用于自定义展开按钮的形式，在半屏形态下会自动隐藏。

```html
<collapsible-view>
  <!-- 自定义内容 -->
  <view slot="button">
      <!-- 自定义展开按钮 -->
  </view>
</collapsible-view>
```

**最佳实践**

为避免由于组件可用空间不足导致自定义内容被全部隐藏，推荐把每一项作为 `collapsible-view` 的直接子节点，这样空间不足时，能正确展示和隐藏对应的节点。

```html
<collapsible-view>

  <!-- 推荐：直接把每一项作为直接子节点
    <block wx:for=""></block>
  -->

  <!-- 不推荐：嵌套结构
    <view>
      <block wx:for=""></block>
    </view>
  -->

  <view slot="button"></view>
</collapsible-view>
```

**注意**

- 半屏原子组件只是原子组件的延伸，仍视为是原子组件，所以原有的限制仍然存在。

#### 3.5.7 同步交互状态给模型

用户在原子组件卡片上进行交互操作（如点击按钮、选择选项等）修改卡片状态时，可通过 `updateModelContext` 接口将状态变更同步给模型，以便模型感知用户的最新选择并做出后续响应。

##### 使用方式

```javascript
Component({
  methods: {
    onOptionSelect(e) {
      const { option } = e.currentTarget.dataset

      // 将用户选择同步给模型
      const viewCtx = wx.modelContext.getViewContext(this)
      viewCtx.updateModelContext({
        content: [
          { type: 'text', text: `用户选择了${option}` }
        ]
      })
    }
  }
})
```

##### 参数说明

| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `content` | `Array<{ type: 'text', text: string }>` | 是 | 内容块数组，当前仅支持 `text` 类型 |

##### 注意事项

- **需在用户点击手势中调用**：必须在用户的 tap 点击事件回调中调用，不可在异步逻辑或定时器中调用
- **节流限制**：短时间内重复调用会被拒绝
- **内容校验**：`content` 必须为非空数组，且每个内容块的 `text` 不能为空

### 3.6 半屏页面

> 暂不支持调试

半屏页面是原子组件内容的延伸，是个非必要流程，只有当需要展示更多详情信息，或者需要用户补充信息时，才使用半屏页面能力，通过点击原子组件上的某个按钮打开半屏页面。

半屏页面的环境可以认为是与小程序的环境一致，可以用已有的小程序页面来加载，但不允许任何跳转，包括所有跳出半屏的接口、页面路由接口、广告相关接口组件。

当用户需要进行下一步时，应是点击半屏里的某个按钮，触发上行一段文本消息，此时会同时关闭半屏页面回到小程序 AI 界面，小程序 AI 继续处理新的用户请求。

> 注：半屏打开小程序页面的场景值为 1433 或 1434

> 注：通过 wx.getDetailPageCloseButtonBoundingClientRect 接口适配左上角关闭按钮

#### 3.6.1 半屏页面实现

在原子组件内，响应点击并打开半屏页面（原子接口内不可调用）：

```html
<view> xxx </view>
<view bind:tap="showDetail">查看未来 15 天的天气</view>
```

```javascript
Component({
  lifetimes: {
    created() {
    }
  },
  methods: {
    showDetail() {
      const viewCtx = wx.modelContext.getViewContext(this);
      viewCtx.openDetailPage({
        url: '/packageA/pages/weather-detail?foo=bar'
      })
    }
  }
})
```

在半屏页面中，代用户上行一条文本消息（同时会关闭半屏页面）：

**注意：需要以用户第一人称的角度来发送这条消息**

```javascript
Page({
  onTap() {
    const ctx = wx.modelContext.getContext()
    ctx.sendFollowUpMessage({
      content: [
        {
          // 必须传
          type: 'text',
          text: '选择拿铁'
        },
        {
          // 指定下一步应该调用哪个原子接口，若不传，则由模型推理
          type: 'api/call',
          data: {
            name: 'selectGoods',
            arguments: {}
          }
        }
      ]
    })
  }
})
```

若半屏页面是 web-view 加载的 h5 页面，则上行文本消息的方式是：

```javascript
// 前置 wx.config()
wx.ready(function () {
    WeixinJSBridge.invoke('invokeMiniProgramAPI', {
      name: 'sendFollowUpMessage',
      arg: {
        content: [
          {
            // 必须传
            type: 'text',
            text: '选择拿铁'
          },
          {
            // 指定下一步应该调用哪个原子接口，若不传，则由模型推理
            type: 'api/call',
            data: {
              name: 'selectGoods',
              arguments: {}
            }
          }
        ]
      }
    }, function (res) {})
})
```

#### 3.6.2 半屏页面的约束

半屏页面的执行环境与小程序页面一致，但可调用的接口能力有所不同。半屏页面存在以下限制：

- 所有跳出半屏去往公众号、视频号、其他小程序、表情、问一问、地图 App 的接口
- 所有页面路由接口
- 所有广告相关的接口组件

#### 3.6.3 半屏页面预加载

为了给用户提供更好的体验，我们提供了预加载半屏页面的能力，若当前原子组件存在调用拉起半屏页面的逻辑，开发者可以在原子组件创建后调用 `preloadDetailPage`

```javascript
Component({
  lifetimes: {
    attached() {
      const viewCtx = wx.modelContext.getViewContext(this);
      viewCtx.preloadDetailPage({
        url: '/packageA/pages/weather-detail?foo=bar'
      })
    }
  },
})
```

#### 3.6.4 从半屏页面返回并更新卡片

在通过小程序 AI 完成用户需求的过程中，有时需要通过拉起半屏页面操作后，更新卡片内容，我们支持了通过重新调用对应的原子接口的方式，来重新渲染原子组件。在这种方式下，既能通过半屏页面操作更新卡片内容，也能通过用户语音交互来更新。

打开半屏页面的调用链为：原子接口 -> 原子组件 -> 半屏页面，在半屏页面里，调用 `ModelContext.reapplyApiCall` 重新执行调用链。

```javascript
const modelCtx = wx.modelContext.getContext()

modelCtx.reapplyApiCall({
  arguments: {
    city: '广州',
    days: 3
  }
})

```

### 3.7 文本混排链接
![](https://res8.wxqcloud.qq.com.cn/wxdoc/75451ac0-3fa8-4026-bb22-7a0f61a84471.png)
![](https://res8.wxqcloud.qq.com.cn/wxdoc/0f9636d1-5376-4b7c-870d-fc8a9de82297.png)

原子接口通过 `mcp.json` 的 `outputSchema` 描述返回的 `structuredContent`。若将某个 string 字段标记为可跳转的小程序页面路径，小程序 AI 即可在回复正文里内联可点击蓝链，用户点击后打开对应页面。

开发者只需改两处：① 在 `mcp.json` 的 `outputSchema` 给页面路径字段加上 `"format": "page-link"`；② 原子接口的 `structuredContent` 按 schema 返回真实页面路径。不必在业务代码里拼 Markdown 或短链。

用户侧展示形态：`[#小程序：Westore Cafe/冰美式]`（可点击，打开 `pages/goods/detail?id=1`）

文本混排链接不替代接力、卡片入口、账号卡片等现有跳转方式，只解决「回复文字中间夹一条详情入口」的场景。

| 能力 | 声明位置 | 用户看到什么 | 适用 |
| - | - | - | - |
| 文本混排链接 | `outputSchema` 字段 `format: "page-link"` | 回复正文中的 `#小程序：名称/描述` 蓝链 | 推荐商品、订单、门店等，在一句话里给出可点详情 |
| 接力到小程序页面 | `_meta.ui.pagePath` + 返回 `handoff` | 对话流程整段接力进指定页 | 需要把当前流程状态带到小程序页 |
| 原子组件关联页 | `components[].relatedPage` | 卡片标题栏右上角进小程序 | 已出 GUI 卡片时的入口 |
| 账号卡片 / 服务直达 | `agent.pageMetadata` | 小程序账号卡片 | 没有合适原子接口时，引导进页面 |

**实现上，分以下两步：**

#### 1、在 outputSchema 中标记页面路径字段

`inputSchema` / `outputSchema` 仍遵循 JSON Schema。扫描只认识常见结构：`type: object` + `properties`、`type: array` + `items`。嵌套对象、数组项都可以标。`oneOf` / `anyOf` / `$ref` 不会展开。

叶子字段必须是 `type: "string"`，再加 `format`。字段名叫 `pagePath` / `path` / `detailPath` 都可以，**以 format 为准**。

| 键 | 必填 | 说明 |
| - | - | - |
| type | 是 | 必须为 `"string"` |
| format | 是 | 固定 `"page-link"`，表示该字段值是可跳转的小程序页面路径 |
| x-link-text-field | 是 | 同级字段名，用作蓝链里的「描述」。必须与路径字段在同一个对象上，不能跨层级 |

```json
"pagePath": {
  "type": "string",
  "format": "page-link",
  "x-link-text-field": "name"
}
```

**路径约定**

- 填小程序页面路径，可带 query / hash：`pages/goods/detail?id=1`、`/packageDetail/pages/sku-picker?drinkId=289`
- 不要填 HTTPS 链接
- 空字符串、缺字段会被跳过；相同 path（含 query）会去重
- 单次原子接口结果最多抽取 20 条链接

#### 2、原子接口返回真实页面路径

返回值字段与原子接口实现相同。混排链接只读取 `structuredContent` 里被 schema 标过的路径字段；`content` 仍给模型作文案，`_meta` 仍不对模型可见。

| 属性 | 与混排链接的关系 |
| - | - |
| isError | `true` 时不抽取链接（此时会忽略 `structuredContent`） |
| content | 给模型的文本，不要在这里手写 Markdown 超链接 |
| structuredContent | 必须包含 schema 中声明的 page-link 字段，值为真实页面 path |
| handoff | 可选，与混排链接独立；需要整页接力时仍按 `handoff` 方式实现 |

下面以商品搜索为例。SKILL 目录与注册方式与其他原子接口相同，只需扩展 `outputSchema` 与返回数据。

```json
{
  "apis": [
    {
      "name": "searchGoods",
      "description": "按关键词搜索可购买的商品，返回名称、价格和详情页路径",
      "inputSchema": {
        "type": "object",
        "properties": {
          "keyword": {
            "type": "string",
            "description": "搜索关键词，如美式、拿铁"
          }
        },
        "required": ["keyword"]
      },
      "outputSchema": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "goodsId": { "type": "number" },
                "name": { "type": "string" },
                "price": { "type": "number" },
                "pagePath": {
                  "type": "string",
                  "format": "page-link",
                  "x-link-text-field": "name"
                }
              }
            }
          },
          "total": { "type": "number" }
        }
      }
    }
  ]
}
```

```javascript
async function searchGoods({ keyword }) {
  // wx.request / 云函数，省略
  return {
    isError: false,
    content: [{ type: "text", text: "已为您找到两款咖啡" }],
    structuredContent: {
      items: [
        {
          goodsId: 1,
          name: "冰美式",
          price: 18,
          pagePath: "pages/goods/detail?id=1"
        },
        {
          goodsId: 2,
          name: "拿铁",
          price: 22,
          pagePath: "/packageDetail/pages/sku-picker?drinkId=289"
        }
      ],
      total: 2
    }
  }
}

module.exports = searchGoods
```

注册方式与其他原子接口相同：

```javascript
const searchGoods = require('./apis/searchGoods')
const skill = wx.modelContext.createSkill('/path/to/pkg/shoppingSkill')
skill.registerAPI('searchGoods', searchGoods)
```

不一定放在数组里。订单详情这类「一次只返回一个页面」可以标在根对象上：

```json
{
  "type": "object",
  "properties": {
    "title": { "type": "string" },
    "detailPath": {
      "type": "string",
      "format": "page-link",
      "x-link-text-field": "title"
    }
  }
}
```

接入后，模型根据 `structuredContent` 组织回复，把条目对应的页面写成正文里的蓝链，例如：

```
为你找到两款咖啡：
冰美式 18 元，点击查看详情
[#小程序：Westore Cafe/冰美式]
拿铁 22 元，可以去规格页选杯型
[#小程序：Westore Cafe/拿铁]
```

> 计算 `mcp.json` 长度时会除去所有 `outputSchema` 字段，因此补上 page-link 标记一般不会额外占用该限额。未生效时优先核对：`mcp.json` 是否被正确上传、`outputSchema` 是否被解析、返回字段路径是否与 schema 一致；`format` 需精确写成 `page-link`（不是 pagelink / pageLink）；必须配置 `x-link-text-field`，指向同级、实际会返回的 string 字段。

### 3.8 隐私授权卡片

开发者在原子接口中调用 `wx.requestAgentPrivacyAuthorization`传入协议列表，可向用户展示授权卡片。用户可点击协议名称查看详情，同意后与小程序原有隐私授权链路对齐。

![](https://res8.wxqcloud.qq.com.cn/wxdoc/45c75e37-620a-489d-84f0-317bd174e7d7.png)

#### 使用方式

```javascript
declare namespace wx {
  function requestAgentPrivacyAuthorization(
    options?: AgentAuthOptions,
  ): Promise<AgentAuthResult>
}
```

#### 参数说明

**AgentAuthOptions**

| 属性 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| privacyAgreements | AgentPrivacyAgreement[] | 是 | 需要用户同意的协议列表 |
| success | function | 否 | 接口调用成功的回调函数 |
| fail | function | 否 | 接口调用失败的回调函数，res.errMsg 为错误信息 |
| complete | function | 否 | 接口调用结束的回调函数（调用成功、失败都会执行） |

**AgentPrivacyAgreement**

| 属性 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| name | string | 是 | 授权卡片中展示的协议名称 |
| path | string | 是 | 协议详情页面路径，可含 query / hash |

**AgentAuthResult**

| 属性 | 类型 | 说明 |
| --- | --- | --- |
| authorized | boolean | `true` 表示用户同意授权；`false` 表示用户拒绝或取消 |

#### 调用示例

```javascript
skill.registerAPI('loginByPhone', async () => {
  const { authorized } = await wx.requestAgentPrivacyAuthorization({
    privacyAgreements: [
      { name: '用户服务协议', path: 'pages/protocol/user-service' },
      { name: '隐私政策', path: 'pages/protocol/privacy' },
    ],
  })

  if (!authorized) {
    return {
      isError: true, 
      content: [{ type: "text", text: '隐私授权未通过' }]
    }
  }

 // 同意后再调用wx.getPrivacySetting，返回结果res.needAuthorization = false

```

#### 注意事项

- 数组顺序即协议展示顺序。
- 点击卡片上隐私协议的链接，在小微中隐私协议页面将以半屏的形式打开

### 3.9 用户 query 透传

> 该能力需基础库 **3.17.5 及以上**。

当原子接口需要将用户本轮 query 传给下游服务等场景时，可声明 `_meta.context.query`，由平台直接提供，无需模型复述。模型生成的业务参数通过函数第一个参数接收，平台透传通过第二个参数接收。

以反馈接口 `submitFeedback` 为例，用户说“支付成功后，订单页一直转圈。”，两类参数分别为：

| 接收到的数据 | 来源 |
| --- | --- |
| `args.category = "bug"` | 模型根据用户需求判断反馈分类 |
| `context.query = "支付成功后，订单页一直转圈。"` | 平台原样透传本轮用户原文 |

#### 3.9.1 声明原文透传

在 `mcp.json` 的 `apis` 数组中，为对应原子接口声明 `_meta.context.query`，并用 `desc` 说明原文的具体用途。`context` 与 `ui` 同属 `_meta`，可同时配置。以下为一个原子接口的声明：

```json
{
  "name": "submitFeedback",
  "description": "处理用户反馈，按问题、建议或其他类型分类。",
  "inputSchema": {
    "type": "object",
    "properties": {
      "category": {
        "type": "string",
        "enum": ["bug", "suggestion", "other"],
        "description": "反馈分类：bug 为问题，suggestion 为建议，other 为其他"
      }
    },
    "required": ["category"]
  },
  "_meta": {
    "context": {
      "query": { "desc": "本轮反馈原文，用于核对反馈内容和排查原文透传问题" }
    }
  }
}
```

分类仍由模型生成，本轮用户 query 无需在 `inputSchema` 中另设字段。

#### 3.9.2 在接口函数中读取

在 `apis/submitFeedback.js` 中，通过第二个参数读取 `context.query`，并按 3.3 注册原子接口。以下示例仅输出分类与本轮用户 query：

```javascript
async function submitFeedback({ category } = {}, context = {}) {
  const query = context.query || ''
  console.log('反馈分类：', category)
  console.log('本轮用户 query：', query)

  return {
    isError: false,
    content: [{ type: 'text', text: '已输出反馈分类与本轮用户 query' }],
    structuredContent: { category }
  }
}

module.exports = submitFeedback
```

#### 3.9.3 注意事项

**使用规范**

| 规则 | 说明 |
| --- | --- |
| 默认参数兜底 | 函数签名建议使用 `context = {}`，防止未注入时因 `context` 为 `undefined` 而读取报错。 |
| 读取时兜底 | 使用 `const query = context.query \|\| ''`，在未获取到用户 query 时回退为空字符串。 |
| 禁止修改 | `context` 为平台注入的只读上下文，原子接口内部不得尝试写回或修改。 |
| 声明即使用 | 确有原文用途时再声明 `_meta.context.query`，并在函数中按声明用途使用；不要声明后不使用，也不要在未声明的接口中依赖该字段。 |
| `desc` 写明用途且贴合语义 | `_meta.context.query.desc` 须用一句话说明本轮用户 query 的用途和有效范围，避免只写“用户输入”，并贴合当前原子接口语义。例如搜索接口可写“本轮搜索原文，用于向下游搜索服务透传”。 |

**迁移提示**：仅用于复述本轮用户 query 的入参，可从 `inputSchema` 中移除，改为通过 `context.query` 读取。同时应调整原子接口的 `description`，删除“需传入复述 query”等旧表述，避免残留描述误导模型继续生成多余入参。需要结合上下文补全的业务参数仍应保留在 `inputSchema` 中，不能直接用本轮用户 query 替代。

**常见问题**

**Q1：未声明 `_meta.context.query` 时如何处理？**

平台不会注入该字段，原子接口保持常规单参数签名 `async function fn(args = {})` 即可，不要在函数中依赖 `context.query`。声明并使用该能力时，再通过 `async function fn(args = {}, context = {})` 接收第二个参数，并做好兜底。

**Q2：`context.query` 会携带历史对话吗？**

不会，仅包含本轮用户 query。例如本轮说“换成杭州的”，`context.query` 也仅为这句话；结合上轮需求补全的搜索条件仍应通过 `inputSchema` 声明。需要跨轮次数据时，应由业务方自行存取或通过显式参数传递。

**Q3：使用此字段有什么限制吗？**

应在确实需要本轮原文的合理场景中使用，例如需要向下游透传原文的 AI 问答、AI 生成等原子接口。若只是为了统计用户需求，可使用 We分析中的相应模块，无需通过此方式获取原文。

## 四、知识库

小程序 AI 开发模式下，添加知识库后，模型可根据具体情况选择检索知识库内容作为回答依据。例如，可以服务于专业领域问答、企业智能助手等场景，通过提供专业知识、FAQ 等自定义内容为用户提供更准确的回答。

### 4.1 知识库的调用逻辑

当 C 端用户提问后，调用知识库的大致逻辑是：

1. 用户输入问题，模型分析问题意图。
2. 模型尝试根据语义来匹配 `SKILL`。
3. 如果用户问题属于知识查询，且与所有 `SKILL` 都不匹配，才会调用知识库查询。
4. 拿到结果后，模型组织语言回复用户。

**注意：**

- **模型会自主决策知识库的调用时机**，建议优化 `SKILL` 描述、文档质量，让模型更容易做出正确判断。
- 如果在特殊情况下更倾向调用知识库来解决用户需求，可以在全局提示词 `AGENTS.md` 或对应 `SKILL` 中进行引导。
- 如果所有` SKILL` 都不匹配，会使用知识库来兜底。

### 4.2 实操流程
![](https://res8.wxqcloud.qq.com.cn/wxdoc/940cd230-8333-4b39-8afc-a21c1a81d9d1.png)

**1. 上传文件**

上传入口： [微信公众平台](https://mp.weixin.qq.com/) > 基础功能 > AI能力 > 知识库

文件要求：
- 文件格式：PDF、DOC、DOCX、PPT、PPTX、TXT、MD、XLSX
- 单文件大小上限：10MB
- 文件总数上限：10个

**2. 自行测试召回效果**

文件解析完成后即可测试召回（不需要等审核通过），目前有两种测试渠道：

- **开发版/体验版**（完整效果）
  - 直接测试C端实现效果
  - 如果想要测 `SKILL` + 知识库的回答效果，只能在开发版/体验版测试

- **公众平台调试器**（简易效果）
  - 上传后快速验证文件是否被正确切片召回
  - 非C端最终回答效果，无上下文，无法调用`SKILL`

![](https://res8.wxqcloud.qq.com.cn/wxdoc/be5545bc-4d73-45c5-9a01-adf2ff93742e.png)

**3. 发布至正式版**

文件审核通过，且确认召回效果符合预期后，点击「发布」。文件状态变为「已发布」则表示生效，正式版可召回该文件。

> 注：由于内测阶段开发模式相关代码不建议合入正式版，目前知识库效果仅在开发版和体验版开放体验。

## 五、小程序服务直达

当用户需要快速触达小程序内的相关服务页面时，小程序 AI 支持以「账号卡片」的形式回复用户，用户可直接点击进入小程序。

该能力需要开发者提供页面元数据，小程序 AI 将根据用户问答的上下文，在满足用户意图时，生成小程序账号卡片发送给用户。通过此方式进入小程序的场景值为 1435 或1436。

账号卡片用于没有合适原子接口、需要按页面元数据引导进小程序的场景。若原子接口已能返回具体页面路径，并希望链接出现在回复正文中，请使用 3.7 文本混排链接，而不是为本条结果单独配 pageMetadata。

**页面元数据定义**

app.json 里增加 `pageMetadata` 字段

```json
// app.json
{
  "agent": {
    "skills": [],
    "pageMetadata": "path/to/page-meta.json",
  }
}
```

`page-meta.json` 中定义了页面的功能和所需 query。	query 为一个标准的 object 格式 JSON-Schema。

| 字段 | 必填 | 功能 |
|------|------|------|
| path | 是 | 页面路径，可带固定的 query 信息 |
| name | 是 | 页面标题 |
| description | 是 | 页面的功能描述 |
| query | 否 | 页面 query |

```json
// page-meta.json
{
  "pages": [
    {
      "path": "pages/home/home",
      "name": "首页",
      "description": "展示最新的内容和推荐"
    },
    {
      "path": "pages/detail/detail",
      "name": "商品详情",
      "description": "展示特定商品的信息",
      "query": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "商品的唯一标识符"
          }
        },
        "required": [
          "id"
        ]
      }
    }
  ]
}
```

目前 page-meta.json 的最大长度为 8000 字节。

![](https://res8.wxqcloud.qq.com.cn/wxdoc/56617deb-b17d-4cf1-bccd-71d2c41ccc76.svg)文档变更日志（2条）
2026 年 08 月 20 日支持图文混排
2026 年 08 月 11 日handoff 接力 query
