Files
Blizzard addaa1b34f feat(space): 共享工作区增量3a —— Agent 编排按 Space 共享(纯 PG 打样)
引入 Space 中间容器(租户>Space>成员),资源作用域从 owner 改为 space_id,
让"个人私有/项目临时组队/整租户共享"出自同一模型(设计见 SPACE_DESIGN.md)。
先在纯 PG 的 Agent 上打样,零存储风险,验证协作+RBAC+切换 UX。

后端:
- 新表 Space{tenant_id,name,kind,creator,archived} + SpaceMember{space_id,user_id,role}
  (如 Tenant 般不 isTenantScoped);User.ActiveSpaceID;Agent 作用域 owner→space_id,
  owner 降级为创建人(供 UI 显示 / 删他人鉴权)
- store/space.go:个人空间幂等/活跃空间解析/切换/列表/建/成员CRUD/归档
- 迁移顺序坑:结构体只放非唯一 index,MigrateAgentSpaces 回填 space_id 后再建唯一
  索引 idx_agent_sn + DROP 旧 idx_agent_on(否则存量空 space_id 撞车);启动序4步幂等
- 中间件 SpaceContext(注入 space_id) + RequireSpaceRole(照 RequireTenantRole)
- handler/space.go 空间端点 + 路由;agent.go 改空间作用域(删/覆盖他人需 admin)
- 计费零改动(Space 与 ResolveBillingTenantID 正交)

桌面端:
- api.ts space 接口;顶栏 SpaceSwitcher(含新建项目空间);StudioView 随空间切换
  重拉编排 + viewer 禁保存;Agent 列表显示创建人 + 按 mine 控删除

验证:中间件6门控单测 + DB迁移(13个人空间/9 Agent全re-key/索引换新) + 后端HTTP全
场景(member见他人编排/删他人403、viewer存403、非成员切空间400+隔离、owner删他人200)
+ 浏览器实机(切换器3空间/Studio空间编排随切换隔离刷新/创建人显示/console无错)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 10:51:33 +08:00

9.9 KiB
Raw Permalink Blame History

sundynix-agentix · 共享工作区设计(Space 抽象)

版本:v1 日期:2026-07-13 定位:SAAS_DESIGN.md P1 增量3(资源共享 / 共享工作区) 的落地设计。 前提(已确认):核心要灵活、不写死——不做"整租户一刀切共享",要支持"针对某项目临时组队贡献"。 基线事实:KB/Agent/Doc 已带 tenant_id 且受 gorm 租户插件过滤(见 store/tenant_scope.go), 但仍按 owner(userID) 锁到个人;存储作用域键是 owner/name。 相关:SAAS_DESIGN.md(多租户脊柱)、RBAC 落地(store.RoleRank + middleware.RequireTenantRole)。


0. 一句话设计

资源不再直接绑 usertenant,而是绑一个中间层「Space(工作区/项目空间)」——一个可大可小、可临时可长期的协作容器。 个人私有、项目临时组队、整租户共享三种场景,全从同一个 Space 模型里长出来。作用域键从 owner/name 换成 space_id/name 就这一处把"锁"从人解到空间,彻底不写死。


1. 为什么不能"整租户一刀切"

用户的核心诉求:灵活。评估过的两个"简单"方案都不行:

