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

26 KiB
Raw Blame History

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.protopackage appks.core.v1

appks 和 imks 各自维护一份相同的 proto 文件:

  • appks 编译为 server stub(提供服务)
  • imks 编译为 client stub(调用服务)

TokenService RPC

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:

{
  "alg": "HS256",
  "typ": "JWT",
  "kid": "01909a..."    // 签名密钥 ID,用于匹配 SigningKey
}

JWT Payload (TokenClaims):

{
  "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 结构

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 核心

pub struct TokenService {
    redis: AppRedis,
    current_key: Arc<ArcSwap<SigningKeyInfo>>,  // 无锁读
}
  • 启动时从 Redis 加载活跃密钥,无则生成
  • 签名使用 jsonwebtoken crate (HS256)
  • 密钥轮换使用 Redis 分布式锁,支持多实例部署
  • ArcSwap 保证签名密钥读取无锁、写入原子

imks 实现指南

启动流程

// 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);
    }
});

连接时验证

// 客户端建立 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)
}

敏感操作验证

// 敏感操作走 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 频道审计日志查询

ChannelServicechannel.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 最后活跃时间

MemberServicemember.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 加入/离开时间

PermissionServicepermission.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_directioninbound/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)