Files
sundynix-agentix/SPACE_DESIGN.md
T
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

183 lines
9.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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. 一句话设计
**资源不再直接绑 `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.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 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. 迁移方案
**一次性迁到正确容器(个人空间),个人租户单成员无冲突**
1. **建个人空间**:给每个用户在其每个所属租户建一个 `personal` Space(幂等,启动回填)。
2. **资源 re-key**:存量 KB/Agent/Doc/DocLink 的 `space_id` 回填为其 owner 在**对应租户**的个人空间 id;唯一索引 `(owner,name)``(space_id,name)`AutoMigrate 不删旧索引,需显式 migration)。
3. **存储层**
- Milvus`kb` 字段 `owner/name``space_id/name`(从 MinIO 原文重灌,原文安全兜底)。
- Neo4j`MATCH (n) WHERE n.kb STARTS WITH 'owner/' SET n.kb = ...` 批量 Cypher。
- MinIO:**不动**(不透明键)。
4. **命名冲突**`(space_id,name)` 上唯一约束前,多成员租户里两人同名资源需先改名(个人空间单成员天然无冲突,只有后续"移进共享空间"时才判重)。
**"把个人资源共享进项目"= 一等操作**:改资源 `space_id` + re-key 存储 `kb` 字段(单资源,非全量)→ 天然支持"默认私有 + 显式共享"。
---
## 7. 分层落地(先 Agent 打样,再 KB)
### 阶段 3aAgent 编排共享 —— 纯 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。**