引入 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>
9.9 KiB
sundynix-agentix · 共享工作区设计(Space 抽象)
版本:v1 | 日期:2026-07-13 定位:
SAAS_DESIGN.mdP1 增量3(资源共享 / 共享工作区) 的落地设计。 前提(已确认):核心要灵活、不写死——不做"整租户一刀切共享",要支持"针对某项目临时组队贡献"。 基线事实:KB/Agent/Doc 已带tenant_id且受 gorm 租户插件过滤(见store/tenant_scope.go), 但仍按owner(userID) 锁到个人;存储作用域键是owner/name。 相关:SAAS_DESIGN.md(多租户脊柱)、RBAC 落地(store.RoleRank+middleware.RequireTenantRole)。
0. 一句话设计
资源不再直接绑 user 或 tenant,而是绑一个中间层「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.go、store/tenant.go)在插件的 tenant 过滤之上又按 userID 过滤 |
去掉 owner 过滤,改按 space_id;owner 降级为"创建人"归属 |
② 唯一索引 (owner,name) |
KB.idx_kb_owner_name / Agent.idx_agent_on / Doc.idx_doc_okn(store/model.go) |
改 (space_id,name)——名字按空间唯一 |
③ 存储作用域键 owner/name |
scopedKB = userID+"/"+name(handler/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 纯 PG(Graph JSON,
store/model.goListAgents/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 string(index);保留tenant_id(防御纵深 + 跨租户查询仍被插件挡);保留owner作创建人归属。 - 唯一索引
(owner,name)→(space_id,name)。 - 作用域键
owner/name→space_id/name。
注:租户隔离仍靠 tenant_id(Space 属租户,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. 迁移方案
一次性迁到正确容器(个人空间),个人租户单成员无冲突:
- 建个人空间:给每个用户在其每个所属租户建一个
personalSpace(幂等,启动回填)。 - 资源 re-key:存量 KB/Agent/Doc/DocLink 的
space_id回填为其 owner 在对应租户的个人空间 id;唯一索引(owner,name)→(space_id,name)(AutoMigrate 不删旧索引,需显式 migration)。 - 存储层:
- Milvus:
kb字段owner/name→space_id/name(从 MinIO 原文重灌,原文安全兜底)。 - Neo4j:
MATCH (n) WHERE n.kb STARTS WITH 'owner/' SET n.kb = ...批量 Cypher。 - MinIO:不动(不透明键)。
- Milvus:
- 命名冲突:
(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 +scopedKB改space_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。