24.08-团队空间

要点

  • 团队空间(Workspace)是租户内部的协作单元,用于组织用户和共享资源
  • 一个租户可以有多个团队空间,每个空间有独立的成员和权限
  • 空间内的资源(知识库、Prompt、对话历史)可以设置共享范围
  • 空间隔离不影响租户级别的配额和计费

内容

1. 团队空间的概念

团队空间解决的核心问题是:在一个租户内,如何组织不同团队或项目的资源?

典型场景:

  • 产品团队:产品经理、设计师、开发共用一个空间,共享产品相关的知识库
  • 项目空间:不同项目使用不同空间,避免知识库和 Prompt 混淆
  • 权限隔离:实习生只能访问特定空间,不能看到核心业务数据

团队空间和租户的关系:

租户 (Tenant)
├── 空间 A (Workspace A)
│   ├── 成员:Alice, Bob
│   ├── 知识库:产品文档
│   └── Prompt:客服模板
├── 空间 B (Workspace B)
│   ├── 成员:Charlie, David
│   ├── 知识库:技术文档
│   └── Prompt:代码审查模板
└── 默认空间 (Default)
    ├── 成员:所有租户成员
    └── 全局配置

2. 数据模型

-- 团队空间表
CREATE TABLE workspaces (
  id TEXT PRIMARY KEY,
  tenant_id TEXT NOT NULL,
  name TEXT NOT NULL,
  description TEXT,
  is_default INTEGER DEFAULT 0,
  created_at INTEGER NOT NULL,
  updated_at INTEGER NOT NULL,
  FOREIGN KEY (tenant_id) REFERENCES tenants(id)
)
 
-- 空间成员表
CREATE TABLE workspace_members (
  id TEXT PRIMARY KEY,
  workspace_id TEXT NOT NULL,
  user_id TEXT NOT NULL,
  role TEXT NOT NULL DEFAULT 'member',  -- owner/admin/member/viewer
  joined_at INTEGER NOT NULL,
  FOREIGN KEY (workspace_id) REFERENCES workspaces(id),
  FOREIGN KEY (user_id) REFERENCES users(id),
  UNIQUE(workspace_id, user_id)
)
 
-- 资源与空间的关联表
CREATE TABLE workspace_resources (
  id TEXT PRIMARY KEY,
  workspace_id TEXT NOT NULL,
  resource_type TEXT NOT NULL,      -- knowledge_base/prompt/conversation
  resource_id TEXT NOT NULL,
  added_at INTEGER NOT NULL,
  FOREIGN KEY (workspace_id) REFERENCES workspaces(id),
  UNIQUE(workspace_id, resource_type, resource_id)
)

TypeScript 类型:

// src/types/workspace.ts
export interface Workspace {
  id: string
  tenantId: string
  name: string
  description?: string
  isDefault: boolean
  createdAt: number
  updatedAt: number
}
 
export interface WorkspaceMember {
  id: string
  workspaceId: string
  userId: string
  role: WorkspaceRole
  joinedAt: number
  user?: {
    id: string
    email: string
    name?: string
  }
}
 
export type WorkspaceRole = 'owner' | 'admin' | 'member' | 'viewer'
 
export interface WorkspaceResource {
  id: string
  workspaceId: string
  resourceType: 'knowledge_base' | 'prompt' | 'conversation'
  resourceId: string
  addedAt: number
}

3. 空间管理

3.1 创建空间

// src/routes/workspaces.ts
import { Hono } from 'hono'
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
import { generateId } from '../lib/utils'
import { requireRole } from '../middleware/permission'
 
const app = new Hono()
 
