# XZCLOUD · 开发对接文档

> 版本 v1.0.1 · 配合 `schema_version 1.0.0` · 每次功能更新必须同步更新本文档

## 1. 系统总览

XZCLOUD 是一套 PHP 8.2 + MySQL 8.0 的多平台机器人云平台，对齐 AstrBot 的功能模型，并**强兼容 AstrBot 原生 Python 插件**。

```
                 ┌────────────────────────────────────────────┐
                 │                Nginx (443/80)              │
                 │  /          → 前端聊天页（QQ桌面风白色主题） │
                 │  /docs      → 文档中心（/doc/{name} 阅读）  │
                 │  /admin     → 后台管理（IP白名单+密码）      │
                 │  /install   → 安装向导（lock 自锁）         │
                 │  /api       → PHP REST API                 │
                 │  /ws        → WebSocket 9527               │
                 └───────┬──────────────┬─────────────────────┘
                         │              │
        ┌────────────────▼───┐      ┌───▼────────────────────┐
        │ PHP-FPM / 内置服务  │      │  PHP 守护进程 (Daemon)  │
        │  HTTP 路由/控制器   │      │  - WebSocket 服务端     │
        └────────┬───────────┘      │  - ConversationRouter   │
                 │                  │  - PluginManager        │
                 │                  │  - 平台适配器 pod       │
                 │                  │  - RedPacketService     │
                 │                  └───────┬────────────────┘
                 │                          │ ws?role=bridge
                 │                  ┌───────▼────────────────┐
                 │                  │ Python 桥接 (bridge/)  │
                 │                  │  - astrbot shim 兼容层 │
                 │                  │  - plugins_py/ 原生插件│
                 │                  └────────────────────────┘
                 ▼
        MySQL 8.0（ai）
```

> **Web 根说明**：Nginx `root` 必须指向 `public/`，且 `location /` 必须带回退
> `try_files $uri $uri/ /index.php?$query_string;` —— 否则 `/docs`、`/doc/xxx`、`/profile`
> 等非文件路由将全部 404（线上故障案例）。样例见 `deploy/nginx.xzqq.conf`。

### 进程角色

| 进程 | 入口 | 职责 |
|---|---|---|
| Web | `public/index.php` | 前台页面、REST API、QQ官方 webhook |
| Daemon | `php bin/xzqq start` | WebSocket 服务、平台适配器长连接、消息中枢、抢红包 |
| Bridge | `python3 bridge/astrbot_bridge.py` | 加载运行 AstrBot 原生 Python 插件 |

三个进程独立，可分别重启；Daemon 与 Bridge 断链自动重连（指数退避 1→30s）。

## 2. 目录结构

```
XZQQ-Cloud/
├── public/                 # Web 根目录（Nginx root）
│   ├── index.php           # 前端入口 + REST 路由注册
│   ├── api/                # API 静态资源
│   ├── assets/             # css/js/img（含表情包 stickers）
│   ├── call/               # WebRTC 通话页面资源
│   ├── avatar/ uploads/    # 头像/上传文件
│   ├── admin/              # 后台管理（文件夹架构，非单文件）
│   │   ├── includes/       # guard.php(白名单+登录) header.php footer.php
│   │   ├── assets/         # admin.css
│   │   └── *.php           # 14 个管理页
│   └── install/            # install.php 安装向导 + schema.sql
├── src/
│   ├── Core/               # App/Config/Database/Auth/Mailer/Logger/helpers
│   ├── Event/              # MessageChain/MessageEvent/EventDispatcher
│   ├── Http/Controllers/   # Auth/Chat/Call/Home/Profile/Webhook
│   ├── Platform/           # Telegram/Discord/QQOfficial/NapCat 适配器
│   ├── Plugin/             # PluginManager/PluginBase/BridgeGateway
│   ├── Server/             # Daemon/ChatServer/WebSocketServer/EventLoop
│   └── Service/            # ConversationRouter/CallService/FileService
├── bridge/                 # Python 桥接（AstrBot 强兼容）
│   ├── shim/astrbot/       # AstrBot API 兼容层（同名包）
│   ├── bridge_core.py      # 插件加载/分发核心
│   ├── astrbot_bridge.py   # WS 传输 + 入口
│   └── config.json
├── plugins/                # PHP 插件（插件管理页上传/扫描）
├── plugins_py/             # Python（AstrBot 原生）插件
├── plugins_available/      # 插件市场本地安装源
├── config/                 # app/database/mail/platforms 配置
├── deploy/                 # systemd / nginx 样例
├── docs/                   # 公开：开发对接 / 插件开发；内部：部署文档 / 更新日志
├── bin/xzqq                # CLI：start/stop/restart/status/plugin/admin
└── tests/                  # 测试与校验脚本
```

