feat: 实现站内通知系统
- 后端: 新增 notifications/user_notifications 表迁移及 ORM 模型
- 后端: 实现 schema/repository/service/router 全套通知 API
- GET /api/v1/notifications (列表+游标分页)
- GET /api/v1/notifications/unread-count
- PATCH /api/v1/notifications/{id}/read (幂等)
- PATCH /api/v1/notifications/mark-all-read (幂等)
- 后端: payload 使用 Pydantic discriminated union (none/open_route/open_url)
- 后端: 19 个单元测试全部通过
- Flutter: 通知 feature 完整实现 (models/apis/repositories/bloc/UI)
- Flutter: Home 页通知按钮接入真实页面,显示未读 badge
- Flutter: 14 个测试全部通过
- 协议文档: notification-inbox-protocol.md 及错误码注册
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,484 @@
|
||||
# 静态通知配置同步计划
|
||||
|
||||
> 更新时间:2026-04-10
|
||||
> 状态:最终执行版
|
||||
|
||||
## 1. 目标
|
||||
|
||||
为通知系统增加一条独立的“静态配置 -> 数据库同步”链路,使服务端可以从仓库内的通知配置文件读取通知定义,并将其注册、更新或撤销到数据库。
|
||||
|
||||
本计划解决的问题:
|
||||
|
||||
- 通过静态文件维护系统通知内容
|
||||
- 手动触发后端读取并同步通知到数据库
|
||||
- 支持已有通知的修改
|
||||
- 支持已有通知的撤销
|
||||
- 保持用户侧已读状态不因通知内容更新而丢失
|
||||
|
||||
本计划不替代主通知系统计划,而是在其基础上增加“静态通知同步”能力。
|
||||
|
||||
关联文档:
|
||||
|
||||
- `docs/plans/notification-system-plan.md`
|
||||
|
||||
---
|
||||
|
||||
## 2. 范围
|
||||
|
||||
### 2.1 In Scope
|
||||
|
||||
- 新增静态通知配置目录
|
||||
- 定义静态通知 YAML 协议
|
||||
- 定义对应的 Pydantic schema
|
||||
- 实现后端扫描、校验、upsert 同步逻辑
|
||||
- 实现对主通知的修改和撤销
|
||||
- 新增手动触发同步脚本
|
||||
|
||||
### 2.2 Out of Scope
|
||||
|
||||
- 系统级离线推送
|
||||
- 自动监听文件变化并实时同步
|
||||
- 通过文件删除自动删库
|
||||
- 复杂运营后台
|
||||
- 严格对齐目标用户集合并自动删除既有投递记录
|
||||
|
||||
---
|
||||
|
||||
## 3. 现有代码基线
|
||||
|
||||
当前仓库已经有可直接复用的“静态配置 -> 数据库初始化”模式:
|
||||
|
||||
- 静态配置目录:`backend/src/core/config/static/database/`
|
||||
- 现有 YAML:
|
||||
- `llm_catalog.yaml`
|
||||
- `system_agents.yaml`
|
||||
- 现有加载与校验:`backend/src/core/config/initial/init_data.py`
|
||||
- 现有 CLI:`backend/src/core/runtime/cli.py`
|
||||
- 现有脚本:`infra/scripts/dev-migrate.sh`
|
||||
|
||||
通知同步应复用这套模式的核心思路:
|
||||
|
||||
- YAML 文件作为配置源
|
||||
- Pydantic schema 做强校验
|
||||
- 后端显式执行同步
|
||||
- 数据库使用 upsert 语义更新
|
||||
|
||||
但通知同步不应直接并入 `init-data/bootstrap` 默认流程,因为通知内容属于持续变更的数据,不是纯启动种子数据。
|
||||
|
||||
---
|
||||
|
||||
## 4. 目录设计
|
||||
|
||||
建议新增静态通知目录:
|
||||
|
||||
```text
|
||||
backend/src/core/config/static/notification/
|
||||
└── notifications/
|
||||
├── welcome_bonus.yaml
|
||||
├── maintenance_2026_04.yaml
|
||||
└── ...
|
||||
```
|
||||
|
||||
第一阶段不增加总索引文件,直接扫描 `notifications/*.yaml`。
|
||||
|
||||
原因:
|
||||
|
||||
- 少一层维护成本
|
||||
- 避免“文件内容”和“索引文件”双源不一致
|
||||
- 更适合增量增加通知文件
|
||||
|
||||
---
|
||||
|
||||
## 5. 数据模型变更
|
||||
|
||||
要支持“静态文件和数据库中的同一条通知”建立稳定映射,`notifications` 表需要增加来源标识字段。
|
||||
|
||||
建议新增字段:
|
||||
|
||||
- `source`
|
||||
- `source_key`
|
||||
- `source_version`
|
||||
- `content_hash`
|
||||
|
||||
建议约束:
|
||||
|
||||
- `UNIQUE(source, source_key)`
|
||||
|
||||
### 5.1 字段职责
|
||||
|
||||
- `source`
|
||||
- 通知来源
|
||||
- 当前静态通知固定为 `static`
|
||||
- `source_key`
|
||||
- 静态通知唯一键
|
||||
- 例如 `welcome_bonus`
|
||||
- 用于可靠 upsert
|
||||
- `source_version`
|
||||
- 配置版本号
|
||||
- 用于审计和变更追踪
|
||||
- `content_hash`
|
||||
- 标准化内容摘要
|
||||
- 用于判断文件内容是否发生变化
|
||||
|
||||
### 5.2 推荐表结构补充
|
||||
|
||||
在 `notifications` 表基础上补充:
|
||||
|
||||
```sql
|
||||
ALTER TABLE notifications
|
||||
ADD COLUMN source VARCHAR(32) NOT NULL DEFAULT 'manual',
|
||||
ADD COLUMN source_key VARCHAR(128),
|
||||
ADD COLUMN source_version INTEGER,
|
||||
ADD COLUMN content_hash VARCHAR(64);
|
||||
|
||||
CREATE UNIQUE INDEX uq_notifications_source_source_key
|
||||
ON notifications(source, source_key)
|
||||
WHERE source_key IS NOT NULL;
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- `manual` 可作为非静态创建通知的默认来源
|
||||
- 静态同步通知统一使用 `source='static'`
|
||||
|
||||
---
|
||||
|
||||
## 6. 静态通知 YAML 协议
|
||||
|
||||
每个 YAML 文件描述一条主通知及其投递目标。
|
||||
|
||||
推荐结构:
|
||||
|
||||
```yaml
|
||||
notification:
|
||||
source_key: welcome_bonus
|
||||
version: 1
|
||||
type: system
|
||||
status: published
|
||||
published_at: 2026-04-10T08:00:00Z
|
||||
|
||||
title: 新用户欢迎通知
|
||||
body: 你已获得注册奖励,可前往积分中心查看。
|
||||
|
||||
payload:
|
||||
action: open_route
|
||||
route: /points
|
||||
entity_id: null
|
||||
tab: balance
|
||||
|
||||
targets:
|
||||
mode: all_users
|
||||
```
|
||||
|
||||
指定用户示例:
|
||||
|
||||
```yaml
|
||||
notification:
|
||||
source_key: maintenance_2026_04
|
||||
version: 3
|
||||
type: system
|
||||
status: published
|
||||
title: 系统维护通知
|
||||
body: 今晚 23:00 到 23:30 进行维护。
|
||||
payload:
|
||||
action: none
|
||||
|
||||
targets:
|
||||
mode: user_ids
|
||||
user_ids:
|
||||
- 11111111-1111-1111-1111-111111111111
|
||||
- 22222222-2222-2222-2222-222222222222
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Pydantic Schema 设计
|
||||
|
||||
静态通知文件必须先经过强校验,不能直接把 YAML 转 dict 入库。
|
||||
|
||||
建议新增模块:
|
||||
|
||||
- `backend/src/core/config/notification/static_schema.py`
|
||||
|
||||
建议 schema:
|
||||
|
||||
- `StaticNotificationDefinition`
|
||||
- `StaticNotificationTargets`
|
||||
- `StaticNotificationFile`
|
||||
|
||||
`payload` 不重新定义,直接复用现有通知协议里的 schema:
|
||||
|
||||
- `NotificationPayloadNone`
|
||||
- `NotificationPayloadRoute`
|
||||
- `NotificationPayloadUrl`
|
||||
|
||||
### 7.1 `StaticNotificationDefinition` 职责
|
||||
|
||||
- `source_key`
|
||||
- 静态通知唯一键
|
||||
- `version`
|
||||
- 配置版本号
|
||||
- `type`
|
||||
- 通知类型,当前默认 `system`
|
||||
- `status`
|
||||
- `draft/published/revoked`
|
||||
- `published_at`
|
||||
- 发布时间
|
||||
- `title/body/payload`
|
||||
- 通知内容
|
||||
|
||||
### 7.2 `StaticNotificationTargets` 职责
|
||||
|
||||
- `mode`
|
||||
- `all_users` 或 `user_ids`
|
||||
- `user_ids`
|
||||
- 仅当 `mode='user_ids'` 时允许
|
||||
|
||||
### 7.3 校验约束
|
||||
|
||||
- `source_key` 必填且全局唯一
|
||||
- `version >= 1`
|
||||
- `status` 只允许 `draft/published/revoked`
|
||||
- `payload` 必须符合现有通知 payload schema
|
||||
- `targets.mode='all_users'` 时不允许传 `user_ids`
|
||||
- `targets.mode='user_ids'` 时 `user_ids` 必填且不能为空
|
||||
|
||||
---
|
||||
|
||||
## 8. 同步语义
|
||||
|
||||
### 8.1 新建
|
||||
|
||||
当数据库中不存在 `(source='static', source_key=...)` 时:
|
||||
|
||||
1. 创建 `notifications`
|
||||
2. 按目标规则写入 `user_notifications`
|
||||
|
||||
### 8.2 修改
|
||||
|
||||
当数据库中已存在同一 `source_key` 时:
|
||||
|
||||
1. 更新 `notifications.title/body/payload/status/published_at/source_version/content_hash`
|
||||
2. 保留已有 `user_notifications`
|
||||
3. 不重置 `is_read/read_at`
|
||||
|
||||
这是强规则:
|
||||
|
||||
- 修改主通知内容,不影响用户已读状态
|
||||
|
||||
### 8.3 撤销
|
||||
|
||||
当 YAML 中:
|
||||
|
||||
- `notification.status = revoked`
|
||||
|
||||
则同步时:
|
||||
|
||||
1. 更新 `notifications.status='revoked'`
|
||||
2. 写入 `revoked_at`
|
||||
3. 不删除 `user_notifications`
|
||||
|
||||
### 8.4 统一删除
|
||||
|
||||
本阶段不使用“文件消失自动删库”语义。
|
||||
|
||||
原因:
|
||||
|
||||
- 文件误删风险高
|
||||
- 容易把版本控制操作误解释为业务删除
|
||||
|
||||
如果需要下线,显式通过配置状态控制:
|
||||
|
||||
- `status: revoked`
|
||||
|
||||
如果未来确实需要静态配置触发软删除,再单独增加明确字段,不在本阶段默认启用。
|
||||
|
||||
### 8.5 目标用户变更
|
||||
|
||||
第一阶段采用保守策略:
|
||||
|
||||
- 新增目标用户时,补插入 `user_notifications`
|
||||
- 被移出目标集合的用户,不自动删除既有 `user_notifications`
|
||||
|
||||
原因:
|
||||
|
||||
- 防止误操作删除已投递历史
|
||||
- 与“通知一旦发出就保留用户侧记录”的语义更一致
|
||||
|
||||
如果未来需要严格对齐文件目标集合,再单独增加显式 `--reconcile-targets` 行为。
|
||||
|
||||
---
|
||||
|
||||
## 9. 后端实现方案
|
||||
|
||||
### 9.1 模块位置
|
||||
|
||||
建议新增:
|
||||
|
||||
```text
|
||||
backend/src/core/config/notification/
|
||||
├── static_schema.py
|
||||
└── static_sync.py
|
||||
```
|
||||
|
||||
不建议把通知同步继续堆进 `core/config/initial/init_data.py`。
|
||||
|
||||
原因:
|
||||
|
||||
- `init_data.py` 当前更适合 bootstrap seed
|
||||
- 通知同步是持续执行的配置同步任务
|
||||
- 语义上应独立
|
||||
|
||||
### 9.2 组件职责
|
||||
|
||||
- `static_schema.py`
|
||||
- 定义 YAML 文件的 Pydantic schema
|
||||
- `static_sync.py`
|
||||
- 扫描目录
|
||||
- 读取 YAML
|
||||
- 校验 schema
|
||||
- 计算差异
|
||||
- 执行 upsert
|
||||
|
||||
现有通知模块中建议补充内部同步能力:
|
||||
|
||||
- `v1/notifications/repository.py`
|
||||
- 补充按 `source/source_key` 查询与 upsert
|
||||
- `v1/notifications/service.py`
|
||||
- 补充内部同步逻辑与事务边界
|
||||
|
||||
### 9.3 日志与错误
|
||||
|
||||
遵循现有后端规则:
|
||||
|
||||
- 使用 `core.logging`
|
||||
- 不使用 `print`
|
||||
- YAML 校验失败要明确报错并中止
|
||||
- 数据库 upsert 失败要中止,不吞错
|
||||
|
||||
---
|
||||
|
||||
## 10. CLI 与脚本方案
|
||||
|
||||
### 10.1 后端 CLI
|
||||
|
||||
在 `backend/src/core/runtime/cli.py` 中新增命令:
|
||||
|
||||
- `sync-notifications`
|
||||
|
||||
建议调用方式:
|
||||
|
||||
```bash
|
||||
PYTHONPATH=backend/src uv run python -m core.runtime.cli sync-notifications
|
||||
```
|
||||
|
||||
建议参数:
|
||||
|
||||
- `--path`
|
||||
- `--source-key`
|
||||
- `--dry-run`
|
||||
|
||||
第一阶段不默认提供危险的全量清理参数。
|
||||
|
||||
### 10.2 infra 脚本
|
||||
|
||||
新增:
|
||||
|
||||
```text
|
||||
infra/scripts/register-notifications.sh
|
||||
```
|
||||
|
||||
脚本风格复用 `infra/scripts/dev-migrate.sh`:
|
||||
|
||||
- 读取 `.env`
|
||||
- 通过 `uv run python -m core.runtime.cli sync-notifications` 调用后端 CLI
|
||||
|
||||
建议用法:
|
||||
|
||||
```bash
|
||||
./infra/scripts/register-notifications.sh
|
||||
./infra/scripts/register-notifications.sh --dry-run
|
||||
./infra/scripts/register-notifications.sh --source-key welcome_bonus
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. 与现有通知系统的关系
|
||||
|
||||
这条静态同步链路只负责:
|
||||
|
||||
- 把 YAML 中的通知定义注册到数据库
|
||||
- 更新通知主记录
|
||||
- 撤销通知主记录
|
||||
- 为目标用户补齐接收关系
|
||||
|
||||
它不替代现有通知 API:
|
||||
|
||||
- 用户列表、未读数、已读接口仍走现有通知系统
|
||||
- Flutter 端仍然从现有通知 API 和 Realtime 获取数据
|
||||
|
||||
如果通知内容被静态同步更新,而前台需要即时看到变更,建议在 Realtime 中补充:
|
||||
|
||||
- `notification_updated`
|
||||
|
||||
否则前台只能在下次 HTTP 拉取时看到更新后的内容。
|
||||
|
||||
---
|
||||
|
||||
## 12. 实施清单
|
||||
|
||||
1. 为 `notifications` 表增加 `source/source_key/source_version/content_hash`
|
||||
2. 增加 `(source, source_key)` 唯一约束
|
||||
3. 新增 `backend/src/core/config/static/notification/notifications/` 目录
|
||||
4. 定义静态通知 YAML 的 Pydantic schema
|
||||
5. 实现 YAML 扫描、加载、校验与 upsert 同步逻辑
|
||||
6. 为通知模块补充按 `source/source_key` 查询与更新能力
|
||||
7. 在 `core.runtime.cli` 中新增 `sync-notifications` 命令
|
||||
8. 新增 `infra/scripts/register-notifications.sh`
|
||||
9. 视需要补充 `notification_updated` Realtime 事件
|
||||
10. 编写最小测试和 dry-run 校验
|
||||
|
||||
---
|
||||
|
||||
## 13. 验收标准
|
||||
|
||||
- [ ] 新增一个 YAML 文件后,可成功同步出对应主通知记录
|
||||
- [ ] 相同 `source_key` 的 YAML 再次同步时,会更新主通知而不是插入重复记录
|
||||
- [ ] 修改 `title/body/payload` 后,再同步可反映到数据库
|
||||
- [ ] 用户侧已读状态在主通知内容更新后保持不变
|
||||
- [ ] 将 `status` 改为 `revoked` 后,再同步可使通知在用户列表中失效
|
||||
- [ ] `--dry-run` 可输出计划变更而不写库
|
||||
- [ ] YAML 结构不合法时同步失败,并给出明确错误
|
||||
- [ ] 脚本可按全量或按 `source_key` 手动触发同步
|
||||
|
||||
---
|
||||
|
||||
## 14. 测试要求
|
||||
|
||||
后端至少覆盖:
|
||||
|
||||
- YAML schema 校验
|
||||
- 新建通知同步
|
||||
- 已有通知更新同步
|
||||
- 撤销同步
|
||||
- 相同 `source_key` 幂等 upsert
|
||||
- 更新主通知时不重置 `user_notifications.is_read/read_at`
|
||||
- 新增目标用户时补插入接收关系
|
||||
- 被移出目标集合时不删除既有接收关系
|
||||
|
||||
脚本至少验证:
|
||||
|
||||
- 正常执行 CLI
|
||||
- `--dry-run` 不写库
|
||||
- `--source-key` 只同步指定通知
|
||||
|
||||
---
|
||||
|
||||
## 15. 后续扩展条件
|
||||
|
||||
只有在真实需求出现时,再考虑:
|
||||
|
||||
- 用删除文件触发软删除
|
||||
- 严格对齐目标用户集合并清理历史接收关系
|
||||
- 通过后台页面管理静态通知
|
||||
- 将静态通知同步纳入更完整的发布工作流
|
||||
@@ -77,6 +77,12 @@ This document is the source of truth for backend RFC7807 `code` values consumed
|
||||
| `AVATAR_SIGNED_URL_FAILED` | 502 | Backend failed to generate avatar signed upload URL | Show retry toast and keep previous avatar |
|
||||
| `AVATAR_UPLOAD_FAILED` | 502 | Backend failed to upload avatar bytes to storage | Show retry toast and keep previous avatar |
|
||||
|
||||
## Notification
|
||||
|
||||
| code | status | meaning | frontend handling |
|
||||
|---|---:|---|---|
|
||||
| `NOTIFICATION_NOT_FOUND` | 404 | Notification not found or not owned by current user | Show not-found message and refresh list |
|
||||
|
||||
## Global
|
||||
|
||||
| code | status | meaning | frontend handling |
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
# Notification Inbox Protocol (Frontend <-> Backend)
|
||||
|
||||
This document defines the notification inbox contract for authenticated users.
|
||||
|
||||
Protocol verification status:
|
||||
|
||||
- Backend route source: `backend/src/v1/notifications/router.py`
|
||||
- Backend service source: `backend/src/v1/notifications/service.py`
|
||||
- Backend repository source: `backend/src/v1/notifications/repository.py`
|
||||
- Backend schema source: `backend/src/v1/notifications/schemas.py`
|
||||
|
||||
## Compatibility strategy
|
||||
|
||||
- Additive evolution only.
|
||||
- Existing response fields are stable and must remain backward-compatible.
|
||||
- New `action` values may be added to `payload`; unknown `action` values must be ignored by the client.
|
||||
|
||||
## Routes
|
||||
|
||||
### GET /api/v1/notifications
|
||||
|
||||
List notifications for the current user.
|
||||
|
||||
**Authorization**: Requires authenticated session. User identity from JWT `sub`.
|
||||
|
||||
**Query parameters**:
|
||||
|
||||
- `limit` (optional, integer, default 20, max 50): number of items per page
|
||||
- `cursor` (optional, string): pagination cursor (ISO 8601 timestamp of last item's `created_at`)
|
||||
|
||||
**Response (200)**:
|
||||
|
||||
```json
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"id": "uuid",
|
||||
"notificationId": "uuid",
|
||||
"type": "system",
|
||||
"title": "Welcome",
|
||||
"body": "Welcome to the app!",
|
||||
"payload": {
|
||||
"action": "none"
|
||||
},
|
||||
"isRead": false,
|
||||
"readAt": null,
|
||||
"createdAt": "2026-04-10T00:00:00Z"
|
||||
}
|
||||
],
|
||||
"nextCursor": "2026-04-09T12:00:00Z",
|
||||
"hasMore": true
|
||||
}
|
||||
```
|
||||
|
||||
Field rules:
|
||||
|
||||
- `items`: array of notification items, ordered by `createdAt` descending
|
||||
- `nextCursor`: timestamp cursor for next page, `null` if no more items
|
||||
- `hasMore`: boolean indicating if more items exist
|
||||
- `type`: string, currently only `system`
|
||||
- `payload`: discriminated union (see Payload section below)
|
||||
- `isRead`: boolean
|
||||
- `readAt`: ISO 8601 timestamp or `null`
|
||||
- Results are filtered: `notifications.status = 'published'` and `notifications.deleted_at IS NULL`
|
||||
|
||||
### GET /api/v1/notifications/unread-count
|
||||
|
||||
Get the number of unread notifications for the current user.
|
||||
|
||||
**Authorization**: Requires authenticated session. User identity from JWT `sub`.
|
||||
|
||||
**Response (200)**:
|
||||
|
||||
```json
|
||||
{
|
||||
"count": 5
|
||||
}
|
||||
```
|
||||
|
||||
Field rules:
|
||||
|
||||
- `count`: integer `>= 0`
|
||||
- Counts only notifications where `notifications.status = 'published'` and `notifications.deleted_at IS NULL`
|
||||
|
||||
### PATCH /api/v1/notifications/{id}/read
|
||||
|
||||
Mark a single notification as read. Idempotent.
|
||||
|
||||
**Authorization**: Requires authenticated session. `id` must belong to the current user's `user_notifications`.
|
||||
|
||||
**Path parameters**:
|
||||
|
||||
- `id`: UUID of the `user_notifications` record
|
||||
|
||||
**Response (200)**:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "uuid",
|
||||
"notificationId": "uuid",
|
||||
"type": "system",
|
||||
"title": "Welcome",
|
||||
"body": "Welcome to the app!",
|
||||
"payload": {
|
||||
"action": "none"
|
||||
},
|
||||
"isRead": true,
|
||||
"readAt": "2026-04-10T01:00:00Z",
|
||||
"createdAt": "2026-04-10T00:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
**Error responses**:
|
||||
|
||||
- 404 `NOTIFICATION_NOT_FOUND`: notification not found or not owned by current user
|
||||
- Already-read notifications return 200 with current state (idempotent)
|
||||
|
||||
### PATCH /api/v1/notifications/mark-all-read
|
||||
|
||||
Mark all unread notifications for the current user as read. Idempotent.
|
||||
|
||||
**Authorization**: Requires authenticated session. User identity from JWT `sub`.
|
||||
|
||||
**Response (200)**:
|
||||
|
||||
```json
|
||||
{
|
||||
"updatedCount": 3
|
||||
}
|
||||
```
|
||||
|
||||
Field rules:
|
||||
|
||||
- `updatedCount`: integer `>= 0`, number of notifications that were actually changed from unread to read
|
||||
- If all notifications are already read, returns `{ "updatedCount": 0 }`
|
||||
- Only affects notifications where `notifications.status = 'published'` and `notifications.deleted_at IS NULL`
|
||||
|
||||
## Payload
|
||||
|
||||
`payload` is a discriminated union based on the `action` field.
|
||||
|
||||
### action = "none"
|
||||
|
||||
No navigation action on tap.
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "none"
|
||||
}
|
||||
```
|
||||
|
||||
### action = "open_route"
|
||||
|
||||
Navigate to an in-app route.
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "open_route",
|
||||
"route": "/divination/history",
|
||||
"entityId": "optional-uuid",
|
||||
"tab": "optional-tab-name"
|
||||
}
|
||||
```
|
||||
|
||||
Field rules:
|
||||
|
||||
- `route`: required, string, max 200 characters, app-internal route path
|
||||
- `entityId`: optional, string, max 64 characters, business object ID
|
||||
- `tab`: optional, string, max 32 characters, sub-page navigation parameter
|
||||
- `url`: must be absent
|
||||
|
||||
### action = "open_url"
|
||||
|
||||
Open an external URL.
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "open_url",
|
||||
"url": "https://example.com/page"
|
||||
}
|
||||
```
|
||||
|
||||
Field rules:
|
||||
|
||||
- `url`: required, string, max 500 characters, external URL
|
||||
- `route`, `entityId`, `tab`: must be absent
|
||||
|
||||
## Error contract linkage
|
||||
|
||||
- RFC7807 + extension `code`, optional `params`.
|
||||
- Shared registry: `docs/protocols/common/http-error-codes.md`.
|
||||
- New error codes for this feature are registered in the same registry.
|
||||
Reference in New Issue
Block a user