团队模式与读者席位
FlareMo 团队模式
定位
团队模式是 FlareMo 开源项目的基础能力,不是独立版本、付费能力或部署时需要选择的模式。
- 只有一个成员时,FlareMo 保持现有的个人使用体验。
- 团队管理员添加其他成员后,成员之间可以共享团队可见的笔记和附件。
- 所有开源用户都可以使用完整的团队能力。
- 每个部署只有一个团队;不引入组织、部门、群组、多团队、多租户、审批流、SSO 或 SCIM。
模型
团队是一等实体(Better Auth organization 插件的 auth_organizations 表),成员关系与角色存于 auth_members 表,是角色唯一的存储事实源:
- 领域
users表不保存角色列; - 笔记通过
memos.team_id挂到团队:NULL表示个人笔记,非空表示团队笔记; - 笔记可见性
private等价于team_id为空;团队笔记只有protected(团队成员可读)和public(全网可读)两种取值。
权限判断统一由 domain 层的 team-permissions.ts 提供(按能力拆分为读、治理、编辑三类判断,并给出统一的 SQL 行级过滤),Web、Memos-compatible API、MCP、附件、搜索、语义搜索和 SSE 不得维护相互独立的权限规则。每个请求在凭证解析时一次性装配"用户 + 团队角色"的 viewer;没有成员关系的 viewer 一律按无团队权限处理(fail-closed)。
目标
完成一个最小、完整的团队协作闭环:
- 团队管理员添加和管理成员。
- 成员使用独立账号登录。
- 成员可以创建仅自己可见、团队可见或全网公开的笔记。
- 有效成员可以读取团队可见的笔记及其附件。
- 团队管理员可以管理团队成员并治理团队内容,但不能读取成员的私密内容,也不能改写他人笔记。
- 成员被移出后不能继续访问 FlareMo;其私密和个人数据被删除,团队及公开内容由 owner 认领后继续保留。
非目标
第一版不实现:
- 组织、部门、群组或自定义角色;
- 多团队或跨团队协作;
- 资源级成员名单或复杂 ACL;
- 为团队模式新增开放注册、邮件验证或邮件邀请流程;
- SSO、SCIM、审批流或复杂审计;
- 团队项目、团队任务或共享 Agent Memory;
- 成员移出后的冻结期或个人数据导出。
成员与角色
角色定义在团队(organization)成员关系上:
owner:团队所有者,通常是首次初始化的账号;可编辑/改可见性/彻底删除其他成员的团队笔记,可设置管理员角色;部署级设置(开放注册、品牌、向量重建)只属于实例 owner(bootstrap 账号),与团队角色相互独立;admin:团队管理员,可读取并治理(归档、回收站、恢复)团队及公开内容,可添加、移出成员和重置成员密码;member:普通团队成员,可读取团队及公开内容;reader:只读席位,带可选有效期(expires_at),到期自动失去团队访问——见下文「读者席位」。
成员状态为:
active:可以登录和正常使用;removed:不能登录,不出现在正常成员列表中,但保留失效成员记录以显示历史作者。
约束:
- 团队中必须至少有一位有效管理员;
- 不允许移出或降级最后一位有效管理员;
- 角色变更只属于团队 owner——管理员之间互不管理(不能互相改角色、重置密码或移出);
- 被移出的成员不能通过 cookie session、PAT、MCP、脚本或兼容 API 继续访问;
- 邮箱只作为唯一登录标识,不发送或验证邮件。
读者席位(reader)
读者席位是一个带有效期的只读角色:auth_members.role 的第四个取值 reader,配合可空的 expires_at。它回答的是"给某人一个有期限的只读席位"这类通用需求——客座读者、课程学员、客户交付、内测期;具体怎么发放(人工、兑换码、支付系统)不属于 FlareMo 的边界,核心仓只提供席位与开通接口。
语义
- 读者可以浏览团队空间(团队可见及全网公开的笔记),但不能向团队发布任何内容——创建、重新发布、PAT/API 写路径统一在
resolveMemoTeamId处拒绝; - 读者自己的个人笔记完全不受影响(作者权限不变):读者也是一个完整的 FlareMo 用户,可以在个人空间记笔记、建任务、传附件;
- 到期自动失效:
getViewerTeamMembership在凭证解析时把过期读者当作无成员处理(fail-closed),无需任何定时任务;续期后访问立即恢复; - 过期不删除账号,也不动个人数据;/me 返回
team_expired: true,界面显示续期提示而非静默消失; - 团队卡片对读者不显示编辑/删除菜单(作者专属谓词天然挡住),统计、标签、全文与语义搜索按空间过滤规则正常工作(只读)。
读者看到什么
- 侧栏与空间切换器照常出现「团队空间」入口;读者未过期前与普通成员一样进出;
- 团队空间顶部显示一条只读提示:"此空间为只读——你是读者,可以浏览但不能发布",不渲染发布框;
- 个人空间正常可用,但发送目标选择器隐藏(只能写给自己,不能发布到团队);
- 设置 → 账户资料 显示"读者有效期至 <日期>"(/me 的
reader_expires_at); - 过期后:团队空间入口消失,时间线顶部出现"有效期已过,续期后自动恢复"提示(
team_expired),个人笔记与账号完全正常。
管理(界面)
账户页面 → 团队:
- 设为读者 / 续期:菜单提供 30 天 / 365 天预设;续期从
max(现在, 现到期日)起算,给有效期内读者续期不损失已购天数; - 撤销读者:删除成员关系,团队空间随之消失(此前若是普通成员,其已发布的团队笔记保留,与成员离队同等处理);
- 成员列表显示「读者」徽章与"读者有效期至 <日期>",过期的席位照常显示(带过去日期)便于对账;
- owner 与 admin 不能被降级为读者(403);
- 升级路径:读者 → 成员走既有的"设置角色"入口(owner 专属),升为成员后旧有效期自动清除。
机器开通接口
外部系统(脚本、支付 webhook、机器人)通过 PAT 调用管理端点。PAT 与浏览器会话拥有完全相同的权限边界:凭证持有人本人的角色决定其能力——实例 owner 的 PAT 可以做实例 owner 能做的一切,普通成员的 PAT 调管理端点一律 403。
先在账户页面创建 PAT(memos_pat_ 前缀),然后对接这一条端点即可完成全自动开通:
# 开通(邮箱不存在 → 建号并返回一次性激活链接;已存在 → 发放/续期席位)
curl -X PUT https://<your-instance>/api/app/admin/team/reader \
-H "Authorization: Bearer memos_pat_xxx" \
-H "Content-Type: application/json" \
-d '{"email": "reader@example.com", "name": "Reader", "expires_at": "2026-12-31T00:00:00.000Z"}'
响应示例(新建,HTTP 201):
{
"id": "users/…",
"email": "reader@example.com",
"name": "Reader",
"username": "reader",
"role": "reader",
"reader_expires_at": "2026-12-31T00:00:00.000Z",
"status": "active",
"created": true,
"activation_path": "/reset?token=…",
"activation_expires_in_seconds": 3600
}
约定:
expires_at为绝对时间戳(ISO-8601),传null表示无期限;续期算术(从现到期日顺延)由调用方决定,端点只存绝对日期;- 幂等:重复调用同一邮箱得到确定结果——不存在则建号一次,存在则把席位设置为本次请求的到期日(HTTP 200,
created: false); - 新建账号返回
created: true与activation_path(一次性激活链接,1 小时有效),把它交给读者即可完成首次登录; - 邮箱对应已移出的账号时返回 409;目标是 owner/admin 时返回 403(永不降级);
name可省略,默认取邮箱前缀。
续费、撤销与查询:
# 续期:直接把新的绝对到期日发上去(调用方可自行从现到期日顺延)
# 撤销席位(按用户 id):
curl -X DELETE https://<your-instance>/api/app/admin/users/<user-id>/reader \
-H "Authorization: Bearer memos_pat_xxx"
# 查询成员与席位(含 role / reader_expires_at):
curl https://<your-instance>/api/app/admin/users \
-H "Authorization: Bearer memos_pat_xxx"
按用户 ID 的细粒度管理(PUT/DELETE /api/app/admin/users/:id/reader、PATCH /api/app/admin/users/:id/role)在浏览器会话下同样可用。
FAQ
- 到期后读者的笔记还在吗? 在。读者的个人空间与账号完全不动,只有团队访问失效;撤销同理。
- 读者能变成正式成员吗? 能。owner 在成员页把角色改回成员(或管理员),席位有效期随之清除。
- 同一邮箱既有席位又被设为管理员会怎样? 不会发生:发放接口对 owner/admin 目标直接 403,管理员降级走 owner 的角色管理入口。
- 席位有数量上限吗? v1 没有;成员配额(
assertMemberQuota)照常生效于新建账号。 - 为什么不用定时任务做过期? 过期判定收口在凭证解析的单点(fail-closed),访问在过期瞬间即失效,且不会出现"任务没跑导致还能访问"的事故窗口。
团队管理
账户页面中的"团队管理"只提供:
- 查看有效成员;
- 添加成员(管理员可执行);
- 设置或取消团队管理员(仅团队 owner 可执行);
- 移出成员(管理员可移出普通成员,团队 owner 可移出管理员);
- 为成员生成一次性密码重置链接(管理员可重置普通成员,团队 owner 可重置管理员)。
添加成员时填写姓名和邮箱,成员自动加入部署的默认团队。团队管理界面不提供开放注册入口,新增成员默认由团队管理员完成;为避免破坏已有部署,原有兼容接口的注册设置继续保留,并保持默认关闭。
笔记可见性
存储层继续使用兼容值,产品界面显示为:
private:仅自己可见(个人笔记,team_id为空);protected:团队可见(团队成员可读);public:全网公开。
权限矩阵(对他人的笔记):
| 能力 | 普通成员 | 团队管理员 | 团队 owner | 读者 |
|---|---|---|---|---|
| 读团队/公开笔记 | ✅(仅正常状态) | ✅(含归档/回收站) | ✅(含归档/回收站) | ✅(仅正常状态) |
| 发布到团队 | ✅ | ✅ | ✅ | ❌ |
| 治理:归档、回收站、恢复 | ❌ | ✅ | ✅ | ❌ |
| 编辑内容 / 改可见性 / 彻底删除 | ❌ | ❌ | ✅ | ❌ |
| 读个人笔记 | ❌ | ❌ | ❌ | ❌ |
作者对自己的笔记始终拥有全部能力。前端按服务端下发的 can_manage(编辑/改可见性/分享/彻底删除)与 can_govern(归档/回收站/恢复)两个位渲染操作,不得在客户端推导权限。切换为全网公开时必须二次确认"任何人均可访问"。
附件
- 已绑定到笔记的附件始终继承所属笔记的权限;
- 未绑定到笔记的附件仅上传者可见;
- 不能通过附件列表、附件详情、blob、文件兼容路径或分享路径绕过笔记权限。
搜索、语义搜索和事件
- 个人内容只能由作者搜索;
- 团队内容可以由所有有效成员搜索;
- 公开内容可以由未登录访客读取;
- Vectorize 只返回候选结果,最终结果必须回 D1 按当前成员和笔记状态过滤;
- SSE 不得向其他成员或管理员泄露个人笔记的名称、作者、事件类型等元数据。
移出成员
团队管理员移出成员时:
- 立即将成员标记为
removed并禁止继续访问; - 撤销该成员全部 session、PAT 和其他应用凭据,并删除其团队成员记录;
- 删除该成员所有个人笔记及附件;
- 删除对应的 R2 对象和 Vectorize 派生索引;
- 删除该成员的个人项目、任务和 Agent Memory;
- 该成员的团队可见和全网公开笔记由 owner 账号认领(保持内容与可见性不变,历史作者名称保留);
- 保留历史作者记录;
- 撤销该成员创建的随机分享链接。
移出操作必须可以安全重试。重复执行不能删除应保留的团队或公开内容。
存量数据迁移
- 迁移自动创建默认团队(slug
flaremo),并为每个有效成员按历史角色写入成员关系; - 历史
owner→ 团队 owner,历史admin→ 团队 admin,其余 → member; - 历史
protected笔记转为团队笔记(挂默认团队),public笔记保持公开并挂默认团队,private笔记转为个人笔记; users.role列在迁移后删除,角色此后只读自成员关系表。
验收
至少使用 owner、admin、成员 A、成员 B 和未登录访客验证:
- 管理员可以添加成员和第二位管理员;
- 最后一位有效管理员不能被移出或降级;
- 管理员不能修改其他管理员的角色、密码,也不能移出其他管理员;
- 默认部署未由兼容接口显式开启注册时,公开注册被拒绝;
- 成员 B、管理员和 owner 都不能读取成员 A 的个人笔记、附件、搜索结果或 SSE 事件;
- 成员 B 可以读取成员 A 的团队笔记及附件,但不能编辑或归档;
- 管理员可以归档/恢复/回收成员 A 的团队笔记,但不能编辑内容或改可见性;
- 团队 owner 可以编辑成员 A 的团队笔记内容、修改可见性并彻底删除;
- 未登录访客只能读取全网公开的笔记及附件;
- Web、Memos-compatible API、MCP、附件、全文搜索、语义搜索和 SSE 使用同一权限矩阵;
- 成员被移出后,旧 cookie、PAT 和 MCP 请求全部失效;
- 成员被移出后,其个人数据被清理,团队及公开内容由 owner 认领后仍可正常访问;
- 读者可以读取团队及公开笔记,但任何发布路径(Web、PAT、兼容 API)都被拒绝,个人笔记不受影响;
- 读者有效期过后,团队空间在下次请求即消失(/me 返回
team_expired: true),无需等待定时任务;续期后立即恢复; - 读者无法被发放到 owner/admin,已移出的账号无法被发放接口复活;
- 外部系统持有 owner PAT 可以幂等开通/续期席位,普通成员 PAT 无法触达管理端点。