## 3. 数据库（MySQL 8.0）

完整 DDL 见 `public/install/schema.sql`，核心表关系：

| 表 | 说明 |
|---|---|
| `users` | 前台用户（qq_email 唯一，仅允许 @qq.com） |
| `email_codes` | 邮箱验证码（register/reset） |
| `login_logs` | 登录审计 |
| `user_tokens` | WebSocket 长连接令牌 |
| `bot_instances` | 平台适配器实例（config_json 存平台私有配置） |
| `conversations` | 统一会话（uk_instance_chatkey 唯一） |
| `messages` | 统一消息（in/out 双向，media_file_id 关联文件） |
| `files` | 文件（sha1 去重，kind: image/video/audio/emoji/file） |
| `plugins` | 插件注册表（runtime=php/python，dir_name 唯一） |
| `commands` | 指令表（QQ官方等可管理指令） |
| `call_rooms` / `call_logs` / `call_messages` / `call_signals` | WebRTC 通话 |
| `redpacket_logs` | 自动抢红包日志 |
| `settings` | 系统设置；**key 以 `cfg.` 开头时在线覆盖 config 文件** |
| `admins` | 后台管理员（用户名 + 密码哈希） |
| `stats_daily` | 仪表盘日统计 |

配置覆盖约定：后台「安全」页保存白名单时写 `settings.k = 'cfg.admin.ip_whitelist'`，`App::boot()` 启动时以 `cfg.` 前缀的 settings 值覆盖 `config/*.php`，因此**网页端改配置即时生效，无需改文件**。


## 4. HTTP API

统一约定：JSON 请求/响应；`{code:0, msg:"", data:{}}` 包装（code=0 成功）；鉴权走 `Authorization: Bearer <token>`（登录颁发）或会话 Cookie。

### 页面路由

| 方法 | 路径 | 说明 |
|---|---|---|
| GET | `/` | 登录后为 QQ 桌面主页，未登录为欢迎页（`HomeController::index`） |
| GET | `/profile` | 个人主页（需登录） |
| GET | `/docs` | 文档中心（索引卡片，**只列出公开文档**） |
| GET | `/doc/{slug}` | 单篇文档在线阅读；`?download=1` 下载源文件。**slug 为纯 ASCII**（`develop` / `plugin` / `deploy` / `changelog`），同时兼容旧中文直链 `/doc/开发对接文档`。**内部文档一律 404**（见下） |
| GET | `/call/enter/{code}` | 通话落地页（QQ 官方按钮跳转入口） |
| GET | `/install/install.php` | 安装向导（`runtime/installed.lock` 自锁） |
| GET | `/admin/` | 后台（IP 白名单 + 密码） |

### 文档可见性分级

`HomeController::DOCS` 是文档的**单一数据源**，每项为 `slug => [文件名, 说明, 图标, 主题色, 是否公开]`：

| slug | 文档 | 公开 |
|---|---|---|
| `develop` | 开发对接文档 | ✅ 前台可读 / 可下载 |
| `plugin` | 插件开发文档 | ✅ 前台可读 / 可下载 |
| `deploy` | 部署文档 | ❌ 内部（含服务器路径、端口、systemd 配置） |
| `changelog` | 更新日志 | ❌ 内部（版本演进细节） |

约定：

- 内部文档**不出现在 `/docs` 列表**，直接访问 `/doc/{slug}` 或其旧中文直链一律返回 404，与「文档不存在」共用同一出口（`docNotFound()`），**不暴露该文档是否存在**
- 需要再公开一份文档：把对应 `DOCS` 项第 5 个字段改为 `true` 即可，前台列表与访问控制自动生效
- 内部文档仅供后台（`/admin/docs.php`，IP 白名单内）通过 `?view=<文件名>` 内联查看；该入口含 `realpath` 前缀校验防路径穿越
- 新增文档时，只需在 `docs/` 放 `.md` 并在 `DOCS` 登记，后台会自动发现（`docMeta` 缺失时按 `📄` 展示）

