资讯详情

资讯详情

Memos Space UID:客户端先行分配的稳定公开标识符是如何设计的

Memos Space UID客户端先行分配的稳定公开标识符是如何设计的【免费下载链接】memosOpen-source, self-hosted note-taking tool built for quick capture. Markdown-native, lightweight, and fully yours.项目地址: https://gitcode.com/GitHub_Trending/me/memosMemos 的 Space空间是实例级的协作边界它的公开身份是资源名spaces/{space UID}中的不可变 UID而非可修改、可重复的标题。这篇技术文章基于架构决策记录 ADR 0003解析 Space UID 的分配时机、格式语法、前后端校验链路以及兼容性策略读完你可以理解为什么由客户端在创建请求之前生成规范化小写 UUID v4、该 UID 语法如何与 Memos 其他公开资源复用同一套字符集规则以及数据库与 API 层面如何保证实例范围内的唯一性。背景标题不是身份UID 才是ADR 0003 的出发点Status: Accepted日期 2026-08-27见 docs/adr/0003-space-uid-allocation-and-format.md指出一个实际的产品问题Space 标题title是可修改的mutable且不唯一。如果在前端的空间切换器、设置页、角标badge和破坏性操作确认框中只靠标题区分空间那么两个同名的不同空间就会难以分辨。真正不可变的公开身份是嵌入资源名spaces/{space UID}的实例级 UID。Memos 领域术语表对这几个概念给出了明确定义见 docs/glossary.mdSpace面向已接受成员与 memo 放置的实例级协作边界它不是租户tenant、文件夹或应用级授权角色。Space IDSpace 的稳定内部身份与公开 Space UID、可变标题三者相互独立。Space UID创建时分配的不可变、实例级公开标识符可以由用户自定义也可以自动生成。Space resource nameSpace 在 API 中的身份形式spaces/{space UID}。Space title可修改、非唯一的展示标签。在 ADR 0003 之前第一方客户端创建 Space 时不带 UID由服务端生成一个短 UUIDshort UUID。这带来两个问题客户端无法在创建之前选定一个稳定标识符例如在重试中复用同一身份而且默认生成值与其他较新的基于 UUID 的公开身份格式不一致。同时存在两条兼容性约束旧客户端仍然可能省略请求字段存量短 UID 必须在不做数据迁移的前提下保持有效。决策客户端生成规范化小写 UUID v4ADR 的核心决策是第一方客户端为每个新建 Space 生成一个规范化小写 UUID v4并通过CreateSpaceRequest.space_id随创建请求提交。客户端可以在创建之前向用户暴露这个值允许用户将其替换为自定义 UID同一次创建交互的重试复用同一个已生成的值。API 字段保持可选为空时服务端生成一个规范化小写 UUID v4非空值则按共享的公开资源 UID 语法校验。这个决策在源码中有完整印证前后端两侧行为一致前端打开对话框即生成重试复用Web 端的创建空间对话框 web/src/components/CreateSpaceDialog.tsx 在组件初始化时就用uuidv4()生成 UIDconst [spaceUid, setSpaceUid] useState(() uuidv4()); const [showCustomId, setShowCustomId] useState(false); const isSpaceUidValid SPACE_UID_PATTERN.test(spaceUid);提交时将该值作为spaceId传给createSpacemutation若服务端返回AlreadyExistsUID 冲突则标记冲突并展开自定义 ID 输入让用户换一个标识space await createSpace.mutateAsync({ title: trimmedTitle, description: description.trim() || undefined, spaceId: spaceUid, }); // catch: if (error instanceof ConnectError error.code Code.AlreadyExists) { setSpaceUidConflict(true); setShowCustomId(true); }对话框每次关闭重置标题、描述与冲突标记时会重新生成一个 UUIDsetSpaceUid(uuidv4())保证下一次创建交互从全新身份开始而同一次交互内的失败重试保持同一 UID——这正是 ADR 中“retry of the same create interaction reuses the same generated value”的落地方式。协议可选的 space_id 字段API 契约定义在 proto/api/v1/space_service.proto 的CreateSpaceRequest中message CreateSpaceRequest { // Required. The space to create. Space space 1 [(google.api.field_behavior) REQUIRED]; // Optional. The space UID to use for this space. // If empty, a canonical UUID v4 will be generated. // Format: ^a-zA-Z0-9?$ string space_id 2 [(google.api.field_behavior) OPTIONAL]; }字段注释直接写明了格式正则与“为空则生成 canonical UUID v4”的服务端兜底行为。Space资源本身声明了pattern: spaces/{space}的 google.api.resource 元数据name字段为 IDENTIFIER——资源名解析、子资源成员、邀请寻址都建立在这个 UID 之上。服务端兜底生成与共享 UID 校验server/router/api/v1/space_service.go 中CreateSpace处理流程校验调用者身份与必填标题后调用ValidateAndGenerateSpaceUID(request.SpaceId)再将结果写入 storeuid, err : ValidateAndGenerateSpaceUID(request.SpaceId) if err ! nil { return nil, err } created, err : s.Store.CreateSpace(ctx, store.Space{ UID: uid, Title: title, Description: strings.TrimSpace(request.Space.Description), }, currentUser.ID)该函数的实现位于 server/router/api/v1/resource_name.go// ValidateAndGenerateSpaceUID validates a user-provided Space UID or generates a UUID v4. // Custom UIDs use the same format as other public-resource UIDs. func ValidateAndGenerateSpaceUID(provided string) (string, error) { if strings.TrimSpace(provided) { return util.GenUUID(), nil } return ValidateAndGenerateUID(provided) }util.GenUUID()internal/util/util.go即uuid.NewV4().String()产出规范化小写形式。而ValidateAndGenerateUID对非空输入执行strings.TrimSpace后用base.UIDMatcher校验func ValidateAndGenerateUID(provided string) (string, error) { uid : strings.TrimSpace(provided) if uid { return shortuuid.New(), nil } if !base.UIDMatcher.MatchString(uid) { return , status.Errorf(codes.InvalidArgument, invalid UID: must be 1-36 characters, ...) } return uid, nil }从源码结构看这里有一处值得注意的对比ValidateAndGenerateUID的通用空值分支走shortuuid.New()短 UUID用于其他公开资源的历史兼容路径而 Space 专用的ValidateAndGenerateSpaceUID空值分支改走util.GenUUID()UUID v4。这正是 ADR 所要求的“默认值与其他较新的 UUID-backed 身份保持一致”——Space 的空值兜底被有意从短 UUID 切换到标准 UUID v4。服务端行为由单元测试 server/router/api/v1/resource_name_test.go 固化空串与纯空白输入生成的值必须是可解析的 UUID、字符串等于其解析后的规范形式canonical lowercase且版本号位为 4带首尾空白的合法自定义值 Team-Notes 被 trim 后原样保留为Team-Notes非法值team_notes含下划线必须报错。UID 格式语法1 到 36 字符的共享公开资源字符集ADR 给出了 Space UID 的 EBNF 语法并要求复用既有公开资源 UID 语法而不是为 Space 单独发明一种 slug 格式SpaceUID : Alphanumeric | Alphanumeric UIDCharacter{0,34} Alphanumeric UIDCharacter : Alphanumeric | - Alphanumeric : ASCII letter | ASCII digit等价地即 proto 注释与测试中出现的正则^a-zA-Z0-9?$。该正则在 internal/base/resource_name.go 中实现为全局共享的匹配器var ( UIDMatcher regexp.MustCompile(^a-zA-Z0-9?$) )由此推导出几条具体的取值规则长度为1 到 36个字符单字符合法36 个字符是上限首尾各一个 alphanumeric中间至多 34 个首尾必须是 ASCII 字母或数字连字符只允许出现在内部连续内部连字符、纯数字值均合法大小写敏感且原样保留case-preservingTeam-Notes与team-notes是两个不同的 UID校验不做大写折叠或归一化下划线、空格、非 ASCII 字符不被接受校验错误信息明确指出“must be 1-36 characters, contain only letters, digits, or hyphens, and start and end with a letter or digit”。ADR 还解释了为什么最小长度定为 1这与其他公开资源 UID 的下限一致提高它并不能实质性地防止碰撞或“命名占位”name claiming问题。UUID v4 长度 368-4-4-4-12 加 4 个连字符恰好贴合该语法的上限。UI 展示规则标题为主UID 按需补充决策的另一半是展示策略。ADR 规定UI 以标题作为主标签设置页及其管理子流程management subflows始终带显式的Space UID标签展示完整 UID其他界面仅在两种情况下展示 UID——两个已知 Space 的标题在区分大小写下完全相同或标题不可用而 UID 是唯一可用身份。需要紧凑身份元数据时采用三种压缩策略规范化 UUID 取8 字符前缀较短的自定义 UID完整展示较长的自定义 UID首尾两端各展示一部分使尾部的差异仍然可见。在这些展示场景下可访问性标签accessible labels与 tooltip 保留完整 UID。兼容性与结果ConsequencesADR 的 Consequences 一节逐条列出了该决策带来的约束与结果均可在仓库中找到对应实现第一方新创建的 Space 默认使用 UUID v4同时保留用户自定义简短、可读标识的能力CreateSpaceDialog的自定义 ID 输入即为此设计旧客户端通过服务端空值兜底ValidateAndGenerateSpaceUID的util.GenUUID()分支继续工作允许重复标题因此不需要改名或唯一性迁移——schema 中space表只对uid加UNIQUE约束title无唯一性约束见 store/migration/sqlite/LATEST.sqlCREATE TABLE space ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT NOT NULL UNIQUE, title TEXT NOT NULL, ... );紧凑 Space 标签保持仅标题直到“标题撞车或标题缺失”的兜底逻辑要求展示 UIDUID 碰撞由既有的实例级唯一约束拒绝——API 层将存储错误映射后返回前端据此识别AlreadyExists并展开冲突处理 UI大小写保留的 UID 输入被接受跨数据库 collation 对齐“精确大小写唯一性与查找”是另一项独立的 schema 决策本 ADR 不涵盖从源码结构看SQLite 迁移中用户表的username TEXT COLLATE BINARY已采用二进制排序规则说明这类 collation 决策确实被单独处理。ADR 同时确认存量 Space UID 保持可读、不被重写历史短 UID 与既有 UUID UID 与新分配策略共存无需数据迁移。被否决的替代方案ADR 最后列出了五项被考虑的替代方案及其否决理由这些论证解释了当前设计的边界要求标题唯一——否决。标题是展示标签用户可能合理地重复使用同一个标题整个 UI 始终展示 UID——否决。这会让技术标识符在没有歧义的情况下与人类可读标题争夺注意力仅在设置页展示 UID——否决。切换器、角标和限定范围搜索中的同名空间仍会歧义仅由服务端分配 UID旧方案——否决。第一方客户端无法跨重试保持同一身份也无法在创建前提供自定义能力要求更长的自定义 UID——否决。长度并不能实质性地解决碰撞或命名空间占用问题。小结与延伸阅读ADR 0003 用一个可验证的调用链把“身份分配权”从服务端移交给客户端前端 CreateSpaceDialog 生成 UUID v4 → 协议字段 space_id 承载可选→ 服务端 ValidateAndGenerateSpaceUID 兜底生成或按 UIDMatcher 校验 → 数据库space.uid唯一约束保证实例级不重复。整套设计同时满足了“创建前身份稳定、重试可复用、旧客户端兼容、存量数据零迁移”四个约束并与 Memos 其他公开资源用户、Identity Provider 等共享同一套 1–36 字符 UID 语法。相关文档ADR 索引与约定docs/adr/README.md同系列决策Tag 语法与识别、用户名格式与引用领域术语docs/glossary.md多空间设计docs/design/multi-spaces.md【免费下载链接】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),仅供参考
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →