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
This commit is contained in:
zhenyi
2026-06-11 13:53:29 +08:00
parent 40241e5db3
commit 0dbac480ae
22 changed files with 3116 additions and 64 deletions
+718
View File
@@ -0,0 +1,718 @@
# 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) |