addaa1b34f
引入 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>
108 lines
5.7 KiB
Go
108 lines
5.7 KiB
Go
package store
|
||
|
||
// 数据库映射模型。表名经 GORM NamingStrategy 统一加 sundynix_ 前缀 + 单数:
|
||
// User → sundynix_user,Task → sundynix_task。
|
||
// 约定:均嵌入 BaseModel(雪花字符串 id + created_at/updated_at + 软删 deleted_at);
|
||
// 建表/改表一律走 AutoMigrate,不手写 DDL。
|
||
|
||
// User 是平台用户(Users)。
|
||
type User struct {
|
||
BaseModel
|
||
Email string `gorm:"uniqueIndex;size:255"`
|
||
Name string `gorm:"size:64"`
|
||
PasswordHash string `gorm:"size:255" json:"-"` // bcrypt;绝不出 JSON
|
||
ActiveTenantID string `gorm:"size:64" json:"-"` // 当前活跃租户(多租户切换;空=用默认)
|
||
ActiveSpaceID string `gorm:"size:64" json:"-"` // 当前活跃工作区(Space)(增量3;空/失效=用活跃租户的个人空间)
|
||
}
|
||
|
||
// Task 是一次提交的 Agent 编排任务(DSL)。
|
||
// 业务 id(task_xxx,用于 NATS subject/stream)单列 TaskID,主键统一雪花。
|
||
type Task struct {
|
||
BaseModel
|
||
TenantID string `gorm:"size:64;index"` // 多租户作用域(tenant 插件按 ctx 自动填/过滤)
|
||
Owner string `gorm:"size:64;index"` // 提交者 user.id(个人工作台按此过滤"我的运行")
|
||
TaskID string `gorm:"uniqueIndex;size:64"` // task_xxx
|
||
Graph string `gorm:"type:jsonb"` // React Flow 导出的 DSL 原文
|
||
Status string `gorm:"size:32"` // submitted / running / done / failed / timeout
|
||
Detail string `gorm:"type:text"` // 失败/超时原因等(状态机回写)
|
||
// 收尾持久化:供「运行历史复盘」永久回放(Redis 流仅 10min TTL,过期后历史任务靠这两列)。
|
||
Output string `gorm:"type:text"` // 最终模型输出(收尾时由网关从流快照落库)
|
||
Trace string `gorm:"type:text"` // 执行轨迹事件 JSON 数组(存为文本,容忍空串;不在库内查它)
|
||
}
|
||
|
||
func (Task) isTenantScoped() {}
|
||
|
||
// Eval 是一次任务的自动化评测结果(dispatcher 评完经 NATS 回写,每任务一条,按 task_id upsert)。
|
||
type Eval struct {
|
||
BaseModel
|
||
TenantID string `gorm:"size:64;index"` // 多租户作用域(SaveEval 从对应 task 复制)
|
||
Owner string `gorm:"size:64;index"` // 提交者 user.id(从对应 task 复制)
|
||
TaskID string `gorm:"uniqueIndex;size:64"`
|
||
Overall float64 // 综合分 [0,1]
|
||
Rule float64 // 规则分
|
||
LLM float64 // LLM 质量分
|
||
Faithful float64 // RAG 忠实度分(0=无来源未评)
|
||
Level string `gorm:"size:16"` // ok / warn / poor
|
||
Flags string `gorm:"type:text"` // 命中问题(JSON 数组字符串)
|
||
Reason string `gorm:"type:text"` // 评语
|
||
Sources int // 检索来源数
|
||
Corrected bool // 是否经低分自动纠偏重生成后采纳(恒温器闭环)
|
||
}
|
||
|
||
func (Eval) TableName() string { return "sundynix_eval" }
|
||
func (Eval) isTenantScoped() {}
|
||
|
||
// AuditLog 是一条敏感操作留痕(改模型/密钥/激活 prompt/审批 等)。
|
||
// 由 Audit 中间件在请求收尾时 best-effort 写入;只增不改,供运维溯源。
|
||
type AuditLog struct {
|
||
BaseModel
|
||
Actor string `gorm:"size:64;index"` // 操作者 uid(未登录/系统留空)
|
||
Action string `gorm:"size:8"` // HTTP 方法:POST / PUT / DELETE
|
||
Route string `gorm:"size:128"` // 路由模式,如 /api/v1/admin/models/:id
|
||
Path string `gorm:"size:256"` // 实际请求路径
|
||
Status int // HTTP 响应状态码
|
||
IP string `gorm:"size:64"`
|
||
Detail string `gorm:"type:text"` // 备注(可选,如目标名/关键参数)
|
||
}
|
||
|
||
func (AuditLog) TableName() string { return "sundynix_audit_log" }
|
||
|
||
// GuardrailEvent 是一次输入护栏命中(硬拦截 blocked / 灰区 suspect)。
|
||
// 由 Guardrail 中间件命中时 best-effort 写入;供安全溯源"谁触发多少次护栏"。
|
||
type GuardrailEvent struct {
|
||
BaseModel
|
||
Actor string `gorm:"size:64;index"` // 操作者 uid(未登录留空)
|
||
Kind string `gorm:"size:16;index"` // blocked(硬拦)/ suspect(灰区放行)
|
||
Reason string `gorm:"size:256"` // 拦截原因(blocked)
|
||
Signals string `gorm:"type:text"` // 命中软信号 JSON 数组(suspect)
|
||
Method string `gorm:"size:8"`
|
||
Path string `gorm:"size:256"`
|
||
IP string `gorm:"size:64"`
|
||
}
|
||
|
||
func (GuardrailEvent) TableName() string { return "sundynix_guardrail_event" }
|
||
|
||
// Tenant 是多租户的计费/隔离单位(组织/账户)。个人用户 = 一个单人默认租户;团队/企业 = 多成员。
|
||
type Tenant struct {
|
||
BaseModel
|
||
Name string `gorm:"size:128"`
|
||
Slug string `gorm:"size:64;uniqueIndex"` // 唯一短标识(默认租户用 default-<uid>)
|
||
Plan string `gorm:"size:32;default:free"` // free / pro / enterprise
|
||
Status string `gorm:"size:16;default:active"` // active / suspended
|
||
CreditBalanceMicro int64 `gorm:"column:credit_balance_micro"` // 物化积分余额 ×10⁻⁶(= credit_ledger 之和;用量扣、充值增)
|
||
SharedBilling bool `gorm:"column:shared_billing"` // 共享计费:开=成员消耗计本租户池;关=成员计各自个人池(owner 恒计本租户)
|
||
}
|
||
|
||
func (Tenant) TableName() string { return "sundynix_tenant" }
|
||
|
||
// TenantMember 是用户与租户的成员关系(带角色)。一个用户可属多个租户。
|
||
type TenantMember struct {
|
||
BaseModel
|
||
TenantID string `gorm:"size:64;uniqueIndex:idx_tenant_user;index"`
|
||
UserID string `gorm:"size:64;uniqueIndex:idx_tenant_user;index"`
|
||
Role string `gorm:"size:16"` // owner / admin / member / viewer / billing_admin
|
||
Status string `gorm:"size:16;default:active"` // active / invited / removed
|
||
}
|
||
|
||
func (TenantMember) TableName() string { return "sundynix_tenant_member" }
|