# Room 模块设计文档 ## 1. 概述 `Room` 模块是本系统核心的协作与消息通信功能模块,提供稳定、高效、可扩展的实时消息平台。系统采用 Rust (Actix-web) 后端 + React 19 前端的技术栈,通过 WebSocket 实现实时通信,集成了 AI 代理进行智能协作。 ### 技术栈 - **后端**: Rust + Actix-web + SeaORM + Redis + NATS - **前端**: React 19 + TypeScript + Tailwind CSS - **实时通信**: WebSocket (自定义协议) - **消息队列**: NATS (事件分发) - **缓存**: Redis (序列号管理、去重) --- ## 2. 数据模型 ### 2.1 核心表结构 #### `room` - 房间表 | 字段 | 类型 | 说明 | |------|------|------| | id | UUID (v7) | 主键,时间排序 | | project | UUID | 所属项目 | | room_name | VARCHAR(128) | 房间名称 | | public | BOOLEAN | 是否公开房间 | | category | UUID (nullable) | 所属分类 | | created_by | UUID | 创建者 | | created_at | TIMESTAMPTZ | 创建时间 | | last_msg_at | TIMESTAMPTZ | 最后消息时间 | #### `room_message` - 消息表 | 字段 | 类型 | 说明 | |------|------|------| | id | UUID (v7) | 主键 | | seq | BIGINT | 全局序列号(递增) | | room | UUID | 所属房间 | | sender_type | ENUM | `member` / `ai` / `system` | | sender_id | UUID (nullable) | 发送者 ID | | thread | UUID (nullable) | 所属线程 | | in_reply_to | UUID (nullable) | 回复的消息 ID | | content | TEXT | 消息内容 | | content_type | ENUM | `text` / `markdown` / `code` / `mention` | | edited_at | TIMESTAMPTZ (nullable) | 编辑时间 | | send_at | TIMESTAMPTZ | 发送时间 | | revoked | TIMESTAMPTZ (nullable) | 撤回时间 | | revoked_by | UUID (nullable) | 撤回操作者 | #### `room_member` - 房间成员表 | 字段 | 类型 | 说明 | |------|------|------| | room | UUID | 房间 ID | | user | UUID | 用户 ID | | role | ENUM | `Owner` / `Admin` / `Member` | | first_msg_in | TIMESTAMPTZ (nullable) | 首次发消息时间 | | joined_at | TIMESTAMPTZ (nullable) | 加入时间 | | last_read_seq | BIGINT (nullable) | 已读序列号 | | do_not_disturb | BOOLEAN | 免打扰 | | dnd_start_hour | INT (nullable) | DND 开始小时 | | dnd_end_hour | INT (nullable) | DND 结束小时 | #### `room_category` - 频道分类表 | 字段 | 类型 | 说明 | |------|------|------| | id | UUID | 主键 | | project | UUID | 所属项目 | | name | VARCHAR(64) | 分类名称 | | position | INT | 排序位置 | #### `room_thread` - 线程表 | 字段 | 类型 | 说明 | |------|------|------| | id | UUID | 主键 | | room | UUID | 所属房间 | | parent_message | UUID | 父消息 ID | | created_by | UUID | 创建者 | | created_at | TIMESTAMPTZ | 创建时间 | #### `room_ai` - AI 配置表 | 字段 | 类型 | 说明 | |------|------|------| | room | UUID | 房间 ID | | model | UUID | AI 模型 ID | | version | VARCHAR (nullable) | 模型版本 | | call_count | INT | 调用次数 | | last_call_at | TIMESTAMPTZ (nullable) | 最后调用时间 | | history_limit | INT | 上下文历史限制 | | system_prompt | TEXT | 系统提示词 | | temperature | FLOAT | 温度参数 | | max_tokens | INT | 最大 token 数 | | use_exact | BOOLEAN | 精确模式 | | think | BOOLEAN | 思考模式 | | stream | BOOLEAN | 流式输出 | | min_score | FLOAT (nullable) | 最小分数阈值 | #### `room_message_reaction` - 消息反应表 | 字段 | 类型 | 说明 | |------|------|------| | id | UUID | 主键 | | message | UUID | 消息 ID | | user | UUID | 用户 ID | | emoji | VARCHAR(32) | emoji | | created_at | TIMESTAMPTZ | 创建时间 | #### `room_pin` - 置顶消息表 | 字段 | 类型 | 说明 | |------|------|------| | id | UUID | 主键 | | room | UUID | 房间 ID | | message | UUID | 消息 ID | | pinned_by | UUID | 置顶者 | | pinned_at | TIMESTAMPTZ | 置顶时间 | ### 2.2 消息内容类型 ```typescript type MessageContentType = 'text' | 'markdown' | 'code' | 'mention'; type MessageSenderType = 'member' | 'ai' | 'system'; ``` --- ## 3. WebSocket 通信协议 ### 3.1 协议格式 #### 请求 ```typescript interface WsRequest { type: 'request'; request_id: string; // 唯一请求 ID action: WsAction; // 操作类型 params?: WsRequestParams; } ``` #### 响应 ```typescript interface WsResponse { type: 'response'; request_id: string; action: string; data?: WsResponseData; error?: WsError; } ``` ### 3.2 Action 列表 #### 房间管理 | Action | 说明 | 权限 | |--------|------|------| | `room.list` | 获取房间列表 | Member | | `room.get` | 获取房间详情 | Member | | `room.create` | 创建房间 | Admin | | `room.update` | 更新房间 | Admin | | `room.delete` | 删除房间 | Admin | | `room.subscribe` | 订阅房间事件 | Member | | `room.unsubscribe` | 取消订阅 | Member | #### 消息管理 | Action | 说明 | 权限 | |--------|------|------| | `message.list` | 获取消息列表(分页) | Member | | `message.create` | 发送消息 | Member | | `message.update` | 编辑消息 | Owner | | `message.revoke` | 撤回消息 | Owner | | `message.get` | 获取单条消息 | Member | | `message.search` | 搜索消息 | Member | #### 成员管理 | Action | 说明 | 权限 | |--------|------|------| | `member.list` | 获取成员列表 | Member | | `member.add` | 添加成员 | Admin | | `member.remove` | 移除成员 | Admin | | `member.leave` | 离开房间 | Member | | `member.update_role` | 更新角色 | Admin | | `member.set_read_seq` | 设置已读位置 | Member | #### 分类管理 | Action | 说明 | 权限 | |--------|------|------| | `category.list` | 获取分类列表 | Member | | `category.create` | 创建分类 | Admin | | `category.update` | 更新分类 | Admin | | `category.delete` | 删除分类 | Admin | #### 线程管理 | Action | 说明 | 权限 | |--------|------|------| | `thread.list` | 获取线程列表 | Member | | `thread.create` | 创建线程 | Member | | `thread.messages` | 获取线程消息 | Member | #### 反应管理 | Action | 说明 | 权限 | |--------|------|------| | `reaction.add` | 添加反应 | Member | | `reaction.remove` | 移除反应 | Owner | | `reaction.list_batch` | 批量获取反应 | Member | #### 置顶管理 | Action | 说明 | 权限 | |--------|------|------| | `pin.list` | 获取置顶列表 | Member | | `pin.add` | 添加置顶 | Admin | | `pin.remove` | 移除置顶 | Admin | #### AI 管理 | Action | 说明 | 权限 | |--------|------|------| | `ai.list` | 获取 AI 配置列表 | Member | | `ai.upsert` | 创建/更新 AI 配置 | Admin | | `ai.delete` | 删除 AI 配置 | Admin | #### 通知管理 | Action | 说明 | 权限 | |--------|------|------| | `notification.list` | 获取通知列表 | Member | | `notification.mark_read` | 标记已读 | Member | | `notification.mark_all_read` | 全部标记已读 | Member | | `notification.archive` | 归档通知 | Member | #### 提及管理 | Action | 说明 | 权限 | |--------|------|------| | `mention.list` | 获取提及列表 | Member | | `mention.read_all` | 全部标记已读 | Member | ### 3.3 实时事件推送 ```typescript type WsEventType = | 'room.created' | 'room.updated' | 'room.deleted' | 'message.created' | 'message.updated' | 'message.revoked' | 'member.joined' | 'member.left' | 'member.role_changed' | 'thread.created' | 'reaction.updated' | 'pin.updated'; ``` --- ## 4. 后端架构 ### 4.1 目录结构 ``` libs/room/src/ ├── lib.rs # 模块入口 ├── service.rs # RoomService 主服务 ├── room.rs # 房间 CRUD ├── message.rs # 消息管理 ├── member.rs # 成员管理 ├── category.rs # 分类管理 ├── thread.rs # 线程管理 ├── reaction.rs # 反应管理 ├── pin.rs # 置顶管理 ├── ai.rs # AI 配置管理 ├── notification.rs # 通知管理 ├── search.rs # 搜索功能 ├── connection.rs # WebSocket 连接管理 ├── room_ai_queue.rs # AI 队列锁 ├── error.rs # 错误类型 ├── types.rs # 请求/响应类型 ├── helpers.rs # 辅助函数 ├── metrics.rs # 指标收集 ├── ws_context.rs # WebSocket 上下文 └── draft_and_history.rs # 草稿和编辑历史 ``` ### 4.2 核心服务 RoomService ```rust pub struct RoomService { db: DatabaseConnection, cache: AppCache, queue: NatsContext, ai_client: AiClient, rate_limiter: RateLimiter, log: Logger, } ``` ### 4.3 并发控制 - **物理并发限制**: `tokio::sync::Semaphore` - **消息序列号**: Redis INCR 原子递增 - **消息去重**: `DashMap>` 内存缓存 - **AI 队列锁**: Redis 分布式锁 (`ai:room:queue:lock:{room_id}`) ### 4.4 AI 队列机制 ```rust // Redis 键结构 ai:room:queue:{room_id} // 队列 ai:room:queue:seq:{room_id} // 序号 ai:room:queue:lock:{room_id} // 分布式锁 ai:room:queue:ticket:{room_id}:{ticket_id} // 票据 // 锁参数 LOCK_TTL_MS: 120_000 // 锁超时 2 分钟 TICKET_TTL_MS: 90_000 // 票据超时 1.5 分钟 MAX_BACKOFF_MS: 200 // 最大退避 200ms ``` ### 4.5 定时任务 - **空闲房间清理**: 30 天无消息的房间标记为归档 - **频率限制刷新**: 每分钟重置计数 - **陈旧指标清理**: 定期清理过期数据 --- ## 5. 前端架构 ### 5.1 目录结构 ``` src/ ├── components/room/ │ ├── DiscordServerSidebar.tsx # 服务器图标侧边栏 (72px) │ ├── DiscordChannelSidebar.tsx # 可折叠频道列表 │ ├── DiscordChatPanel.tsx # 主聊天面板 │ ├── DiscordMemberList.tsx # 成员列表 (带在线状态) │ ├── message/ │ │ ├── MessageList.tsx # 消息列表 │ │ ├── MessageInput.tsx # 消息输入框 │ │ ├── MessageBubble.tsx # 消息气泡 │ │ └── MessageActions.tsx # 消息操作菜单 │ ├── RoomThreadPanel.tsx # 线程侧边栏 │ ├── RoomMentionPanel.tsx # 提及面板 │ ├── RoomPinBar.tsx # 置顶栏 │ ├── RoomMessageSearch.tsx # 消息搜索 │ ├── RoomSettingsPanel.tsx # 设置面板 │ ├── RoomAiAuthBanner.tsx # AI 认证提示 │ ├── RoomAiTasksPanel.tsx # AI 任务面板 │ └── ... ├── contexts/ │ └── room-context.tsx # 房间状态管理 └── lib/ └── ws-protocol.ts # WebSocket 协议定义 ``` ### 5.2 React Context 状态管理 ```typescript interface RoomContextValue { // 房间状态 rooms: RoomResponse[]; currentRoom: RoomResponse | null; roomsLoading: boolean; // 消息状态 messages: MessageWithMeta[]; threads: RoomThreadResponse[]; // 成员状态 members: RoomMember[]; membersLoading: boolean; // AI 配置 roomAiConfigs: RoomAiResponse[]; // WebSocket 状态 wsStatus: 'open' | 'connecting' | 'disconnected'; wsError: Error | null; wsClient: WebSocketClient | null; // 操作方法 sendMessage: (content: string, inReplyTo?: string) => Promise; editMessage: (messageId: string, content: string) => Promise; revokeMessage: (messageId: string) => Promise; updateRoom: (roomId: string, data: Partial) => Promise; refreshThreads: () => Promise; } ``` ### 5.3 Discord 风格 UI 布局 ``` ┌─────────────────────────────────────────────────────────────────┐ │ [Icon] │ # channel-name [🔍][👤] │ ├──────────┼─────────────────────────────────────────┬────────────┤ │ │ ┌─────────────────────────────────┐ │ │ │ Category │ │ Message List │ │ Members │ │ ────────│ │ (with virtual scrolling) │ │ Online(3) │ │ # gen │ │ │ │ ● user1 │ │ # dev │ │ user1 [10:30] Hello! │ │ ● user2 │ │ # ai │ │ ↳ user2 [10:32] Hi! │ │ │ │ │ │ │ │ Offline(5)│ │ Category │ │ 🤖 AI [10:33] Thinking... │ │ ○ user3 │ │ ────────│ │ │ │ │ │ 🔒 priv │ └─────────────────────────────────┘ │ │ │ ├─────────────────────────────────────────┤ │ │ │ [+] Type a message... [@][📎] │ │ └──────────┴─────────────────────────────────────────┴────────────┘ 72px Flex: 1 240px ``` ### 5.4 消息数据流 ``` 用户输入 → MessageInput ↓ WebSocket.send({ action: 'message.create', params: { content, room_id } }) ↓ 后端处理 → Redis INCR seq → DB insert → NATS publish ↓ WebSocket.push({ event: 'message.created', data: message }) ↓ RoomContext.handleMessage() → setMessages(prev => [...prev, message]) ↓ MessageList 渲染 ``` --- ## 6. API 端点 (REST) ### 6.1 房间管理 ``` GET /api/projects/{project}/rooms # 房间列表 GET /api/projects/{project}/rooms/{room} # 房间详情 POST /api/projects/{project}/rooms # 创建房间 PATCH /api/projects/{project}/rooms/{room} # 更新房间 DELETE /api/projects/{project}/rooms/{room} # 删除房间 ``` ### 6.2 消息管理 ``` GET /api/rooms/{room}/messages # 消息列表 (分页) GET /api/rooms/{room}/messages/{message} # 单条消息 POST /api/rooms/{room}/messages # 发送消息 PATCH /api/rooms/{room}/messages/{message} # 编辑消息 DELETE /api/rooms/{room}/messages/{message} # 撤回消息 GET /api/rooms/{room}/messages/search # 搜索消息 GET /api/rooms/{room}/messages/{message}/history # 编辑历史 ``` ### 6.3 AI 端点 ``` GET /api/rooms/{room}/ai # AI 配置列表 POST /api/rooms/{room}/ai # 创建 AI 配置 PATCH /api/rooms/{room}/ai/{model} # 更新 AI 配置 DELETE /api/rooms/{room}/ai/{model} # 删除 AI 配置 ``` --- ## 7. 关键设计原则 | 原则 | 实现方式 | |------|----------| | 高内聚低耦合 | 模块化服务层、清晰的领域边界 | | 异步非阻塞 | Tokio async/await、Redis 异步客户端 | | 事件驱动 | NATS Pub/Sub 解耦组件 | | 强类型安全 | TypeScript + Rust 编译期检查 | | 分层架构 | Service → Repository → Database | | 鲁棒性优先 | 完善的错误处理、限流、资源回收 | --- ## 8. 已实现功能清单 ### 8.1 核心功能 ✅ - [x] 房间 CRUD (创建/读取/更新/删除) - [x] 消息 CRUD (发送/编辑/撤回/历史) - [x] 成员管理 (邀请/移除/角色变更) - [x] 频道分类 (创建/排序/折叠) - [x] 线程回复 (创建线程/线程消息) - [x] 消息反应 (emoji 反应) - [x] 消息置顶 - [x] 消息搜索 - [x] 未读计数 ### 8.2 实时功能 ✅ - [x] WebSocket 长连接 - [x] 消息实时推送 - [x] 成员状态同步 - [x] 在线状态显示 - [x] 消息去重 - [x] IndexedDB 离线缓存 - [x] 虚拟滚动列表 (@tanstack/react-virtual) ### 8.3 AI 集成 ✅ - [x] AI 模型配置 - [x] AI 消息发送 - [x] 系统提示词 - [x] 上下文历史限制 - [x] 流式输出支持 - [x] AI 队列锁 ### 8.4 富媒体消息 ✅ - [x] 消息内容类型: text, image, audio, video, file - [x] Tiptap 富文本编辑器 (IMEditor) - [x] FileNode Tiptap 扩展 (文件/图片节点) - [x] 文件上传状态管理 (uploading/done/error) - [x] Markdown 支持 ### 8.5 全文搜索 ✅ - [x] PostgreSQL 全文索引 (GIN + tsvector) - [x] 搜索 API (room_message_search) - [x] 分页与结果计数 ### 8.6 通知系统 ✅ - [x] 提及通知 - [x] 线程通知 - [x] DND 免打扰时段 (do_not_disturb, dnd_start/end_hour) ### 8.4 用户体验 ✅ - [x] 消息草稿自动保存 - [x] @提及功能 - [x] 回复引用 - [x] 消息时间格式化 - [x] Discord 风格 UI - [x] 侧边栏折叠 - [x] 成员列表按角色着色 --- ## 9. 已完成功能 (详细技术方案) ### 9.1 富媒体消息支持 ✅ #### 9.1.1 图片消息 ``` 技术实现: - 前端: FileNode.tsx Tiptap 扩展,支持 inline 文件节点 - 富媒体消息类型: MessageContentType::Image, Audio, Video, File - 文件上传: Tiptap IMEditor 支持拖拽上传 - 状态管理: uploading/done/error 三种状态 代码位置: - src/components/room/message/editor/FileNode.tsx (Tiptap 文件节点) - src/components/room/message/editor/IMEditor.tsx (富文本编辑器) ``` #### 9.1.2 消息内容类型 ✅ ``` 已实现的消息类型: - Text (text) - Image (image) - Audio (audio) - Video (video) - File (file) 代码位置: - libs/models/rooms/mod.rs (MessageContentType enum) ``` ### 9.2 历史消息优化 ✅ #### 9.2.1 虚拟滚动列表 ``` 技术实现: - @tanstack/react-virtual + useVirtualizer - 按需渲染可见区域消息 (overscan: 30) - 动态高度估算 (estimateMessageRowHeight) - 日期分隔符自动插入 - 滚动位置保持 (加载更多时) 代码位置: - src/components/room/message/MessageList.tsx 功能特性: - [x] IntersectionObserver 自动加载更多 - [x] 滚动位置恢复 - [x] 滚动到底部按钮 - [x] 日期分组分隔符 ``` #### 9.2.2 IndexedDB 离线缓存 ✅ ``` 技术实现: - IndexedDB 本地持久化存储 - 双索引: by_room, by_room_seq - 支持离线消息恢复 - 自动保存/加载消息 API: - saveMessage(msg) # 保存单条 - saveMessages(roomId, msgs) # 批量保存 - loadMessages(roomId) # 加载房间消息 - loadOlderMessagesFromIdb() # 加载历史消息 - getMaxSeq(roomId) # 获取最大序列号 (去重) 代码位置: - src/lib/storage/indexed-db.ts - src/contexts/room-context.tsx (集成缓存) ``` ### 9.3 全文搜索 ✅ #### 9.3.1 PostgreSQL 全文索引 ``` 技术实现: - content_tsv TSVECTOR 列 - GIN 索引 (idx_room_message_content_tsv) - plainto_tsquery('simple', query) 全文搜索 代码位置: - libs/room/src/search.rs (room_message_search) - libs/migrate/sql/m20250628_000080_add_message_reactions_and_search.sql 搜索功能: - [x] 全文搜索 API - [x] 分页支持 (limit, offset) - [x] 结果计数 (total) - [x] 显示名称解析 ``` ### 9.4 通知系统完善 ✅ #### 9.4.1 多维度通知配置 ✅ ``` 技术实现: - room_member 表新增字段: - do_not_disturb: BOOLEAN (免打扰开关) - dnd_start_hour: INT (DND 开始时间, 0-23) - dnd_end_hour: INT (DND 结束时间, 0-23) 数据库迁移: - libs/migrate/m20250628_000078_add_room_member_do_not_disturb.rs 代码位置: - libs/models/rooms/room_member.rs ``` #### 9.4.2 通知系统 ``` 已实现功能: - [x] 提及通知 (Mention) - [x] 线程通知 (Thread) - [x] DND 免打扰时段 - [x] 通知列表 API - [x] 标记已读 API 代码位置: - libs/room/src/notification.rs - libs/service/user/notification.rs ``` --- ## 10. 待实现功能 ### 10.1 富媒体消息完善 ``` 待实现: 1. [ ] 对象存储集成 (S3/MinIO) 2. [ ] 文件下载 API 3. [ ] 图片预览 Modal 4. [ ] 视频播放器集成 5. [ ] Office 文档预览 6. [ ] 文件大小/类型验证 7. [ ] 图片压缩 (WebWorker) ``` ### 10.2 全文搜索增强 ``` 待实现: 1. [ ] 时间范围筛选 2. [ ] 用户筛选 (@username) 3. [ ] 文件类型筛选 (content_type) 4. [ ] 搜索历史记录 5. [ ] 结果高亮 6. [ ] 正则搜索支持 7. [ ] 数据库触发器自动更新 tsvector ``` ### 10.3 推送通知 ``` 待实现: 1. [ ] Web Push 集成 (service worker) 2. [ ] 移动端推送 3. [ ] 通知中心 UI 4. [ ] 未读计数 Badge 5. [ ] 关键词提醒 ``` ### 10.4 性能优化 ``` 待实现: 1. [ ] room_message 表分区 (按时间) 2. [ ] 读写分离 3. [ ] 房间列表缓存 (Redis) 4. [ ] 成员列表缓存 5. [ ] Redis Pipeline 批量操作 6. [ ] 组件代码分割 (React.lazy) 7. [ ] 图片懒加载 ``` ### 10.5 AI 增强功能 ``` 待实现: 1. [ ] AI 连续对话上下文管理 2. [ ] AI 会话历史管理 3. [ ] AI 切换对话线程 4. [ ] AI 输出 Markdown 渲染优化 5. [ ] AI 工具调用扩展 (消息引用/代码执行/搜索) 6. [ ] 定时 AI 任务 7. [ ] 会议纪要生成 ``` ### 10.6 国际化 (i18n) ``` 待实现: 1. [ ] 前端 i18n (react-i18next) 2. [ ] 后端 i18n (rust-i18n) 3. [ ] 提取 UI 字符串 4. [ ] 语言切换器 5. [ ] 日期/时间本地化 6. [ ] RTL 语言支持 ``` --- ## 11. 测试计划 ### 11.1 单元测试 - [ ] RoomService 业务逻辑测试 - [ ] 消息序列号生成测试 - [ ] 权限检查测试 - [ ] React Hooks 测试 ### 11.2 集成测试 - [ ] WebSocket 连接测试 - [ ] 数据库事务测试 - [ ] Redis 缓存测试 - [ ] NATS 消息分发测试 ### 11.3 E2E 测试 - [ ] 房间创建流程 - [ ] 消息发送与接收 - [ ] 消息编辑与撤回 - [ ] AI 对话流程 --- ## 12. 部署与运维 ### 11.1 环境变量 ``` # 数据库 DATABASE_URL=postgresql://user:pass@host:5432/db # Redis REDIS_URL=redis://host:6379 # NATS NATS_URL=nats://host:4222 # 对象存储 S3_ENDPOINT=https://s3.example.com S3_BUCKET=room-media AWS_ACCESS_KEY_ID=xxx AWS_SECRET_ACCESS_KEY=xxx # AI OPENAI_API_KEY=sk-xxx OPENROUTER_API_KEY=xxx ``` ### 11.2 Kubernetes 配置 ``` # Room Service Deployment resources: requests: memory: "256Mi" cpu: "100m" limits: memory: "1Gi" cpu: "500m" # HPA 自动扩缩容 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 70 ``` ### 11.3 监控指标 - WebSocket 连接数 - 消息吞吐量 (msg/s) - AI 调用延迟 - Redis 缓存命中率 - 数据库查询延迟 --- ## 13. 安全考虑 ### 12.1 权限模型 ``` 项目权限 → 房间权限 → 成员角色 Admin/Owner → Admin/Owner/Member ``` ### 12.2 输入验证 - 消息内容长度限制 (MAX: 10000 chars) - 文件大小限制 (MAX: 100MB) - 文件类型白名单 ### 12.3 CSRF/XSS 防护 - WebSocket 请求携带 JWT - 消息内容转义 - 文件名 sanitize