Files
wunianxiao 7deae286ca feat: 0.2.27 - 抽离数据库基础设施模块
- 新增 ias-db crate,集中管理 PostgreSQL 连接池和 SQLx 迁移
- 迁移 ias-service/migrations 到 ias-db/migrations
- ias-main 改用 ias_db::connect_db(),ias-service 只接 PgPool
- 新增操作日志 HTTP 查询接口 (GET /v1/logs, GET /v1/logs/{id})
- 新增操作日志 Web 前端页面,支持按级别/模块/关键词筛选
- 消息队列关键节点增加操作日志写入
- ias-mail 新增 mark_remote_message_seen,IMAP 已读同步
- 工作流配置加载改为文件级容错 (load_configs_with_warnings)
- ias-context 工具调用说明更新为 query_api_docs + call_http_api
- 新增高德地图地理编码 HTTP 能力配置

模块版本: ias-db 0.1.0, ias-ai 0.1.14, ias-context 0.1.6, ias-http 0.1.15, ias-mail 0.1.16, ias-service 0.1.25, ias-main 0.2.27
2026-07-08 17:44:21 +08:00
..

wechat 模块使用文档

wechat 是微信 iLink Bot API 的 Rust 封装,提供两层能力:

  • WeChatClient:单账号客户端,负责扫码登录、恢复登录态、注册监听器、长轮询收消息、发送文本消息和输入中状态。
  • WeChatMultiAccountManager:多账号运行时管理器,按账号启动多个独立长轮询任务,并通过事件队列把消息交给业务层。

设计原则:一个 WeChatClient 只代表一个微信 Bot 账号;多账号监听就是每个账号一个独立长轮询任务。这样可以保证 tokenbase_urlget_updates_bufcontext_token 不串账号。

模块结构

wechat/
  src/
    lib.rs       # 模块导出
    client.rs    # 单账号 WeChatClient
    manager.rs   # 多账号 WeChatMultiAccountManager
    types.rs     # iLink Bot API 协议类型

公开模块:

pub mod client;
pub mod manager;
pub mod types;

常用导入:

use wechat::client::WeChatClient;
use wechat::manager::{WeChatAccountConfig, WeChatEvent, WeChatMultiAccountManager};
use wechat::types::{MessageItem, WeixinMessage};

Cargo 引用

workspace 内其他 crate 使用:

[dependencies]
wechat = { path = "../wechat" }

环境变量

变量 必填 默认值 说明
WEIXIN_BASE_URL https://ilinkai.weixin.qq.com iLink Bot API 地址。显式传入 base_url 时优先使用显式值。
WEIXIN_APP_ID bot iLink App ID,请求头会随所有 API 请求发送。

单账号使用

单账号生命周期:

WeChatClient::new
login 或 set_auth
set_updates_buf
notify_start
receive_messages loop
send_typing / cancel_typing
send_text
notify_stop

扫码登录

use wechat::client::WeChatClient;

#[tokio::main]
async fn main() -> Result<(), String> {
    let client = WeChatClient::new(None);

    let login = client
        .login(
            |qrcode_url| println!("二维码地址: {qrcode_url}"),
            120,
        )
        .await?;

    println!("account_id={}", login.account_id);
    println!("base_url={}", login.base_url);

    // 业务层需要持久化:
    // login.token
    // login.account_id
    // login.base_url
    // login.user_id

    Ok(())
}

恢复登录态并收消息

use std::time::Duration;

use wechat::client::WeChatClient;
use wechat::types::WeixinMessage;