### 认证

| 方法 | 路径 | 说明 |
|---|---|---|
| POST | `/api/auth/send-code` | 发送注册/重置验证码（仅 QQ 邮箱） |
| POST | `/api/auth/register` | 注册（email + code + password + nickname） |
| POST | `/api/auth/login` | 登录，返回 token |
| POST | `/api/auth/logout` | 登出 |
| POST | `/api/auth/reset` | 重置密码 |
| GET | `/api/auth/me` | 当前用户信息 |
| GET | `/api/auth/ws-token` | 换取 WebSocket 令牌（user_tokens） |

### 聊天 / 文件

| 方法 | 路径 | 说明 |
|---|---|---|
| GET | `/api/conversations` | 会话列表（平台聚合收件箱） |
| GET | `/api/messages?conversation_id=&before_id=` | 历史消息（游标分页） |
| GET | `/api/messages/latest` | 各会话最新消息（轮询兜底） |
| POST | `/api/messages/send` | 网页端发消息（自动路由到平台） |
| POST | `/api/conversations/read` | 清除未读 |
| POST | `/api/upload` | 上传（图片/视频/语音/文件，sha1 去重） |
| GET | `/file/{id}` | 文件下载（支持 Range，可在线播放视频） |

### 个人资料

| 方法 | 路径 | 说明 |
|---|---|---|
| POST | `/api/profile/update` | 昵称/简介 |
| POST | `/api/profile/avatar` | 头像上传 |

### 通话（WebRTC）

| 方法 | 路径 | 说明 |
|---|---|---|
| POST | `/api/call/create` | 创建房间（返回 room_code + landing_url） |
| POST | `/api/call/signal` | 上报 offer/answer/ice |
| GET | `/api/call/signal/poll` | 拉取信令（HTTP 中转，可换 WS） |
| POST | `/api/call/message` | 通话内文字聊天 |
| POST | `/api/call/close` | 关闭房间 |
| GET | `/call/enter/{code}` | 通话落地页（QQ markdown 按钮跳转） |

### 平台回调

| 方法 | 路径 | 说明 |
|---|---|---|
| POST | `/api/qqbot/webhook` | QQ 官方机器人事件回调（Ed25519 验签） |
| GET | `/api/qqbot/webhook` | URL 校验（op=13 在 POST 内处理） |

## 5. WebSocket 协议

### 5.1 前端聊天（`ws://host:9527/ws?token=<user_token>`）

客户端 → 服务端：

```json
{"t":"send",  "conversation_id":12, "msg_type":"text", "content":"你好"}
{"t":"read",  "conversation_id":12}
{"t":"call_signal", "room_id":3, "signal_type":"offer", "payload":"<sdp>"}
{"t":"call_msg",    "room_id":3, "content":"大家好"}
{"t":"ping"}
```

服务端 → 客户端：

```json
{"t":"message", "data":{ ...消息对象... }}
{"t":"ack",     "client_msg_id":"...", "message_id":123}
{"t":"presence","conversation_id":12,"unread":0}
{"t":"call_signal","signal_type":"answer","payload":"<sdp>"}
{"t":"call_msg","sender_name":"...","content":"..."}
{"t":"pong"}
```

### 5.2 NapCat 反向 WS（OneBot 11，`ws://host:6199/ws`）

NapCat 作为客户端连入；action 调用走 echo（带 `echo` 字段响应）。平台消息（私聊/群聊/通知）由 `NapCatAdapter` 转换为统一 MessageChain 后进入 ConversationRouter。

### 5.3 桥接协议（PHP ↔ Python，`ws://host:9527/ws?role=bridge&token=<bridge_token>`）

帧格式与消息组件与 PHP 侧完全一致（组件 `{"type":"Plain","data":{"text":"..."}}`）。

