> ## Documentation Index
> Fetch the complete documentation index at: https://docs.langbot.app/llms.txt
> Use this file to discover all available pages before exploring further.

# 组件：页面

Page 组件允许插件在 LangBot 的 WebUI 侧边栏中注册自定义的可视化页面。页面运行在 iframe 沙箱中，通过 Page SDK 与宿主通信，并可调用插件后端 API。

## 适用场景

* **管理面板**：为插件提供可视化的配置和数据管理界面（如 FAQ 管理、数据统计仪表盘）
* **数据展示**：展示插件运行时的统计数据、日志、图表等
* **交互式工具**：提供表单、编辑器等交互式界面

<img width="600" src="https://mintcdn.com/langbot/v7wPcZ4YeY2ATy5i/images/zh/plugin/dev/components/plugin-page-demo.png?fit=max&auto=format&n=v7wPcZ4YeY2ATy5i&q=85&s=5fd4a19db7fc53adcd6bcec75a82222e" data-path="images/zh/plugin/dev/components/plugin-page-demo.png" />

## 添加页面组件

在插件目录执行命令：

```bash theme={null}
lbp comp Page
```

根据提示输入页面名称后，会在 `components/pages/` 目录下生成页面文件：

```bash theme={null}
├── components
│   └── pages
│       └── dashboard
│           ├── dashboard.yaml   # 页面清单
│           ├── dashboard.py     # 页面后端处理程序
│           ├── index.html       # 页面入口
│           └── i18n             # 翻译文件（可选）
│               ├── en_US.json
│               └── zh_Hans.json
```

同时，`manifest.yaml` 中会添加组件发现配置：

```yaml theme={null}
spec:
  components:
    Page:
      fromDirs:
      - path: components/pages/
        maxDepth: 2
```

## 清单文件：页面

```yaml theme={null}
apiVersion: v1  # 请勿修改
kind: Page  # 请勿修改
metadata:
  name: dashboard  # 页面唯一 ID，同一插件内不可重复
  label:
    en_US: Dashboard  # 页面显示名称，显示在 WebUI 侧边栏，支持多语言
    zh_Hans: 仪表盘
spec:
  path: index.html  # HTML 入口文件，相对于该 YAML 所在目录的路径
execution:
  python:
    path: dashboard.py  # 页面后端处理程序
    attr: DashboardPage  # 处理程序类名
```

## 后端处理

Page 组件的后端处理程序继承 `Page` 基类，实现 `handle_api` 方法来处理来自前端页面的 API 请求。

```python theme={null}
from langbot_plugin.api.definition.components.page import Page, PageRequest, PageResponse


class DashboardPage(Page):

    async def handle_api(self, request: PageRequest) -> PageResponse:
        # request.endpoint: API 端点路径，如 '/stats'
        # request.method:   HTTP 方法（GET、POST、PUT、DELETE）
        # request.body:     请求体（已解析的 JSON，或 None）

        if request.endpoint == '/stats' and request.method == 'GET':
            # 通过 self.plugin 访问插件实例的共享状态
            return PageResponse.ok({
                'total': len(self.plugin.entries),
            })

        return PageResponse.fail(f'Unknown endpoint: {request.endpoint}')
```

### PageRequest 字段

| 字段         | 类型    | 说明                                   |
| ---------- | ----- | ------------------------------------ |
| `endpoint` | `str` | API 端点路径（如 `'/entries'`）             |
| `method`   | `str` | HTTP 方法（`GET`、`POST`、`PUT`、`DELETE`） |
| `body`     | `Any` | 请求体（已解析的 JSON，或 `None`）              |

### PageResponse 构造

| 方法                         | 说明                           |
| -------------------------- | ---------------------------- |
| `PageResponse.ok(data)`    | 成功响应，`data` 为任意可 JSON 序列化的数据 |
| `PageResponse.fail(error)` | 失败响应，`error` 为错误信息字符串        |

## 前端页面开发

在 HTML 文件中引入 Page SDK，即可与插件后端通信：

```html theme={null}
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <style>
    body {
      background: var(--langbot-bg, #ffffff);
      color: var(--langbot-text, #0a0a0a);
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
      padding: 24px;
    }
  </style>
</head>
<body>
  <h1 data-i18n="title">Dashboard</h1>
  <p id="stats"></p>

  <script src="/api/v1/plugins/_sdk/page-sdk.js"></script>
  <script>
    langbot.onReady(async function(ctx) {
      // 调用后端 API
      var data = await langbot.api('/stats', null, 'GET');
      document.getElementById('stats').textContent = 'Total: ' + data.total;
    });
  </script>
</body>
</html>
```

