Files
re-teamspeak/docs/components.md
T
ReTeamSpeak ea08823c97
CI/CD / Build Frontend (push) Failing after 11s
CI/CD / Test (macos-latest) (push) Has been cancelled
CI/CD / Test (windows-latest) (push) Has been cancelled
CI/CD / Build Desktop (linux) (push) Has been cancelled
CI/CD / Build Desktop (macos) (push) Has been cancelled
CI/CD / Build Desktop (windows) (push) Has been cancelled
CI/CD / Release (push) Has been cancelled
CI/CD / Test (ubuntu-latest) (push) Failing after 2s
Initial commit: ReTeamSpeak cross-platform TeamSpeak client
- tscore: Protocol implementation (packets, crypto, connection handshake)
- tsaudio: Audio engine (capture, playback, codec, VAD, jitter buffer)
- tsdb: SQLite database (identities, bookmarks, messages, settings)
- shared: Core types and events
- tauri-app: Tauri v2 desktop application with React frontend
- docs: SRS, SAD, SDD documentation
- CI/CD: GitHub Actions workflow
- 32 unit tests passing
2026-05-12 14:41:54 +09:00

19 KiB

组件设计文档 (SDD)

1. 概述

本文档详细描述系统各组件的设计,包括功能、接口、数据结构和实现细节。

2. 核心组件

2.1 协议库组件 (tsclientlib)

2.1.1 tsproto-types (基础类型)

职责: 定义 TeamSpeak 协议中使用的基础类型、枚举和加密原语

关键类型:

pub struct ClientId(pub u16);          // 客户端 ID
pub struct ChannelId(pub u64);         // 频道 ID
pub struct UidBuf(pub Vec<u8>);        // 用户唯一标识
pub struct Permission(pub u32);        // 权限 ID
pub enum ClientType { Normal, Query { admin: bool } }
pub enum MaxClients { Unlimited, Inherited, Limited(u16) }

加密模块:

pub struct EccKeyPubP256(p256::PublicKey);      // P-256 公钥
pub struct EccKeyPrivP256(p256::SecretKey);     // P-256 私钥
pub struct EccKeyPubEd25519(CompressedEdwardsY); // Ed25519 公钥
pub struct EccKeyPrivEd25519(Scalar);            // Ed25519 私钥

错误码枚举: 从 CSV 自动生成,包含所有 TeamSpeak 错误码

2.1.2 tsproto-structs (声明式数据)

职责: 提供协议的机器可读声明数据

声明文件:

  • Book.toml - 服务器状态数据模型
  • Messages.toml - 协议消息结构
  • Enums.toml - 枚举定义
  • Errors.csv - 错误码列表
  • Versions.csv - 版本信息

2.1.3 tsproto-packets (包解析)

职责: 解析和序列化 TeamSpeak 网络包和命令

关键类型:

pub enum PacketType { Voice, VoiceWhisper, Command, CommandLow, Ping, Pong, Ack, AckLow, Init }
pub enum Direction { S2C, C2S }

// 输入包(零拷贝)
pub struct InPacket<'a> { header: InHeader<'a>, content: &'a [u8] }
pub struct InCommand<'a> { packet: InPacket<'a> }
pub struct InAudio<'a> { packet: InPacket<'a>, data: AudioData<'a> }

// 输出包
pub struct OutPacket { dir: Direction, data: Vec<u8> }
pub struct OutCommand(pub OutPacket);

命令解析器:

pub struct CommandParser<'a> { data: &'a [u8], index: usize }
pub enum CommandItem<'a> { Argument(CommandArgument<'a>), NextCommand }

2.1.4 ts-bookkeeping (状态管理)

职责: 维护 TeamSpeak 服务器的完整状态模型

核心数据模型:

pub struct Connection {
    pub own_client: ClientId,
    pub server: Server,
    pub clients: HashMap<ClientId, Client>,
    pub channels: HashMap<ChannelId, Channel>,
    pub channel_groups: HashMap<ChannelGroupId, ChannelGroup>,
    pub server_groups: HashMap<ServerGroupId, ServerGroup>,
}

