No description
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-10-19 11:55:53 +08:00
.vscode feat: exporting telegram message to external gemini instance 2025-01-13 09:28:39 +08:00
crates/bridge feat: bridge QQ faces with animated artwork and text fallbacks 2026-10-19 11:55:53 +08:00
docs feat: bridge QQ faces with animated artwork and text fallbacks 2026-10-19 11:55:53 +08:00
.envrc fix: move to snowluma 2026-07-04 22:09:27 +08:00
.gitignore fix: move to snowluma 2026-07-04 22:09:27 +08:00
Cargo.lock feat: replace TypeScript bridge with durable Rust service 2026-10-17 15:44:51 +08:00
Cargo.toml feat: replace TypeScript bridge with durable Rust service 2026-10-17 15:44:51 +08:00
config.rust.yaml.template feat: unify service config and resolve cross-platform mentions 2026-10-17 16:47:35 +08:00
config.yaml.template unused template 2024-06-24 22:45:27 +08:00
CONTEXT.md feat: replace TypeScript bridge with durable Rust service 2026-10-17 15:44:51 +08:00
devenv.lock fix: move to snowluma 2026-07-04 22:09:27 +08:00
devenv.nix feat: unify service config and resolve cross-platform mentions 2026-10-17 16:47:35 +08:00
devenv.yaml fix: move to snowluma 2026-07-04 22:09:27 +08:00
justfile feat: unify service config and resolve cross-platform mentions 2026-10-17 16:47:35 +08:00
laika-summary-schema.sql agent: refactor gemini into standalone service 2026-06-11 01:12:06 +08:00
LICENSE Create LICENSE 2023-10-09 15:48:39 +08:00
README.md feat: bridge QQ faces with animated artwork and text fallbacks 2026-10-19 11:55:53 +08:00

Matrix–QQ Bridge(Rust 实现)

matrix-qq-bridge 是连接 QQ 群(OneBot v11)与 Matrix 房间的单进程双向消息桥接服务。本仓库提供纯 Rust(crates/bridge)实现的桥接服务与运维 CLI,已完整替代原有的 TypeScript / Node.js 与 napi-rs 混合运行链路。

仓库结构说明:旧版 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> 链接)并提供纯文本 body fallback;合并转发消息(含嵌套 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 或 OneBot reply 段)绑定在同一输出事件上;回复历史一对多关联消息时按目标会话过滤并以插入顺序(正向优先、反向回退)做确定性回退解析;双向撤回仍全量查找并撤回目标会话中的所有关联消息(QQ 撤回同步至 Matrix 时统一使用 admin 管理员身份发起撤回;OneBot delete_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 号并生成 OneBot at。目标平台有多个账号时必须用 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_driving yes / no)原子控制。
  • 持久化流水线、平台消息尝试账本与重启保护:
    • 入站事件先写入 SQLite inbox 后确认(Matrix AS 按 txn_id 幂等去重;常规 OneBot heartbeat / 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 行,但严格复用首次持久化的请求字节与发送者身份(避免重试期间因代码或资料变更改变同一 Matrix txn_id 的请求内容),且保留此前的 uncertain 尝试记录不被覆盖;迁移前无 platform_messages 记录的历史投递保持未知,绝不根据 AST 倒推伪造过去发出的请求。
  • 旧库自动迁移:从旧版 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 扩展降级机制)。

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 --release
  • just check:cargo check --workspace --all-targets
  • just test:cargo nextest run
  • just migrate:cargo run --release -- --config ./build/config.yaml migrate
  • just start:cargo run --release -- --config ./build/config.yaml run
  • just 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(本仓库不新增 systemd unit 文件),在 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 含有敏感凭据(OneBot verifyKey、可选 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:

  1. 冷归档(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 密钥。
  2. 媒体后端(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>"
      
  3. 诊断日志、按大小轮转与 Panic 捕获(log):
    • log.filter(LogConfig.filter)默认值为 "info",排障时建议设为 "info,matrix_qq_bridge=debug"(仅打开桥接模块 DEBUG,不开启全局底层 HTTP trace 以免泄露密钥或鉴权头;可按 inbox/delivery/batch ID 关联、member_profile/avatar_download/avatar_decode 阶段、出站重试与 uncertain、归档 HTTP status 与 operation 排查;常规 OneBot heartbeat/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(同时保留默认 stderr hook),向专用 target matrix_qq_bridge::panic(ERROR 级别强制启用,即使 log.filter = "off" 仍会记录)输出 event = "panic"、源码位置、线程与 tokio task_id、payload_kind(及可选回溯),不在 JSON 日志中记录 panic payload 原文;Supervisor 捕获受监督任务 panic 时还会额外记录 endpoint 与 task_id 关联。由于保留了默认 stderr panic hook,stderr / systemd journal 中仍可能包含 panic 原文或敏感内容,部署时需控制 journal / stderr 访问权限。JSON 日志通过既有有界非阻塞队列写入(已通过子进程测试验证 tokio task 与主线程 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 与 Ghost user_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_messages attempt_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 + Zstd format 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 + admin getEvent 恢复原 Matrix 发送者提及(account_holders > 原作者 > admin)、嵌套合并转发 node.data 正文与 card > nickname 作者提取、OneBot heartbeat/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 规范(HTML img 与 Media captions)而非私有协议,但尚未在真实 Element 客户端界面上验证实际显示效果。QQ face 首选素材为 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