Memos 多空间(Multi-Spaces)设计解析:Space 数据模型、权限矩阵与 0.31 迁移全解 Memos 多空间Multi-Spaces设计解析Space 数据模型、权限矩阵与 0.31 迁移全解【免费下载链接】memosOpen-source, self-hosted note-taking tool built for quick capture. Markdown-native, lightweight, and fully yours.项目地址: https://gitcode.com/GitHub_Trending/me/memos本文围绕 Memos 仓库中的设计文档 multi-spaces.md 展开系统讲解 Memos 实例内“多空间协作”的设计决策Space / SpaceMember / SpaceInvitation 的资源模型、SPACE可见性域与读权限推导规则、治理动作权限矩阵、迁移版本0.31的三库 Schema 变更以及 space_service.proto 中完整的 v1 API 形态。读完后可理解 Memos 如何在保持“备忘录属于作者”这一既有模型的前提下引入一套不转移所有权、不级联删除的共享协作上下文并能对照源码验证其关键实现。设计定位Space 是协作上下文不是租户边界设计文档 docs/design/multi-spaces.md 开篇即给出边界定义Multi-Spaces 在一个 Memos 实例内部引入共享协作上下文。Space 把成员和备忘录memo分组但它不是租户也不是工作区隔离边界。核心不变式是每条备忘录始终是作者所有、独立存在的资源。放置placement、受众audience、作者身份authorship、分发distribution与关系上下文relation context是五个相互独立的维度——备忘录作者通过既有的备忘录更新 API 控制其放置位置而 Space 治理层只管理 Space 本身、其成员关系以及“对直接分配到该 Space 的备忘录做聚合删除”治理层不转移作者身份也不授予协作编辑权。文档还明确了目标与非目标这是理解整个设计取舍的钥匙目标Goals允许一个活跃用户创建或加入多个 Space每个 Space 接受ADMIN与USER两级成员身份SpaceADMIN可邀请实例内已存在的活跃用户并指定角色但该用户必须接受邀请后才成为成员成员可在共享 Space 上下文中贡献和浏览备忘录每条备忘录含评论要么 Unassigned未分配要么恰好属于一个 Space同时保留自己的作者、受众与生命周期新增“仅限 Space 成员可见”的备忘录受众保留既有备忘录身份、关系与数据兼容非 Space 工作流既有备忘录保持 Unassigned不生成默认 Space提供 Space 创建、邀请、成员管理、备忘录放置与 Space 浏览的完整后端工作流。非目标Non-goals不做租户隔离、按 Space 独立的认证或实例设置不支持多条或嵌套放置、文件夹、替代标签与保存视图不做 Space 拥有备忘录、共享作者身份或协作编辑Space 管理员不能修改、审查单条备忘录包括移动其放置位置不做线程所有权、放置/受众继承、基于关系级联删除COMMENT关系不可变、不可改挂不做 Space 归档与恢复不做访客、邮件/外部用户邀请、公开注册、联邦、公开 Space 发现不重定义 Inbox、用户资料等用户全局面。竞品调研支撑的四项选择文档记录了 2026-08-22 对 Discourse、Notion、Mastodon 三类产品的对比调研原文附有各产品官方文档来源此处从略。对比结论如下表其中“Memos 应避免”一列直接塑造了设计产品可借鉴的模型Memos 应避免的做法Discourse帖子保留原作者而主题只有一个类别组权限控制访问把 Group、Category、版主三种概念压在同一协作区域嵌套类别默认开放访问NotionTeamspaces 是一等的成员与导航上下文页面可在个人区与共享区间移动强制默认 Teamspaces、共享页面所有权、页面树、多层权限继承Mastodon帖子的作者、可见性、Feed 分发相互独立把个人列表或关注关系当作共享成员关系、把受众与 Feed 放置耦合、引入联邦问题由此支撑四项选择Space 是一等资源具备显式成员关系与角色并以“被邀请者接受”作为门槛放置、受众、作者、分发彼此独立Unassigned 是一种真实存在的放置缺失状态而非默认 SpaceSpace 出现在常规浏览、创建与管理流程中但用户全局面Inbox、用户资料等不随当前 Space 变化。文档同时声明这些调研材料只说明产品机制不构成需求或普及率证据本次调研未包含 Memos 数据分析、用户访谈或可用性测试。核心模型与读取规则核心对象Space实例范围内的协作资源不是租户、组、文件夹或备忘录作者。它要么存在要么被硬删除没有归档状态Memo保留memos/{uid}身份与作者其放置要么是 Unassigned要么是一个 SpaceAudience单选取值 Author / Instance / Space / Public。文档强调这是“命名的域”named domains不是有序访问等级不能做数值比较Comment一条独立的备忘录通过一条随其原子创建的、不可变的COMMENT关系连接到上下文备忘录该关系不授予任何所有权、继承或生命周期权限Reaction隶属于一条备忘录没有独立受众SpaceInvitation面向一个已存在的活跃注册用户、带有选定ADMIN/USER角色的待定要约。在接受之前不授予任何 Space 访问权SpaceMember一个活跃注册用户与一个 Space 之间已接受的关系角色为ADMIN或USER。创建者自动成为第一个ADMIN系统没有永久 owner概念应用级ADMIN属于控制平面角色不是隐式 Space 成员也不是隐式备忘录读取者。读访问表v1 中 visibility 即受众v1 API 出于兼容仍称备忘录受众为visibility。普通身份型读访问只由下表定义见 memo_service.proto 中的Visibility枚举SPACE 4Visibility可读者PRIVATE活跃已认证作者本人PROTECTED活跃已认证用户PUBLIC活跃已认证用户实例策略允许时还包括匿名调用者SPACE所放置 Space 的活跃成员对 Unassigned 备忘录无效由此推出几条重要规则Space 放置不额外增加读取门槛一个活跃非成员可以直接读取已放置的PROTECTED或PUBLIC备忘录已放置PUBLIC备忘录的匿名访问同样遵循实例策略。这类访问不会泄露 Space 元数据、成员列表或 Space Feed。备忘录的 Space 引用只返回给作者或活跃成员未过期的 bearer 分享链接是对“某一条精确备忘录”的显式能力例外不授予列表、Feed、Space 或关联备忘录访问备忘录的 Space 引用仅在调用者有资格时才在响应中出现。分发是推导出来的而不是单独配置的全局 Feed 返回“可读且不带COMMENT关系”的备忘录Space Feed 先要求活跃成员身份然后返回“可读、已分配到该 Space 且不带COMMENT关系”的备忘录。成员身份不会让他人放置的PRIVATE备忘录变得可读直接读取备忘录时逐条独立判定评论同理评论/会话查询要求上下文备忘录可读然后按每条回复备忘录自己的受众过滤COMMENT/REFERENCE关系及其摘要只在两端都可读时返回反应reaction在其备忘录可读时即可读公开资料页等公开面继续使用PUBLIC与 Space 放置无关。参与与治理动作权限矩阵设计明确区分“读”与“参与”调用者可以在无成员身份时读取已放置的PUBLIC/PROTECTED备忘录但参与已放置内容评论、反应等需要活跃成员身份。完整的动作—权限矩阵如下动作所需权限创建 Space任何活跃注册用户创建时原子地加入第一个ADMIN查看 Space 元数据、成员、Feed活跃 Space 成员身份更新 Space 元数据SpaceADMIN邀请既有活跃用户、选定其角色、列出或撤销邀请SpaceADMIN接受或拒绝邀请仅被邀请用户本人移除其他成员或修改已接受成员角色SpaceADMIN且 Space 必须始终保留一个活跃ADMIN退出 Space该成员本人退出后必须仍保留活跃ADMIN在 Space 中创建或放置备忘录作者是目标 Space 的活跃成员评论/反应一条已放置备忘录调用者能读取该备忘录且是其 Space 的活跃成员新评论作为新备忘录独立鉴权编辑内容或受众管理附件、引用、分享备忘录作者若已放置作者同时须是该 Space 活跃成员删除一条备忘录备忘录作者不会连带删除其他备忘录撤回withdraw或移动备忘录备忘录作者不需要源 Space 成员身份但需要目标 Space 成员身份硬删除 SpaceSpaceADMIN配套规则任何会导致 Space 没有活跃ADMIN的成员变更或用户归档操作都会被拒绝邀请永远不等于成员身份。除邀请自身携带的只读 Space 摘要外待接受的ADMIN邀请不授予任何元数据、Feed、备忘录、参与或治理权限也不计入“最后活跃 ADMIN”不变式。Accept保持邀请创建时选定的角色拒绝或管理员撤销都只移除待定要约之后同一 Space-用户组合可以被再次邀请。Store 层对“接受者必须是被邀请者本人”做了硬校验见 store/space.go 中的AcceptSpaceInvitation/DeclineSpaceInvitationif accept.UserID ! actorUserID { return nil, ErrSpacePermissionDenied }放置变更的原子性约束只有备忘录作者可以变更放置位置且复用既有备忘录更新 API。文档给出三条实操约束分配备忘录不改变其受众同时变更放置与受众可以是同一个原子更新把一条SPACE备忘录移动到新 Space要求更新掩码中同时包含space与visibilityvisibility 置为SPACE以确认新的成员受众撤回放置则要求同一次更新中给出一个非 Space 受众。另外为SPACE备忘录创建分享链接会被拒绝把一条存在活跃分享链接的备忘录改为SPACE受众也会被拒绝直到这些分享被撤销。生命周期规则成员退出/被移除不移除、不删除其备忘录。被移除的作者仅在其受众允许时才能读取自己留在原 Space 的备忘录——因此他读不到该 Space 中自己的SPACE备忘录。他保留窄范围的作者生命周期权限可以删除、撤回、或把它移动到自己仍活跃的成员 Space但备忘录保持已放置期间不能做其他变更。删除单条备忘录只删除该备忘录、其自有资源、以及以它为端点的关系不遍历关系、不删除其他备忘录。Inbox 记录相互独立——即使其载荷引用了被删备忘录也不会被删通知面在所需备忘录缺失或不可读时省略整条通知。硬删除 Space原子删除 Space、其成员关系、所有直接分配的备忘录、这些备忘录自有的数据库资源以及端点已被删的关系。不沿关系追踪到其他 Space 或 Unassigned 备忘录也不删 Inbox 记录。执行删除的ADMIN不会收到其无权读取的备忘录清单。外部附件对象走既有的提交后post-commit清理路径该特性不新增清理队列或调度器。账号删除的 fail-closed 护栏通用账号擦除仍被推迟但作为护栏当用户仍有活跃 Space 成员身份时用户硬删除会失败且force不能绕过此规则待定邀请不阻止删除并会在账号删除事务中被移除。账号删除只删发送者或接收者是该用户的 Inbox 行删除其备忘录不会删掉其他引用它们的 Inbox 行。持久化模型与 0.31 迁移设计文档给出的目标持久化形状space(id, uid, title, description) space_member(space_id, user_id, status INVITED | ACTIVE, role ADMIN | USER) memo(..., space_id NULL means Unassigned, visibility encodes audience) memo_relation(memo_id, related_memo_id, type COMMENT | REFERENCE)关键设计点Space-用户组合唯一表示一个“当前关系槽位”。INVITED对外暴露为 Space InvitationACTIVE暴露为 Space Membership。接受操作把当前行从INVITED原子改为ACTIVE拒绝与撤销只删除INVITED行。刻意没有邀请代号 UID——同一 Space-用户组合的同一请求永远指向其当前待定邀请status必填、无默认值、数据库层无 CHECK 约束Store 逻辑写入并识别INVITED/ACTIVE未知取值在鉴权查询中 fail closed。角色约束保持不变初版不记录邀请人、邀请/Space/成员时间戳待出现具体归因、过期、审计或排序需求时再补memo.space_id可空直接表达“零或一”放置无需关联表既有memo_relation行继续是评论的真理来源。三库迁移脚本0.31变更随迁移版本0.31发布到 SQLite、MySQL、PostgreSQL并提供等价的新装 Schema。以 store/migration/sqlite/0.31/03__multi_spaces.sql 为例CREATE TABLE space ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT NOT NULL UNIQUE, title TEXT NOT NULL, description TEXT NOT NULL DEFAULT ); CREATE TABLE space_member ( space_id INTEGER NOT NULL, user_id INTEGER NOT NULL, role TEXT NOT NULL CHECK (role IN (ADMIN, USER)), PRIMARY KEY (space_id, user_id) ); -- ... memo 表重建因需扩展 visibility CHECK 约束 visibility TEXT NOT NULL CHECK (visibility IN (PUBLIC,PROTECTED,PRIVATE,SPACE)) DEFAULT PRIVATE, space_id INTEGER DEFAULT NULLSQLite 因ALTER TABLE能力受限需要重建 memo 表原样拷贝全部既有值放置初始为 Unassigned即space_id置 NULL迁移后重建索引并特别注意保留sqlite_sequence中已发出的最大自增 ID包括已删备忘录占用的 ID避免新备忘录 UID/ID 与历史冲突。MySQL 侧的 03__multi_spaces.sql 则直接ALTER TABLE memo ADD COLUMN space_id INT DEFAULT NULLPostgreSQL 同目录提供等价 DDL。紧随其后的 04__space_member_status.sql 为space_member增加status列并回灌为ACTIVE且如设计所述不建数据库 CHECKCREATE TABLE space_member_new ( space_id INTEGER NOT NULL, user_id INTEGER NOT NULL, status TEXT NOT NULL, role TEXT NOT NULL CHECK (role IN (ADMIN, USER)), PRIMARY KEY (space_id, user_id) ); INSERT INTO space_member_new (space_id, user_id, status, role) SELECT space_id, user_id, ACTIVE, role FROM space_member;与既有数据兼容的行为既有备忘录保留 UID、作者、可见性、关系与永久链接成为 Unassigned评论行与评论可见性不做改写。查询性能方面SQLite/MySQL 迁移均建立了idx_memo_space_idspace_id, row_status, created_ts DESC, id DESC与 Space Feed 的典型查询形态按放置 状态 时间倒序分页对齐。事务与并发Space 创建、邀请状态迁移、成员变更、放置与受众变更、评论创建、备忘录删除、Space 删除在“部分应用会直接破坏数据”的场景下使用普通事务MySQL 与 PostgreSQL 上创建/激活 Space 关系与“目标用户行上的用户删除”串行化邀请创建与“Space 行上的 Space 删除”串行化防止并发删除留下孤儿活跃成员或邀请Space 删除以与既有用户删除相同的风格直接删除已放置备忘录及其自有行然后在提交后做既有的尽力型附件存储清理初版不引入通用事务重试、清理队列或全局并发框架。API 形态专用 SpaceService 与备忘录字段扩展SpaceService 完整 RPC 一览proto/api/v1/space_service.proto 定义了独立 Space 服务覆盖创建、列表、获取、更新、硬删除活跃成员读/改/删以及独立的 Space Invitation 资源RPCHTTP 映射说明CreateSpacePOST /api/v1/spacesbody:space创建 Space调用者成为首个管理员ListSpacesGET /api/v1/spaces只列出调用者是其成员的 SpaceGetSpaceGET /api/v1/{namespaces/*}成员才可读UpdateSpacePATCH /api/v1/{space.namespaces/*}仅title、descriptionDeleteSpaceDELETE /api/v1/{namespaces/*}永久删除 Space 与其全部已放置备忘录不沿关系追踪CreateSpaceInvitationPOST /api/v1/{parentspaces/*}/invitations邀请既有活跃用户ListSpaceInvitationsGET /api/v1/{parentspaces/*}/invitations某 Space 的待定邀请ListUserSpaceInvitationsGET /api/v1/{parentusers/*}/spaceInvitations用户收到的待定邀请GetSpaceInvitationGET /api/v1/{namespaces/*/invitations/*}读取单条邀请DeleteSpaceInvitationDELETE /api/v1/{namespaces/*/invitations/*}管理员撤销AcceptSpaceInvitationPOST /api/v1/{namespaces/*/invitations/*}:accept被邀请者接受返回SpaceMemberDeclineSpaceInvitationPOST /api/v1/{namespaces/*/invitations/*}:decline被邀请者拒绝ListSpaceMembers/GetSpaceMemberGET /api/v1/{parentspaces/*}/members、GET .../members/*成员列表/详情UpdateSpaceMemberPATCH /api/v1/{space_member.namespaces/*/members/*}仅支持修改roleDeleteSpaceMemberDELETE /api/v1/{namespaces/*/members/*}移除成员成员删自己即退出消息结构要点Spacenamespaces/{space}、title必填、description以及两个仅输出字段current_user_role与member_count。通过成员授权操作返回的 Space 响应携带当前用户角色与已接受成员数元数据型摘要如邀请携带的摘要使用默认角色与零计数SpaceMembernamespaces/{space}/members/{username}、userusers/{username}、roleADMIN 1/USER 2SpaceInvitationnamespaces/{space}/invitations/{username}、invitee、role接受后获得的角色以及仅输出的space摘要——让被邀请者无需成员身份下的GetSpace权限即可理解要约内容不存在任何直接创建活跃成员身份的操作这是有意为之。可见性与列表范围备忘录响应新增可选 Space 资源名字段Visibility增加SPACE 4且不重排既有值。领域到 v1 的映射Author→PRIVATE、Instance→PROTECTED、Public→PUBLIC、Space→SPACEVISIBILITY_UNSPECIFIED保持为输入哨兵创建时视作PRIVATE显式更新时拒绝它响应永不返回它全局默认备忘录可见性设置仍只接受PRIVATE、PROTECTED、PUBLIC——因为它无法定位某个 Space备忘录列表获得显式的“全部可读 / Unassigned / Space”范围Space 范围要求活跃成员身份既有全局与 Space Feed 默认排除评论放置与受众变更复用既有备忘录更新机制实现原子变更Space API 不新增任何让ADMIN移动、撤回或变更单条备忘录的操作。一个耐人寻味的细节memo_service.proto 中ListMemosRequest末尾声明了reserved 7, 8; reserved space, unassigned;——说明早期曾设想过用请求字段表达范围最终改为在 CEL 过滤表达式中表达。当前 CEL 过滤支持space字段字符串资源名无 Space 时为 null与visibility含SPACE字符串例如space spaces/team or space null pinned true visibility PUBLIC文档同时声明Space 身份不加入 CEL 过滤 schema 的独立鉴权维度——CEL 只能收窄鉴权谓词不能放宽。MCP 侧备忘录操作复用同一备忘录策略Space 管理不通过 MCP 暴露。Space UID 的分配与格式ADR 0003Space 标题可变且不唯一因此稳定的公共身份是资源名spaces/{space UID}中实例范围的 UID。配套 ADR docs/adr/0003-space-uid-allocation-and-format.md 规定第一方客户端为每个新 Space 生成规范的小写 UUID v4放入CreateSpaceRequest.space_id同一创建交互的重试复用同一生成值用户也可在创建前替换为自定义 UID该 API 字段保持可选以兼容旧客户端为空时服务端生成规范小写 UUID v4共享的公共资源 UID 语法与 ADR 中定义一致SpaceUID : Alphanumeric | Alphanumeric UIDCharacter{0,34} Alphanumeric UIDCharacter : Alphanumeric | - Alphanumeric : ASCII letter | ASCII digit即 1 到 36 字符、首尾必须是字母数字、内部允许连字符大小写拼写原样保留。这与 space_service.proto 中space_id字段的注释Format: ^a-zA-Z0-9?$完全对应Store 层入口 store/space.go 的CreateSpace也会用base.UIDMatcher校验后再落库。UI 展示规则设置面与管理子流程始终显示完整 UID 并带Space UID标签其他界面仅在两个已知 Space 的区分大小写标题完全相同、或标题不可用时才显示 UID紧凑场景下规范 UUID 取 8 字符前缀短自定义 UID 全显长自定义 UID 显示两端。已有 Space UID 保持可读、不重写。UID 冲突由实例范围唯一性约束拒绝。源码中的安全不变式设计文档的“安全不变式”一节要求在SPACE可以落库之前存在一个共享的、备忘录本地的、fail-closed 的策略覆盖点读、列表与计数、文件、反应、关系、通知、邮件、Webhook、分享、搜索、统计、公开 Feed 与 MCP子资源解析其直接隶属的备忘录应用ADMIN没有隐式旁路。数据库层的鉴权谓词在分页之前应用形如PRIVATE active authenticated author OR PROTECTED active authenticated caller OR PUBLIC permitted for caller OR SPACE active membership in memo.space_idserver/access/memo.go 中的实现印证了这一点——对已认证调用者SPACE可见性要求存在一条status ACTIVE且角色为ADMIN/USER的成员关系(visibility SPACE AND EXISTS ( SELECT 1 FROM space_member AS m WHERE m.space_id memo.space_id AND m.user_id ? AND m.status ACTIVE AND m.role IN (ADMIN, USER) ))同时还有一条“放置有效性”子句visibility SPACE或space_id IS NOT NULL且对应的space行存在——即缺失或无效的放置拒绝SPACE读取未知可见性一律拒绝未知关系状态、非活跃用户、待定邀请与无效成员角色都拒绝一切依赖它们的访问。其余不变式还包括成员身份按请求校验或走“立即失效”的缓存状态数据库支持行锁时关系创建/激活与用户删除串行化邀请创建与 Space 删除串行化缺失或不可读的通知主题与关系端点 fail closed不泄露部分元数据实时刷新是一条已认证、无主体的缓存失效通道备忘录/反应变更成功后广播{type:memo.changed}Space/邀请/成员变更成功后广播{type:space.changed}。事件不携带资源名、受众、行为人、邀请或成员数据客户端失效相应缓存后经普通鉴权重取SSE 不物化接收者集合、不做备忘录级鉴权对应前端 web/src/contexts/SpaceContext.tsx 维护的当前 Space 上下文与设置面中的 Spaces 管理区通知在呈现时鉴权邮件与用户 Webhook 载荷在进入既有异步队列前完成鉴权初版不取消“排队期间权限已变化”的待发投递评论 Webhook 要求评论与上下文备忘录都可被 Webhook 属主读取已删备忘录 Webhook 只基于作者可读的删前快照构建PRIVATE、PROTECTED、SPACE备忘录的文件使用private, no-store响应头公开文件可走再验证。备选方案与否决记录设计文档以决策表形式留下了完整的取舍记录这是评估该设计稳健性的最好材料备选方案决策Space 作为租户、通用 Group、嵌套文件夹或多放置标签否决Space 是实例内一个扁平协作上下文由放置推导受众或把成员身份加为第二道读取门槛否决备忘录受众单独定义普通读取成员身份另行控制 Space 浏览与参与放置关联表、嵌套备忘录名或默认 Space否决可空memo.space_id保留稳定备忘录身份与真实 Unassigned 状态用父/根列替代关系或继承线程放置/受众/生命周期否决每条评论是独立备忘录没有“规范线程”参与鉴权允许修改COMMENT、要求评论共享放置、或级联单条删除否决评论上下文不可变且非所有权两端各自独立归档 Space、拒绝删非空 Space、或自动取消其备忘录初版否决硬删除对直接放置的备忘录有显式聚合生命周期Space 删除时沿关系追踪否决关系不能把删除延伸到其他 Space 或 Unassigned 内容成员关系结束时移动/删除其备忘录否决历史贡献保留至作者生命周期动作或 Space 删除给 SpaceADMIN驱逐/变更单条备忘录的独立操作否决放置属于备忘录作者生命周期Space 治理限于元数据、成员与聚合硬删给应用ADMIN隐式 Space/备忘录访问否决审核与恢复需要独立控制平面设计允许 SpaceADMIN直接创建活跃成员否决只有被邀请者能把待定要约变成成员身份现在就加独立邀请表本迭代否决关系状态可放进唯一的 Space-用户槽位刻意省略历史、过期与投递凭据需求出现时再改把 v1visibility改名为audience或双字段暴露否决领域术语是 Audience保留一个遗留 v1 字段作为兼容表示延后的设计与适用前提以下事项在设计中被显式延后邮件/外部用户邀请、邀请过期与邀请人归因、邀请历史、公开注册、超出成员护栏的账号擦除、通知投递与保留策略、应用管理员的审核与恢复、审计历史、软删除与恢复、可重试的外部对象清理、排队邮件/Webhook 的投递时取消、更宽的并发变更加固、超大 Space 的异步删除。后续工作必须保留被邀请者同意、备忘录独立鉴权、不传播的关系、显式的 Space 聚合删除——除非本设计被重开。对使用者与二次开发者的适用前提提示该能力随迁移版本0.31生效适用于 SQLite/MySQL/PostgreSQL 三库升级后既有备忘录全部处于 Unassigned评论与既有可见性不受影响API 层面Visibility.SPACE 4是命名域不可做数值大小比较列表鉴权在数据库分页之前执行客户端 CEL 过滤只能收窄若你基于该 API 构建客户端注意ListSpaces只返回自己是活跃成员的 Space非成员含待定邀请者对 Space 元数据、成员、Feed 请求一律收到NotFound——这是刻意的隐藏语义而非查询条件不满足。小结Memos 的 Multi-Spaces 设计可以概括为一句话在“备忘录永远属于作者”的前提下用一个可空外键加一个唯一关系槽位获得一个扁平、非租户化、以邀请接受为门槛的协作上下文。其工程价值体现在三个层面模型上把放置、受众、作者、分发解耦避免权限继承的复杂度存储上用可空space_id与唯一 Space-用户槽位同时表达 Unassigned 与“邀请即成员的前态”安全上以一份备忘录本地的 fail-closed 谓词统一所有读取面并用“最后活跃 ADMIN”“接受者必须是受邀者本人”等少量强不变式守住治理边界。仓库内 docs/design/multi-spaces.md、proto/api/v1/space_service.proto、store/space.go 与store/migration/*/0.31/下的迁移脚本共同构成了一套可从设计到落盘逐层对照的完整证据链。【免费下载链接】memosOpen-source, self-hosted note-taking tool built for quick capture. Markdown-native, lightweight, and fully yours.项目地址: https://gitcode.com/GitHub_Trending/me/memos创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考