### Page SDK API

| 方法                                      | 说明                                                          |
| --------------------------------------- | ----------------------------------------------------------- |
| `langbot.onReady(callback)`             | SDK 就绪后触发回调，`callback` 接收 `ctx` 参数（包含 `theme` 和 `language`） |
| `langbot.api(endpoint, body?, method?)` | 调用插件后端 `handle_api`，返回 Promise                              |
| `langbot.t(key, fallback?)`             | 获取翻译字符串                                                     |
| `langbot.onThemeChange(callback)`       | 主题变更回调                                                      |
| `langbot.onLanguageChange(callback)`    | 语言变更回调                                                      |
| `langbot.applyI18n()`                   | 手动重新应用 `data-i18n` 翻译                                       |

### 暗色模式

SDK 会自动在页面上设置 CSS 自定义属性，直接使用即可：

| CSS 变量                 | 用途    |
| ---------------------- | ----- |
| `--langbot-bg`         | 页面背景色 |
| `--langbot-bg-card`    | 卡片背景色 |
| `--langbot-text`       | 主文字色  |
| `--langbot-text-muted` | 次要文字色 |
| `--langbot-border`     | 边框色   |
| `--langbot-accent`     | 强调色   |

### 页面 i18n

在页面目录下创建 `i18n/` 目录，放入 JSON 翻译文件：

```
pages/dashboard/
├── index.html
└── i18n/
    ├── en_US.json
    └── zh_Hans.json
```

翻译文件格式为扁平 JSON 键值对：

```json theme={null}
{
  "title": "仪表盘",
  "totalEntries": "条目总数"
}
```

在 HTML 元素上添加 `data-i18n` 属性，SDK 会自动替换文本：

```html theme={null}
<h1 data-i18n="title">Dashboard</h1>
```

## 完整示例：FAQ 管理器

以下是一个完整的 Page 组件示例，实现了 FAQ 条目的增删改查。

**后端处理程序**（`components/pages/manager/manager.py`）：

```python theme={null}
from langbot_plugin.api.definition.components.page import Page, PageRequest, PageResponse


class ManagerPage(Page):

    async def handle_api(self, request: PageRequest) -> PageResponse:
        plugin = self.plugin

        if request.endpoint == '/entries' and request.method == 'GET':
            return PageResponse.ok({'entries': plugin.entries})

        if request.endpoint == '/entries' and request.method == 'POST':
            question = (request.body or {}).get('question', '').strip()
            answer = (request.body or {}).get('answer', '').strip()
            if not question or not answer:
                return PageResponse.fail('question and answer are required')
            entry = plugin.add_entry(question, answer)
            await plugin.persist()
            return PageResponse.ok({'entry': entry})

        if request.endpoint == '/entries' and request.method == 'DELETE':
            entry_id = (request.body or {}).get('id', '')
            if plugin.delete_entry(entry_id):
                await plugin.persist()
                return PageResponse.ok({'deleted': entry_id})
            return PageResponse.fail('entry not found')

        return PageResponse.fail(f'Unknown: {request.method} {request.endpoint}')
```

**前端页面**（`components/pages/manager/index.html`）中通过 `langbot.api()` 调用后端：

```javascript theme={null}
// 加载条目
var data = await langbot.api('/entries', null, 'GET');

// 添加条目
await langbot.api('/entries', { question: '...', answer: '...' }, 'POST');

// 删除条目
await langbot.api('/entries', { id: '...' }, 'DELETE');
```

完整示例代码请参考 [FAQManager 插件](https://github.com/langbot-app/langbot-plugin-demo/tree/main/FAQManager)。

## 注意事项

* 页面运行在 `sandbox="allow-scripts allow-forms"` 的 iframe 中，不能弹窗或导航父页面
* 通过 `self.plugin` 访问插件实例的共享状态，Page 和 Tool 组件可以共享数据
* 使用 `PageResponse.ok()` 和 `PageResponse.fail()` 构造响应，确保格式一致
* Page SDK 的 `<script>` 标签必须放在使用 `langbot` 对象的代码之前
