a04447c7bc
为 iAs 项目的所有 Rust 源文件添加了系统化的模块级文档注释(//!), 覆盖全部 src/ 下的文件。每个文件包含模块职责、架构设计、数据流 和关键设计决策的说明。 主要变更: - 入口/CLI: main.rs, cli.rs — 系统架构概览、子命令说明 - Daemon/Worker: daemon.rs, worker.rs — 三消费者架构、旧架构说明 - 微信通道: mod.rs, client.rs, types.rs — API 端点、登录流程 - LLM 系统: mod.rs, types.rs, provider.rs, deepseek.rs, conversation.rs — 架构分层、流式处理、工具循环、摘要机制 - 上下文管理: mod.rs, types.rs, builder.rs, tools.rs — Checkpoint 机制、Token Budget、双重摘要 - 工具系统: mod.rs, types.rs, builtin.rs, approval.rs — 两层元工具架构、审批流程 - 内置工具: mod.rs + 6 个工具 — API 端点、认证方式、参数说明 - 消息队列: mod.rs, message_queue.rs, runner.rs — 公平轮转算法、三消费者路由 - 数据库/状态/调度/日志: db/mod.rs, state.rs, scheduler.rs, logger.rs — 表结构、回退策略、定时任务、日志初始化
370 lines
12 KiB
Rust
370 lines
12 KiB
Rust
//! ## 微信 iLink Bot API 协议类型
|
||
//!
|
||
//! 定义了与微信 iLink Bot API 通信的全部数据结构:
|
||
//!
|
||
//! - `BaseInfo` — 公共请求元数据
|
||
//! - `WeixinMessage` — 微信消息结构(支持 text/image/voice/file/video)
|
||
//! - `MessageItem` — 消息条目(5 种类型)
|
||
//! - `GetUpdatesReq/Resp` — 长轮询收消息的请求/响应
|
||
//! - `SendMessageReq` — 发送消息请求
|
||
//! - `QRCodeResponse` / `StatusResponse` — 扫码登录相关
|
||
//! - `LoginResult` / `MessageEvent` — 业务层结果类型
|
||
|
||
use serde::{Deserialize, Serialize};
|
||
|
||
// ─── 基础类型 ───
|
||
|
||
/// 公共请求元数据
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
#[serde(rename_all = "snake_case")]
|
||
pub struct BaseInfo {
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub channel_version: Option<String>,
|
||
}
|
||
|
||
impl BaseInfo {
|
||
pub fn new() -> Self {
|
||
Self {
|
||
channel_version: Some(env!("CARGO_PKG_VERSION").to_string()),
|
||
}
|
||
}
|
||
}
|
||
|
||
// ─── 消息类型 ───
|
||
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
pub struct TextItem {
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub text: Option<String>,
|
||
}
|
||
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
pub struct CDNMedia {
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub encrypt_query_param: Option<String>,
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub aes_key: Option<String>,
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub encrypt_type: Option<i32>,
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub full_url: Option<String>,
|
||
}
|
||
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
pub struct ImageItem {
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub media: Option<CDNMedia>,
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub thumb_media: Option<CDNMedia>,
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub aeskey: Option<String>,
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub url: Option<String>,
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub mid_size: Option<i64>,
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub thumb_size: Option<i64>,
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub thumb_height: Option<i32>,
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub thumb_width: Option<i32>,
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub hd_size: Option<i64>,
|
||
}
|
||
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
pub struct VoiceItem {
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub media: Option<CDNMedia>,
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub encode_type: Option<i32>,
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub bits_per_sample: Option<i32>,
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub sample_rate: Option<i32>,
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub playtime: Option<i64>,
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub text: Option<String>,
|
||
}
|
||
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
pub struct FileItem {
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub media: Option<CDNMedia>,
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub file_name: Option<String>,
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub md5: Option<String>,
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub len: Option<String>,
|
||
}
|
||
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
pub struct VideoItem {
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub media: Option<CDNMedia>,
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub video_size: Option<i64>,
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub play_length: Option<i64>,
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub video_md5: Option<String>,
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub thumb_media: Option<CDNMedia>,
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub thumb_size: Option<i64>,
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub thumb_height: Option<i32>,
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub thumb_width: Option<i32>,
|
||
}
|
||
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
pub struct RefMessage {
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub message_item: Option<Box<MessageItem>>,
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub title: Option<String>,
|
||
}
|
||
|
||
/// ## 消息条目 —— 消息的具体内容
|
||
///
|
||
/// 支持 5 种消息类型:text(1), image(2), voice(3), file(4), video(5)。
|
||
/// 每种类型有对应的 `*_item` 字段,其他字段保持 None。
|
||
///
|
||
/// `text()` 便捷方法用于快速构造文本消息(发送消息时使用)。
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
pub struct MessageItem {
|
||
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
|
||
pub item_type: Option<i32>,
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub create_time_ms: Option<i64>,
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub update_time_ms: Option<i64>,
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub is_completed: Option<bool>,
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub msg_id: Option<String>,
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub ref_msg: Option<Box<RefMessage>>,
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub text_item: Option<TextItem>,
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub image_item: Option<ImageItem>,
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub voice_item: Option<VoiceItem>,
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub file_item: Option<FileItem>,
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub video_item: Option<VideoItem>,
|
||
}
|
||
|
||
impl MessageItem {
|
||
pub const TYPE_TEXT: i32 = 1;
|
||
#[allow(dead_code)]
|
||
pub const TYPE_IMAGE: i32 = 2;
|
||
#[allow(dead_code)]
|
||
pub const TYPE_VOICE: i32 = 3;
|
||
#[allow(dead_code)]
|
||
pub const TYPE_FILE: i32 = 4;
|
||
#[allow(dead_code)]
|
||
pub const TYPE_VIDEO: i32 = 5;
|
||
|
||
pub fn text(text: &str) -> Self {
|
||
Self {
|
||
item_type: Some(Self::TYPE_TEXT),
|
||
text_item: Some(TextItem {
|
||
text: Some(text.to_string()),
|
||
}),
|
||
..Default::default()
|
||
}
|
||
}
|
||
}
|
||
|
||
impl Default for MessageItem {
|
||
fn default() -> Self {
|
||
Self {
|
||
item_type: None,
|
||
create_time_ms: None,
|
||
update_time_ms: None,
|
||
is_completed: None,
|
||
msg_id: None,
|
||
ref_msg: None,
|
||
text_item: None,
|
||
image_item: None,
|
||
voice_item: None,
|
||
file_item: None,
|
||
video_item: None,
|
||
}
|
||
}
|
||
}
|
||
|
||
/// ## 微信消息结构
|
||
///
|
||
/// 对应 iLink API 返回的单个消息。所有字段都是 Option 的,
|
||
/// 因为不同消息类型携带不同字段。
|
||
///
|
||
/// ### 关键字段
|
||
/// * `msg_type` — 消息类型:1=用户消息(TYPE_USER), 2=机器人消息(TYPE_BOT)
|
||
/// * `from_user_id` — 发送者微信 ID
|
||
/// * `to_user_id` — 接收者微信 ID
|
||
/// * `item_list` — 消息内容列表(支持 text/image/voice/file/video)
|
||
/// * `context_token` — 微信上下文令牌(用于多轮对话的上下文恢复)
|
||
/// * `group_id` — 群聊 ID(群消息时非空)
|
||
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
pub struct WeixinMessage {
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub seq: Option<i64>,
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub message_id: Option<i64>,
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub from_user_id: Option<String>,
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub to_user_id: Option<String>,
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub client_id: Option<String>,
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub create_time_ms: Option<i64>,
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub update_time_ms: Option<i64>,
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub delete_time_ms: Option<i64>,
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub session_id: Option<String>,
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub group_id: Option<String>,
|
||
#[serde(rename = "message_type", skip_serializing_if = "Option::is_none")]
|
||
pub msg_type: Option<i32>,
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub message_state: Option<i32>,
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub item_list: Option<Vec<MessageItem>>,
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub context_token: Option<String>,
|
||
}
|
||
|
||
impl Default for WeixinMessage {
|
||
fn default() -> Self {
|
||
Self {
|
||
seq: None,
|
||
message_id: None,
|
||
from_user_id: None,
|
||
to_user_id: None,
|
||
client_id: None,
|
||
create_time_ms: None,
|
||
update_time_ms: None,
|
||
delete_time_ms: None,
|
||
session_id: None,
|
||
group_id: None,
|
||
msg_type: None,
|
||
message_state: None,
|
||
item_list: None,
|
||
context_token: None,
|
||
}
|
||
}
|
||
}
|
||
|
||
impl WeixinMessage {
|
||
#[allow(dead_code)]
|
||
pub const TYPE_NONE: i32 = 0;
|
||
pub const TYPE_USER: i32 = 1;
|
||
pub const TYPE_BOT: i32 = 2;
|
||
|
||
/// 获取文本内容(如果有纯文本 item)
|
||
pub fn text_content(&self) -> Option<&str> {
|
||
self.item_list
|
||
.as_ref()?
|
||
.iter()
|
||
.find(|item| item.item_type == Some(MessageItem::TYPE_TEXT))
|
||
.and_then(|item| item.text_item.as_ref())
|
||
.and_then(|t| t.text.as_deref())
|
||
}
|
||
}
|
||
|
||
// ─── API 请求/响应 ───
|
||
|
||
/// 获取二维码响应
|
||
#[derive(Debug, Deserialize)]
|
||
pub struct QRCodeResponse {
|
||
pub qrcode: String,
|
||
pub qrcode_img_content: String,
|
||
}
|
||
|
||
/// 扫码状态响应
|
||
#[derive(Debug, Deserialize)]
|
||
pub struct StatusResponse {
|
||
pub status: String,
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub bot_token: Option<String>,
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub ilink_bot_id: Option<String>,
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub baseurl: Option<String>,
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub ilink_user_id: Option<String>,
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub redirect_host: Option<String>,
|
||
}
|
||
|
||
/// GetUpdates 请求
|
||
#[derive(Debug, Serialize)]
|
||
pub struct GetUpdatesReq {
|
||
pub get_updates_buf: String,
|
||
pub base_info: BaseInfo,
|
||
}
|
||
|
||
/// GetUpdates 响应
|
||
#[derive(Debug, Deserialize)]
|
||
pub struct GetUpdatesResp {
|
||
#[serde(default)]
|
||
pub ret: i32,
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub errcode: Option<i32>,
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub errmsg: Option<String>,
|
||
#[serde(default)]
|
||
pub msgs: Vec<WeixinMessage>,
|
||
#[serde(default)]
|
||
pub get_updates_buf: String,
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
#[allow(dead_code)]
|
||
pub longpolling_timeout_ms: Option<i64>,
|
||
}
|
||
|
||
/// SendMessage 请求
|
||
#[derive(Debug, Serialize)]
|
||
pub struct SendMessageReq {
|
||
pub msg: WeixinMessage,
|
||
}
|
||
|
||
/// NotifyStart/NotifyStop 响应
|
||
#[derive(Debug, Deserialize)]
|
||
pub struct NotifyResp {
|
||
#[serde(default)]
|
||
pub ret: i32,
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub errmsg: Option<String>,
|
||
}
|
||
|
||
/// 登录结果
|
||
#[derive(Debug, Clone)]
|
||
pub struct LoginResult {
|
||
pub token: String,
|
||
pub account_id: String,
|
||
pub base_url: String,
|
||
#[allow(dead_code)]
|
||
pub user_id: String,
|
||
}
|
||
|
||
/// 收到的消息事件
|
||
#[derive(Debug, Clone)]
|
||
#[allow(dead_code)]
|
||
pub struct MessageEvent {
|
||
pub from_user_id: String,
|
||
pub text: String,
|
||
pub context_token: Option<String>,
|
||
pub message_id: i64,
|
||
}
|