在 Discord 频道和 QQ 群之间双向转发消息。
本项目 fork 自 koishi-plugin-dcqq-relay,针对 WillBot 工作区重构并继续维护。
- 每条
relation连接一个 Discord 频道和一个 QQ 群,所有关系必须严格一对一。同一个 Discord 频道或 QQ 目标不能出现在多条关系中。为兼容旧数据库记录,不同 QQ 平台也不能复用相同的原始forwardChannel。 discordGuild和discordChannel可在 Discord 开发者模式下,通过服务器和频道的右键菜单复制 ID。forwardPlatform填写 QQ 适配器的平台名(例如onebot或satori),forwardChannel填写 QQ 群的频道 ID。- 插件必须使用
database服务。资源转存使用标准assets服务;未启用资源转存时,assets服务可以不安装。
downloadAssets 是一组仅作用于 Discord → QQ 方向的开关:
downloadAssets:
image: true
file: true
audio: true
video: true四个字段分别控制图片、文件、音频和视频。启用某一类型后,插件会先通过 Koishi 的标准 assets 服务保存对应资源,再把保存后的地址发送到 QQ;未启用的类型继续使用原始远程地址。
如果 assets 服务不可用,或单个资源下载、识别或上传失败,插件会记录警告并保留该资源的原始地址,不会因此丢弃整条消息。QQ → Discord 方向不会执行资源转存。
- Discord 消息编辑会同步到 QQ。插件会替换该消息此前产生的全部 QQ 消息并重建映射;发送新内容失败时会保留旧消息。编辑同步不会触发真实 @,只保留可见文本,避免重复通知。QQ 侧的消息编辑不会同步。
- 删除会处理同一源消息对应的全部目标消息,避免一条消息被适配器拆分后留下孤儿消息。
- 回复消息时,插件会查找原消息的有效映射;如果存在多个目标消息 ID,则引用其中第一个仍然有效的消息。
- 在平台和 relation 不变时,旧版本数据库记录会继续兼容读取,无需清空
dcqq_relay表。更换 QQ 适配器属于例外,见下方的 Milky 迁移说明。
QQ → Discord 方向会直接发送图片和视频;音频会上传为普通附件,以便同时保留 QQ 作者、引用和 @。QQ 文件会转成可见链接,避免被 Discord encoder 静默丢弃。
reactions 控制一条关系中的双向表情回应同步,默认启用。无需配置时,QQ 和 Discord 共有的单个 Unicode emoji 码点会自动双向映射:
reactions:
enabled: true
presets: []
mappings: []
fallbacks: {}QQ 系统表情、Discord 自定义 emoji,以及旗帜、肤色、键帽、ZWJ 家庭等复合 emoji,需要一对一显式映射:
reactions:
enabled: true
mappings:
# QQ 系统表情 -> Discord custom/application emoji
- qq: face:14
discord: qface_smile:123456789012345678
# 也可以将 QQ 系统表情近似为一个 Discord Unicode reaction
- qq: face:76
discord: 👍🏽QQ 一侧填写 face:<QQ 表情 ID> 或一个 Unicode emoji;Discord 一侧填写 Unicode emoji,或 <name>:<snowflake ID>(也接受 custom:<name>:<ID>,不接受客户端展示格式 <:name:id>)。其中 name 只能由 2~32 位 ASCII 字母、数字或下划线组成。显式映射的两端都必须唯一。被显式规则占用的端点不会再参与自然映射,避免一次取消误删另一种 reaction。Discord sticker 不是 reaction,不在此功能范围内。
Discord reaction 只能使用 Unicode 或已经存在的 custom/application emoji,不能直接使用图片 URL。Discord Application Emoji 由应用持有,可以跨服务器使用。插件提供 qq-application-emojis 预设:它会从当前 relay bot 的应用读取名字严格符合 qq_face_<QQ 表情 ID> 的 emoji,并自动生成双向规则。例如 qq_face_424:123456789012345678 会映射为 QQ 的 face:424,无需把每个 Discord snowflake ID 写进配置。
插件可以直接读取 NapCat 或桌面 QQ 的 emoji-resource 目录。路径是插件级配置,不写在单条 relation 中:
qqFaceUpload:
resourceDir: ~/.config/QQ/global/nt_data/Emoji/emoji-resource
overlayDirs:
- ~/.config/QQ/nt_qq_<account>/nt_data/Emoji
preferAnimated: true
ffmpegPath: ffmpeg
includeUnlisted: true上传器自动识别两种布局:NapCat/全局资源包使用 face_config.json 中的 QSid 与 sysface_res/{static,apng}/s<ID>.png;桌面 QQ 的账号缓存使用 BaseEmojiSyastems/EmojiSystermResource 下的 ID 索引与 <ID>/{png,apng}/<ID>.png。overlayDirs 会按顺序叠加到主目录之上,适合用全局资源包保留历史 ID,再由更新的账号缓存补充 face:424 等新表情;同 ID 优先后面的静态/动画资源,同时保留前面目录提供的描述或动画。
includeUnlisted 默认开启,以覆盖磁盘上存在、但当前索引没有列出的历史或刚更新资源;临时关闭可使用指令的 --listed-only。过滤发生在全部目录合并之后,因此一个目录提供的新索引也能启用另一个目录中的旧图片。
dcqq.upload-qq-faces 是权限等级 4 的管理指令。先查看计划,再执行上传:
dcqq.upload-qq-faces --dry-run
dcqq.upload-qq-faces
--faces 14,66 可以先试传少量 ID,--static 强制使用静态版本。存在多个在线 Discord Bot 时必须用 --bot <selfId> 选择 application;从 Discord 会话调用时会优先使用当前 Bot。指令直接复用 Discord 适配器已经认证的 HTTP 客户端,不需要额外填写或传递 token。
动画目录中的 PNG 会按 acTL chunk 判断是否为真正 APNG,随后通过 ffmpeg 逐级降低帧率和质量,转换为 128×128、最大 256 KiB 的透明 animated WebP。转换失败或始终超限时会回退静态 PNG并在结果中报告;非 128×128、超限或实际仍含动画的静态资源,也会由 ffmpeg 固化首帧并规范成 128×128 PNG。源资源目录始终只读,临时文件会自动清理。公共 Unicode emoji 不占 application emoji 配额,继续使用自然映射。
上传器会复用同名 qq_face_<ID>,只顺序创建缺少的表情,不会删除、改名或替换现有项。若现有项是静态版本,也只报告冲突;静态升级为动态必须删除重建并产生新的 snowflake ID,自动替换会使正在使用的 reaction 无法按旧 ID 清理。成功创建后,插件会自动刷新对应 Bot 的预设缓存,无需重启。
上传后启用预设:
reactions:
enabled: true
presets:
- qq-application-emojis
mappings: []
fallbacks: {}预设表会在首次需要时读取一次,并按 Discord emoji 的不可变 ID 反向识别。上述指令会在创建后自动刷新;如果在 Discord 外部重命名或删除了 application emoji,则应重启插件。显式 mappings 可以覆盖预设规则。若只上传部分 QQ 表情,未上传的表情仍可继续走自然映射或下面的兜底规则。
QFace 可以帮助查询 QQ 表情 ID;其当前索引中“续标识”为 face:424。旧式全局资源快照可能最高只到 417;桌面 QQ 的账号缓存通常能在 EmojiSystermResource/424 下补齐静态 PNG 和 APNG。若合并后的全部目录仍缺少 424,上传指令会明确提示。QFace 也明确说明表情素材版权归腾讯、仅供学习交流,插件因此不会自动下载、内置或代为授权这些图片。需要原样呈现时,请确认你对所用素材具有相应权利;不要求原样时,映射到语义相近的 Unicode emoji 是更简单、无需另备图片素材的方案。
fallbacks 是两个有方向的通配规则。下例会让无法通过显式、预设或自然规则处理的 QQ reaction 在 Discord 显示为 🚨,让 Discord 不支持的 reaction 在 QQ 显示为“续标识”:
reactions:
enabled: true
presets:
- qq-application-emojis
mappings: []
fallbacks:
qqToDiscord: 🚨
discordToQQ: face:424这里必须填写真实 Unicode 字符 🚨,不能填写 Discord 客户端 shortcode :rotating_light:。规则优先级固定为:显式 mappings → application emoji 预设 → 自然 Unicode 映射 → fallbacks。
通配规则有意允许多个源 reaction 汇聚到同一个目标。relay 会按目标聚合状态:例如两个不同 QQ 表情都变成 🚨 时,取消其中一个不会提前删除 Discord 的 🚨,只有全部对应源 reaction 都消失后才删除。对于重启前、状态过期或事件不完整的消息,如果不能证明所有源 reaction 都已取消,插件会保守地保留兜底 reaction;这可能留下残留,但不会误删仍在使用的 reaction。
同一远端 reaction 有多人参与时,对侧只显示 relay bot 自己的一枚 reaction,表示“远端仍至少有一人使用”。只有最后一名远端用户取消后才会移除。对于一条消息被拆成多条、或 QQ 合并转发被展开为子区的情况,所有同侧分片共同参与判断,但 reaction 只显示在对侧第一条有效消息上;合并转发对应父频道中的 summary。
NapCat 需要支持 group_msg_emoji_like、set_msg_emoji_like 和 get_emoji_likes,建议使用 v4.12.1 或更新版本,并在正式启用前确认部署实例确实会上报普通群消息的 reaction 事件。本工作区的 adapter-napcat 已将这些能力封装为 Koishi 标准 reaction 事件和 API。Discord 目标目前仅支持服务器频道;bot 需要接收 GUILD_MESSAGE_REACTIONS,并具备查看历史消息和添加 reaction 的权限。application-owned emoji 不需要 USE_EXTERNAL_EMOJIS;若显式映射到其他应用或服务器持有的 custom emoji,则仍需满足 Discord 对该 emoji 的可用性和权限要求。
NapCat 的 reaction 通知没有 face / emoji 类型字段,只能沿用其 ID 长度规则推断类型:不超过三位视为 QQ 系统表情,较长 ID 视为 Unicode 码点。因此 ©(169)、®(174)等短码点会与同 ID 的 QQ 系统表情冲突,不能通过 NapCat 无歧义地自然映射;Milky 1.2 的事件包含显式 reaction_type,没有这一限制。
koishi-plugin-adapter-milky@0.1.1 已实现 Koishi 的 createReaction()、deleteReaction() 和 reaction-added / reaction-removed,仅支持群消息。该适配器在 Koishi 侧只负责拼接和拆分 face|<不透明 ID> / emoji|<不透明 ID>;本 relay 的自然映射器目前进一步要求 emoji ID 是十进制 Unicode 码点。Milky 1.2 协议本身传输的是不透明的 reaction 字符串和显式的 reaction_type: face|emoji,并不规定 emoji ID 的具体编码。因此迁移前必须用所选 Milky 实现分别实测 QQ 系统表情和 Unicode emoji。Milky 协议没有 reaction 用户列表查询,因此适配器没有 getReactionList();本进程内新建消息可以按逐用户事件判断最后一人,重启后加载的旧映射只能采取“宁可残留、不提前删除”的保守策略。
NapCat 不是 Milky 协议端,不能把 adapter-milky 直接连接到现有 NapCat WebSocket。需要先部署 Milky 实现,再把 Koishi 适配器改为 adapter-milky,并把 relation 的 forwardPlatform 改成 milky。不建议仅为 reaction 立即迁移,原因如下:
- 新格式行的
relationKey、groupKey和mappingKey固化了平台名,切换后不会匹配新 relation;但真正的旧版行这些字段可能是null,兼容查询反而可能继续命中它们。 - NapCat 对外使用合成的 OneBot
message_id,Milky 使用 QQ 原始message_seq,不能靠批量替换平台字段转换历史消息 ID。 - 当前
adapter-milky把合并转发解码为milky:forward,尚未接入本插件的展开路径;其发送编码器也尚未处理file,QQ face 的通用元素解码同样缺失。 - 工作区内其他写死
onebot:<id>或platform: onebot的插件配置也要逐一迁移。
建议先用测试 QQ 并行验证普通消息、回复、媒体、文件、合并转发和 reaction,再安排明确的切换窗口。切换前必须备份数据库,并把 relationKey 或 forwardPlatform 为 null 的旧版行从在线数据中隔离:例如迁移到单独的归档表,或在确认不再需要联动后标记为已删除。否则这些行可能把 OneBot message_id 当成 Milky message_seq 使用。切换后从新消息重新建立映射;旧映射只作归档,不再承诺回复、编辑、删除和 reaction 联动。若必须保留部分历史能力,只能在 NapCat 仍在线、短 ID 映射尚未过期时,通过经实测的非标准 real_seq 或内部消息数据尽力导出 OneBot message_id → QQ 原始 seq;NapCat 的 get_msg.message_seq 仍是合成 ID,不能用于该迁移,也无法保证导出覆盖全部旧记录。
Discord 侧回复一条从 QQ 转发来的消息时,插件会在保留引用关系的同时,在 QQ 自动 @ 原发送者。QQ 侧回复一条从 Discord 转发来的消息时,只有消息开头先 @ 转发机器人,插件才会在 Discord 自动 @ 原发送者;没有开头 @ 机器人则只保留引用。开头允许存在纯空白文本,如果正文已经显式 @ 同一用户,则不会重复添加。回复本平台发出的原消息时只保留引用,不会额外通知发送者。
也可以在普通消息正文中直接指定目标平台用户:Discord → QQ 使用 @[QQ:<uid>],QQ → Discord 使用 @[DC:<id>]。平台前缀不区分大小写,用户 ID 必须是纯数字;合法语法会转换为目标平台的原生 @ 元素。若只想原样显示该语法,可在 @ 前加一个反斜杠,例如 \@[QQ:123456];普通正文转发时,反斜杠会被移除且不会触发 @。
自定义语法只解析当前消息的普通正文。行内代码、代码块、引用内容和合并转发中的历史节点会保持为普通内容,不会因为其中出现上述字符串而触发跨平台 @。
为便于 QQ 用户识别并复用 Discord 用户 ID,Discord 普通消息作者会在 QQ 侧单独显示为一行 [DC:<id>] 显示名:,正文从下一行开始;Discord 原生 @ 则显示为可直接复制的 @[DC:<id>]显示名。服务器昵称和 Discord 显示名均不存在时,才会使用 username 作为名称兜底,不会额外追加 (@username)。
回复自动 @ 依赖新映射中记录的原发送者 ID。旧数据库记录仍可正常用于引用、删除等既有功能,但由于没有该字段,回复旧消息时只会保留引用,不会自动 @;仍可手动使用上述自定义语法,无需清空数据表。
QQ 合并转发消息会展开为 Discord 子区:父频道显示一条由外层 QQ 转发者发出的摘要,包含展开后的消息数和查看提示;全部原始节点都发送到新建子区,并保留各节点的昵称、头像和可支持的媒体元素。嵌套合并转发会展平到同一个子区;循环引用、层级过深、消息过期或适配器不支持展开时,会发送可见的降级说明。
父频道入口与子区节点属于同一条消息映射;手动删除其中任一条消息,都会联动删除该合并转发在 Discord 和 QQ 上的其余映射消息。
NapCat 既支持直接读取上报中已经展开的节点,也支持通过 get_forward_msg 获取节点。其他 QQ 适配器需要提供已展开的通用消息元素,或提供兼容的 getForwardMsg() 内部方法。
当前 Discord 适配器会将子区命名为 Forward,发送完成后自动锁定并归档。目标必须是支持从消息创建公开子区的文字频道或公告频道;Bot 需要查看频道、发送消息、创建公开子区、在子区发送消息、管理 Webhook、管理子区和管理消息的权限。节点包含附件时还需要附加文件权限。
Discord 的 Super Reaction(burst)、“清空某种 reaction”与“清空全部 reaction”事件首版不会同步;插件也不会追溯启用前或离线期间发生的 reaction 变化。
只有本进程内成功转发并完整观察、且仍在内存状态窗口中的消息,才能证明其 reaction 事件基线完整。状态最多保留 24 小时,每类内存状态最多保留 10,000 项;重启、Bot 离线、超时或淘汰后,如果适配器查询能力也不足以证明仍无其他远端用户,relay 会保守地保留对侧 reaction,避免提前移除;这可能产生残留。通配兜底需要同时确认所有汇聚到同一目标的源 reaction,因此尤其遵循这一保守策略。
启用中的 reaction 若尚未全部取消,不要直接修改其自定义映射端点、兜底目标,或重建同名 application emoji。当前数据库没有记录每枚已投影 reaction 当时使用的规则;先改规则可能使旧端点无法被后续取消事件清理。请先清除旧 reaction,或保留原规则和 emoji ID 直到它们全部取消。
部分 QQ 适配器的文件上传 API 不返回消息 ID;只有文件、没有其他内容的消息可能无法建立可供后续引用或删除的映射。
Discord 适配器会分多步发送合并转发。如果入口消息已经发出,但后续创建子区、发送节点或归档子区失败,适配器不会返回已成功发送的部分,插件可能无法为这些消息建立映射或自动回滚。请在启用前确认上述频道类型和权限。
远端删除采用尽力而为策略:映射会先标记为已删除,再调用平台 API。Bot 离线或平台删除失败时,远端可能残留无法自动重试的消息。
https://github.com/willbot-koishi/koishi-plugin-w-dcqq-relay