// 创建空间
app.post('/', requireRole('admin'), zValidator('json', z.object({
  name: z.string().min(1).max(100),
  description: z.string().optional(),
  memberIds: z.array(z.string()).optional(),
})), async (c) => {
  const tenantId = c.get('tenantId')
  const userId = c.get('userId')
  const { name, description, memberIds } = c.req.valid('json')
 
  // 检查空间数量限制
  const plan = c.get('plan')
  const workspaceCount = await c.env.DB.prepare(`
    SELECT COUNT(*) as count FROM workspaces WHERE tenant_id = ?
  `).bind(tenantId).first()
 
  if (plan.maxWorkspaces > 0 && workspaceCount.count >= plan.maxWorkspaces) {
    return c.json({ error: '空间数量已达上限' }, 403)
  }
 
  const workspaceId = generateId()
  const now = Date.now()
 
  // 创建空间
  await c.env.DB.prepare(`
    INSERT INTO workspaces (id, tenant_id, name, description, is_default, created_at, updated_at)
    VALUES (?, ?, ?, ?, 0, ?, ?)
  `).bind(workspaceId, tenantId, name, description || null, now, now).run()
 
  // 添加创建者为 owner
  await c.env.DB.prepare(`
    INSERT INTO workspace_members (id, workspace_id, user_id, role, joined_at)
    VALUES (?, ?, ?, 'owner', ?)
  `).bind(generateId(), workspaceId, userId, now).run()
 
  // 添加其他成员
  if (memberIds?.length) {
    const memberValues = memberIds.map(id => ({
      id: generateId(),
      workspaceId,
      userId: id,
      role: 'member',
      joinedAt: now,
    }))
 
    for (const member of memberValues) {
      await c.env.DB.prepare(`
        INSERT INTO workspace_members (id, workspace_id, user_id, role, joined_at)
        VALUES (?, ?, ?, ?, ?)
      `).bind(member.id, member.workspaceId, member.userId, member.role, member.joinedAt).run()
    }
  }
 
  return c.json({
    id: workspaceId,
    name,
    description,
  }, 201)
})

3.2 获取空间列表

// 获取当前用户可访问的空间
app.get('/', async (c) => {
  const tenantId = c.get('tenantId')
  const userId = c.get('userId')
 
  const workspaces = await c.env.DB.prepare(`
    SELECT w.*, wm.role as myRole
    FROM workspaces w
    JOIN workspace_members wm ON w.id = wm.workspace_id
    WHERE w.tenant_id = ? AND wm.user_id = ?
    ORDER BY wm.role = 'owner' DESC, w.created_at DESC
  `).bind(tenantId, userId).all()
 
  // 获取每个空间的成员数
  const workspacesWithCounts = await Promise.all(
    workspaces.results.map(async (ws) => {
      const memberCount = await c.env.DB.prepare(`
        SELECT COUNT(*) as count FROM workspace_members WHERE workspace_id = ?
      `).bind(ws.id).first()
 
      return {
        ...ws,
        isDefault: ws.is_default === 1,
        memberCount: memberCount.count,
      }
    })
  )
 
  return c.json({ workspaces: workspacesWithCounts })
})
 
// 获取单个空间详情
app.get('/:id', async (c) => {
  const workspaceId = c.req.param('id')
  const userId = c.get('userId')
 
  // 验证用户有权限访问
  const membership = await c.env.DB.prepare(`
    SELECT role FROM workspace_members
    WHERE workspace_id = ? AND user_id = ?
  `).bind(workspaceId, userId).first()
 
  if (!membership) {
    return c.json({ error: '没有权限访问该空间' }, 403)
  }
 
  const workspace = await c.env.DB.prepare(`
    SELECT * FROM workspaces WHERE id = ?
  `).bind(workspaceId).first()
 
  // 获取成员列表
  const members = await c.env.DB.prepare(`
    SELECT wm.*, u.email, u.name
    FROM workspace_members wm
    JOIN users u ON wm.user_id = u.id
    WHERE wm.workspace_id = ?
    ORDER BY wm.role = 'owner' DESC, wm.joined_at ASC
  `).bind(workspaceId).all()
 
  return c.json({
    workspace: {
      ...workspace,
      isDefault: workspace.is_default === 1,
    },
    members: members.results.map(m => ({
      id: m.id,
      userId: m.user_id,
      role: m.role,
      joinedAt: m.joined_at,
      user: {
        id: m.user_id,
        email: m.email,
        name: m.name,
      },
    })),
    myRole: membership.role,
  })
})