| 方向 | 类型 | 载荷要点 |
|---|---|---|
| bridge→daemon | `hello` | 插件清单：`{dir,name,author,desc,version,enabled,commands:[{name,alias,description,usage}]}` |
| daemon→bridge | `ready` | 各插件启用态 + `admin_ids` |
| daemon→bridge | `message` | `{id, umo, message_str, chain:[组件], platform, sender_id, group_id, self_id, is_group}` |
| bridge→daemon | `result` | `{id, chain:[组件], plugin}`（带 id 时回复到对应事件） |
| bridge→daemon | `log` / `error` / `stop` | 日志与停止传播通知 |
| daemon→bridge | `enabled` | 单个插件启用/停用（3 秒 diff，热加载） |
| 双向 | `ping`/`pong` | 保活 |

`BridgeGateway` 为每个 `message` 保存 60s TTL 的回复闭包；`result` 到达时重建 `MessageChain::fromArray()` 并回调。插件停发 `stopPropagation` 后续插件不再收到该事件。

## 6. 插件体系（PHP + Python 桥接）

### 6.1 PHP 插件

- 位置：`plugins/<作者>/<插件名>/main.php` + `metadata.json` + `_conf_schema.json`
- 加载：`PluginManager::scan()` 递归找 `main.php`，返回匿名类或 `PluginBase` 子类实例
- 启用态：`plugins.enabled`；热加载：下次 `loadAll()` 生效（后台切换即时）
- 指令注册：反射收集方法上的 `#[Filter\Command]` 等属性 → `self::$handlers`
- 分发：`PluginManager::dispatchMessage(MessageEvent $event)`，顺序匹配 handler，命中即执行，`stopPropagation` 可阻断后续插件

### 6.2 Python 插件（AstrBot 原生，强兼容）

- 位置：`plugins_py/<作者>/<插件名>/main.py` + `metadata.json`
- `bridge/shim/astrbot/` 提供与 AstrBot **同名包**的 API 兼容层，插件源码几乎原样可跑
- 桥接进程以包方式注册模块（`xzqq_plugins_py.<作者>.<插件>.main`），支持插件内相对导入
- hello 时用 **AST 静态扫描** `@filter.command` 上报指令元数据（不 import，无副作用）
- 启用/停用：Daemon 每 3s diff `plugins` 表，通过 `enabled` 帧推送，桥接热加载/卸载
- 兼容矩阵：指令/别名/正则/事件类型/平台类型/权限/GreedyStr/消息链 send/生命周期 ✅；LLM Provider ❌（本平台不内置 AI 调用）；自定义 filter ⚠️（可用但无法静态上报指令）

### 6.3 事件对象（MessageEvent）关键能力

| 能力 | 说明 |
|---|---|
| `get_platform_name()` | napcat/qqofficial/telegram/discord |
| `get_sender_id()/get_group_id()/get_message_id()` | 平台内 ID |
| `is_group()/is_private_chat()` | 会话类型 |
| `get_umo()` | 统一消息对象标识 |
| `get_extra()/set_extra()` | 插件间传参（同一事件内） |
| `stop_event()` | 停止事件继续传播 |
| `send(chain)/plain_result()/chain_result()/make_result()` | 回复（Python 侧经桥接 result 帧回传） |

## 7. 消息流转（端到端）

```
平台消息 ──► Adapter(NapCat/QQOfficial/TG/DC)
   │  解析为 MessageChain + umo
   ▼
ConversationRouter::onPlatformMessage()
   ├─ 落库 conversations/messages（in）
   ├─ WS 广播给前端在线用户
   ▼
EventDispatcher::dispatch()
   ├─ PluginManager::dispatchMessage()      # PHP 插件
   ├─ BridgeGateway::dispatch()             # Python 插件（未 stopPropagation 时）
   ▼
回复（MessageChain）
   ├─ PHP：直接回调
   ├─ Python：桥接 result 帧 → 重建链 → 回调原事件闭包
   ▼
ConversationRouter::recordOutbound()        # 落库（out）+ 回推平台
```

网页端发消息：`POST /api/messages/send` → `ConversationRouter::sendFromWeb()` → Adapter 发到平台 → 平台回执。

## 8. 平台配置速查（bot_instances.config_json）