#[tokio::main]
async fn main() -> Result<(), String> {
    let mut client = WeChatClient::new(None);

    client
        .set_auth(
            "持久化保存的 token",
            "持久化保存的 account_id",
            "持久化保存的 base_url",
        )
        .await;
    client
        .set_updates_buf("持久化保存的 get_updates_buf")
        .await;

    client.notify_start().await?;

    loop {
        let updates = client.receive_messages().await?;
        let latest_buf = client.get_updates_buf().await;

        // 建议每次成功 receive 后持久化 latest_buf。
        persist_updates_buf(&latest_buf).await;

        for msg in updates.msgs {
            if msg.msg_type != Some(WeixinMessage::TYPE_USER) {
                continue;
            }

            let Some(from_user_id) = msg.from_user_id.as_deref() else {
                continue;
            };
            let Some(text) = msg.text_content() else {
                continue;
            };

            client
                .send_text(
                    from_user_id,
                    &format!("收到: {text}"),
                    msg.context_token.as_deref(),
                )
                .await?;
        }

        tokio::time::sleep(Duration::from_secs(1)).await;
    }
}

async fn persist_updates_buf(_buf: &str) {}

多账号监听

多账号监听使用 WeChatMultiAccountManager。它会为每个账号创建一个独立 WeChatClient 和一个独立 tokio 任务:

account A -> WeChatClient A -> getupdates(token A, buf A)
account B -> WeChatClient B -> getupdates(token B, buf B)
account C -> WeChatClient C -> getupdates(token C, buf C)

这不是短轮询刷接口,而是长轮询。单次 getupdates 请求通常会被服务端挂起到有消息或超时,因此多个账号对应多个挂起请求是预期模型。

账号配置

use wechat::manager::WeChatAccountConfig;

let account = WeChatAccountConfig::new(
    "account_id",
    "token",
    "base_url",
    "get_updates_buf",
);

字段说明:

字段 说明
account_id 微信 iLink Bot 账号 ID,来自登录返回的 LoginResult.account_id
token 登录返回的 LoginResult.token
base_url 登录返回的 LoginResult.base_url
updates_buf 上次轮询保存的 get_updates_buf,新账号可传空字符串。

启动多个账号

use wechat::manager::{WeChatAccountConfig, WeChatEvent, WeChatMultiAccountManager};
use wechat::types::WeixinMessage;

#[tokio::main]
async fn main() -> Result<(), String> {
    let (mut manager, mut event_rx) = WeChatMultiAccountManager::new(1024);

    let accounts = load_accounts().await;
    for account in accounts {
        manager.start_account(account).await?;
    }

    while let Some(event) = event_rx.recv().await {
        match event {
            WeChatEvent::UpdatesBuf {
                account_id,
                updates_buf,
            } => {
                persist_updates_buf(&account_id, &updates_buf).await;
            }
            WeChatEvent::Message {
                account_id,
                message,
                updates_buf,
            } => {
                persist_updates_buf(&account_id, &updates_buf).await;

                if message.msg_type != Some(WeixinMessage::TYPE_USER) {
                    continue;
                }

                let Some(from_user_id) = message.from_user_id.as_deref() else {
                    continue;
                };
                let Some(text) = message.text_content() else {
                    continue;
                };

                manager
                    .send_text(
                        &account_id,
                        from_user_id,
                        &format!("账号 {account_id} 收到: {text}"),
                        message.context_token.as_deref(),
                    )
                    .await?;
            }
            WeChatEvent::PollError { account_id, error } => {
                tracing::warn!("微信账号轮询失败 account_id={account_id}: {error}");
            }
        }
    }

    Ok(())
}

async fn load_accounts() -> Vec<WeChatAccountConfig> {
    vec![]
}

async fn persist_updates_buf(_account_id: &str, _buf: &str) {}

停止账号

停止单个账号:

manager.stop_account("account_id").await?;

停止全部账号:

let results = manager.stop_all().await;
for (account_id, result) in results {
    if let Err(err) = result {
        tracing::warn!("停止微信账号失败 account_id={account_id}: {err}");
    }
}

stop_account 会先中止本地轮询任务,再尽力调用 notify_stop()。如果进程直接退出,Drop 会 abort 本地任务,但无法异步调用 notify_stop(),所以生产环境建议在收到退出信号时显式调用 stop_all()

为什么不能一个轮询监听多个账号