3.3 更新空间

app.put('/:id', zValidator('json', z.object({
  name: z.string().min(1).max(100).optional(),
  description: z.string().optional(),
})), async (c) => {
  const workspaceId = c.req.param('id')
  const userId = c.get('userId')
  const updates = c.req.valid('json')
 
  // 验证权限(只有 owner 和 admin 可以修改)
  const membership = await c.env.DB.prepare(`
    SELECT role FROM workspace_members
    WHERE workspace_id = ? AND user_id = ?
  `).bind(workspaceId, userId).first()
 
  if (!membership || !['owner', 'admin'].includes(membership.role)) {
    return c.json({ error: '没有权限修改该空间' }, 403)
  }
 
  const setClauses = []
  const values: any[] = []
 
  if (updates.name) {
    setClauses.push('name = ?')
    values.push(updates.name)
  }
 
  if (updates.description !== undefined) {
    setClauses.push('description = ?')
    values.push(updates.description)
  }
 
  if (setClauses.length === 0) {
    return c.json({ error: '没有要更新的字段' }, 400)
  }
 
  setClauses.push('updated_at = ?')
  values.push(Date.now())
  values.push(workspaceId)
 
  await c.env.DB.prepare(`
    UPDATE workspaces SET ${setClauses.join(', ')} WHERE id = ?
  `).bind(...values).run()
 
  return c.json({ success: true })
})

3.4 删除空间

app.delete('/:id', async (c) => {
  const workspaceId = c.req.param('id')
  const userId = c.get('userId')
 
  // 验证权限(只有 owner 可以删除)
  const membership = await c.env.DB.prepare(`
    SELECT role FROM workspace_members
    WHERE workspace_id = ? AND user_id = ?
  `).bind(workspaceId, userId).first()
 
  if (!membership || membership.role !== 'owner') {
    return c.json({ error: '只有创建者可以删除空间' }, 403)
  }
 
  // 检查是否是默认空间
  const workspace = await c.env.DB.prepare(`
    SELECT is_default FROM workspaces WHERE id = ?
  `).bind(workspaceId).first()
 
  if (workspace.is_default === 1) {
    return c.json({ error: '不能删除默认空间' }, 400)
  }
 
  // 删除空间及其关联
  await c.env.DB.batch([
    c.env.DB.prepare(`DELETE FROM workspace_resources WHERE workspace_id = ?`).bind(workspaceId),
    c.env.DB.prepare(`DELETE FROM workspace_members WHERE workspace_id = ?`).bind(workspaceId),
    c.env.DB.prepare(`DELETE FROM workspaces WHERE id = ?`).bind(workspaceId),
  ])
 
  return c.json({ success: true })
})

4. 成员管理

// 添加成员
app.post('/:id/members', zValidator('json', z.object({
  userId: z.string(),
  role: z.enum(['admin', 'member', 'viewer']).optional(),
})), async (c) => {
  const workspaceId = c.req.param('id')
  const currentUserId = c.get('userId')
  const { userId, role = 'member' } = c.req.valid('json')
 
  // 验证权限
  const membership = await c.env.DB.prepare(`
    SELECT role FROM workspace_members
    WHERE workspace_id = ? AND user_id = ?
  `).bind(workspaceId, currentUserId).first()
 
  if (!membership || !['owner', 'admin'].includes(membership.role)) {
    return c.json({ error: '没有权限添加成员' }, 403)
  }
 
  // 检查用户是否已在空间中
  const existing = await c.env.DB.prepare(`
    SELECT id FROM workspace_members WHERE workspace_id = ? AND user_id = ?
  `).bind(workspaceId, userId).first()
 
  if (existing) {
    return c.json({ error: '用户已在空间中' }, 409)
  }
 
  // 验证用户属于同一租户
  const workspace = await c.env.DB.prepare(`
    SELECT tenant_id FROM workspaces WHERE id = ?
  `).bind(workspaceId).first()
 
  const user = await c.env.DB.prepare(`
    SELECT id FROM users WHERE id = ? AND tenant_id = ?
  `).bind(userId, workspace.tenant_id).first()
 
  if (!user) {
    return c.json({ error: '用户不存在' }, 404)
  }
 
  const memberId = generateId()
  const now = Date.now()
 
  await c.env.DB.prepare(`
    INSERT INTO workspace_members (id, workspace_id, user_id, role, joined_at)
    VALUES (?, ?, ?, ?, ?)
  `).bind(memberId, workspaceId, userId, role, now).run()
 
  return c.json({ success: true, memberId })
})
 