通用字段：`appid/secret/token/access_token/enabled`，以下按平台：

- **napcat**：`ws_reverse_host/ws_reverse_port/access_token`，`redpacket{enabled,whitelist_groups,blacklist_groups,cooldown,daily_limit,max_per_minute,action,action_params,retry}`
- **qqofficial**：`appid/secret/sandbox/api_base/token_api/verify_signature/call_landing`；回调地址 `https://<域名>/api/qqbot/webhook`
- **telegram**：`bot_token/webhook_url/polling`
- **discord**：`bot_token/application_id/public_key/guild_id`

适配器统一实现 `PlatformInterface`：`meta()/defaultConfig()/validate()/start()/stop()/callAction()/send()/sendMarkdown()`。

## 9. 环境变量与安全

| 项 | 位置 | 说明 |
|---|---|---|
| `XZQQ_BRIDGE_TOKEN` | 环境变量 | 桥接令牌，默认 `xzqq-bridge-default-token`（生产必须改） |
| `admin.ip_whitelist` | config/app.php 或 settings `cfg.admin.ip_whitelist` | 后台白名单，默认仅 `120.225.18.96` |
| 后台密码 | `admins.password_hash`（bcrypt） | 主来源。安装向导写入；后台「安全」页可改 |
| `admin.password_plain` | `config/app.php` | **兜底明文**：哈希为空/非法时自动重建；输入与它一致时直接放行（详见下方「后台登录机制」） |
| SMTP | `config/mail.php` | QQ 邮箱授权码（敏感，勿入库/勿入公开仓库） |
| 平台密钥 | `bot_instances.config_json` | 仅存数据库，后台不回显明文（secret/token 需重新粘贴） |

### 后台登录机制（`public/admin/includes/guard.php`）

两层判定，顺序为 **IP 白名单 → 密码**：

1. **IP 白名单**：`admin.ip_whitelist` 内的 IP 才放行，其余一律 403（`127.0.0.1` / `::1` 亦放行）。403 与「密码不正确」是**两种不同提示**，排障时先区分
2. **密码判定**，满足其一即通过：
   - 与 `config/app.php` 的 `admin.password_plain` 完全一致（`hash_equals` 防时序侧信道）—— **明文兜底通道**
   - 或 `password_verify()` 通过数据库 bcrypt 哈希

> **不启用登录失败频率限制 / 账号锁定**（v1.0.1 起按需求移除）。`admins` 表的 `login_fails` / `locked_until` 列保留但不再写入与判断，失败只返回「密码不正确」。
> 后台防线为 **IP 白名单 + 密码**，白名单是主要防线。

> 设计要点：`schema.sql` 中 `admins` 初始 `password_hash` 为**空串**，需安装向导回填。若直接导入 SQL 未走向导，登录时会触发**自愈** —— 无 admin 记录则新建，哈希为空/非法则用 `password_plain` 重建，避免「任何密码都验不过」的死锁。
> 忘记密码时用 `php bin/xzqq admin reset ['新密码']` 重置（缺省取 `password_plain`）。

#### 配置优先级（易踩坑）

```
数据库 settings 表的 cfg.* 覆盖项   >   config/app.php 文件值
```

`App::boot()` 启动时会遍历 `settings` 表，把**所有 `cfg.` 前缀的键**（值为 JSON）覆盖到内存配置上。因此：

- 后台「安全」页保存白名单 → 写入 `settings.cfg.admin.ip_whitelist` → **之后改 `config/app.php` 不再生效**
- 想让文件配置重新生效：后台点「恢复 config/app.php 文件配置」，或 `DELETE FROM settings WHERE k='cfg.admin.ip_whitelist';`
- 修改 `config/app.php` 后建议 `systemctl restart php8.2-fpm`（OPcache 可能缓存旧文件）

#### 改密码时的同步约定

`admin.password_plain` 是**可用登录通道**（`hash_equals` 直接放行），因此任何改密码的入口都必须同步它，否则旧密码仍可登录：

| 入口 | 是否自动同步 `password_plain` |
|---|---|
| 后台「安全」页「修改密码」 | ✅ 自动改写 `config/app.php`（文件不可写时提示手动） |
| `php bin/xzqq admin reset '新密码'` | ❌ 只改数据库哈希，**需手动同步**配置文件 |
| `php bin/reset-admin-password.php` | ❌ 同上 |

