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. 小结
团队空间的关键点:
- 空间概念:租户内部的协作单元,用于组织用户和共享资源
- 角色权限:owner/admin/member/viewer 四级角色
- 资源关联:知识库、Prompt、对话可以添加到空间
- 隔离机制:空间内资源对非成员不可见
- 默认空间:租户注册时自动创建,所有成员自动加入
一句话带走:
团队空间是租户内的协作容器。空间隔离资源,角色控制权限,默认空间兜底。空间不影响租户级配额,只影响资源可见性。