方案 问题 结论
A. 整租户统一共享(资源 owner→tenant 只有"全私有 / 全租户"两态,没有中间态;一个用户无法"私有一批 + 项目X共享一批 + 项目Y共享一批" 死板,且将来想灵活要再迁一次
B. 每资源加 shared 标记 + ACL 无天然分组,"一队人围着一个项目协作这 5 个库"很难表达;per-resource ACL 迅速失控
C. Space 中间容器(本方案) 一个抽象覆盖三种场景;匹配"一队人围着一个项目贡献"的心智;一次迁到正确容器、永不返工 采纳

这是成熟产品的通用解法:Notion teamspace、Slack channel、Linear team、GitHub repo+协作者。


2. 核心抽象:租户 > Space > 成员

层级Tenant(计费/隔离边界) → Space(共享容器) → SpaceMember(谁在里面)。 资源(KB/Agent/Doc/Run)挂在 Space 上,不再挂在 user 上。

三种场景 → 一个模型

场景 Space 表达 kind
个人私有(= 现状) 每人自动一个个人 Space(1 成员 = 自己),新资源默认落这里 personal
针对项目临时组队贡献 任意成员建个 Space,拉租户里几个人进来,共建 KB/编排,完事归档 project
整租户共享("开关" 一个成员 = 全租户的**"全员 Space"**(开关开 = 自动建它、自动纳入全部成员) tenant

"临时"是免费的:临时团队 = 建 project Space → 拉人 → 贡献 → 归档(软删,可选自动过期)。临时只是生命周期属性,不是另一套机制。 grain 提醒:共享绑 Space(项目),不要绑单次 task(task 是一次执行、跑完即逝);一个 Space 可关联多次 task 运行。


3. 现状盘点(改造起点,已核实)

三层"锁到个人",尽管 tenant_id 已存在:

现状(file 参考) 改造
① 查询显式 WHERE owner=? ListKB/ListAgents/ListVault/GetDocByID…store/model.gostore/tenant.go)在插件的 tenant 过滤之上又按 userID 过滤 去掉 owner 过滤,改按 space_idowner 降级为"创建人"归属
② 唯一索引 (owner,name) KB.idx_kb_owner_name / Agent.idx_agent_on / Doc.idx_doc_oknstore/model.go (space_id,name)——名字按空间唯一
③ 存储作用域键 owner/name scopedKB = userID+"/"+namehandler/kb.go:38),作 mcp-go 工具 kb 参数 space_id+"/"+name

关键有利事实(决定迁移不重):

  • Milvus 用标量字段 kb(VarChar) 过滤kb == "owner/name"rag/milvus.go:112/207),不是物理分区 → re-key = 改字段值 / 从 MinIO 原文重灌,不用重建分区。
  • MinIO 对象键不透明owner/kb/doc,存进 Doc.ObjectKey,按 key 精确取,handler/kb.go:154)→ 不用迁:老对象照常可读,新对象走新键。
  • Agent 纯 PGGraph JSONstore/model.go ListAgents/SaveAgent/DeleteAgent,零 Milvus/Neo4j/MinIO 依赖)→ 可先在 Agent 上把整套 Space 打样,零存储风险
  • 偏好记忆(recall_user_memory/remember_user_fact)保持个人,不进 Space(个人偏好共享无意义)。

4. 数据模型

+2 张表

Space {
  id          snowflake
  tenant_id   string   // 归属租户(隔离/计费边界仍在租户)
  name        string
  kind        string   // personal / project / tenant
  creator     string   // user.id
  archived    bool     // 软归档(临时空间完事即归档)
}
// 唯一:(tenant_id, kind='personal', creator) 保证每人每租户仅一个个人空间

SpaceMember {
  space_id    string
  user_id     string
  role        string   // 复用租户角色语义:owner/admin/member/viewer(见 §5
}
// 唯一:(space_id, user_id)

资源改造KB / Agent / Doc / DocLink / Task-Run):

  • space_id stringindex);保留 tenant_id(防御纵深 + 跨租户查询仍被插件挡);保留 owner创建人归属
  • 唯一索引 (owner,name)(space_id,name)
  • 作用域键 owner/namespace_id/name

:租户隔离仍靠 tenant_idSpace 属租户,space.tenant_id 兜底);主作用域切到 space_id