## 10. 新增一个平台适配器的步骤

1. `src/Platform/<Name>/<Name>Adapter.php` 实现 `PlatformInterface` + `PlatformMetadata`
2. `AdapterRegistry` 注册 runtime key
3. `Daemon` 按 `bot_instances.platform` 实例化并 `start(EventLoop)`
4. 收到平台消息 → 调 `ConversationRouter::onPlatformMessage()`（字段见第 7 节）
5. 发消息 → 实现 `send(int $conversationId, MessageChain $chain)`
6. 后台 `platforms.php` 平台下拉自动出现（读取 `Config::get('platforms')` 模板）
7. 更新本文档与《更新日志》

## 11. 前端设计规范（动态玻璃）

全站前端遵循统一的玻璃拟态（Dynamic Glass）语言，新增/修改页面请复用下列资产，不要自造样式体系。

### 11.1 依赖

| 资源 | 引入方式 | 用途 |
|---|---|---|
| `assets/css/base.css` | 本地 | 玻璃 token、`.glass` 系列、动画工具箱。**所有前台页面必引** |
| `assets/vendor/fontawesome/css/all.min.css` | **本地** | 全站矢量图标（**禁止再用 emoji 当图标**）。仅含 solid + regular 字体 |

> **零外部依赖约定**：全站**不引入任何第三方 CDN**（Tailwind / Font Awesome / 字体 / JS 库一律本地化）。
> 原因：① 浏览器隐私拦截（Tracking Prevention）会阻断第三方存储导致样式失效；② 商业站点不应把访客 IP 交给第三方；
> ③ 国内访问不稳、断网即白屏。历史上曾用 `cdn.tailwindcss.com` 与 `cdnjs.cloudflare.com`，已于 v1.0.1 移除。
>
> 本项目**不使用 Tailwind**。历史上 Tailwind 全站只用到 3 个类，已改为自定义类（`.brand-link`）。新增样式请写进各页面的 CSS 文件。
> Font Awesome 升级：替换 `public/assets/vendor/fontawesome/` 整目录；若用到 brands 图标需补下 `fa-brands-400.woff2`。

### 11.2 玻璃类（base.css）

| 类 | 效果 | 典型场景 |
|---|---|---|
| `.glass` | 白 72% + `blur(18px)`，浅边框 + 软阴影 | 侧栏、卡片、面板默认层 |
| `.glass-strong` | 白 86% + `blur(24px)`，更强阴影 | 弹窗、浮层、控制条 |
| `.glass-soft` | 白 55% + `blur(14px)` | 局部小标签、深色背景上的胶囊 |
| `.glass-hi` | 附加顶部 1px 高光 | 与上面三个叠加使用 |

CSS 变量：`--glass-bg`(72% 白) / `--glass-bg-strong` / `--glass-bg-soft` / `--glass-brd` / `--glass-blur`(18px) / `--ease`。

### 11.3 动画工具箱

`fadeUp` / `fadeIn` / `popIn` / `slideDown` / `floatY` / `drift1~3` / `shine` / `pulseRing`，
对应类 `.fade-up` `.fade-in` `.pop-in` `.slide-down` `.float`，错峰用 `.d1`~`.d8`（每档 +60ms）。
`base.css` 已带 `prefers-reduced-motion` 降级，勿在业务页重复处理。

### 11.4 图标替换对照（本次改版）

| 原 emoji | 现 Font Awesome |
|---|---|
| 😊 表情 | `fa-regular fa-face-smile` |
| 📎 文件 | `fa-solid fa-paperclip` |
| 🖼️ 图片 | `fa-regular fa-image` |
| 🎬 视频 | `fa-solid fa-video` |
| 📞 通话 | `fa-solid fa-phone` / `fa-solid fa-phone-slash`（挂断） |
| 👤 个人主页 | `fa-regular fa-circle-user` |
| 📖 文档 | `fa-solid fa-book` / `fa-book-open-reader` |
| 🚪 退出 | `fa-solid fa-arrow-right-from-bracket` |
| 📦 文件气泡 | `fa-solid fa-file-arrow-down` |

### 11.5 响应式与手机端

各页面在 `base.css` 之外自带断点，改动时对照下表，不要重复定义：

| 断点 | chat 桌面（`app.css`） | 通话页（`call.css`） | 欢迎页（`welcome.css`） | 个人主页（`profile.css`） |
|---|---|---|---|---|
| `<= 1180px` | 隐藏右侧会话信息栏 | — | — | — |
| `<= 960px` | — | — | 主推卡片转单列 | — |
| `<= 900px` | 侧栏收成 76px 图标条 | 聊天列移到底部（220px） | — | — |
| `<= 720px` | **单屏切换**（见下） | **视频铺满 + 底部聊天抽屉** | — | — |
| `<= 640px` | — | — | 导航/hero/卡片/模拟图压缩，弹窗改底部抽屉 | 间距压缩 |
| `<= 480px` | — | — | — | **单列 + 全宽按钮** |
| `<= 400px` | 超窄屏压缩 | 超窄屏压缩 | 超窄屏压缩 | — |

**手机端单屏切换（`app.css` `<= 720px`）**：侧栏与聊天区不再并排，`.app` **无** `.show-chat` 时只显示会话列表，**有** `.show-chat` 时只显示聊天区。

- 进入会话：`selectConversation()` 内调用 `showChat()`，并在手机端 `history.pushState` 压一条历史
- 返回列表：`#btnBack` 点击 → `showList()`；浏览器物理返回键 → `popstate` 监听 → `showList()`
- `.btn-back` 桌面端基线为 `display:none`，仅在 `<= 720px` 内改为 `inline-flex`；`<button class="btn-back" id="btnBack">` 必须在 `home.php` 保留
- **软键盘适配**：`visualViewport` 的 `resize`/`scroll` 把键盘高度写入 CSS 变量 `--kb`，表情面板与输入区据此吸附；输入框聚焦时自动收起表情面板
- 移动端输入框字号统一 `>= 14px`（`home` / `profile` / `call` 三处），避免 iOS 聚焦自动放大页面
- 四个前台视图 viewport 统一为 `width=device-width, initial-scale=1, viewport-fit=cover`（后者供 `env(safe-area-inset-*)` 生效）

### 11.6 约束

- **DOM 契约优先**：`views/welcome.php`、`home.php`、`call.php` 的元素 `id` 与 `app.js` / `welcome.js` / `call.js` 强耦合，
  改版只允许改结构与样式，`id` 必须原样保留（含 `#btnGotoLogin2`、`#sendCodeBtn`、`#msgInput`、`#stageEmpty`、`#btnBack` 等）。
- 一个页面只引一次 `base.css`；页面专属样式放各自的 `welcome.css` / `app.css` / `profile.css` / `call.css`。
- 后台另用 `admin/assets/admin.css`（已在其中内置等价玻璃样式），不要再引 base.css。
- 字重只用 **400 / 500** 两档，禁用 600 / 700（与全站统一视觉一致）。

## 12. 二次开发检查清单

- [ ] 数据库变更 → 同步 `public/install/schema.sql` 并在安装向导加迁移
- [ ] 新 API → `public/index.php` 注册路由 + Controller + 本文档第 4 节
- [ ] 新 WS 帧 → `src/Server/ChatServer.php` + 前端 `public/assets/js` + 本文档第 5 节
- [ ] 新插件过滤器 → `src/Plugin/Filter.php` + PluginManager 匹配逻辑 + 《插件开发文档》
- [ ] 桥接协议变更 → `src/Plugin/BridgeGateway.php` + `bridge/bridge_core.py` 成对修改
- [ ] 前端页面改动 → 遵循第 11 节设计规范（玻璃类 + FA 图标 + 动画工具箱），保持 DOM `id` 不变；改动布局时**同步核对第 11.5 节各断点**
- [ ] 新增对外路由 → 确认 Nginx `try_files` 回退可用，并在《部署文档》备案
- [ ] 新增/调整文档 → 在 `HomeController::DOCS` 登记，并**明确标注公开或内部**（第 2 节「文档可见性分级」）
- [ ] 每次发布更新：本文档、《插件开发文档》、《部署文档》、《更新日志》