// 移除成员
app.delete('/:id/members/:memberId', async (c) => {
  const workspaceId = c.req.param('id')
  const memberId = c.req.param('memberId')
  const currentUserId = c.get('userId')
 
  // 验证权限
  const membership = await c.env.DB.prepare(`
    SELECT role FROM workspace_members
    WHERE workspace_id = ? AND user_id = ?
  `).bind(workspaceId, currentUserId).first()
 
  if (!membership || !['owner', 'admin'].includes(membership.role)) {
    return c.json({ error: '没有权限移除成员' }, 403)
  }
 
  // 不能移除 owner
  const targetMember = await c.env.DB.prepare(`
    SELECT * FROM workspace_members WHERE id = ? AND workspace_id = ?
  `).bind(memberId, workspaceId).first()
 
  if (!targetMember) {
    return c.json({ error: '成员不存在' }, 404)
  }
 
  if (targetMember.role === 'owner') {
    return c.json({ error: '不能移除创建者' }, 400)
  }
 
  await c.env.DB.prepare(`
    DELETE FROM workspace_members WHERE id = ? AND workspace_id = ?
  `).bind(memberId, workspaceId).run()
 
  return c.json({ success: true })
})
 
// 更新成员角色
app.put('/:id/members/:memberId', zValidator('json', z.object({
  role: z.enum(['admin', 'member', 'viewer']),
})), async (c) => {
  const workspaceId = c.req.param('id')
  const memberId = c.req.param('memberId')
  const currentUserId = c.get('userId')
  const { role } = c.req.valid('json')
 
  // 验证权限(只有 owner 可以修改角色)
  const membership = await c.env.DB.prepare(`
    SELECT role FROM workspace_members
    WHERE workspace_id = ? AND user_id = ?
  `).bind(workspaceId, currentUserId).first()
 
  if (!membership || membership.role !== 'owner') {
    return c.json({ error: '只有创建者可以修改角色' }, 403)
  }
 
  // 不能修改 owner 的角色
  const targetMember = await c.env.DB.prepare(`
    SELECT role FROM workspace_members WHERE id = ? AND workspace_id = ?
  `).bind(memberId, workspaceId).first()
 
  if (targetMember.role === 'owner') {
    return c.json({ error: '不能修改创建者角色' }, 400)
  }
 
  await c.env.DB.prepare(`
    UPDATE workspace_members SET role = ? WHERE id = ?
  `).bind(role, memberId).run()
 
  return c.json({ success: true })
})

5. 资源管理

5.1 添加资源到空间