pub struct Server { /* 名称、版本、最大客户端数、加密模式等 */ }
pub struct Channel { /* 名称、类型、编解码器、权限等 */ }
pub struct Client { /* 名称、频道、静音状态、权限等 */ }

事件系统:

pub enum Event {
    PropertyAdded { id: PropertyId, invoker: Option<Invoker>, extra: ExtraInfo },
    PropertyChanged { id: PropertyId, old: PropertyValue, invoker: Option<Invoker>, extra: ExtraInfo },
    PropertyRemoved { id: PropertyId, old: PropertyValue, invoker: Option<Invoker>, extra: ExtraInfo },
    Message { target: MessageTarget, invoker: Invoker, message: String },
}

2.1.5 tsproto (协议引擎)

职责: 实现 TeamSpeak 3 协议的底层网络通信

核心类型:

pub struct Identity {
    key: EccKeyPrivP256,
    counter: u64,        // Hash Cash 计数器
    max_counter: u64,
}

pub struct Client {
    con: Connection,
    pub private_key: EccKeyPrivP256,
}

pub struct Connection {
    pub is_client: bool,
    pub params: Option<ConnectedParams>,
    pub address: SocketAddr,
    pub resender: Resender,
    pub codec: PacketCodec,
    pub udp_socket: Box<dyn Socket + Send>,
}

连接握手流程:

Client                          Server
  │                               │
  │──── Init0 (version, ts) ────>│
  │<─── Init1 (random1) ─────────│
  │──── Init2 (random1_r) ──────>│
  │<─── Init3 (RSA puzzle) ──────│
  │──── Init4 (solve + ECDH) ───>│
  │<─── initivexpand2 (license) ─│
  │──── clientek (ephemeral key) >│
  │<─── initserver ──────────────│
  │         (connected)           │

2.1.6 tsclientlib (高层客户端库)

职责: 提供用户友好的客户端 API

核心类型:

pub struct Connection {
    state: ConnectionState,
    options: ConnectOptions,
    stream_items: VecDeque<Result<StreamItem>>,
}

enum ConnectionState {
    Connecting(BoxFuture<...>, bool),
    IdentityLevelIncreasing { recv, state },
    Connected { con: ConnectedConnection, book: data::Connection },
}

pub enum StreamItem {
    BookEvents(Vec<events::Event>),
    MessageEvent(InMessage),
    Audio(InAudioBuf),
    IdentityLevelIncreasing(u8),
    IdentityLevelIncreased,
    DisconnectedTemporarily(TemporaryDisconnectReason),
    MessageResult(MessageHandle, Result<(), CommandError>),
    FileDownload(...), FileUpload(...), FiletransferFailed(...),
    NetworkStatsUpdated,
    AudioChange(AudioEvent),
}

地址解析: 解析优先级:

  1. 直接 IP 地址
  2. 服务器昵称 (HTTP 查询)
  3. DNS SRV 记录
  4. TSDNS 服务
  5. 系统 DNS 解析

音频处理:

pub struct AudioHandler<Id> {
    queues: HashMap<Id, AudioQueue>,
    avg_buffer_samples: usize,
}

pub struct AudioQueue {
    decoder: Decoder,              // Opus 解码器
    packet_buffer: VecDeque<QueuePacket>,
    decoded_buffer: Vec<f32>,
    last_buffer_size_min: SlidingWindowMinimum<u8>,
}

2.2 客户端组件 (Qint)

2.2.1 前端组件

连接状态管理 (connection.ts):

class Connection {
    private book: Book;
    private backend: IBackendConnection;
    private state: ConnectionState;
    
    // 状态机: Uninitialized -> Connecting -> Connected -> ChannelListFinished -> Disconnected
    async connect(onMsg, onError, onClose): Promise<void>;
    sendMessage(target, message): void;
    switchChannel(channelId): void;
    startWhispering(whisperData): void;
}

数据状态镜像 (book.ts):

class Book {
    channels: Map<ChannelId, Channel>;
    clients: Map<ClientId, Client>;
    serverGroups: Map<ServerGroupId, ServerGroup>;
    channelGroups: Map<ChannelGroupId, ChannelGroup>;
    
    processEvent(event: InBookChangeMsg): void;
}

后端抽象层 (backend/):

