Files
imks/docs/rpc.md
T
zhenyi 0dbac480ae feat(telemetry): integrate OpenTelemetry observability stack with health metrics
- Add OpenTelemetry SDK, OTLP exporter, Prometheus integration
- Implement connection tracking with active/total/disconnection metrics
- Add health endpoint with uptime and connection counts
- Integrate tracing spans for socket events and engine messages
- Add metrics collection for event handling duration
- Update health endpoint to include live runtime state
- Add graceful telemetry shutdown in main function
- Implement engine session active metrics tracking
- Add namespace-specific attributes to connection metrics
- Introduce message edit history retrieval endpoint
- Add scheduled message CRUD operations and dispatcher
- Update Socket.IO event registration with observability
- Refactor component update to remove dead code allowance
- Add comprehensive environment variables documentation
- Implement detailed development guidelines in AGENTS.md
2026-06-11 13:53:29 +08:00

719 lines
26 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.
# Auth 认证方案
## 架构总览
```
┌─────────┐ ┌──────────┐ ┌──────────┐
│ Client │ │ appks │ │ imks │
│ (浏览器/ │ │ (core) │ │ (IM服务) │
│ APP) │ │ │ │ │
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
│ 1. POST /api/v1/auth/login │
│───────────────────▶│ │
│ 2. {access_token, refresh_token} │
│◀───────────────────│ │
│ │ │
│ 3. WS/gRPC/HTTP 携带 JWT │
│──────────────────────────────────────▶│
│ │ │
│ │ 4a. VerifyToken RPC (RPC模式)
│ │◀─────────────────│
│ │ 4b. GetSigningKeys (本地模式)
│ │◀─────────────────│
│ │ │
│ │ 5. TokenClaims / SigningKeys
│ │─────────────────▶│
│ │ │
│ 6. 业务响应 │ │
│◀─────────────────────────────────────│
```
**角色分工:**
| 服务 | 职责 |
|------------------|----------------------------------------------------|
| **appks** (core) | 颁发 JWT、刷新 JWT、撤销 JWT、管理签名密钥、提供 `TokenService` gRPC |
| **imks** (IM) | 接收客户端 JWT,通过 RPC 或本地密钥验证用户身份 |
## Proto 契约
定义在 `proto/core/auth.proto`package `appks.core.v1`
appks 和 imks 各自维护一份相同的 proto 文件:
- appks 编译为 **server** stub(提供服务)
- imks 编译为 **client** stub(调用服务)
### TokenService RPC
```protobuf
service TokenService {
// 令牌生命周期 (appks 内部调用)
rpc IssueToken(IssueTokenRequest) returns (IssueTokenResponse);
rpc RefreshToken(RefreshTokenRequest) returns (RefreshTokenResponse);
rpc RevokeToken(RevokeTokenRequest) returns (RevokeTokenResponse);
// imks 验证 (RPC 模式)
rpc VerifyToken(VerifyTokenRequest) returns (VerifyTokenResponse);
// imks 密钥拉取 (本地验证模式)
rpc GetSigningKeys(GetSigningKeysRequest) returns (GetSigningKeysResponse);
}
```
## JWT 令牌
### 结构
JWT Header:
```json
{
"alg": "HS256",
"typ": "JWT",
"kid": "01909a..." // 签名密钥 ID,用于匹配 SigningKey
}
```
JWT Payload (`TokenClaims`):
```json
{
"sub": "user-uuid",
"iss": "appks",
"iat": 1718000000,
"exp": 1718003600,
"jti": "01909b...",
"scope": "im:read im:write",
"extra": {
"workspace_id": "..."
}
}
```
### 令牌类型
| 类型 | 格式 | 存储 | 用途 |
|-------------------|----------------------|----------------------------------------------|---------------------------------------|
| **access_token** | JWT (HS256) | 无状态,客户端持有 | 每次请求携带,验证用户身份 |
| **refresh_token** | `rt_{UUIDv7}` 不透明字符串 | Redis `core:token:refresh:{token}` → user_id | 换取新的 access_token + refresh_token(旋转) |
## 双模式验证
imks 可选择以下任一模式验证客户端 JWT:
### 模式 ARPC 验证(`VerifyToken`
```
imks → appks TokenService.VerifyToken(jwt) → {valid, claims}
```
- **优点**:实时权威,能感知撤销
- **缺点**:每次请求增加一次 RPC 往返
- **适用场景**:高安全要求操作(管理员操作、敏感数据)
### 模式 B:本地验证(`GetSigningKeys`
```
imks 启动时 → appks TokenService.GetSigningKeys() → 缓存密钥到本地
后续请求 → imks 用本地密钥解码 JWT(HS256 验签)
定期刷新 → 根据 next_rotation_at 拉取新密钥
```
- **优点**:零 RPC 延迟,appks 不可用时仍能验证
- **缺点**:撤销有最多一个密钥窗口(3h)的延迟
- **适用场景**:高频低延迟操作(消息收发、实时通信)
### 推荐策略
混合使用:
- 普通操作(发消息、读频道)→ 本地验证
- 敏感操作(踢人、删频道、改权限)→ RPC 验证
## 签名密钥管理
### 密钥窗口
```
时间轴:
─────────┬──────────┬──────────┬────────
│ key A │ key B │ key C
│ (过期) │ (活跃) │ (未来)
└──────────┴──────────┴────────
issued_at issued_at issued_at
+3h +3h +3h
```
- 每个签名密钥有效期 **3 小时**
- 同一时刻可能有 **2 个有效密钥**(滚动窗口,平滑过渡)
- JWT header 的 `kid` 字段标识使用哪个密钥签名
### 密钥轮换流程
```
1. 当前密钥到达 3h → TokenService.rotate_if_needed()
2. Redis 分布式锁 (core:token:rotation_lock, 10s TTL) 防止多实例竞争
3. 旧密钥标记 active=false,仍保留在 Redis 用于验证旧 token
4. 生成新密钥,active=true
5. ArcSwap 原子替换当前签名密钥
6. 旧密钥 TTL = 6h (2× window) 后从 Redis 自动清除
```
### 密钥存储(Redis
```
core:token:active_key → kid (当前活跃密钥 ID)
core:token:key:{kid} → SigningKey JSON (TTL = 6h)
core:token:rotation_lock → "1" (TTL = 10s, 分布式锁)
```
### SigningKey 结构
```rust
pub struct SigningKeyInfo {
pub kid: String, // UUIDv7
pub algorithm: String, // "HS256"
pub key_material: String, // base64(32 bytes random)
pub issued_at: i64,
pub expires_at: i64, // issued_at + 3h
pub active: bool,
}
```
## 撤销机制
### Redis 布局
```
core:token:revoked:{jti} → "1" (TTL = token 剩余有效期)
core:token:refresh:{token} → user_id (TTL = 7d)
```
### 撤销方式
| 操作 | RPC | 效果 |
|--------------|------------------------|-----------------------|
| 撤销单个 token | `RevokeToken(jti)` | 将 jti 加入撤销列表 |
| 撤销用户所有 token | `RevokeToken(user_id)` | 删除该用户所有 refresh token |
### 撤销感知延迟
| 验证模式 | 延迟 |
|-----------------------|------------------------------|
| RPC (`VerifyToken`) | **实时** — 每次检查撤销列表 |
| 本地 (`GetSigningKeys`) | **最多 3h** — 密钥过期前无法感知 jti 撤销 |
## appks 实现
### 模块结构
```
service/internal_auth.rs → TokenService (业务逻辑)
grpc/auth.rs → TokenGrpcService (gRPC handler)
grpc/mod.rs → TokenServiceServer 注册到 tonic server
api/internal/issue_api_key.rs → REST: POST /api/v1/internal/tokens
```
### TokenService 核心
```rust
pub struct TokenService {
redis: AppRedis,
current_key: Arc<ArcSwap<SigningKeyInfo>>, // 无锁读
}
```
- 启动时从 Redis 加载活跃密钥,无则生成
- 签名使用 `jsonwebtoken` crate (HS256)
- 密钥轮换使用 Redis 分布式锁,支持多实例部署
- `ArcSwap` 保证签名密钥读取无锁、写入原子
## imks 实现指南
### 启动流程
```rust
// 1. 连接 appks TokenService
let mut token_client = TokenServiceClient::connect(appks_addr).await?;
// 2. 拉取签名密钥
let resp = token_client.get_signing_keys(GetSigningKeysRequest { kid: "" }).await?;
let keys = resp.keys;
let next_rotation = resp.next_rotation_at;
// 3. 缓存密钥到本地 (HashMap<kid, SigningKey>)
key_store.insert_all(keys);
// 4. 安排定时刷新
tokio::spawn(async move {
loop {
let delay = next_rotation - now();
tokio::time::sleep(Duration::from_secs(delay as u64)).await;
let resp = token_client.get_signing_keys(...).await;
key_store.update(resp.keys);
}
});
```
### 连接时验证
```rust
// 客户端建立 WebSocket/gRPC 连接时携带 JWT
fn on_connect(headers: &Headers) -> Result<TokenClaims, AuthError> {
let token = headers.get("Authorization")
.and_then(|v| v.strip_prefix("Bearer "))
.ok_or(AuthError::MissingToken)?;
// 本地验证 (快速路径)
let header = decode_header(token)?;
let kid = header.kid.ok_or(AuthError::MissingKid)?;
let key = key_store.get(&kid).ok_or(AuthError::UnknownKey)?;
let mut validation = Validation::new(Algorithm::HS256);
validation.set_issuer(&["appks"]);
validation.validate_exp = true;
let data = decode::<TokenClaims>(token, &key, &validation)?;
Ok(data.claims)
}
```
### 敏感操作验证
```rust
// 敏感操作走 RPC 验证 (权威路径)
async fn on_sensitive_action(token: &str) -> Result<TokenClaims, AuthError> {
let resp = token_client.verify_token(VerifyTokenRequest {
token: token.to_string(),
}).await?;
if resp.valid {
Ok(resp.claims.unwrap())
} else {
Err(AuthError::from(resp.reason))
}
}
```
## 安全考虑
1. **密钥传输**appks → imks 的 gRPC 连接应使用 mTLS,防止密钥在传输中被截获
2. **密钥生命周期**:3h 窗口平衡了安全性和可用性;缩短窗口可减少撤销延迟但增加轮换频率
3. **HS256 vs 非对称**:当前使用 HS256(对称密钥),imks 拿到的密钥可以伪造 token。如果 imks 不可完全信任,应改用 RS256/EdDSAimks 只持有公钥
4. **Refresh Token 安全**:每次刷新都旋转(旧 token 立即失效),防止重放
5. **撤销列表 TTL**:与 token 剩余有效期对齐,过期 token 无需保留撤销记录
---
# IM 服务 Proto 说明书
以下是 imks 侧 `proto/core/` 下各 gRPC 服务的完整说明。所有 IM 服务定义在 `appks.im.v1` 包下,由 appks 提供 server 端,imks 消费 client 端。
## 服务总览
| Proto 文件 | 服务 | RPC 数量 | 职责 |
|---|---|---|---|
| `auth.proto` | TokenService | 5 | JWT 令牌生命周期 + 验证 + 密钥分发 |
| `channel.proto` | ChannelService | 10 | 频道/分类 CRUD + 统计 |
| `member.proto` | MemberService | 7 | 成员邀请/踢出/加入/离开/查询 |
| `permission.proto` | PermissionService | 7 | 权限检查 + 覆盖规则 + 频道解析 |
| `channel_settings.proto` | ChannelRoleService | 4 | 频道自定义角色 |
| | ChannelInvitationService | 4 | 邀请生命周期 |
| | ChannelWebhookService | 4 | Webhook CRUD |
| | ChannelSlashCommandService | 4 | 斜杠命令注册 |
| | ChannelRepoLinkService | 3 | 频道 ↔ 代码仓库关联 |
| | ImIntegrationService | 4 | 外部平台集成(Slack/Discord 等) |
| | CustomEmojiService | 3 | 工作区自定义表情 |
| | ForumTagService | 4 | 论坛频道标签 |
| | VoiceService | 2 | 语音频道参与者状态 |
| | StageService | 4 | 舞台频道管理 |
| | ChannelAuditService | 1 | 频道审计日志查询 |
---
## ChannelService`channel.proto`
频道和分类的 CRUD 管理,以及频道统计。
### 枚举
**ChannelType** — 频道类型:
| 值 | 含义 |
|---|---|
| `PUBLIC` | 公开频道,workspace 内所有人可见 |
| `PRIVATE` | 私有频道,仅被邀请成员可见 |
| `DIRECT` | 私聊(一对一) |
| `GROUP` | 群聊(多人私聊) |
| `REPO` | 仓库关联频道(自动与 git repo 绑定) |
| `SYSTEM` | 系统频道(公告、通知等,只读) |
**ChannelKind** — 频道形态:
| 值 | 含义 |
|---|---|
| `TEXT` | 文本频道 |
| `VOICE` | 语音频道 |
| `STAGE` | 舞台频道(主持人+观众模式) |
| `FORUM` | 论坛频道(帖子/主题式讨论) |
| `ANNOUNCEMENT` | 公告频道(仅管理员可发消息) |
**Visibility** — 可见性级别(从低到高):
| 值 | 含义 |
|---|---|
| `PUBLIC` | 所有人可见(含未登录用户) |
| `WORKSPACE` | workspace 成员可见 |
| `INTERNAL` | 内部可见(组织成员) |
| `PRIVATE` | 仅频道成员可见 |
| `PROTECTED` | 受保护(不可被搜索/索引) |
| `HIDDEN` | 隐藏(不显示在频道列表中) |
| `SECRET` | 机密(仅通过直链访问) |
### RPC 列表
```
GetChannel(channel_id) → Channel 获取频道详情
ListChannels(workspace, ...) → [Channel], total 列出频道(支持分类/类型/形态过滤)
CreateChannel(workspace, name) → Channel 创建频道
UpdateChannel(channel_id, ...) → Channel 更新频道属性
DeleteChannel(channel_id) → {} 删除频道
GetChannelStats(channel_id) → ChannelStats 获取频道统计(成员/消息/线程/反应数)
ListCategories(workspace) → [ChannelCategory] 列出分类
CreateCategory(workspace, name) → ChannelCategory 创建分类
UpdateCategory(category_id, ...) → ChannelCategory 更新分类
DeleteCategory(category_id) → {} 删除分类
```
### 核心消息
**Channel** — 频道主体:
| 字段 | 类型 | 说明 |
|---|---|---|
| `id` | UUID | 频道 ID |
| `workspace_id` | UUID | 所属 workspace |
| `category_id` | UUID? | 所属分类(可选) |
| `parent_channel_id` | UUID? | 父频道(用于子频道/线程) |
| `name` | string | 频道名称 |
| `topic` / `description` | string? | 主题 / 描述 |
| `channel_type` | ChannelType | 频道类型 |
| `channel_kind` | ChannelKind | 频道形态 |
| `visibility` | Visibility | 可见性 |
| `position` | int32 | 排序位置 |
| `nsfw` | bool | NSFW 标记 |
| `read_only` | bool | 只读(仅管理员可发消息) |
| `archived` | bool | 已归档 |
| `rate_limit_per_user` | int32? | 慢速模式(秒/消息) |
| `last_message_id` / `last_message_at` | — | 最后一条消息信息 |
**ChannelStats** — 频道统计:
| 字段 | 说明 |
|---|---|
| `members_count` | 成员数 |
| `messages_count` | 消息数 |
| `threads_count` | 线程数 |
| `reactions_count` | 反应数 |
| `mentions_count` | @提及数 |
| `files_count` | 文件数 |
| `last_activity_at` | 最后活跃时间 |
---
## MemberService`member.proto`
频道成员管理。
### 枚举
**Role** — 角色层级(从高到低):
| 值 | 含义 |
|---|---|
| `OWNER` | 频道所有者 |
| `ADMIN` | 管理员(全部权限) |
| `MAINTAINER` | 维护者(管理频道设置、成员) |
| `MODERATOR` | 版主(管理消息、踢人) |
| `MEMBER` | 普通成员 |
| `CONTRIBUTOR` | 贡献者(可发消息,部分限制) |
| `VIEWER` | 观察者(只读) |
| `GUEST` | 访客(临时访问) |
| `BOT` | 机器人 |
**MemberStatus** — 成员状态:
| 值 | 含义 |
|---|---|
| `ACTIVE` | 活跃成员 |
| `INVITED` | 已邀请(尚未加入) |
| `LEFT` | 已离开 |
| `KICKED` | 被踢出 |
| `BANNED` | 被封禁 |
### RPC 列表
```
ListMembers(channel_id, ...) → [ChannelMember], total 列出成员(支持状态过滤)
InviteMember(channel_id, user_id) → ChannelMember 邀请用户加入频道
UpdateMember(channel_id, user_id) → ChannelMember 更新成员(角色/禁言/置顶)
KickMember(channel_id, user_id) → {} 踢出成员
JoinChannel(channel_id, user_id) → ChannelMember 用户主动加入
LeaveChannel(channel_id, user_id) → {} 用户主动离开
IsMember(channel_id, user_id) → is_member, role 检查是否为成员
```
### 核心消息
**ChannelMember** — 频道成员:
| 字段 | 说明 |
|---|---|
| `channel_id` / `user_id` | 频道 + 用户 |
| `role` | 角色(Role 枚举值字符串) |
| `status` | 状态(MemberStatus 枚举值字符串) |
| `muted` | 是否被禁言 |
| `pinned` | 是否被置顶(频道侧标记) |
| `last_read_message_id` / `last_read_at` | 已读进度 |
| `joined_at` / `left_at` | 加入/离开时间 |
---
## PermissionService`permission.proto`
频道级权限系统,独立于 workspace/repo 的通用权限。
### 权限枚举(ImPermission
| 权限 | 说明 |
|---|---|
| `READ_CHANNEL` | 查看频道 |
| `SEND_MESSAGE` | 发送消息 |
| `MANAGE_THREADS` | 管理线程 |
| `MANAGE_REACTIONS` | 管理反应 |
| `MANAGE_PINS` | 管理置顶消息 |
| `INVITE_MEMBERS` | 邀请成员 |
| `KICK_MEMBERS` | 踢出成员 |
| `MANAGE_CHANNEL` | 管理频道设置 |
| `MANAGE_ROLES` | 管理角色 |
| `MANAGE_WEBHOOKS` | 管理 Webhook |
| `MANAGE_EMOJIS` | 管理自定义表情 |
| `VIEW_AUDIT_LOG` | 查看审计日志 |
| `MANAGE_INTEGRATIONS` | 管理外部集成 |
| `SEND_TTS` | 发送 TTS 消息 |
| `USE_SLASH_COMMANDS` | 使用斜杠命令 |
| `ATTACH_FILES` | 上传文件 |
| `MENTION_EVERYONE` | @所有人 |
| `MANAGE_MESSAGES` | 管理消息(删除他人消息) |
| `ADMIN` | 管理员(拥有所有权限) |
### RPC 列表
```
CheckPermission(channel, user, perm) → allowed, role 检查单项权限
GetPermissions(channel, user) → [ImPermission] 获取用户全部权限
SetPermissionOverwrite(channel, target) → Overwrite 设置权限覆盖
GetPermissionOverwrites(channel) → [Overwrite] 获取覆盖列表
DeletePermissionOverwrite(channel, target) → {} 删除覆盖
ResolveChannel(channel_id) → 频道摘要信息 解析频道元数据
EnsureReadable(channel, user) → allowed 确保用户可读(快速检查)
```
### 权限覆盖(PermissionOverwrite
权限覆盖允许对特定用户/角色在特定频道上覆盖默认权限:
| 字段 | 说明 |
|---|---|
| `target_type` | `"user"``"role"` |
| `target_id` | 用户 ID 或角色 ID |
| `allow` | 显式允许的权限列表 |
| `deny` | 显式拒绝的权限列表 |
权限解析优先级:`deny 覆盖 > allow 覆盖 > 角色权限`
---
## ChannelSettings 服务组(`channel_settings.proto`
所有频道配置相关的服务定义在同一个 proto 文件中。
### ChannelRoleService — 频道自定义角色
频道级别的自定义角色(不同于 `member.proto` 中的全局 Role 枚举)。
```
ListChannelRoles(channel_id) → [ChannelRole]
CreateChannelRole(channel, name) → ChannelRole
UpdateChannelRole(role_id, ...) → ChannelRole
DeleteChannelRole(role_id) → {}
```
**ChannelRole** 字段:`name`, `permissions[]`ImPermission 字符串列表), `assignable`(是否可被普通成员分配)
### ChannelInvitationService — 邀请管理
```
ListInvitations(channel_id) → [ChannelInvitation]
CreateInvitation(channel, user) → ChannelInvitation
AcceptInvitation(invitation_id) → ChannelInvitation
RevokeInvitation(invitation_id) → {}
```
**ChannelInvitation** 字段:`invited_by`, `invited_user_id`, `role`(预设角色), `status`
### ChannelWebhookService — Webhook 管理
```
ListWebhooks(channel_id) → [ChannelWebhook]
CreateWebhook(channel, name, url) → ChannelWebhook
UpdateWebhook(webhook_id, ...) → ChannelWebhook
DeleteWebhook(webhook_id) → {}
```
**ChannelWebhook** 字段:`name`, `url`, `secret`(签名验证用), `events[]`(订阅事件列表), `active`
### ChannelSlashCommandService — 斜杠命令注册
```
ListSlashCommands(channel_id) → [ChannelSlashCommand]
CreateSlashCommand(channel, cmd, url) → ChannelSlashCommand
UpdateSlashCommand(command_id, ...) → ChannelSlashCommand
DeleteSlashCommand(command_id) → {}
```
**ChannelSlashCommand** 字段:`command`(命令名如 `/deploy`, `description`, `request_url`(回调地址), `scopes[]`
### ChannelRepoLinkService — 仓库关联
将频道与代码仓库关联,自动推送仓库事件到频道。
```
ListRepoLinks(channel_id) → [ChannelRepoLink]
CreateRepoLink(channel, repo, type) → ChannelRepoLink
DeleteRepoLink(link_id) → {}
```
**ChannelRepoLink** 字段:`repo_id`, `link_type`, `events[]`(订阅的仓库事件:push、pr、issue 等)
### ImIntegrationService — 外部平台集成
与 Slack、Discord 等外部平台的消息同步。
```
ListIntegrations(channel_id) → [ImIntegration]
CreateIntegration(channel, provider, ...)→ ImIntegration
UpdateIntegration(integration_id, ...) → ImIntegration
DeleteIntegration(integration_id) → {}
```
**ImIntegration** 字段:`provider`(平台名), `external_channel_id`(外部频道 ID, `sync_direction``inbound`/`outbound`/`bidirectional`, `active`
### CustomEmojiService — 自定义表情
工作区级别的自定义表情管理。
```
ListCustomEmojis(workspace_id) → [CustomEmoji]
CreateCustomEmoji(workspace, name, url) → CustomEmoji
DeleteCustomEmoji(emoji_id) → {}
```
**CustomEmoji** 字段:`workspace_id`, `name`(表情名如 `:appks:`, `image_url`
### ForumTagService — 论坛标签
论坛频道(`ChannelKind::FORUM`)的帖子分类标签。
```
ListForumTags(channel_id) → [ForumTag]
CreateForumTag(channel, name, ...) → ForumTag
UpdateForumTag(tag_id, ...) → ForumTag
DeleteForumTag(tag_id) → {}
```
**ForumTag** 字段:`name`, `moderated`(是否需要管理员审核), `position`
### VoiceService — 语音频道
语音频道的参与者状态管理。
```
ListVoiceParticipants(channel_id) → [VoiceParticipant]
UpdateVoiceState(channel, user, ...) → VoiceParticipant
```
**VoiceParticipant** 字段:`user_id`, `muted`(静音), `deafened`(屏蔽音频), `joined_at`
### StageService — 舞台频道
舞台频道(`ChannelKind::STAGE`)的管理。主持人说话,观众收听。
```
GetStage(channel_id) → Stage
CreateStage(channel, topic, ...) → Stage
UpdateStage(stage_id, ...) → Stage
DeleteStage(stage_id) → {}
```
**Stage** 字段:`topic`(当前话题), `privacy_level`, `discoverable`(是否可被发现), `started_at` / `ended_at`
### ChannelAuditService — 审计日志
频道操作审计日志查询(只读)。
```
ListChannelEvents(channel_id, ...) → [ChannelAuditEvent], total
```
**ChannelAuditEvent** 字段:`actor_id`(操作者), `event_type`(事件类型字符串), `target_type` / `target_id`(操作对象), `old_value` / `new_value`(变更前后值)
---
## imks 与 appks 的调用关系
```
┌────────────────────────────────────────────────────────────┐
│ imks │
│ │
│ Socket.IO / WebSocket / WebTransport │
│ │ │
│ ▼ │
│ 连接握手 ──→ TokenService.VerifyToken() 或 本地密钥验证 │
│ │ │
│ ▼ │
│ 消息收发 ──→ ChannelService + MemberService │
│ │ PermissionService.EnsureReadable() │
│ │ │
│ ▼ │
│ 频道管理 ──→ ChannelService CRUD │
│ │ ChannelRoleService │
│ │ ChannelInvitationService │
│ │ │
│ ▼ │
│ 语音/舞台 ──→ VoiceService + StageService │
│ │
│ 集成/扩展 ──→ WebhookService + SlashCommandService │
│ RepoLinkService + ImIntegrationService │
│ │
│ 审计查询 ──→ ChannelAuditService │
└────────────────────────┬───────────────────────────────────┘
│ gRPC
┌────────────────────────────────────────────────────────────┐
│ appks │
│ TokenService server │ Channel/Member/Permission server │
│ Redis (JWT keys) │ Postgres (channel data) │
└────────────────────────────────────────────────────────────┘
```
### imks 本地缓存建议
| 数据 | 缓存策略 | 刷新时机 |
|--------------------------|--------------|--------------------------|
| 签名密钥 (`SigningKey[]`) | 内存 HashMap | `next_rotation_at` 到达时拉取 |
| 频道信息 (`Channel`) | LRU + TTL | 频道更新事件 (NATS) |
| 成员列表 (`ChannelMember[]`) | LRU + TTL | 成员变更事件 (NATS) |
| 权限缓存 | 短期 TTL (30s) | 权限变更事件 (NATS) |
| 自定义表情 | 全量加载 + 事件增量 | emoji 增删事件 (NATS) |