iLink Bot 的 getupdates 是按 Authorization: Bearer <token> 鉴权的。一个 token 只对应一个 Bot 账号,服务端不会在同一个请求里返回其他账号的消息。

必须隔离的账号状态:

状态 为什么必须隔离
token 决定当前请求属于哪个账号。
base_url 登录可能返回账号实际使用的 IDC 地址。
get_updates_buf 轮询游标,混用会导致消息重复、丢失或串账号。
context_token 回复上下文,混用会导致回复到错误会话或上下文断裂。

因此多账号正确模型是“多个长轮询任务”,不是“一个共享轮询”。

持久化建议

本 crate 不绑定数据库或文件系统,业务层应自行持久化以下数据。

账号表建议字段:

account_id      string primary key
token           string
base_url        string
user_id         string optional
updates_buf     string
updated_at      timestamp

收到 WeChatEvent::Message 后,使用事件里的 updates_buf 更新对应账号的游标。

如果要支持主动发送消息,建议额外维护:

account_id
from_user_id
context_token
updated_at

收到用户消息时按 account_id + from_user_id 保存 context_token。主动发送时优先显式传 account_id,再取对应用户最近的 context_token

发送消息路由

多账号场景下发送消息必须明确使用哪个账号:

manager
    .send_text(
        "account_id",
        "to_user_id",
        "回复内容",
        Some("context_token"),
    )
    .await?;

不建议只按 to_user_id 自动推断账号。原因是同一个用户可能和多个 Bot 账号都有会话,自动推断会产生歧义。

API 端点

当前封装使用以下 iLink Bot API

方法 路径 用途
GET /ilink/bot/get_bot_qrcode 获取扫码登录二维码
GET /ilink/bot/get_qrcode_status 查询扫码状态
POST /ilink/bot/msg/notifystart 注册监听器
POST /ilink/bot/msg/notifystop 注销监听器
POST /ilink/bot/getupdates 长轮询拉取消息
POST /ilink/bot/sendmessage 发送消息
POST /ilink/bot/getconfig 获取用户配置,包括输入中状态所需的 typing_ticket
POST /ilink/bot/sendtyping 发送或取消输入中状态

登录后请求会带:

Authorization: Bearer <token>
AuthorizationType: ilink_bot_token

错误处理

场景 当前行为 建议
二维码过期 login() 返回 二维码已过期 重新登录。
登录超时 login() 返回 登录超时 增大超时或重新登录。
长轮询超时 receive_messages() 返回空消息 正常情况,继续轮询。
游标更新 manager 发送 WeChatEvent::UpdatesBuf 立即持久化对应账号的 updates_buf
轮询错误 manager 发送 WeChatEvent::PollError 并继续重试 记录日志,必要时触发重新登录。
token 失效 API 返回错误或 HTTP 鉴权失败 清理该账号 token,重新扫码登录。
多账号重复启动 start_account() 返回错误 保证同一 account_id 只启动一次。

日志

模块使用 tracing,主要 target

ias::auth

推荐初始化:

tracing_subscriber::fmt()
    .with_env_filter("info,ias::auth=debug")
    .init();

当前限制

  • send_text() 只封装文本发送;输入中状态通过独立的 send_typing() / cancel_typing() 发送。
  • manager 不内置账号持久化,业务层负责数据库或文件存储。
  • manager 通过 JoinHandle::abort() 停止长轮询任务;如需立即优雅取消 HTTP 请求,可后续在 client 层增加 cancellation 支持。
  • ILINK_APP_ID 当前为空字符串;如果接入方要求 app id,需要在 client.rs 中配置或改为环境变量。

排障清单

  1. 确认 base_url 可访问。
  2. 确认账号已经扫码登录并保存了 token。
  3. 每个账号只启动一个监听任务。
  4. 每个账号独立保存 updates_buf
  5. 登录或恢复后先调用 notify_start(),再开始 getupdates
  6. 发送消息时显式指定 account_id,并尽量透传 context_token
  7. 打开 ias::auth=debug 查看请求和轮询状态。