架构与概念

团队模式与读者席位

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)。

目标

完成一个最小、完整的团队协作闭环:

  1. 团队管理员添加和管理成员。
  2. 成员使用独立账号登录。
  3. 成员可以创建仅自己可见、团队可见或全网公开的笔记。
  4. 有效成员可以读取团队可见的笔记及其附件。
  5. 团队管理员可以管理团队成员并治理团队内容,但不能读取成员的私密内容,也不能改写他人笔记。
  6. 成员被移出后不能继续访问 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: trueactivation_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/readerPATCH /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 不得向其他成员或管理员泄露个人笔记的名称、作者、事件类型等元数据。

移出成员

团队管理员移出成员时:

  1. 立即将成员标记为 removed 并禁止继续访问;
  2. 撤销该成员全部 session、PAT 和其他应用凭据,并删除其团队成员记录;
  3. 删除该成员所有个人笔记及附件;
  4. 删除对应的 R2 对象和 Vectorize 派生索引;
  5. 删除该成员的个人项目、任务和 Agent Memory;
  6. 该成员的团队可见和全网公开笔记由 owner 账号认领(保持内容与可见性不变,历史作者名称保留);
  7. 保留历史作者记录;
  8. 撤销该成员创建的随机分享链接。

移出操作必须可以安全重试。重复执行不能删除应保留的团队或公开内容。

存量数据迁移

  • 迁移自动创建默认团队(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 无法触达管理端点。