// 添加知识库到空间
app.post('/:id/resources', zValidator('json', z.object({
  resourceType: z.enum(['knowledge_base', 'prompt', 'conversation']),
  resourceId: z.string(),
})), async (c) => {
  const workspaceId = c.req.param('id')
  const userId = c.get('userId')
  const { resourceType, resourceId } = c.req.valid('json')
 
  // 验证权限
  const membership = await c.env.DB.prepare(`
    SELECT role FROM workspace_members
    WHERE workspace_id = ? AND user_id = ?
  `).bind(workspaceId, userId).first()
 
  if (!membership || !['owner', 'admin', 'member'].includes(membership.role)) {
    return c.json({ error: '没有权限添加资源' }, 403)
  }
 
  // 检查资源是否已在空间中
  const existing = await c.env.DB.prepare(`
    SELECT id FROM workspace_resources
    WHERE workspace_id = ? AND resource_type = ? AND resource_id = ?
  `).bind(workspaceId, resourceType, resourceId).first()
 
  if (existing) {
    return c.json({ error: '资源已在空间中' }, 409)
  }
 
  const id = generateId()
  const now = Date.now()
 
  await c.env.DB.prepare(`
    INSERT INTO workspace_resources (id, workspace_id, resource_type, resource_id, added_at)
    VALUES (?, ?, ?, ?, ?)
  `).bind(id, workspaceId, resourceType, resourceId, now).run()
 
  return c.json({ success: true, id })
})
 
// 获取空间内的资源
app.get('/:id/resources', async (c) => {
  const workspaceId = c.req.param('id')
  const userId = c.get('userId')
  const resourceType = c.req.query('type')
 
  // 验证用户有权限访问
  const membership = await c.env.DB.prepare(`
    SELECT id FROM workspace_members
    WHERE workspace_id = ? AND user_id = ?
  `).bind(workspaceId, userId).first()
 
  if (!membership) {
    return c.json({ error: '没有权限访问该空间' }, 403)
  }
 
  let query = `
    SELECT * FROM workspace_resources
    WHERE workspace_id = ?
  `
  const params: any[] = [workspaceId]
 
  if (resourceType) {
    query += ` AND resource_type = ?`
    params.push(resourceType)
  }
 
  query += ` ORDER BY added_at DESC`
 
  const resources = await c.env.DB.prepare(query).bind(...params).all()
 
  return c.json({ resources: resources.results })
})
 
// 从空间移除资源
app.delete('/:id/resources/:resourceId', async (c) => {
  const workspaceId = c.req.param('id')
  const resourceId = c.req.param('resourceId')
  const userId = c.get('userId')
 
  // 验证权限
  const membership = await c.env.DB.prepare(`
    SELECT role FROM workspace_members
    WHERE workspace_id = ? AND user_id = ?
  `).bind(workspaceId, userId).first()
 
  if (!membership || !['owner', 'admin'].includes(membership.role)) {
    return c.json({ error: '没有权限移除资源' }, 403)
  }
 
  await c.env.DB.prepare(`
    DELETE FROM workspace_resources
    WHERE workspace_id = ? AND resource_id = ?
  `).bind(workspaceId, resourceId).run()
 
  return c.json({ success: true })
})

6. 初始化默认空间

租户注册时,自动创建一个默认空间:

// src/lib/workspace.ts
export async function createDefaultWorkspace(
  tenantId: string,
  adminUserId: string,
  env: Env
) {
  const workspaceId = generateId()
  const now = Date.now()
 
  await env.DB.batch([
    env.DB.prepare(`
      INSERT INTO workspaces (id, tenant_id, name, description, is_default, created_at, updated_at)
      VALUES (?, ?, '默认空间', '所有成员自动加入', 1, ?, ?)
    `).bind(workspaceId, tenantId, now, now),
    env.DB.prepare(`
      INSERT INTO workspace_members (id, workspace_id, user_id, role, joined_at)
      VALUES (?, ?, ?, 'owner', ?)
    `).bind(generateId(), workspaceId, adminUserId, now),
  ])
 
  return workspaceId
}

7. 小结

团队空间的关键点:

  1. 空间概念:租户内部的协作单元,用于组织用户和共享资源
  2. 角色权限:owner/admin/member/viewer 四级角色
  3. 资源关联:知识库、Prompt、对话可以添加到空间
  4. 隔离机制:空间内资源对非成员不可见
  5. 默认空间:租户注册时自动创建,所有成员自动加入

一句话带走:

团队空间是租户内的协作容器。空间隔离资源,角色控制权限,默认空间兜底。空间不影响租户级配额,只影响资源可见性。