- Rust 99.9%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| .vscode | ||
| crates/bridge | ||
| docs | ||
| .envrc | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| config.rust.yaml.template | ||
| config.yaml.template | ||
| CONTEXT.md | ||
| devenv.lock | ||
| devenv.nix | ||
| devenv.yaml | ||
| justfile | ||
| laika-summary-schema.sql | ||
| LICENSE | ||
| README.md | ||
Matrix–QQ Bridge(Rust 实现)
matrix-qq-bridge 是连接 QQ 群(OneBot v11)与 Matrix 房间的单进程双向消息桥接服务。本仓库提供纯 Rust(crates/bridge)实现的桥接服务与运维 CLI,已完整替代原有的 TypeScript / Node.js 与 napi-rs 混合运行链路。
- 领域核心术语定义见
CONTEXT.md。 - 架构设计与重构计划见
docs/plans/rust-refactor.md。 - 现有桥管理员的切换、备份、自动迁移、历史查询与故障恢复操作手册见
docs/operations/rust-migration.md。
仓库结构说明:旧版
packages/、TypeScript 与napi源码、package.json、yarn.lock、.yarn*、tsconfig*.json均已移除;devenv.nix已移除 Node.js 与 Yarn 并通过ffmpeg-full提供ffmpeg/ffprobe,justfile全部采用纯 Cargo 命令(不再导出LAIKA_ROOT环境变量)。仓库保留config.yaml.template、config.rust.yaml.template、laika-summary-schema.sql与LICENSE供配置迁移与历史 schema 对照。
1. 功能范围与边界
1.1 支持的桥接与运维能力
- 双向消息桥接与会话内一对一转发:
- 每个目标会话单条输出:底层
message_links保持多对多并兼容历史一对多关联,但从本轮起,QQ 群 ⇄ Matrix 房间对每个目标会话仅生成 1 条输出消息(若同一 Matrix 房间绑定了多个 QQ 群,则每个绑定的目标群各自收到 1 条消息)。 - QQ ➔ Matrix 标准富文本与图文渲染(遵循 Matrix v1.11 规范):QQ 单张非贴纸媒体(可选附带文字说明)发送为单个事件类型
m.room.message、携带对应content.msgtype(如m.image、m.video、m.audio、m.file)及filename,并以body(含 HTML 时配合formatted_body)作为媒体说明(Media caption,Matrix v1.10+ / v1.11 规范约定);纯贴纸仍发送为单个m.sticker事件,而附带说明文字的贴纸、多张图片、图文混排或含媒体的合并转发消息均发送为单个m.room.message(content.msgtype = "m.text")事件,在formatted_body中按序内嵌<img src="mxc://..." />(非图片附件内嵌为安全<a>链接)并提供纯文本bodyfallback;合并转发消息(含嵌套node.data转发节点)提取可读正文与作者名(保留群名片优先于昵称card > nickname,缺省名称时保持@id显示不变),每一非空层级渲染为一个默认折叠的<details><summary>聊天记录(N 条)</summary><blockquote>…</blockquote></details>(N 为该层消息数),多层时逐层嵌套。 - QQ 系统表情(
face):按 QFace 索引把数字 ID 映射为表情名称;索引中有素材的表情会下载首选 APNG(精确匹配<id>.png,否则回退 PNG)并作为内联<img>与文字保持原序混排,纯表情也走同一条m.text+formatted_body路径而不会被提升为原生m.image。索引已知但无素材的表情降级为[名称],未知 ID 为[QQ表情:ID],缺失 ID 为[QQ表情]。resultId/chainCount等互动效果与mface商城表情不在本次范围内。素材为腾讯版权,客户端对 APNG 动图的播放支持因客户端而异(不支持时会显示静态首帧或占位)。 - Matrix ➔ QQ 单次完整 segments 发送:支持解析 Matrix
formatted_body中的内联<img src="mxc://..." />(非mxc://链接降级为alt文本或[image]标记,绝不当作本地下载)及标准媒体消息 caption,将整条消息的回复、发送者前缀、文本、提及、媒体与转发节点合并在单次send_group_msg的完整segments数组中发出。 - 提及、引用与撤回关联:Matrix
m.mentions汇总同一事件内的user_ids与全体提及room: true;QQ 提及机器人自身账号时,若同时处于引用回复场景,会通过永久保留的message_links结合管理员getEvent查询被引用 Matrix 事件的原始发送者(不依赖 7 天热历史inbox,解析优先级为account_holders显式持有者配置 > 被引用原 Matrix 发送者 >admin管理员用户;HTTP 查询失败时记录告警并降级为可见 fallback 而不中断投递,SQLite 存储错误仍作为致命错误处理,且入站归档raw与持久化 AST 保持原样不改写),非引用场景或回退时映射为admin。回复关系(m.relates_to.m.in_reply_to或 OneBotreply段)绑定在同一输出事件上;回复历史一对多关联消息时按目标会话过滤并以插入顺序(正向优先、反向回退)做确定性回退解析;双向撤回仍全量查找并撤回目标会话中的所有关联消息(QQ 撤回同步至 Matrix 时统一使用admin管理员身份发起撤回;OneBotdelete_msg成功返回空data时以被撤回的目标message_id确认完成,不误判为uncertain)。 - 跨平台账户持有者(
account_holders)与真实身份提及:可在顶层account_holders中按“持有者 → 各平台账号”一次性登记同一真实用户的多个平台账号(accounts为平台名到账号列表,mention_targets为平台名到单个账号)。QQ 提及已登记的 QQ 号时解析为该持有者的真实 Matrix 账号(m.mentions.user_ids与 HTML anchor 均指向真实 MXID,而非 Ghost);Matrix 提及持有者的任一真实账号(含多个别名)时解析为其 QQ 号并生成 OneBotat。目标平台有多个账号时必须用mention_targets显式指定,单账号可自动选取,未登记平台返回空并回退到旧 Ghost/admin 行为与可读文本降级;人类映射优先于旧 Ghost/admin 行为。account_holders缺省为空,旧配置行为完全不变。平台名是可扩展数据,但本版本仅matrix与qq有真实桥接支持(telegram等仅登记校验、不新增适配器);该校验不涉及权限或认证,也不保证目标用户实际收到通知。
- 每个目标会话单条输出:底层
- 身份与群控制:Matrix 虚拟用户(Ghost,
@{namePrefix}_qq_{id}:{domain})昵称与头像(qq_avatar,同时同步房间内资料与 Ghost 全局头像)同步、管理员用户(@{namePrefix}_admin:{domain})资料初始化及以管理员 intent 读取房间成员资料m.room.member(仅在明确收到M_FORBIDDEN时执行一次管理员register+join后重读,仍失败或其它错误则记录WARN并回退,附带member_profile/avatar_download/avatar_decode阶段诊断)、以及各群开关状态(is_drivingyes/no)原子控制。 - 持久化流水线、平台消息尝试账本与重启保护:
- 入站事件先写入 SQLite
inbox后确认(Matrix AS 按txn_id幂等去重;常规 OneBotheartbeat/lifecycle元事件与 Matrix 事务外壳仅保留raw归档,不再报Unknown数据丢失告警,未知业务事件仍告警); - 出站投递在每次实际发起网络发送前,先向
platform_messages表(schema 迁移20261017000003_platform_messages.sql)持久化当次attempt_no的实际平台请求体(state = 'prepared',绝不包含认证头或 token),而不仅保存高层投递计划;发送完成后,在同一 SQLite 事务内原子更新回执(Matrix 记录{"kind":"matrix_response","event_id":...},OneBot 记录客户端提供的{"kind":"onebot_data","data":...},不伪造完整 HTTP 响应或远端平台回显)、remote_id、message_links与关联快照links_json;失败或结果不确定时记录脱敏诊断信息并进入终态failed或uncertain。 - 重试会创建新的
attempt_no行,但严格复用首次持久化的请求字节与发送者身份(避免重试期间因代码或资料变更改变同一 Matrixtxn_id的请求内容),且保留此前的uncertain尝试记录不被覆盖;迁移前无platform_messages记录的历史投递保持未知,绝不根据 AST 倒推伪造过去发出的请求。
- 入站事件先写入 SQLite
- 旧库自动迁移:从旧版
extra-storage-sqlite.db先通过VACUUM INTO生成.pre-rust.sqlite一致性快照,再以流式有界分块读取(每批 256 行)与分表批量INSERT(如legacy_kv每批 256 行、message_links每批 100 行)在单个 SQLite 事务内导入bridge.sqlite(依次输出INFO事件migration_snapshot_start/completed/reused、migration_count_completed、migration_import_start、migration_progress[含processed_rows、total_rows、percent、rows_per_sec及可选eta_secs]、migration_committing、migration_completed;注意100%与migration_committing不代表事务已提交,仅在COMMIT成功后才输出migration_completed;本地150,002条[约 15 万条]样本在debug下实跑4.55s,不外推生产规模),保留全部原始 KV、恢复一对多双向消息关联(message_links)及群开关/头像设置,旧源库与快照保持不变;run迁移期间若收到SIGINT/SIGTERM,在VACUUM INTO完整写完快照后或分批导入检查点处回滚未提交事务(不写legacy_import标记,保留原库、完整快照及-wal/-shm附属文件,从头重试而非续传;若rollback自身失败则按真实数据库错误返回非零退出码;若已提交则保持成功不撤销),回滚成功后输出"status": "cancelled"并以退出码0退出。 - 冷归档与热冷查询:已完成处理的入站事件(
record_kind = "inbox")与终态出站平台消息尝试(record_kind = "platform_message",仅sent/failed/uncertain,prepared不归档)通过各自独立的归档流按批次编码为Parquet + Zstd(format v3,向后兼容读取 v1 / v2)写入归档存储;支持通过 CLI 执行热数据与冷归档按(record_kind, record_id)去重的统一查询及 JSONL / Markdown 导出。
1.2 已停用与首轮不支持项
- 停用 Gemini 与 Mumble:Rust 实现不加载 Gemini 或 Mumble 的配置与外部依赖服务(如 PostgreSQL、Redis),但旧数据库中的相关历史键值会原样保留在
legacy_kv表中,不会删除。 - 首轮不支持能力:
- 不支持 Matrix 加密房间(收到
m.room.encrypted事件时仅原样落入inbox保存原始事件,不执行跨端桥接投递); - 不支持私聊(DM)、表情反应(Reactions)与线程(Threads);
- 不支持运行期动态插件加载(仅在内部保留编译期
ExtensionRegistry扩展降级机制)。
- 不支持 Matrix 加密房间(收到
2. 构建与运行环境要求
2.1 编译构建与 justfile 快捷命令
编译需要 Rust 工具链与 Cargo。编译生成的二进制程序独立运行,编译期与运行期均无需安装 Node.js 或 Yarn:
cargo build --release
编译产物位于 ./target/release/matrix-qq-bridge。仓库的 devenv.nix 已提供 Rust 工具链、cargo-nextest、just 与 ffmpeg-full(含 ffmpeg 与 ffprobe),开发者与管理员可直接在 devenv shell 中完成编译与运行。仓库根目录的 justfile 提供了常用纯 Cargo 配方(默认读取 config := "./build/config.yaml",不再导出 LAIKA_ROOT 环境变量):
just build:cargo build --releasejust check:cargo check --workspace --all-targetsjust test:cargo nextest runjust migrate:cargo run --release -- --config ./build/config.yaml migratejust start:cargo run --release -- --config ./build/config.yaml runjust start_dumb *args:cargo run -- --config ./build/config.yaml dumb {{args}}
2.2 外部必需依赖:ffmpeg 与 ffprobe
桥接运行期需要调用 ffmpeg 与 ffprobe 可执行文件:
- 用于贴纸、动图及特殊图像格式(如视频/WebM ➔ GIF、静态 WebP ➔ PNG)的受控子进程转码与媒体探测;
- 转码作业在
<data_dir>/media-work下运行,受超时与取消令牌(CancellationToken)控制,单次转码失败不会中断其他消息流转; - 运行环境说明:仓库
devenv.nix中的ffmpeg-full已同时提供ffmpeg与ffprobe,在devenv shell内构建或直接启动服务时可直接使用;但宿主机systemd等外部服务管理器不会自动继承devenv的PATH(本仓库不新增systemdunit 文件),在devenv shell之外启动二进制时,仍须确保启动环境的PATH中包含可用的ffmpeg与ffprobe。
2.3 服务配置全量收敛至 config.yaml 与凭据安全(chmod 600)
桥接的所有服务配置项(含对象存储密钥、日志过滤级别与数据路径)均统一收敛在 config.yaml 中,部署运行不再要求额外配置 AWS_ACCESS_KEY_ID、AWS_SECRET_ACCESS_KEY、RUST_LOG、LAIKA_ROOT 或 INIT_CWD 等服务环境变量:
- S3 凭据直接配置与脱敏:当冷归档(
archive.backend: s3)或媒体存储(media.backend: s3)使用 S3 兼容对象存储时,直接在config.yaml中配置archive.access_key_id/archive.secret_access_key(保留archive.prefix可配)或media.s3.access_key_id/media.s3.secret_access_key(保留media.s3.prefix可配);模板与文档示例仅使用假占位符。代码内部统一使用SecretStr封装并脱敏(Debug、日志与check-config输出绝不打印明文密钥)。archive.backend: local本地归档不需要配置密钥。 - 无环境变量回退(Fail-fast 校验):已移除旧
access_key_env/secret_key_env字段及AWS_*环境变量读取逻辑;当启用 S3 后端但未在配置文件中提供非空access_key_id与secret_access_key时,配置校验直接失败,不会回退读取进程环境变量。 - 旧配置升级注意:此前使用
access_key_env/secret_key_env(仅靠环境变量注入 S3 密钥)的存量部署,升级时须由管理员手工将真实密钥值转写进私有config.yaml的access_key_id/secret_access_key字段,程序不会自动读取环境变量迁移 secret。 - 文件权限与保密纪律:生产环境的真实
config.yaml含有敏感凭据(OneBotverifyKey、可选downloadImage.authorizationHeader及 S3 密钥),必须将文件权限收紧为chmod 600 config.yaml,严禁将含真实凭据的配置文件提交(commit)至 Git 仓库,也不要在工单、日志或对话中明文发送凭据或 token。
3. 配置文件与 Matrix 注册文件
完整配置示例请参考仓库根目录下的 config.rust.yaml.template(旧版配置结构参考 config.yaml.template)。
3.1 兼容旧配置与身份保真要求
Rust 实现可直接解析旧版 YAML 配置顶层键(支持 mirai 或别名 onebot,自动忽略 rembgService、Gemini、Mumble 等旧插件字段)。从旧版切换时,必须原样保留以下配置项,否则会导致虚拟用户身份或历史消息关联失效:
matrix.namePrefix(决定 Ghost ID 前缀@{namePrefix}_qq_{id}:{domain}与管理员@{namePrefix}_admin:{domain})matrix.domain(Matrix Homeserver 域名)matrix.registration.path与matrix.registration.localpart(AS 发送者身份@{localpart}:{domain})bridgedGroups(QQ 群号qq与 Matrix 房间mx绑定列表;mx为完整房间 ID,既可用旧式!opaque:server,也可用房间版本 12 起的无冒号!opaque形式;允许多个 QQ 群映射到同一 Matrix 房间,按原样保留,不做隐式路由;旧库消息映射恢复依赖此列表解析房间 ID)
3.2 注册文件(registration.yaml)处理原则
- 桥接程序直接读取并校验现有的 Matrix Application Service 注册文件(含
as_token、hs_token与namespaces.users)。 - 程序绝不会自动生成、覆盖或替换既有注册文件中的 token。
- 入站 AS 接口仅接受标准的
Authorization: Bearer <hs_token>请求头,不支持通过 URL query string 传递access_token。
3.3 run 必需的新增配置段(archive 与 media)及诊断日志(log)
仅执行离线旧库迁移(migrate)时无需填写 archive 与 media;但执行配置检查(check-config)或启动服务(run)时,必须显式配置 archive 与 media:
- 冷归档(
archive):- 生产环境应配置
backend: s3并直接在archive下提供bucket、endpoint、region、可选prefix以及凭据access_key_id/secret_access_key(代码内部以SecretStr脱敏保存,缺少或为空时直接校验失败,不再支持access_key_env/secret_key_env且无环境变量 fallback)。 backend: local仅用于本地测试、dumb演练与离线排障,不作为无限容量的远程生产归档,且不需要配置 S3 密钥。
- 生产环境应配置
- 媒体后端(
media):backend: matrix(MatrixMediaStore):复用本地同域mxc://原件,必须由管理员确认 Matrix 媒体服务已配置永久保留策略并将matrix_retention_confirmed设为true,同时将 MMR 数据库与底层媒体对象共同纳入日常备份。backend: s3(S3MediaStore):将媒体原件写入自有 S3 存储桶,在media.s3下提供bucket、endpoint、region、可选prefix以及凭据access_key_id/secret_access_key(同样以SecretStr脱敏且无环境变量 fallback);发往 Matrix 房间时,桥接会额外向 Matrix 上传一份分发副本以获取符合规范的mxc://URI。- 注意:在
matrix与s3媒体后端之间切换配置不会自动迁移历史媒体对象;另外归档记录的是媒体原件定位,与ffmpeg生成的临时转码副本严格区分。 - Backblaze B2 配置示例与 HTTP
501兼容说明:Backblaze B2 不支持带If-None-Match: *的条件PUT(返回501)。当endpoint为官方域名s3.<region>.backblazeb2.com时程序自动识别(无新增配置开关),改为先GET:内容相同按幂等成功、内容不同报Conflict、仅明确404时执行不含If-None-Match的普通PUT(已在本地通过 HTTP 测试验证,线上真实 B2 待部署联调)。该流程不是原子 CAS,同一bucket+prefix仅允许单实例写入;其它 S3 仍保留条件写;自定义反代域名不会自动识别且不声称支持。简洁示例(region与endpoint须填写实际存储桶区域而非固定值,两个层级的prefix保持不变):archive: backend: s3 bucket: my-archive-bucket endpoint: https://s3.us-west-004.backblazeb2.com region: us-west-004 # 须填写存储桶实际所在区域 prefix: chat access_key_id: "<your-access-key-id>" secret_access_key: "<your-secret-access-key>" media: backend: s3 s3: bucket: my-media-bucket endpoint: https://s3.us-west-004.backblazeb2.com region: us-west-004 # 须填写存储桶实际所在区域 prefix: chat access_key_id: "<your-access-key-id>" secret_access_key: "<your-secret-access-key>"
- 诊断日志、按大小轮转与 Panic 捕获(
log):log.filter(LogConfig.filter)默认值为"info",排障时建议设为"info,matrix_qq_bridge=debug"(仅打开桥接模块DEBUG,不开启全局底层 HTTP trace 以免泄露密钥或鉴权头;可按inbox/delivery/batchID 关联、member_profile/avatar_download/avatar_decode阶段、出站重试与uncertain、归档 HTTPstatus与operation排查;常规 OneBotheartbeat/lifecycle与 Matrix transaction 外壳保留raw但不再报数据丢失告警,未知业务事件仍告警);程序不再读取RUST_LOG环境变量。log.panic_backtrace默认值为false;设为true时通过Backtrace::force_capture()捕获回溯,无需设置RUST_BACKTRACE环境变量(若 release 构建开启了符号剥离 strip symbols,会影响回溯中的符号解析)。log.max_files(默认7)与log.max_file_bytes(默认33554432,即32 MiB)保持不变。- 诊断日志同时输出至 stderr(人类可读文本)与
<data_dir>/logs/bridge.log.*(结构化 JSON 行,按单文件大小轮转,默认最多保留7 × 32 MiB)。 - Panic 日志与隐私/可靠性边界:安装自定义 panic hook(同时保留默认
stderrhook),向专用 targetmatrix_qq_bridge::panic(ERROR级别强制启用,即使log.filter = "off"仍会记录)输出event = "panic"、源码位置、线程与tokiotask_id、payload_kind(及可选回溯),不在 JSON 日志中记录 panic payload 原文;Supervisor捕获受监督任务 panic 时还会额外记录endpoint与task_id关联。由于保留了默认stderrpanic hook,stderr/systemdjournal 中仍可能包含 panic 原文或敏感内容,部署时需控制 journal /stderr访问权限。JSON 日志通过既有有界非阻塞队列写入(已通过子进程测试验证tokiotask 与主线程 unwind panic 留下日志),但不承诺无损或耐崩溃落盘——在队列满、SIGKILL、OOM 或直接abort等场景下无法捕获或保证写入(硬崩溃同样不保证stderr一定留存日志),且仅用于本地诊断留存(非远程日志收集或告警通道)。
3.4 Conduit + matrix-media-repo(MMR)路径与鉴权说明
当线上采用 Conduit 配合 matrix-media-repo(MMR)时:
- 桥接仅调用标准 Matrix Client-Server API:
- 上传:
POST /_matrix/media/v3/upload(携带AS token与 Ghostuser_id) - 认证下载:
GET /_matrix/client/v1/media/download/{serverName}/{mediaId}(桥接绝不自行将mxc://拼接为外部 HTTP URL,也不调用 MMR 私有 API 或直接读写 MMR 数据库)
- 上传:
- 反向代理需将
/_matrix/media、/_matrix/client/v1/media、/_matrix/federation/v1/media路由至 MMR,其余路径路由至 Conduit。 check-config的局限性:check-config仅在本地校验 YAML 与registration.yaml的字段完整性和命名空间规则,不会发起网络请求,也无法证明线上 Conduit/MMR 的鉴权转发或永久保留策略真实生效。线上切流前必须对 Ghost 身份媒体上传/下载执行实连验证。
4. CLI 命令使用说明
全局选项 --config <PATH> 用于显式指定配置文件路径;若省略,则默认读取当前工作目录下的 ./config.yaml(cwd/config.yaml),不再读取 LAIKA_ROOT 或 INIT_CWD 环境变量。配置文件内的所有相对数据路径(如 database、legacy_database、matrix.registration.path)仍一律相对于配置中的 data_dir(默认为配置文件所在目录)解析,而非进程当前工作目录。
机器可读结果(JSON / JSONL)仅输出至 stdout;告警与错误诊断输出至 stderr 并返回非零退出码。
4.1 校验配置:check-config
不打开数据库、不发起网络连接,校验运行期配置(含 archive、media)与 registration.yaml 是否匹配:
./target/release/matrix-qq-bridge --config /path/to/config.yaml check-config
4.2 存量数据库迁移:migrate
对旧版 extra-storage-sqlite.db 创建 .pre-rust.sqlite 一致性快照并幂等导入 bridge.sqlite(执行前必须先停止旧版桥接进程):
./target/release/matrix-qq-bridge --config /path/to/config.yaml migrate
4.3 启动桥接服务:run
启动完整桥接运行时(依次执行自动迁移、配置与注册文件校验、20 GiB 预算初始扫描、中断投递恢复与 AS 端口监听,并由 Supervisor 监督运行 matrix_ingress、onebot_driver、onebot_ingest、planner、delivery、media、profile、maintenance 8 个常驻任务端点;默认 log.filter: "info" 下依次记录 startup_started、迁移阶段事件[migration_snapshot_start/completed/reused、migration_count_completed、migration_import_start、migration_progress、migration_committing、migration_completed,其中 100% 与 migration_committing 不代表已提交,仅 migration_completed 代表 commit 成功]、matrix_listener_bound[含本地 address]、runtime_started[含 endpoint_count 与 admission,仅表示本地运行时启动而不代表远端已连接]及 shutdown_requested / shutdown_completed,若 log.filter 设为 warn/off 则隐藏普通 INFO 进度[强制 ERROR 的 matrix_qq_bridge::panic 除外];收到 SIGINT / Ctrl-C 或 SIGTERM 时置位 CancellationToken:若仍在未提交的旧库导入中则在快照写完后或分批检查点回滚[若回滚自身失败则报真实数据库错误,回滚成功则以 "status": "cancelled"、退出码 0 退出;旧已部署二进制需拉取重建后才支持迁移中断],若已进入运行期则执行有界优雅关停并在所有受监督任务退出且数据库连接池关闭后向 stdout 输出关停摘要 JSON):
./target/release/matrix-qq-bridge --config /path/to/config.yaml run
4.4 历史记录查询:query
查询本地热数据与已配置冷归档中的历史记录,按 (record_kind, record_id) 去重合并后以 JSONL 格式输出到 stdout:
# 默认安全视图(仅返回入站消息记录,不重复显示桥接自身发出的出站副本;已撤回消息隐藏原文为 "[已撤回]",标记为 retracted)
./target/release/matrix-qq-bridge --config /path/to/config.yaml query \
--room "!someroom:matrix.example.com" \
--from 1710000000000 \
--to 1710086400000 \
--keyword "关键词" \
--limit 100
# 显式使用 --raw 获取完整保真视图(含入站原件、撤回事件、AS 事务信封及出站平台消息尝试 platform_message 快照,并包含被撤回消息的原文)
./target/release/matrix-qq-bridge --config /path/to/config.yaml query \
--endpoint onebot \
--author "10001" \
--native-id "12345" \
--raw
- 支持的过滤参数:
--from <MS>、--to <MS>、--endpoint <onebot|matrix>、--room <ROOM_ID>、--author <AUTHOR>、--native-id <ID>、--keyword <TEXT>、--limit <N>、--raw。 - 视图约定:默认
HistoryView::Message仅列出record_kind = "inbox"的消息记录(排除撤回事件、matrix_transaction信封及record_kind = "platform_message"的出站尝试),因此桥接转发不会导致聊天历史翻倍,且已撤回消息的正文替换为"[已撤回]"、不输出ast/raw/media。显式传入--raw(HistoryView::Raw)时输出全部记录(含出站platform_message快照、撤回事件及被撤回消息的完整原文)。 - 若配置文件未配置
archive,命令会向 stderr 输出hot-only告警,仅返回本地 SQLite 中的近期热数据。
4.5 历史记录导出:export
支持与 query 完全相同的过滤参数及 --raw 开关,可将结果导出为 jsonl(默认)或可读的 markdown:
./target/release/matrix-qq-bridge --config /path/to/config.yaml export \
--room "!someroom:matrix.example.com" \
--format markdown \
--output /tmp/room-history.md
4.6 人工重入队投递:retry
将处于 failed 或 uncertain 状态的投递记录重新标记为 queued(仅修改数据库持久化状态,不会在 CLI 进程内直接发起网络发送,由运行中的桥接服务消费发送):
./target/release/matrix-qq-bridge --config /path/to/config.yaml retry <DELIVERY_ID>
重要警告:OneBot 投递进入
uncertain状态意味着请求可能已发出但未收到确定回执。在执行retry前,管理员必须先核对目标 QQ 群中是否已实际出现该消息;盲目重试可能导致 QQ 群内消息重复发送。重试执行时会新增一条platform_messagesattempt_no记录,同时复用该delivery_id首次准备的请求体与发送者(发往 Matrix 还会复用由delivery.id派生的固定txn_id保证幂等),并保留此前已标记为uncertain的历史尝试行不被覆盖。
4.7 标记不可恢复的缺失媒体:media-missing
当某条媒体的源端原件确实已永久失效、无法下载,且阻塞了所在归档批次上传时,管理员可显式确认并将其标记为 missing:
./target/release/matrix-qq-bridge --config /path/to/config.yaml media-missing <MEDIA_ID> \
--reason "OneBot source URL expired before download"
- 不可替代正常上传:暂态上传或下载失败(
failed/pending)会在后台自动重试,绝不会被系统当作已归档;只有确认原件彻底无法取得时才可使用media-missing。 - 若该媒体已成功存储(
stored),命令会拒绝覆盖并返回错误。 - 标记后,消息的原始事件(
raw)、来源元数据(origin)与引用信息仍完整保留;--reason仅写入数据库备查,绝不打印到终端或日志。
4.8 离线模拟演练:dumb
在不连接任何网络、不触碰生产库与旧库的前提下,对 JSONL 输入在隔离数据库中运行完整解析、规划与模拟投递流水线:
./target/release/matrix-qq-bridge --config /path/to/config.yaml dumb \
--input /path/to/events.jsonl \
--database /tmp/dumb.sqlite
--database默认为<data_dir>/dumb.sqlite,若指向生产database或legacy_database会被直接拒绝。dumb模式在独立的持久 SQLite 数据库(dumb.sqlite)中运行完整入站落库、规划与投递状态流转,由模拟执行器执行离线模拟投递(返回带"kind":"dumb_response"/"mode":"dumb"标记的模拟回执与dumb-{delivery_id}远端 ID,绝不冒充真实的平台网络发送或写入真实平台请求),含媒体的动作仅输出simulated_media: true模拟记录,media表保持pending,并非实际下载、转码或归档媒体。
5. 本地容量预算、SQLite 回收与实现边界说明
- 20 GiB 软预算与 5% 安全余量:默认
storage.budget_bytes为21474836480(20 GiB),storage.hot_days为7天。预算统计扫描data_dir下所有常规文件,包含bridge.sqlite及 WAL/SHM、保留的旧库extra-storage-sqlite.db、迁移快照.pre-rust.sqlite、<data_dir>/logs/bridge.log.*JSON 诊断日志(默认7 × 32 MiB)、本地归档目录以及media-work转码目录。系统预留 5% 安全余量(已用达 95% 时关闭新入站准入),且长期保留的跨端映射表message_links与去重账本也计入该预算,因此该预算为应用层软保护而非操作系统硬配额。 - 配置外置路径不计入
data_dir预算扫描:Budget::scan_usage仅扫描data_dir目录树;若管理员将database、legacy_database、archive.local_path或dumb --database显式指向data_dir之外的路径,这些外置文件不会计入data_dir的容量统计。 - 清理前提与超限背压:维护循环每轮分别驱动
kind = 'inbox'与kind = 'platform'两个独立的归档流。仅当某条入站记录自身已归属uploaded的inbox归档批次、其名下所有平台消息尝试(platform_messages)均非prepared且已全部归属uploaded的platform归档批次、并且既有投递与媒体终态门槛全部满足时,才允许按“容量优先 + 7 天窗口”清理本地热数据(先删关联的platform_messages与deliveries,再删inbox,长期保留message_links与source_keys)。当空间占用超过 95% 阈值时,Matrix AS 入站接口直接返回503 M_UNAVAILABLE(由 Homeserver 稍后重试);而 OneBot WebSocket 协议无服务端重推补发承诺。 - SQLite 空间回收:新建的
bridge.sqlite自动启用PRAGMA auto_vacuum = INCREMENTAL,维护循环按有界页数执行incremental_vacuum并配合 WAL checkpoint 回收磁盘空间,绝不在运行期自动执行长时间阻塞的VACUUM。若既有数据库为auto_vacuum = NONE,则需管理员在停机窗口离线执行VACUUM重建。 failed媒体重试在满载时延后:MediaProcessor每轮按批上限(50条)优先处理pending新媒体,仅在pending不足批上限时用剩余配额重试failed媒体;当新媒体持续满载时,failed媒体的自动重试会顺延。- 冷归档查询为全量对象扫描:
query/export读取冷归档(Parquet + Zstdformat v3,兼容读取 v1 / v2)时按对象全量扫描(Full Scan)并在内存过滤,无远程索引或谓词下推。 - 未恢复的旧版次要表现(非阻塞):旧版发送图片前的临时 typing 提示、以及 Matrix 撤回事件固定附带
"撤回了一条消息"的reason文案未恢复(不影响双向消息与撤回功能)。 - 其他平台与会话级
account_holders覆盖暂不支持:account_holders中登记的telegram等非 Matrix/QQ 平台仅做配置校验,不支持实际收发桥接;提及解析接口虽已预留目标会话参数,但当前不支持按会话覆盖持有者映射。
6. 验证范围与当前局限
6.1 已在本地验证的范围与基线区分
- 本轮全量静态检查、构建与测试套件验证:
cargo fmt --all -- --check:通过;cargo clippy --workspace --all-targets -- -D warnings:零警告通过;cargo nextest run --workspace:498 passed、1 skipped(唯一 skipped 项为#[ignore]手动规模基准测试,已另行在debug下实跑150,002条[约 15 万条]样本通过,耗时4.55s);cargo test --workspace --doc:2 passed;cargo build --release及./target/release/matrix-qq-bridge --help:构建与运行通过;git diff --check:通过;- 完整覆盖 Room v12 无冒号
RoomId(ruma)支持、旧库迁移流式分块读取与分表批量写入单事务及取消/真实rollback错误处理、Backblaze B2 官方域名先GET后PUT兼容、管理员 intent 读取m.room.member(仅明确M_FORBIDDEN时register+join重试一次后回退)与头像下载/解码阶段诊断(含对profile告警原文与头像 URL query 脱敏防泄漏的实际路径哨兵测试)、Ghost 全局头像同步、QQ 撤回以admin身份同步至 Matrix、QQ 引用回复 +@bot通过永久message_links+admingetEvent恢复原 Matrix 发送者提及(account_holders> 原作者 >admin)、嵌套合并转发node.data正文与card > nickname作者提取、OneBotheartbeat/lifecycle与 Matrix 事务外壳去噪归档,以及服务配置全量收敛至config.yaml、matrix_qq_bridge::panic强制 target、会话内一对一转发、标准 Matrix v1.11 图文与内联mxc://<img>双向编解码、platform_messages出站尝试持久化与 Parquet format v3 双流归档。
- 历史基线附加验证证据:本地真实
ffmpeg子进程已完成 4 帧视频(WebM)➔ 4 帧 GIF、静态 WebP ➔ PNG 转码及任务取消回收验证;空PATH(无 Node.js / Yarn)环境下./target/release/matrix-qq-bridge --help、真实旧库首次与二次幂等migrate及独立dumb演练已在早期基线中实测验证通过(相关行为由自动化测试套件持续覆盖)。
6.2 尚未实测的外部与生产环境项
- 尚未在真实 Element 客户端 UI 中实测渲染效果:用户主客户端为 Element,桥接生成的多图/合并转发内联
<img src="mxc://..." />HTML 与单图filename+body媒体说明均遵循标准 Matrix v1.11 规范(HTMLimg与 Media captions)而非私有协议,但尚未在真实 Element 客户端界面上验证实际显示效果。QQface首选素材为 APNG;不同 Matrix 客户端对 APNG 动图的播放支持不一致,本仓库未验证具体客户端的播放行为,也没有做动画转换或兼容性降级。 - 尚未在真实生产数据库副本与线上配置上实测迁移:尚未接入线上真实的
extra-storage-sqlite.db、config.yaml与registration.yaml副本进行演练。 - 尚未与线上 Conduit + MMR、真实 OneBot 及远程 S3 / Backblaze B2 实连联调:上述协议与存储行为目前仅通过本地 HTTP / WebSocket / SQLite 测试验证;线上反向代理媒体路径分流、Ghost/Admin
AS token + user_id鉴权、MMR 永久保留策略与备份恢复方案、真实 Backblaze B2 / S3 桶及真实 OneBot 端长期运行表现均有待部署阶段实测。 - 目标环境
ffmpeg的动画 WebP 支持未实测:本地已验证 4 帧视频转 GIF 与静态 WebP 转 PNG,但生产目标机器上ffmpeg对动画 WebP 的解码支持尚待现场确认。
6.3 可重复执行的本地检查命令
管理员与开发者可在工作区执行以下命令复核构建、静态检查与测试套件:
# 代码格式与 Clippy 静态检查
cargo fmt --all -- --check
cargo clippy --workspace --all-targets -- -D warnings
# 运行测试套件与文档测试(或执行 `just test`)
cargo nextest run --workspace
cargo test --workspace --doc
# 构建 Release 二进制并查看 CLI 帮助(或执行 `just build`)
cargo build --release
./target/release/matrix-qq-bridge --help