interface IBackend {
    createNewConnection(returnCodes: ReturnCodeTracker): IBackendConnection;
    graphql<T>(query: string, variables?: Record<string, unknown>): Promise<{data: T}>;
    get_settings(): Promise<Record<string, unknown>>;
    set_settings(diff: Record<string, unknown>): Promise<void>;
}

interface IBackendConnection {
    id: string;
    connect(onMsg, onError, onClose): Promise<void>;
    send(data: OutMsg): void;
    close(): void;
    fetch_image(req: IFileRequest): Promise<string | undefined>;
    upload_bytes(req: IFileRequest, data: Blob): Promise<TransferResult>;
}

2.2.2 壳层组件

Tauri 命令处理器 (cmd.rs):

#[command]
async fn create_ws(state: State<'_, QintCore>, window: Window, con: String) -> Result<(), String>

#[command]
async fn pass_ws_msg(state: State<'_, QintCore>, con: String, msg: MessageF2P) -> Result<(), String>

#[command]
async fn db(state: State<'_, QintState>, query: String, variables: String) -> Result<String, String>

WebSocket 处理器 (websocket.rs):

struct Ws {
    state: QintState,
    id: String,
    addr: Addr<QintConnection>,
}

impl Ws {
    fn handle_message(&mut self, msg: F2PMsg) {
        match msg.cmd {
            "create_ws" => { /* 创建连接 */ }
            "pass_ws_msg" => { /* 转发消息 */ }
            "get_settings" => { /* 获取设置 */ }
            _ => { /* 其他命令 */ }
        }
    }
}

2.2.3 代理层组件

全局状态管理 (QintState):

pub struct QintState {
    pub connections: Mutex<HashMap<String, Addr<QintConnection>>>,
    pub audio_data: Arc<AudioData>,
    pub hotkeys: HotkeyManager,
    pub settings: RwLock<Settings>,
    pub database: Addr<DbHandler>,
    pub graphql_schema: Schema<QueryRoot, MutationRoot, EmptySubscription>,
    pub file_cache: FileCache,
    pub link_previewer: LinkPreviewer,
    pub secret: chacha20poly1305::Key,
    pub search_index: SearchIndex,
}

连接管理器 (QintConnection):

pub struct QintConnection {
    state: QintState,
    id: String,
    con: Option<tsclientlib::Connection>,
    bridge: Box<dyn AppToFrontendBridge>,
    audio_to_ts: Addr<AudioToTs>,
    ts_to_audio: Addr<TsToAudio>,
    database: Addr<DbHandler>,
}

impl Actor for QintConnection {
    type Context = Context<Self>;
}

impl Handler<MessageF2PWrapper> for QintConnection {
    fn handle(&mut self, msg: MessageF2PWrapper, ctx: &mut Self::Context) {
        match msg.0 {
            MessageF2P::Connect(options) => { /* 建立连接 */ }
            MessageF2P::Disconnect(options) => { /* 断开连接 */ }
            MessageF2P::SendMessage { target, message, return_code } => { /* 发送消息 */ }
            // ...
        }
    }
}

数据库管理器 (DbHandler):

pub struct DbHandler {
    pool: SqlitePool,
}

impl Actor for DbHandler {
    type Context = Context<Self>;
}

impl Handler<GetIdentityAndServerMsg> for DbHandler {
    fn handle(&mut self, msg: GetIdentityAndServerMsg, ctx: &mut Self::Context) -> Self::Result {
        // 从数据库获取身份和服务器信息
    }
}

impl Handler<WriteMessageMsg> for DbHandler {
    fn handle(&mut self, msg: WriteMessageMsg, ctx: &mut Self::Context) {
        // 写入聊天消息到数据库
    }
}

音频管道:

// AudioToTs - 麦克风采集和编码
pub struct AudioToTs {
    connections: Vec<Addr<QintConnection>>,
    encoder: OpusEncoder,
    vad: VadDetector,
    loudness_meter: LoudnessMeter,
}

impl Handler<SendPacketMsg> for AudioToTs {
    fn handle(&mut self, msg: SendPacketMsg, ctx: &mut Self::Context) {
        // 编码并发送音频数据
    }
}

// TsToAudio - 音频解码和播放
pub struct TsToAudio {
    queues: HashMap<String, AudioQueue>,
    output_device: AudioDevice,
}

impl Handler<PlayMsg> for TsToAudio {
    fn handle(&mut self, msg: PlayMsg, ctx: &mut Self::Context) {
        // 解码并播放音频
    }
}

2.3 机器人组件 (SimpleBot)

核心结构:

pub struct Bot {
    base_dir: PathBuf,
    settings_path: PathBuf,
    actions: ActionList,
    settings: Settings,
    rate_limiting: Vec<Instant>,
    list: Vec<String>,
    should_reload: Cell<bool>,
}

动作系统:

pub struct ActionDefinition {
    contains: Option<String>,
    regex: Option<String>,
    chat: Option<String>,
    response: Option<String>,
    command: Option<String>,
    shell: Option<String>,
}

pub struct Action {
    matchers: Vec<Matcher>,
    reaction: Option<Reaction>,
}

pub enum Matcher {
    Regex(Regex),
    Mode(Option<TextMessageTargetMode>),
}

pub enum Reaction {
    Plain(String),
    Command(String),
    Shell(String),
    Function(ReactionFunction),
}

配置系统:

pub struct Settings {
    key_file: String,
    dynamic_actions: String,
    address: String,
    channel: Option<ChannelDefinition>,
    name: String,
    disconnect_message: String,
    rate_limit: u8,
    prefix: String,
    actions: ActionFile,
}

pub struct ActionFile {
    include: Vec<String>,
    on_message: Vec<ActionDefinition>,
}

2.4 统计工具组件 (ts3stats)

核心类:

class DiagramCreator:
    env: jinja2.Environment
    diagramTemplate: jinja2.Template
    htmlTemplate: jinja2.Template
    users: dict[int, User]
    vip: list[User]
    tabs: list[Tab]
    
    def load_meta(self): pass
    def load_data(self): pass
    def create_diagrams(self): pass
    def fun_per_connected_slot(self, users, callback): pass

class User:
    name: str
    lastConnected: list[datetime]
    connections: list[Connection]
    botPlays: list[tuple]
    botCommands: list[tuple]

class Connection:
    start: datetime
    end: datetime
    timeout: bool

class Diagram:
    filename: str
    title: str
    plots: list[str]
    
    def render(self): pass

class Tab:
    name: str
    diagrams: list[Diagram]

插件架构: 每个 diags/*.py 文件暴露一个函数:

def create_diag(dc: DiagramCreator) -> None

配置系统:

# Settings.py
vips = ["MyName", "friend42"]
merges = [["MyName", "MyNameLaptop"]]
maxUsers = 50
botStats = True
inputFolder = "Logs"
outputFolder = "Result"

3. 接口设计

3.1 Tauri IPC 接口

命令 参数 返回值 说明
create_ws con: String () 创建 WebSocket 连接
close_ws con: String () 关闭 WebSocket 连接
pass_ws_msg con: String, msg: MessageF2P () 转发前端消息
db query: String, variables: String String GraphQL 查询
get_settings Record<string, unknown> 获取设置
set_settings diff: Record<string, unknown> () 更新设置

3.2 WebSocket 协议

前端发送:

{"cmd": "create_ws", "returnCode": "0", "args": {"con": "uuid"}}
{"cmd": "pass_ws_msg", "returnCode": "1", "args": {"con": "uuid", "msg": {"Connect": {...}}}}
{"cmd": "get_settings", "returnCode": "2", "args": {}}

后端发送:

{"cmd": "resp", "returnCode": "2", "msg": {...}}
{"cmd": "resp_err", "returnCode": "1", "msg": "error"}
{"cmd": "ws", "con": "uuid", "msg": {"Connected": {...}}}
{"cmd": "ws_close", "con": "uuid"}
{"cmd": "loudness", "msg": [0.5, 0.3]}

3.3 GraphQL 查询

type Query {
    bookmarks: [Bookmark!]!
    servers: [Server!]!
    channels(serverId: ID!): [Channel!]!
    clients(serverId: ID!): [Client!]!
    identities: [Identity!]!
    chats(serverId: ID!): [Chat!]!
    messages(chatId: ID!, limit: Int): [Message!]!
}

type Mutation {
    updateIdentity(id: ID!, input: IdentityInput!): Identity!
    updateBookmark(id: ID!, input: BookmarkInput!): Bookmark!
}

4. 数据流

4.1 连接建立流程

前端                          代理                          TeamSpeak 服务器
  │                            │                                  │
  │── Connect(addr, name) ───>│                                  │
  │                            │── GetIdentityAndServerMsg ────> DbHandler
  │                            │<── (identity, server) ──────────│
  │                            │── Connection::build().connect ─>│
  │                            │                                  │
  │                            │<── TsStreamItem::BookEvents ────│
  │                            │    (PropertyAdded: Server)       │
  │                            │                                  │
  │<── Connected {server, own} │                                  │
  │                            │── ConnectedMsg ────────────────> DbHandler
  │                            │                                  │
  │<── Events [PropertyAdded..]│                                  │

4.2 消息发送流程

前端                          代理                          TeamSpeak 服务器
  │                            │                                  │
  │── SendMessage {target, msg}│                                  │
  │                            │── state.send_message(target) ──>│
  │                            │── WriteMessageMsg ─────────────> DbHandler
  │                            │                                  │
  │                            │<── InMessage (from other) ──────│
  │                            │── JsInMessage ─────────────────>│
  │<── Message(JsInMessage) ──│                                  │
  │                            │── WriteMessageMsg ─────────────> DbHandler

4.3 音频流程

麦克风 → AudioToTs (Actor)
            ├── VAD 检测
            ├── 响度测量
            ├── Opus 编码
            └── 发送到服务器

服务器 → tsclientlib::Connection
            ├── AudioData::S2C
            └── TsToAudio (Actor)
                 ├── Opus 解码
                 ├── 每客户端音量
                 ├── 混音
                 ├── 噪声抑制
                 └── SDL2/Oboe 输出

5. 错误处理

5.1 协议错误

pub enum CommandError {
    TsError(Ts3ErrorCode),
    ConnectionClosed,
    Timeout,
    InvalidResponse,
    // ...
}

5.2 连接错误

pub enum TemporaryDisconnectReason {
    Timeout,
    ServerShutdown,
    ConnectionLost,
    // ...
}

5.3 前端错误

interface ErrorMessage {
    type: "error";
    message: string;
    code?: string;
}

6. 配置管理

6.1 应用配置

pub struct Settings {
    pub name: String,
    pub away: Option<String>,
    pub input_muted: bool,
    pub output_muted: bool,
    pub hotkeys: Vec<HotkeyAction>,
    pub client_volumes: HashMap<String, f32>,
    pub theme: String,
    pub language: String,
}

6.2 连接配置

pub struct ConnectOptions {
    address: ServerAddress,
    local_address: Option<SocketAddr>,
    identity: Option<Identity>,
    server: Option<UidBuf>,
    name: String,
    version: Version,
    channel: Option<String>,
    channel_password: Option<String>,
    server_password: Option<String>,
    default_token: Option<String>,
}

7. 性能考虑

7.1 音频处理

  • Opus 编码/解码使用硬件加速(如果可用)
  • 自适应抖动缓冲减少延迟
  • 噪声抑制减少带宽使用

7.2 状态同步

  • 增量更新减少数据传输
  • 事件批处理减少 IPC 调用
  • 懒加载减少初始加载时间

7.3 数据库

  • SQLite WAL 模式支持并发读取
  • 连接池减少连接开销
  • 索引优化查询性能

8. 安全设计

8.1 身份加密

// ChaCha20-Poly1305 加密身份私钥
fn encrypt_identity(key: &[u8; 32], identity: &[u8]) -> Vec<u8> {
    let cipher = ChaCha20Poly1305::new(key.into());
    let nonce = generate_nonce();
    cipher.encrypt(&nonce, identity)
}

8.2 传输加密

  • 所有命令和语音数据使用 AES-128-EAX 加密
  • ECDH 密钥交换确保前向保密
  • RSA 拼图防止 DoS 攻击

8.3 输入验证

  • 所有用户输入进行验证和转义
  • SQL 查询使用参数化语句
  • WebSocket 消息进行 JSON 验证