Files
memind/docs/architecture/page-data-api-public-access-plan-20260708.md
john 6b0c633a75 feat(page-data): complete Phase 4-5, ops UI, and publish integration
Add visitor roles, row-level scope, owner ops APIs, MySQL policy index,
Turnstile captcha, browser client SDK, publish-panel dataset binding,
acceptance tests, and usage documentation.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 14:52:49 +08:00

453 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Page Data API 与公开页面数据访问方案
日期: 2026-07-08
状态: 已实现(Phase 1–5 + 产品层);以代码与 `docs/page-data-api-usage.md` 为准。
## 1. 背景
MindSpace 已有用户私有数据空间能力:
- 每个用户工作区有一个 `.mindspace/private-data.sqlite`
- Agent 通过 `private_data_info``private_data_schema``private_data_query``private_data_execute` 使用该 SQLite。
- 当前能力适合问卷、表单、清单、调研数据、台账和分析中间表。
现在需要补齐的是页面侧的数据访问能力:
- 用户构建 HTML 页面时,可以让页面读取必要数据。
- 页面可以提交表单、更新记录、删除记录。
- 公开分享页面也可以在授权范围内读写后台数据。
- 不碰现有 session 存储,不改现有平台 MySQL 主业务表,不把用户数据塞进现有 MySQL。
本文定义 Page Data API 的边界、公开页面访问模式、权限模型和建议落地顺序。
## 2. 核心结论
结构化业务数据以 per-user SQLite 作为主存储,JSON 只作为 API 载荷、导入导出和少量页面配置格式。
Page Data API 不直接暴露 SQL。HTML 页面只访问 dataset API;后端根据页面授权、dataset scope、字段白名单和动作权限,受控读写 owner 的 private SQLite。
一句话:
```text
Agent 可以建模和写 SQLHTML 只能调用受限 dataset API。
```
## 3. 非目标
第一阶段不做以下事情:
- 不让公开页面直接执行 SQL。
- 不让公开页面创建表、删表、改表结构。
- 不把用户私有数据迁入现有 MySQL 主业务表。
- 不把 Page Data API 绑死到当前 session 结构。
- 不开放跨用户任意查询。
- 不默认允许公开页面读取 owner 的全部 SQLite。
- 不默认开放 hard delete。
## 4. 建议架构
```text
Agent / Goose MCP
-> private_data_* tools
-> UserDataSpaceService
-> owner .mindspace/private-data.sqlite
HTML Page
-> Page Data API
-> Page Access Policy
-> Dataset Scope
-> UserDataSpaceService
-> owner .mindspace/private-data.sqlite
```
建议先抽出 `UserDataSpaceService`,让 Agent MCP 和 Page Data API 复用同一个底层实现。
`UserDataSpaceService` 建议职责:
- 定位用户 workspace 和 `.mindspace/private-data.sqlite`
- 创建或检查用户私有数据库。
- 执行只读查询。
- 执行 Agent 侧受控 SQL。
- 执行页面侧结构化 insert/update/soft delete。
- 维护大小限制、超时、行数限制和危险 SQL 防护。
- 提供 dataset schema、统计、分页查询等上层能力。
## 5. Dataset 模型
页面不直接访问 table,而是访问 dataset。dataset 是页面可见的数据契约,可以映射到 SQLite table、view 或受控查询。
示例:
```json
{
"name": "registrations",
"table": "form_registrations",
"description": "活动报名数据",
"actions": ["read", "insert", "update", "soft_delete"],
"columns": {
"read": ["id", "name", "phone", "status", "note", "created_at"],
"insert": ["name", "phone", "status", "note"],
"update": ["status", "note"],
"soft_delete": ["id"]
},
"limits": {
"maxRowsPerRead": 100,
"maxInsertBytes": 8192
}
}
```
dataset 元数据可以先存在用户 SQLite 的系统表中,例如:
```sql
CREATE TABLE IF NOT EXISTS __page_data_datasets (
name TEXT PRIMARY KEY,
table_name TEXT NOT NULL,
config_json TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
```
该系统表属于用户私有数据空间,不需要新增平台 MySQL schema。后续如果需要统一管理公开发布状态,可以再引入平台侧只存 pageId、ownerUserId、scope hash、token hash 的轻量索引。
## 6. API Surface 草案
登录用户页面:
```text
GET /api/page-data/:dataset
GET /api/page-data/:dataset/schema
GET /api/page-data/:dataset/stats
POST /api/page-data/:dataset/rows
PATCH /api/page-data/:dataset/rows/:id
DELETE /api/page-data/:dataset/rows/:id
```
公开页面:
```text
POST /api/public/pages/:pageId/data-auth
GET /api/public/pages/:pageId/data/:dataset
GET /api/public/pages/:pageId/data/:dataset/schema
GET /api/public/pages/:pageId/data/:dataset/stats
POST /api/public/pages/:pageId/data/:dataset/rows
PATCH /api/public/pages/:pageId/data/:dataset/rows/:id
DELETE /api/public/pages/:pageId/data/:dataset/rows/:id
```
约束:
- `GET` 只支持白名单过滤、分页、排序和预设统计。
- `POST` 只允许插入 dataset 允许的字段。
- `PATCH` 只允许更新 dataset 允许的字段。
- `DELETE` 第一阶段只做 soft delete。
- 所有公开 API 都必须校验 pageId、访问模式、token、dataset、action、columns 和 limits。
## 7. 页面访问模式
用户构建或发布页面时必须选择访问模式。访问模式决定页面如何获得 Page Data API 权限。
### 7.1 完全公开
适合:
- 展示页。
- 公开统计页。
- 匿名表单。
- 不需要追责的反馈收集。
默认权限:
- 可以允许 `insert`
- 可以允许读取公开 view、聚合结果或白名单字段。
- 默认关闭 `update``delete`
风险控制:
- 不要求访问者登录。
- 不应该展示敏感字段。
- 表单提交必须有限流和大小限制。
### 7.2 简单令牌或密码访问
适合:
- 小团队共享台账。
- 临时活动登记。
- 内部共享清单。
- 不想让访问者注册,但需要阻止完全公开访问的轻量协作页。
默认权限:
- 可以允许 `read`
- 可以允许 `insert`
- 可以允许 `update` 白名单字段。
- 可以允许 `soft_delete`
- 不默认允许 hard delete。
建议流程:
```text
访问者打开页面
-> 输入页面密码
-> 后端校验密码 hash
-> 返回短期 pageDataSessionToken
-> 页面后续 CRUD 都带 token
-> 后端每次校验 token 和 dataset scope
```
注意:
- 密码只证明访问者知道入口口令,不代表访问者拥有 owner 权限。
- 密码不应写入 HTML。
- token 要短期有效,并支持撤销、轮换和重置密码。
- 如需审计,可要求访问者输入显示名,写入 `updated_by_label`
### 7.3 注册登录访问
适合:
- 长期小系统。
- 敏感度更高的数据。
- 需要准确追踪谁修改了数据。
- 需要区分 owner、editor、viewer 或不同访问者数据隔离的页面。
默认权限:
- 可以按登录用户、角色和行级规则控制 read/insert/update/soft delete。
- 可以记录真实 `visitorUserId`
- 可以在后续阶段支持更细的 row policy。
第一阶段可以只保留模式定义,不急于做完整外部访问者账号体系。
## 8. Page Access Policy
Page Data API 的授权来源是 Page Access Policy。它由用户构建或发布页面时选择的访问模式和数据能力生成。
示例:
```json
{
"pageId": "pub_abc",
"ownerUserId": "user_123",
"workspaceRef": "MindSpace/user_123",
"accessMode": "password",
"datasets": {
"registrations": {
"read": true,
"insert": true,
"update": true,
"softDelete": true,
"hardDelete": false,
"columns": {
"read": ["id", "name", "phone", "status", "note", "created_at"],
"insert": ["name", "phone", "status", "note"],
"update": ["status", "note"]
}
}
}
}
```
Policy 要点:
- 页面只能访问 policy 显式声明的 dataset。
- dataset 只能执行 policy 显式声明的 action。
- action 只能使用 policy 显式声明的 columns。
- 公开页面必须使用 pageId + token/session 访问,不能借用 owner 登录态。
- policy 修改后要能立即收紧权限。
## 9. CRUD 语义
### Insert
`insert` 是公开页面最安全的写入能力,适合表单和上报。
要求:
- 字段白名单。
- 类型校验。
- 长度限制。
- 频率限制。
- 自动写入 `created_at`
### Update
`update` 适合共享清单和协作台账。
要求:
- 只能改白名单字段。
- 必须带稳定 row id。
- 可选乐观锁字段,例如 `updated_at``version`
- 自动写入 `updated_at` 和访问者标识。
### Delete
第一阶段只允许 soft delete。
推荐语义:
```sql
UPDATE target_table
SET deleted_at = CURRENT_TIMESTAMP,
deleted_by = ?
WHERE id = ?
```
要求:
- 查询默认过滤 `deleted_at IS NULL`
- owner 或 Agent 可以恢复。
- hard delete 只保留给登录 owner 或后台维护操作。
## 10. 对现有服务的影响
按本文方案实施,对现有服务影响应控制在新增能力范围内:
- 不改现有 session 存储模型。
- 不改现有 MySQL 主业务表。
- 不要求现有公开 HTML 立即迁移。
- 不影响 Agent 现有 `private_data_*` 工具。
- 新增 Page Data API 可作为独立 route 接入。
- 底层复用 private SQLite 能力,减少重复存储逻辑。
需要注意的新增运行风险:
- 公开写入会带来垃圾数据和刷接口风险。
- 公开读如果 scope 配错,可能泄露 owner 私有数据。
- SQLite 是 per-user 低到中等并发数据层,不适合作为高并发公共数据库。
- 密码访问无法可靠证明真实操作者身份,只能作为轻量协作权限。
- 删除和更新必须有审计和恢复策略。
## 11. 安全要求
必须满足:
- 公开页面永远不能传 SQL。
- 后端每次请求都校验 pageId、token、dataset、action、columns。
- 默认关闭公开读,除非 dataset 明确允许。
- 默认关闭公开 update/delete,除非访问模式和 dataset 都明确允许。
- 公开 delete 默认 soft delete。
- 密码只存 hash,不存明文。
- token 可撤销、可轮换、可过期。
- 公开接口有限流、请求体大小限制和行数限制。
- 错误信息不能泄露 SQLite 路径、SQL 细节或 owner 内部信息。
建议满足:
- 所有公开写入记录 `created_ip_hash``user_agent_hash``pageDataSessionId`
- 公开 update/delete 记录操作日志。
- 对频繁失败密码尝试做限流。
- 对公开表单提交可选验证码或 Turnstile。
## 12. 构建期用户体验
Agent 或页面构建器应在创建数据页面时询问:
```text
这个页面谁可以使用?
1. 完全公开
任何人打开链接即可访问
2. 口令访问
访问者输入页面口令后使用
3. 登录访问
访问者需要注册或登录后使用
```
然后选择数据能力:
```text
这个页面可以对数据做什么?
- 读取数据
- 新增数据
- 修改数据
- 删除数据
```
系统内部把选择转换成 Page Access Policy 和 dataset scope。用户不直接选择 SQL 权限。
## 13. 建议落地顺序
### Phase 1: 私有登录页读取与提交
-`UserDataSpaceService`
- Page Data API 支持登录用户访问自己的 dataset。
- 支持 `GET rows``POST rows``GET schema`
- Agent 继续用 `private_data_*` 建表和注册 dataset。
### Phase 2: 完全公开 insert
- 支持公开页面 `pageId`
- 支持公开页面向授权 dataset `insert`
- 默认不开放公开读。
- 加入限流、字段校验和大小限制。
### Phase 3: 口令访问 read/update/soft delete
- 支持页面密码 hash。
- 支持 `data-auth` 换短期 token。
- 支持白名单 `read/update/soft_delete`
- 加入操作日志和恢复语义。
### Phase 4: 注册登录访问
- 引入访问者身份。
- 支持 viewer/editor/owner。
- 支持行级规则和更完整审计。
### Phase 5: 管理与运维
- 页面 owner 可查看 dataset、操作日志、最近提交。
- 支持重置密码、撤销 token、关闭 dataset。
- 支持导出 JSON/CSV。
- 支持软删除恢复。
## 14. 第一版验收标准
第一版完成后,至少应能证明:
- Agent 可以创建表和 dataset。
- 登录用户页面可以读取 dataset。
- 登录用户页面可以提交表单到 SQLite。
- 公开页面在无密码模式下只能执行被授权的 insert。
- 未授权 dataset/action/column 会被拒绝。
- 公开页面无法传 SQL。
- 公开页面无法读取未授权字段。
- Page Data API 不影响现有 session 和 MySQL 主链路。
## 15. 推荐默认策略
默认策略如下:
```text
完全公开:
insert 可选开启
read 默认关闭
update/delete 默认关闭
口令访问:
read/insert 可选开启
update 可选开启并限制字段
delete 仅 soft delete
登录访问:
可按角色开放 read/insert/update/soft delete
hard delete 仍默认关闭
```
整体原则:
```text
SQLite 是用户私有主库。
Agent 有受控 SQL 能力。
页面只有 dataset API 能力。
公开页面按 Page Access Policy 授权,不按 owner 权限授权。
```