5. 权限(接已落地的 RBAC

每个 Space 有自己的成员角色,直接复用 store.RoleRank 阶梯(owner>admin>member>viewer/billing_admin):

动作 viewer member admin/owner
看 / 搜索空间内资源
建 KB/Agent、入库、改自己建的
改 / 删别人建的
管理空间成员(拉人/改角色/移除)
  • 新增中间件 RequireSpaceRole(db, minRole)(照 RequireTenantRole 套路,从 ctx 取 space_id + user 判 SpaceMember.role)。
  • 这才让 owner/admin/member/viewer 五个角色真正有区别——RBAC 落地时留的"架子"在此点亮。

6. 迁移方案

一次性迁到正确容器(个人空间),个人租户单成员无冲突

  1. 建个人空间:给每个用户在其每个所属租户建一个 personal Space(幂等,启动回填)。
  2. 资源 re-key:存量 KB/Agent/Doc/DocLink 的 space_id 回填为其 owner 在对应租户的个人空间 id;唯一索引 (owner,name)(space_id,name)AutoMigrate 不删旧索引,需显式 migration)。
  3. 存储层
    • Milvuskb 字段 owner/namespace_id/name(从 MinIO 原文重灌,原文安全兜底)。
    • Neo4jMATCH (n) WHERE n.kb STARTS WITH 'owner/' SET n.kb = ... 批量 Cypher。
    • MinIO不动(不透明键)。
  4. 命名冲突(space_id,name) 上唯一约束前,多成员租户里两人同名资源需先改名(个人空间单成员天然无冲突,只有后续"移进共享空间"时才判重)。

"把个人资源共享进项目"= 一等操作:改资源 space_id + re-key 存储 kb 字段(单资源,非全量)→ 天然支持"默认私有 + 显式共享"。


7. 分层落地(先 Agent 打样,再 KB)

阶段 3a:Agent 编排共享 —— 纯 PG,轻,先行 推荐起点

  • Space/SpaceMember 表 + Agent.space_id;个人空间自动建 + 存量 Agent re-key。
  • ListAgents/SaveAgent/DeleteAgent 去 owner 过滤、改按 space_id;唯一 (space_id,name)owner 存创建人。
  • 空间管理接口:建 project 空间 / 拉人(邮箱,复用 AddMemberByEmail 思路)/ 改角色 / 移除 / 归档。
  • RequireSpaceRole 门控 + 前端空间切换器(顶栏,类似租户切换器)+ StudioView 按空间角色 readOnly。
  • 一周内端到端验证协作 + 权限 + 切换 UX,零存储风险。

阶段 3b:KB 知识库共享 —— 存储 re-key,随后

  • 把同一套 space_id 作用域推到 KB/Doc/DocLink + scopedKBspace_id/name
  • Milvus 重灌 / Neo4j 批量 SET / MinIO 不迁 / 存量数据迁移 + 命名冲突处理(§6)。
  • 模式已被 3a 验证,风险最低。

8. 计费衔接(无新工作)

  • 运行共享 Agent / 搜共享 KB 的计费目标不变:仍由 ResolveBillingTenantID(user, activeTenant) 按活跃租户 + shared_billing 决定记谁的池子(SAAS_DESIGN.md 增量2)。
  • Space 只管"看得见/改得动谁的资源",不改"谁付钱"——两个维度正交,互不干扰。

9. 开放问题(待定,别提前写死)

议题 倾向 说明
默认落哪 个人空间 新资源默认私有,要共享才移/复制进项目空间
删别人建的 admin/owner 才行 member 只能删自己建的(§5
Space 嵌套 v1 不做 扁平:租户下平铺 Space;有需求再说
临时空间自动过期 v1 手动归档 先给 archived + 手动归档;自动过期(TTL)后置
跨租户 Space 不做 Space 严格属单租户,隔离边界不破
个人偏好记忆 保持个人 不进 Space

10. 明确不做(防过度设计)

  • 不做 per-resource ACL(用 Space 分组替代)。
  • v1 不做嵌套空间 / 跨租户空间 / 空间模板市场。
  • 不动 MinIO 对象键、不重建 Milvus 分区(现状标量字段 + 不透明键让迁移可控)。
  • 不碰计费目标解析(正交,复用增量2)。

附:一句话给未来的自己

Space 相对"整租户统一"的额外成本 ≈ 两张小表 + 空间成员管理 + 切换器 UI;最重的存储 re-key 两方案一样。 用对容器几乎免费,还省掉二次迁移。先拿纯 PG 的 Agent 打样跑通协作+权限+切换,再上 KB 存储 re-key。