# jumpbyte-plugin **Repository Path**: fox-glaze/jumpbyte-plugin ## Basic Information - **Project Name**: jumpbyte-plugin - **Description**: trss-yunzai 的douyin适配器 - **Primary Language**: Unknown - **License**: GPL-3.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-09-04 - **Last Updated**: 2026-09-06 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # jumpbyte-plugin TRSS-Yunzai 的抖音适配器。让抖音私信 / 群聊像 QQ 一样被普通插件收发。 抖音协议那侧由 [jumpbyte-bot 网关(Go)](https://github.com/sisi0318/jumpbyte-bot)实现,而**网关由本插件自动托管**:检测 Go、拉源码、编译、起进程、守活都是自动的,你只需扫一次码。自己跑网关的手动模式也保留着,见[手动模式](#手动模式第二选择)。 ## 安装 在 **Yunzai 根目录**执行,然后 `#重启`: ```bash git clone --depth 1 https://gitee.com/fox-glaze/jumpbyte-plugin ./plugins/jumpbyte-plugin ``` 首次启动会依次做这几件事(视网络约 3-8 分钟): ```text 检测 Go → 没有就下一份便携版(约 60MB)→ 克隆网关源码 → 编译 → 启动网关 → 等你登录 ``` 日志停在「等待扫码登录」时,**主人私聊机器人发 `#jp登录`**,二维码就回到你这个会话里。用抖音 App 扫码并在手机上确认,账号随即自动接入,日志出现 `JumpByte(douyin) v1.0.0 已连接` 就是好了。整个过程只需扫这一次码。 此时抖音号还没登录上,所以这条指令要发给**别的已在线适配器**(通常是 QQ)—— 它就是个普通指令,任何适配器的私聊都能触发,见[谁能用哪条](#谁能用哪条)。 用手机号 + 短信登录的号不出码,改走短信:收到验证码后私聊发 `#jp验证码 123456`。 不用装任何 npm 依赖:`ws` / `image-size` / `chokidar` 根目录已有,HTTP 走 Node 内置 `fetch`,下载解压编译分别用系统的 `fetch` / `tar` / `git`。 ## 工作方式 ```text 抖音 IM ←→ jumpbyte-bot 网关 ←─ HTTP /api/{动作} ─→ jumpbyte-plugin ←→ TRSS-Yunzai (由本插件托管的子进程) ←─ WS /ws 事件流 ── ``` 抖音协议的活全在网关那侧:它自己登录、自己维持 IM 长连接、自己解密图片与视频,对外只暴露一个 HTTP + WebSocket 接口。本插件不碰抖音协议,只做两件事——**托管这个进程**,以及把网关的事件翻成 Yunzai 事件、把 Yunzai 的 `segment` 翻成网关动作。 网关必须是独立进程:a_bogus 签名、protobuf 收包、图片 AES-GCM 解密、CENC 视频解密这套东西在 Go 侧有八千多行,翻成 JS 不现实。所以自动化的目标不是消灭这个进程,而是让它对你不可见。 ## 自动托管都做了什么 | 步骤 | 做法 | 落点 | | --- | --- | --- | | 检测 Go | 先找便携版再找系统 `go`,版本要求读网关 `go.mod` | — | | 装 Go | 官方清单取权威 sha256,从国内镜像下载,校验不过就换源重下 | `data/jumpbyte/go` | | 取源码 | `git clone --depth 1` | `data/jumpbyte/jumpbyte-bot` | | 编译 | `CGO_ENABLED=0 go build`,commit 没变就跳过 | `data/jumpbyte/gateway/jumpbyte-bot` | | 起进程 | stdio 全接管道,stdout 转 Yunzai 日志 | 端口默认 `127.0.0.1:9503` | | 守活 | 意外退出后退避重启(5s 起,最长 60s) | — | 几个刻意的取舍: - **不碰系统环境**。便携 Go 只解压到 `data/jumpbyte/go`,`GOROOT` / `PATH` 只塞进编译那一次 `spawn` 的 env,不写环境变量、不改 PATH、不进注册表;`GOMODCACHE` / `GOCACHE` 也指到这个目录。卸载就是删 `data/jumpbyte`。 - **能编译的前提是纯 Go 依赖**。网关用 `modernc.org/sqlite`(纯 Go 实现的 sqlite),所以 `CGO_ENABLED=0` 能出静态二进制、不需要 C 编译器 —— 否则 Windows 上的「自动编译」根本做不到。 - **镜像提速但不被信任**。下载体走 `mirrors.aliyun.com`,sha256 只认官方清单(`golang.google.cn/dl/?mode=json`,与 go.dev 同源)。校验不过一律删文件换源,绝不拿来源不明的压缩包去解压执行。 - **挑「最老的够用版」Go**,不挑最新:只是编译一个现成项目,新大版本刚发时生态常有兼容问题。 - **产物与工具链都在 `data/`**。该目录被 `data/.gitignore` 整个忽略,几十 MB 的东西不会污染 `git status`。 - **自动化失败不会拖垮启动**。装不上 Go 或编译失败只记日志,你仍能用 `#jp设置` 接一个手动跑的网关 —— 把 Yunzai 启动流程搞崩才是真的帮倒忙。 ## 指令 日常只会用到前四条。「权限」列见下面[谁能用哪条](#谁能用哪条)。 | 指令 | 权限 | 说明 | | --- | --- | --- | | `#jp登录` | 主人 · 仅私聊 | 取登录二维码。别名 `#jp登陆` / `#jp二维码` | | `#jp状态` | 主人 | 网关进程 / 登录阶段 / 账号在线情况 | | `#jp验证码 <6位数字>` | 主人 · 仅私聊 | 网关要短信登录时把码送进去 | | `#jp日志 [行数]` | 主人 | 看网关自己的输出,合并转发,默认 100 行、上限 200 | | `#jp会话` | 主人 | 手动刷新好友表 / 群表(在抖音账号的会话里用) | | `#jp安装` | 主人 | 手动跑一遍自动部署(上次装失败了想重来时用)。别名 `#jp部署` | | `#jp启动` / `#jp停止` / `#jp重启` | 主人 | 管网关进程,比重启整个 Yunzai 轻 | | `#jp更新` | 主人 | 更新**本插件**(git pull + 发更新日志),更新完自动 `#重启` 生效。别名 `#jp升级` / `#jp更新插件` | | `#jp强制更新` | 主人 | 同上,但先 `reset --hard`(丢弃本地改动)—— 普通更新报冲突时用 | | `#jp更新网关` | 主人 | 拉最新**网关**源码,重编并重启网关进程。别名 `#jp升级网关` | | `#jp目录 <网关目录>` | 主人 | 手动模式:同机接入,自动读 `bot.json` 的地址与 token | | `#jp设置 <地址> [token] [备注]` | 主人 | 手动模式:跨机手填,连接成功才落盘 | | `#jp账号` / `#jp删除 <地址>` | 主人 | 查看与删除已配置的网关(`#jp账号` 的别名是 `#jp列表`) | | `#jp帮助` | 主人 | 指令清单。别名 `#jp菜单` | `#jp登录` 是首次部署的最后一步:码不是这条指令生成的 —— 网关一启动读不到 `cookie.json` 就立刻出码并写成 `qrcode.png`,这条指令只是把最新那张取出来发给你,所以还会告诉你它还剩多少秒有效(见[关于登录](#关于登录))。 `#jp验证码` 是托管模式的关键一环:网关的短信验证码只从 stdin 读,托管之后那根 stdin 在 Yunzai 手里 —— 这条指令就是管子的另一端。没有它,托管起来的网关一旦要短信登录就彻底卡死。 **`#jp更新` 更新插件,`#jp更新网关` 更新网关**,两者互不相干:插件是 `plugins/jumpbyte-plugin`(这个仓库),网关是 `data/jumpbyte/jumpbyte-bot`(那个 Go 项目)。插件更新走的就是框架 `#更新jumpbyte-plugin` 那套(`git pull` + 更新日志合并转发 + 冲突/鉴权失败分类提示),只是不用记目录名;**真更新了就自动重启** —— 本插件目录含 `index.js`,按框架规则不注册热重载,不重启等于代码躺在磁盘上不生效。已是最新则什么都不做。不想自动重启就把 `update_restart` 置 `false`。配置(`config/jumpbyte.yaml`)与网关状态(`data/jumpbyte`)都在 `plugins` 之外,更新不碰它们。 **前缀与参数之间的空格可省**:`#jp验证码123456` 和 `#jp验证码 123456` 等价,`#jp日志50` 和 `#jp日志 50` 也一样。参数**之间**才必须有空格(`#jp设置 <地址> <备注>` 靠空格切三段)。开头的 `#` 也可以不打,大小写不敏感(`JP登录` 也认)。 前缀用 `jp`(jumpbyte 的缩写),照 ICQQ 适配器 `#Q设置` 的思路——用协议/后端名当前缀。不用 `#抖音…` 或 `#dy…`:同目录的 hl-douyin-plugin 已占了整套 `^#?(dy|抖音)(账号|设置|状态|帮助|菜单|登录|…)`,撞车时 `priority` 小的那个会把指令吃掉。 ### 谁能用哪条 **上表全部指令默认只有主人能用。** 普通用户(包括群主与管理员)发这些指令,会被框架拦在处理函数之前,只收到一句「暂无权限,只有主人才能操作」,插件的代码根本不会执行 —— 没有「普通用户能用一部分」的说法,要么全能用要么全不能。 「主人」认的是 `config/other.yaml` 里 `master` 那份 **`Bot账号:主人账号` 映射**(不是上面的 `masterQQ` 列表,权限判定不看它): ```yaml master: - "stdin:stdin" - "10000:20000" # QQ 号 10000 这个 Bot 的主人是 20000 - "7100000000000000000:7200000000000000000" # 抖音 uid 71… 这个 Bot 的主人是 72… ``` 所以「你是谁的主人」是**按 Bot 分的**,这也是二维码改成按需索取的原因(见[关于登录](#关于登录))。两条推论: - 你在**哪个适配器的会话里**发指令,就得是**那个 Bot** 的主人。在 QQ 上发 `#jp登录` 要你是那个 QQ Bot 的主人,在抖音会话里发就要你是那个抖音 Bot 的主人。 - **不必等抖音号先登录上**。这些指令是普通的 message 事件处理器,任何适配器的会话都能触发 —— 首次部署时抖音还没登录,你就在 QQ 上私聊发 `#jp登录`,码回到 QQ 那个会话里。 门槛由配置项 `permission` 控制,改它就是改全部指令的权限,三档取值(见 `lib/plugins/loader.js` 的 `filtPermission`): | `permission` | 谁能用 | | --- | --- | | `master`(默认) | 只有主人 | | `owner` | 主人 + 群主(**只在群里校验**,私聊等于放开给任何人) | | `admin` | 主人 + 群主 + 群管理(同上,私聊不设限) | 不建议改。这些指令能读写网关配置、停掉进程、取二维码,等于抖音号的控制权;`owner` / `admin` 在私聊里根本不校验,把它们当成「稍微放宽」是误解。 **另外两条比权限更严,`#jp登录` 与 `#jp验证码` 只在私聊回应**:即便是主人在群里发,也只会收到一句拒绝,连用法示例都不给。二维码扫一下就等于把抖音号交出去,验证码同理属于账号凭据 —— 这两条写死在代码里,不受 `permission` 影响。 ## 手动模式(第二选择) 网关跑在另一台机器上,或你本来就自己开着终端跑它 —— 这两种情况把 `config/jumpbyte.yaml` 的 `auto.enable` 改成 `false`,插件就完全不碰 Go 与子进程,只做纯适配器。 ```bash # 自己编译(产物是单个静态二进制) cd jumpbyte-bot CGO_ENABLED=0 go build -ldflags="-s -w" -o dist/jumpbyte-bot ./cmd/bot # 首次运行:无 cookie.json 时自动进入扫码登录 ./dist/jumpbyte-bot ``` 首次运行会在终端打印二维码(同时存一份 `qrcode.png`),扫码确认后登录信息写进 `cookie.json`,网关随后连接 IM 并监听 `127.0.0.1:9503`,同时生成 `bot.json`。想用手机号 + 短信登录:在 `cookie.json` 里写 `{"phone": "13800138000", "enabled": true}`,启动后**在网关的终端里**输入收到的验证码(手动模式下 `#jp验证码` 帮不上,那根 stdin 不在 Yunzai 手里)。 接入: ```text #jp目录 D:\path\to\jumpbyte-bot # 同机,地址与 token 从 bot.json 读 #jp设置 http://192.168.1.10:9503 [备注] # 跨机,只能手填 ``` 两者都是连接成功才写配置,连不上的地址不会存进 YAML。也可以只在 `config/jumpbyte.yaml` 里填 `gateway_dir`,启动时自动按该目录接入,无需任何指令。 网关工作目录里这三个文件插件都用得上: | 文件 | 内容 | 插件怎么用 | | --- | --- | --- | | `bot.json` | 监听地址、端口、接入 token | `#jp目录` 直接读,免手填 | | `cookie.json` | 抖音登录凭据、uid、手机号 | 只读 uid / 昵称 / 手机号用于 `#jp状态`,**cookie 本体从不读出** | | `qrcode.png` | 重新登录时的二维码 | 记下它的出码时间,`#jp登录` 取最新那张(跨机部署读不到,只能去网关终端扫) | ## token 是什么,跟抖音网页那个 tk 有关系吗 **没关系。** 这里说的 token 是**网关自己给自己设的接入口令**,跟抖音网页的 `tk` / `msToken` / `ttwid` 完全无关,也不需要你去抖音那边获取任何东西。 它是 `crypto/rand` 生成的 24 字节随机数、hex 编码成 48 个字符,写在 `bot.json` 的 `token` 字段里,唯一作用是防止同机其他程序随便调用网关的发消息接口。真正的抖音登录凭据是 `cookie.json` 里的 `cookie`(几千字符),它只在网关进程内使用,**永远不会进入 Yunzai,也不会出现在任何指令、日志或聊天记录里**。 自动模式下这个 token 由插件生成、由插件读取,你从头到尾看不到它。手动模式同机部署也别手抄——用 `#jp目录` 让插件自己读;48 个字符抄进聊天框既容易错,又会把口令永久留在聊天记录里。只有跨机部署(读不到对端文件)才需要 `#jp设置` 手填。 ## 关于登录 登录逻辑全在网关进程内,插件做的是把「需要人」的那几个瞬间接出来。 - **二维码由主人来取,不主动推**。发 `#jp登录`,码就回到你发指令的那个会话里。 为什么不推:TRSS 是多适配器框架,`cfg.master` 是「bot_id → 主人列表」的映射 —— 一台 Yunzai 上可能挂着 QQ、微信、Telegram 好几个适配器,每个都有自己的主人。主动推码就得替你决定推给哪个适配器的哪个主人,而那个问题没有正确答案。改成你自己来要,码回给你所在的会话,身份天然确定,也堵死了码被发进群的可能。 **只私聊、不发群**:二维码扫一下就是账号的控制权,群里发 `#jp登录` 只会收到一句拒绝。 - **码有 180 秒有效期,插件会告诉你还剩多少**。码不是 `#jp登录` 生成的:网关一启动就检查 `cookie.json`,读不到就立刻进扫码流程,把码写成 `<工作目录>/qrcode.png` —— 也就是说码在**进程启动那一刻**就已经出了。`#jp登录` 只是把最新那张取出来,所以要判有效期:过期的图发出去,你扫完只会看到抖音报错,反而更难排查。真过期了就等几秒再发一次(网关自己会发现码过期并重新登录),或者 `#jp重启` 强制重新出码。 - **需要登录时会私信提醒主人**(`qr_push`,默认开)。提醒里**只有一句「请发 `#jp登录`」,不含码**。cookie 失效后网关会自动重新登录,而网关是后台进程、终端里的码没人盯着,抖音号可能悄悄掉线好几天 —— 这条提醒就是为了让你知道该来取码了。 **只发给在管这个号的那个主人**:提醒由网关日志信号触发,那一刻并没有「谁触发的」,所以插件记住最后一个下过 `#jp` 指令的私聊会话,提醒就发给他。一个都定不出来(Yunzai 刚启动、还没人发过指令)才回落广播给全部主人 —— 宁可多几个人收到「去扫码」,也不要没人知道号掉了。想固定发给某个人,在 `config/jumpbyte.yaml` 里填 `notify_master: "10000:20000"`(写法同 `config/other.yaml` 的 `master`)。提醒会**避开抖音这个适配器自己**(它正掉线,发不出去),改用其他在线的 Bot(通常是 QQ)。关掉 `qr_push` 只是不再提醒,`#jp登录` 照样能取。 - **短信验证码走 `#jp验证码`**(仅托管模式)。网关的 `PromptSmsCode` 只从 stdin 读,托管时那根管道在插件手里,这条指令就是往里写一行。网关一开始等码,插件就私信提醒主人(同一个 `qr_push` 开关、同一套「只发给最近操作者」的规则——两者是同一类「登录要人工介入」的通知)—— 半夜失效时没有这条提醒,号会一直掉着。同样**仅私聊**。 - **账号状态反映到 `#jp状态`**。cookie 失效时网关会把账号置为 `invalid`,`#jp状态` 里能看到,并顺带告诉你这个号失效后走扫码还是走短信。 ## 配置 配置文件 `config/jumpbyte.yaml`,首次加载自动生成。装了 [锅巴](https://github.com/guoba-yunzai/guoba-plugin) 的话也可以在面板里改(插件列表 → 抖音适配器),两处改的是同一份文件。 自动托管相关(`auto` 段): | 字段 | 默认值 | 说明 | | --- | --- | --- | | `auto.enable` | `true` | 总开关。置 `false` 完全不碰 Go 与子进程,只做纯适配器 | | `auto.install_go` | `true` | 允许自动下载便携 Go;置 `false` 则只用系统已装的 | | `auto.repo` | gh-proxy 镜像 | 网关源码仓库,直连 GitHub 的话改成 `https://github.com/…` | | `auto.update` | `false` | 每次启动 `git pull` 一次**网关**源码(插件自身用 `#jp更新`) | | `auto.src_dir` | `""` | 源码目录,留空用 `data/jumpbyte/jumpbyte-bot` | | `auto.work_dir` | `""` | 网关工作目录,留空用 `data/jumpbyte/gateway` | | `auto.port` | `9503` | 网关监听端口,仅首次生成 `bot.json` 时用 | | `auto.send_channel` | `http` | 发送通道:`http`(imapi)/ `ws`(安卓端身份,群聊被风控时换它) | | `auto.restart` | `true` | 网关意外退出后自动重启(退避,最长 60 秒一次) | | `auto.wait` | `90` | 等待网关就绪的秒数。首次要拉依赖,别设太短 | | `auto.goproxy` | `goproxy.cn` | Go 模块代理,境外机器可改 `https://proxy.golang.org,direct` | 适配器本身: | 字段 | 默认值 | 说明 | | --- | --- | --- | | `permission` | `master` | 全部配置指令所需权限,见[谁能用哪条](#谁能用哪条) | | `bot` | `[]` | 手动模式的网关列表,元素为 `{url, token, name, dir}`,一般用指令维护 | | `gateway_dir` | `""` | 手动模式的网关工作目录;自动模式不用填 | | `qr_push` | `true` | 网关需要登录时私信提醒主人(**只提醒,不含码**,取码用 `#jp登录`) | | `notify_master` | `""` | 把登录提醒固定发给某人,格式 `Bot账号:主人账号`。留空则发给最近用过 `#jp` 的主人,见[关于登录](#关于登录) | | `update_restart` | `true` | `#jp更新` 真更新到新代码后自动 `#重启`(不重启不生效)。置 `false` 则只提示 | | `heartbeat` | `30` | 上行 `{"type":"ping"}` 间隔(秒),用于探活半开连接 | | `reconnect` | `5` | 断线重连间隔(秒),失败后翻倍退避、最长 60 秒 | | `timeout` | `120` | HTTP 动作超时(秒)。发视频要走 TOS 分片上传,别设太短 | | `image_quality` | `origin` | 收图取哪一档解密链接:`origin` / `large` / `medium` / `thumb` | | `image_direct` | `true` | 图片直传:自己加密并走抖音上传链,绕开网关坏掉的 `upload_image`。要读 `cookie.json`,见[发图是怎么修好的](#发图是怎么修好的) | | `image_fallback` | `auto` | 直传也失败时的退路:`auto` 公网就按 URL 发图、否则发链接 / `emoji` 总按 URL 发 / `link` 总发链接 / `off` 照旧报错 | | `image_host.url` | `""` | 图床上传接口。**强烈建议配上**,否则发图只能退回一行链接。搭建教程见 [自建图床.md](自建图床.md) | | `image_host.field` | `file` | multipart 里文件字段的名字 | | `image_host.url_path` | `url` | 图片地址在响应 JSON 里的点路径,如 `data.url`。留空则自动试探 | | `image_host.headers` | `{}` | 额外请求头,图床要鉴权时用。不要自己写 `Content-Type` | | `image_host.timeout` | `30` | 图床上传超时(秒) | | `reply_quote` | `false` | 引用回复是否真用 `send_reply`。**默认关**:网关返回成功但抖音客户端把这条渲染成空白,见[协议限制](#协议限制) | | `emit_self` | `false` | 是否把自己发出的消息也投成事件,需同时在网关 `bot.json` 开 `emit_self` | | `upload_limit` | `8388608` | 上传原始字节上限(8MB)。网关 `maxBody` 12MB,base64 膨胀 4/3 | | `video_cover` | `""` | 发视频的默认封面,支持本地路径 / http(s) / `base64://`;留空用内置占位图 | | `at_to_text` | `true` | at 降级为 `@昵称` 文本前缀;置 `false` 则丢弃 at | `bot` 数组是 lodash.merge 按下标合并的,改配置时不要在默认值里塞元素,否则删不掉。 ## 协议限制 这些不是实现偷懒,是抖音协议 / 网关能力的边界,写插件时需要知道: - **撤回只能撤自己发的**。网关 `recall` 只认 `server_msg_id`,而 WS 事件不带这个字段(`event.id` 只是网关生成的随机事件 id)。撤别人的消息会被提前拦下并给出 warn。 - **昵称一律为空**。网关事件只有 `sender_id` / `sender_sec_uid`,`get_conversations` 的成员也只有 `{uid, sec_uid, role}`,Go 端的用户解析未暴露成动作。依赖 `nickname` 的插件在抖音上会拿到空串,`sender.sec_uid` 可用作对外主键。 - **一条消息只能一种内容**。抖音没有 CQ 码式混排,「文字 + 图」会被拆成两条依次发送,返回的 `message_id` 是数组。 - **发图要自己加密再传**。网关的 `upload_image` 在等抖音永远不返回的字段,适配器改成自己走上传链 —— 单独一节讲:见[发图是怎么修好的](#发图是怎么修好的)。 - **引用回复发出去是空白**(`reply_quote` 默认 `false`)。网关的 `send_reply` 会返回成功(status=0 且给 `server_msg_id`),但抖音客户端把这条消息渲染成**空白**,用户什么都看不到。它的 content 是 `refmsg_type: 7` 外加一层嵌套的被引用消息 JSON(逆自网页版抓包),手机客户端似乎不认这个形态。而 Yunzai 里 `reply(msg, true)` 是惯例写法(本插件自己每条指令回复都带 quote),照着发等于**句句发不出来** —— 所以默认丢掉 `segment.reply` 只发正文。抖音私信本来是一对一,没有引用照样看得懂。想试就把 `reply_quote` 置 `true`。 - **发视频封面必填**。网关的 `send_video` 把 `cover` 定为必填,而抽帧需要 ffmpeg —— 没配 `video_cover` 时用内置 90×160 占位 PNG,宽高按封面推算。 - **语音 / 文件降级**。抖音 IM 没有对应类型,转成 `Bot.fileToUrl` 的临时直链发文本,链接有有效期(`cfg.bot.file_to_url_time`)。 - **按钮 / Markdown 忽略**。抖音没有按钮和富文本卡片,网关的 `send_card` / `send_action_card` 也未实现,遇到会记 debug 日志后跳过。 - **合并转发摊平**。`node` 段没有对应形态,摊成多条依次发送。 - **跨机部署会重写媒体 host**。网关 bind 在 `0.0.0.0` 时下发的 `/img`、`/video` 链接写死 `127.0.0.1`,适配器按配置里的真实网关地址重写 host;CDN 直链(douyinpic / douyinvod)保持原样。 - **token 错误表现为「连上立刻断」**。WS 鉴权发生在 Upgrade 之后 —— 网关先升级连接、再发一帧 `{"type":"error","msg":"token 无效"}` 然后关闭,不是 HTTP 401。适配器会把这帧翻成 `网关拒绝连接:token 无效`。 - **群聊发不出去可能是风控**。网关默认走 HTTP 通道(imapi),群聊在该通道可能被拒(`status_code 7523`);把 `auto.send_channel`(或网关 `bot.json` 的 `send_channel`)改成 `ws` 用安卓端身份可绕开。 ## 发图是怎么修好的 **结论先说:默认配置就能发图,不用配任何东西。** 下面是原理,遇到发图问题再看。 ### 网关的 upload_image 在等一个不存在的字段 网关报这句时,发图 100% 失败 —— 不是图太大,也不是偶发: ```text upload_image: CommitUpload 失败: 无加密信息: {"ResponseMetadata":{…,"Service":"vod",…} ``` 抖音私信的图是**加密存储**的:收图那侧要拿消息里的 `skey` 走 AES-256-GCM 才解得出明文(网关的 `internal/media/image.go` 就是干这个的)。所以发图必须先有一对 `oid` / `skey`,网关的做法是: ```text im/upload/config/v2 拿 STS 凭证 + space_name ↓ ApplyUploadInner → TOS PUT 把明文字节传上去 ↓ CommitUploadInner ← 在这里等 Encryption.Uri 与 Encryption.SecretKey ``` 实测(四组凭证 × 十余种参数组合):**抖音这个接口从不返回 `Encryption` 字段**,只回 `{Uri, UriStatus}`。网关在等一个不存在的东西。 ### 加密是发送方自己做的 判据就在网关自己的解密代码里:它拿**消息里**的 `skey` 解密。如果是服务端加密,密钥该由服务端保管,不会跟着消息传输。所以正确做法是: ```text 自己 random 一个 AES-256 密钥 ↓ 加密图:iv(12) ‖ 密文 ‖ GCM_tag(16) ↓ 上传密文(Apply → TOS PUT → Commit 拿 Uri) ↓ 把密钥当 skey 写进消息,交 send_image 发出去 ``` 这条路已在真机验证,发出的图在抖音客户端正常显示为图片消息。适配器现在就这么做(见 `Model/upload.js`),网关那个坏动作被彻底绕开。 ### 代价:适配器要读 cookie.json 换 STS 上传凭证只能用账号 cookie,所以直传会读 `<网关工作目录>/cookie.json`。这打破了原本「cookie 从不离开网关进程」的设计,是有意的让步 —— 否则发图无解。三条约束写在代码里: - cookie 只在内存流转,**不写日志、不进错误消息**(相关 `catch` 特意不带 `cause`,因为错误对象可能挂着含 cookie 的 request 信息) - 只发往抖音官方域(`www.douyin.com` / `vod.bytedanceapi.com`),不经任何第三方 - 跨机部署读不到该文件时直传整体失效,自动回落 不接受这个让步就把 `image_direct` 置 `false`,代价是发图只能走下面的退路。 ### 退路:`image_fallback` 直传与网关 `upload_image` 都失败(cookie 失效、抖音改接口、跨机部署)时才轮到它。 | 取值 | 行为 | | --- | --- | | `auto`(默认) | 链接是公网地址就走 `emoji`,否则退回 `link`,并在日志里说明原因 | | `emoji` | 用 `send_emoji` 按 URL 发。抖音表情不加密,但**客户端对外部 URL 的支持并不可靠**(实测可能什么都不显示),所以这只是退路的退路 | | `link` | 改发一条直链文本,点开能看图 | | `off` | 不降级,照旧抛错(想让失败暴露在日志里时用) | 退路里的链接必须对**用户手机**可达。`image_host` 接一个图床是最省事的做法(教程见 [自建图床.md](自建图床.md)),不配则用 `Bot.fileToUrl` —— 那个链接来自 `config/config/server.yaml` 的 `url`(**不是 `bot.yaml`**,那里那个没有任何代码读它),默认 `http://localhost:2536` 在手机上永远打不开。 用 `Bot.fileToUrl` 那条路要把 Yunzai 自己的端口暴露到公网,而那个端口上还挂着锅巴面板之类的东西;链接也有有效期(`cfg.bot.file_to_url_time`),过期后聊天记录里的图变死链。所以退路优先用图床。 ## 给插件开发者 抖音账号在 Yunzai 里就是一个普通 Bot,`e.reply`、`e.group.sendMsg`、`pickFriend(uid)` 等照常可用。抖音特有的东西通过两处暴露: - 事件上额外带 `conv_id`(抖音会话 id)、`account`(网关账号名)、`sender.sec_uid`。 - `Bot[self_id].gateway.action(动作名, 参数)` 可直调任意网关动作,动作清单见网关的 `API.md`。也可以用 `segment.raw({ action, params })` 在消息里直通。 ```js // 例:直接调网关动作 await Bot[e.self_id].gateway.action("send_emoji", { conv_id: e.conv_id, url: "https://p3.douyinpic.com/xxx.png", display_name: "[笑]", }) ``` ## 排查 | 症状 | 原因与处理 | | --- | --- | | `#jp登录` 说没有待扫的码 | 说明网关现在不在等登录:cookie 还有效(`#jp状态` 看在线情况)、或这个号走短信不走扫码、或网关根本没跑起来(`#jp日志` 看它说了什么)。想强制重新出码就 `#jp重启` | | `#jp登录` 说码已过期 | 码只有 180 秒。等 5-15 秒让网关自己重新出码再发一次;一直没有就 `#jp重启` | | 卡在「正在编译」 | 首次要拉 8 个 Go 模块依赖,1-3 分钟正常;久了看日志是不是 `goproxy` 连不上 | | 提示 Go 下载失败 | 换 `auto.goproxy` 或自己装 Go 后把 `auto.install_go` 置 `false` | | 端口被占 | `#jp状态` 会说明是「已认下的外部进程」还是本插件的子进程;前者去它自己的终端关 | | 发指令没反应只回「暂无权限」 | 你不在 `config/other.yaml` 的主人列表里,见[谁能用哪条](#谁能用哪条) | | 群里发 `#jp登录` 被拒 | 故意的,二维码与验证码只在私聊回应 | | 图片发出来是一条链接 | 直传失效了(日志里有「图片直传失败」)。多为 cookie 过期:`#jp状态` 看账号,必要时 `#jp登录` 重新扫码。跨机部署读不到 `cookie.json`,直传本就不可用,只能靠退路 —— 配 `image_host.url` 接图床(教程见 [自建图床.md](自建图床.md))。原理见[发图是怎么修好的](#发图是怎么修好的) | | 日志有「图片直传失败」 | 看后面跟的原因:`拿不到上传凭证` = cookie 失效;`CheckAuthenticationError` = 凭证过期(会自动重试一次);`cookie.json 里没有 cookie` = 网关还没登录成功 | | 图片发出来了但一直转圈 | 走到退路了,而链接对用户手机不可达(`localhost` / 内网 IP)。配了图床就检查图床自己的对外地址,没配就看 `config/config/server.yaml` 的 `url` | | 日志有「图床上传失败」 | 图床没跑、地址写错、或端口没放通。从**别的机器**跑 `curl http://IP:5733/api/status` 验证。图床失败会自动回落 `Bot.fileToUrl`,消息不会丢 | | 配了图床还是发出一行链接 | `image_fallback` 是 `link` —— 那一档的语义就是「总是发链接」。**从旧版本升级上来的必踩**:插件改默认值不会动你已有的 `config/jumpbyte.yaml`(框架是「文件覆盖默认值」)。改成 `auto` 再 `#重启`;启动日志里也会警告这个组合 | | 一直「网关连接关闭,N 秒后重连」 | 网关进程没了而适配器不知道。多发生在 `#重启` 之后 —— `process.execve` 不触发退出钩子,上一轮的网关成了孤儿被新进程认下,而认下的进程没有 `exit` 事件。现在会轮询探活(连续 3 次失败即接管重启),日志重复也做了降噪。仍不恢复就 `#jp重启` | | 引用回复对方看到空白 | 抖音客户端不认网关 `send_reply` 的 content 形态。插件默认已把引用降级成纯文本(`reply_quote: false`),若你手动开了就关掉 | | 登录提醒发给了全部主人 | 说明一个收件人都定不出来:Yunzai 刚起来还没人发过 `#jp` 指令。这是有意的回落;想固定发给某人就配 `notify_master`,见[关于登录](#关于登录) | | 改了代码没生效 | 本插件目录含 `index.js`,按 `lib/plugins/loader.js` 的规则**不注册热重载**,要 `#重启`(`#jp更新` 会自己重启,除非 `update_restart` 关了) | | `#jp更新` 说没有 .git | 插件是下载压缩包装的,git 无从更新。照[安装](#安装)用 `git clone` 重装一次即可,配置与登录状态都在 `plugins` 之外不会丢 | | `#jp更新` 报合并冲突 | 你改过插件里的文件。想保留改动就手动 `git stash` / 自己合;不在乎就发 `#jp强制更新`(`reset --hard`,丢弃本地改动) | ## 目录结构 ```text index.js 装配:加载配置、注册适配器、配置指令 Model/config.js 配置定义(config/jumpbyte.yaml) Model/toolchain.js Go 工具链:检测系统 go,缺失就装便携版(sha256 校验) Model/runner.js 网关进程托管:取源码 → 编译 → 启动 → 守活 Model/api.js 网关客户端(HTTP 动作 + WS 事件流 + 重连) Model/gateway.js 网关工作目录:读 bot.json / 监视 qrcode.png / 登录提醒 Model/media.js 会话标识、媒体字节转换、图床上传 Model/upload.js 图片直传(自己加密 + 走抖音上传链,绕开网关坏掉的 upload_image) Model/selfupdate.js 插件自更新(#jp更新,复用框架 plugins/other/update.js) Model/adapter.js 协议映射(事件↔Yunzai、segment↔网关动作、Bot 对象契约) guoba.support.js 锅巴配置面板 自建图床.md 图床服务端搭建教程(发图退路用) smoke.test.mjs 冒烟测试:不联网不 spawn,只验纯逻辑分支 ``` 改完代码跑一遍冒烟测试(在 Yunzai 根目录,无需启动框架): ```bash node plugins/jumpbyte-plugin/smoke.test.mjs ``` 它覆盖版本比较、二维码行识别、日志行切与登录信号提取这类「错了不报错、只是静静失灵」的地方。真正的下载编译与 spawn 有外部依赖,测不了,靠 `#jp安装` 跑真机。 ## 许可证 [GPL-3.0](LICENSE),沿用网关 [jumpbyte-bot](https://github.com/sisi0318/jumpbyte-bot) 的许可证 —— 本插件是围绕它做的适配层,协议实现全在那边。 --- by H [hlz7.com](https://hlz7.com)