在 Kindle 上无头部署与调试 Readest KOReader 同步插件:SSH 空密码配方与统计推送排障实战

在 Kindle 上无头部署与调试 Readest KOReader 同步插件:SSH 空密码配方与统计推送排障实战 桌面应用跨平台前端【免费下载链接】readestReadest is a modern, feature-rich ebook reader designed for avid readers offering seamless cross-platform access, powerful tools, and an intuitive interface to elevate your reading experience.项目地址https://gitcode.com/gh_mirrors/re/readest点击查看免费下载本指南完整还原 Readest 项目在 Kindle VoyageKOReader v2026.07.2、固件 5.13.6上的一次真实运维实践如何绕过本机 SSH key 认证、以纯命令行方式把readest.koplugin安装包推送到设备并替换旧版本以及当Failed to push reading statistics这类同步失败发生时如何绕过无用的crash.log从插件状态表、统计数据库和网络连通性三个层面逐层定位根因。读完本文你将掌握一套可复用的 Kindle KOReader 插件无 GUI 部署 设备端排障完整流程并能直接对照仓库源码apps/readest.koplugin理解每一步背后的实现机制。背景这份配方解决什么问题本文内容源自仓库中的一条实战记忆记录 kindle-ssh-deploy-debug-recipe.md它记录了 2026-08-14 的一次真实任务向 Kindle Voyage 部署Readest-0.12.1-1.koplugin.zip并排查一次阅读统计推送失败stats-push failure。其核心结论有三条也是本文的三条主线认证与传输KOReader 的 SSH 插件dropbear监听 2222 端口登录是空密码、拒绝 Mac 本机id_rsa必须用SSH_ASKPASS非交互式完成登录上传文件不能用 scp/sftp要用标准输入管道。设备端排障crash.log对同步类失败基本无用真正的线索在 KOReader 的settings.reader.lua插件状态、statistics.sqlite3统计积压和一条 curl 网络探测命令里。已知坑KOReader 2026.07 会对_meta.lua里的name字段发出弃用警告无害本地构建 zip 时容易把源码目录里遗留的旧 zip 一起打进去。一、无头 SSH 认证空密码 dropbear 与 SSH_ASKPASS1.1 连接参数与环境确认目标设备与服务的连接参数参数值设备Kindle VoyageKOReader 版本v2026.07.2Kindle 固件5.13.6SSH 服务KOReader SSH 插件内置的 dropbear主机 / 端口root192.168.2.180 -p 2222注意端口是2222而非标准的22这是 KOReader SSH 插件dropbear的默认监听端口账号是root。IP192.168.2.180是该设备在局域网内的地址实际使用时应替换为你自己设备的地址。1.2 为什么 Mac 的 id_rsa 会被拒绝记录明确指出本机Mac的id_rsa被服务端拒绝REJECTED登录方式是空密码。这与 KOReader SSH 插件的默认配置一致——它没有配置公钥授权只接受密码认证。因此在自动化和脚本化场景下必须显式声明使用密码认证而不是依赖 SSH 默认的 key 协商顺序。1.3 非交互空密码认证配方SSH 客户端出于安全考虑默认不会从标准输入读取密码且在没有 TTY 的 headless 环境中根本不会弹出密码提示。解决方案是利用SSH_ASKPASS机制SSH 会调用该脚本把脚本的标准输出当作密码。完整配方分两步。首先写一个 askpass 脚本内容只有一个空回显#!/bin/bash echo 然后执行 SSH 连接关键环境变量与选项缺一不可DISPLAYnone \ SSH_ASKPASS脚本绝对路径 \ SSH_ASKPASS_REQUIREforce \ ssh \ -o PreferredAuthenticationspassword \ -o NumberOfPasswordPrompts1 \ -p 2222 \ root192.168.2.180各参数含义DISPLAYnone让 SSH 认为没有图形环境从而转入 askpass 路径。SSH_ASKPASSscript指向上面那个输出空行的脚本即密码就是空字符串。SSH_ASKPASS_REQUIREforce强制启用 askpass即使有可用的 TTY 或环境变量缺失也走该路径较新版本 OpenSSH 的开关。-o PreferredAuthenticationspassword显式只使用密码认证跳过 publickey 协商否则会先被 id_rsa 拒绝。-o NumberOfPasswordPrompts1只允许一次密码尝试避免脚本化场景下多次重试卡住。1.4 从源码看插件侧的认证链路设备端插件保存的认证字段与这条 SSH 配方的空密码没有直接关系但理解它们能帮你把连上了但同步失败与根本没法登录区分开。在 main.lua 中插件默认设置表ReadestSync.default_settings定义了与云端认证相关的字段ReadestSync.default_settings { supabase_url https://readest.supabase.co, supabase_anon_key sha2.base64_to_bin(SUPABAE_ANON_KEY_BASE64), auto_sync false, user_email nil, user_name nil, user_id nil, access_token nil, refresh_token nil, expires_at nil, expires_in nil, last_sync_at nil, localsend_enabled false, localsend_alias nil, }这些设置通过G_reader_settings:readSetting(readest_sync, self.default_settings)持久化见 main.lua。也就是说SSH 只是运维通道插件与 Readest 云端之间的认证走的是 Supabase JWTaccess_token/refresh_token/expires_at后续排障中我们要检查的expires_at就来自这张表。二、插件包部署管道从构建到替换一气呵成2.1 构建安装包readest.koplugin的安装包是标准 zip文件名形如Readest-version-1.koplugin.zip。仓库内置了构建脚本 build-koplugin.mjs用法node apps/readest.koplugin/scripts/build-koplugin.mjs --version 0.12.1脚本关键行为--version会把版本号注入_meta.lua形如version 0.12.1,构建完成后自动还原除非传--keep-meta见 build-koplugin.mjs未指定--version时使用dev-git-sha占位便于识别已安装插件的来源打包时排除了scripts/、docs/、spec/、.busted、native/以及*.koplugin.zip通配见 build-koplugin.mjs依赖系统zip命令并会检查bin/目录是否存在 LocalSend helper 静态二进制否则给出警告LocalSend receive will be unavailable。2.2 通过管道上传并校验不用 scp/sftp设备端 dropbear 环境受限配方明确要求不要用 scp/sftp而是把 zip 内容通过 SSH 标准输入管道直接写入目标文件cat Readest-0.12.1-1.koplugin.zip | \ ssh askpass 参数同上 root192.168.2.180 \ cat /var/local/x.zip先放到/var/local/这个临时位置而不是直接解压到插件目录是为了让上传与安装两个阶段可分开校验、可回滚。上传完成后在两端分别计算 md5 并比对确认传输完整# 本机 md5 Readest-0.12.1-1.koplugin.zip # 设备端通过同样的 ssh 通道执行 ssh ... md5sum /var/local/x.zip2.3 替换旧插件并处理 LocalSend helper 进程上传校验通过后按以下顺序执行安装# 1. 停掉正在运行的 LocalSend 接收进程 killall localsend-helper-armv7 # 2. 删除旧插件目录 rm -rf /mnt/us/koreader/plugins/readest.koplugin # 3. 解压新包到 plugins/ 目录 unzip -oq /var/local/x.zip -d /mnt/us/koreader/plugins/ # 4. 落盘 sync两条容易踩坑的细节必须用killall绝不能pgrep -fpgrep -f的匹配串本身也可能匹配到执行它的进程自匹配导致误杀或行为异常killall按精确进程名匹配更安全。为什么一定要重启 LocalSend helper插件升级时旧的 helper 静态二进制进程可能仍在监听控制 socket。从 readest_localsend.lua 可以看到helper 是作为独立后台进程运行的bin/localsend-helper-*其二进制名由设备平台决定Helper.binaryNameFor根据is_android、jit.os、jit.arch解析Kindle 这类 ARM 设备对应的正是localsend-helper-armv7。不杀掉旧进程新插件启动后会与旧进程的 socket 状态不一致。最后重启 KOReader 让新插件被加载KOReader 退出重进即可。安装后建议立刻回到 2.2 的步骤确认/mnt/us/koreader/plugins/readest.koplugin目录结构与版本号_meta.lua中的version正确。2.4 busybox unzip 的差异设备端是 Kindle 的 busybox 环境其unzip与 GNU 工具集有明显差异配方记录了三点没有-ttest选项无法用unzip -t校验压缩包完整性所以必须走 2.2 的 md5sum 双端比对-xexcludeglob 模式可用如果需要在解压时排除某些文件-x 某模式是可行的使用-oq-o覆盖已存在文件避免交互询问-q静默输出。三、设备端同步与统计推送排障三板斧当部署完成后出现 Failed to push reading statistics 之类的问题时按下面的顺序排查切忌一上来就怀疑插件本身。3.1 crash.log 为什么没用logger.dbg 与 INFO 级别记录明确指出crash.log对同步/统计类失败是无效的USELESS。原因是插件相关日志readest_syncstats.lua 与 sync client大量使用logger.dbgdebug 级别而 KOReader 默认日志级别是 INFOdbg级别的输出根本不会落盘。Failed to push reading statistics 这条提示对应的是 readest_syncstats.lua 中 pushChanges 回调successfalse的分支——此时body/status里往往也没有更多细节属于典型的静默失败。因此排障的第一步不是翻 crash.log而是跳到 3.23.4 的三条实证路径。如果确实需要 debug 日志可以在 KOReader 中调高日志级别后重试推送但本配方给出了更高效的设备端探针。3.2 插件状态表settings.reader.lua 里的 readest_sync插件的全部状态保存在 KOReader 全局设置文件中/mnt/us/koreader/settings.reader.lua其中的[readest_sync]表对应 main.lua 的default_settings。排障时需要重点检查的字段字段单位 / 语义检查方法access_tokenJWT非空即已登录为空说明未登录推送自然失败expires_atUnix 秒用date %s得到当前时间戳若expires_at已过或接近当前值token 过期stats_push_cursor秒push 游标记录已推送到哪条统计事件stats_pull_cursor毫秒pull 游标记录已拉取到哪个更新点auto_sync布尔是否开启自动同步注意 push 与 pull 游标单位不一致秒 vs 毫秒排查游标推进异常时不要被这个差异迷惑。关于过期判定readest_syncauth.lua 的needsLogin逻辑是expires_at os.time() 60——即剩余不足 60 秒就视为需要重新登录而 tryRefreshToken 会在剩余时间低于 TTL 一半时用refresh_token静默刷新。如果expires_at明显过期而refresh_token又失效就会表现为按钮点了没反应或持续推送失败。3.3 统计积压分析把 statistics.sqlite3 拉到本地查KOReader 的阅读统计库是设备上的 SQLite 文件/mnt/us/koreader/settings/statistics.sqlite3Failed to push reading statistics 的常见原因之一是游标停滞、积压过大——比如一次全新 pull 把多设备历史全部拉到本地后push 游标仍停在很旧的位置。排障手法是把这个数据库文件拷到本地用 SQL 直接看积压量。设备端先拷贝ssh ... cp /mnt/us/koreader/settings/statistics.sqlite3 /var/local/ ssh ... cat /var/local/statistics.sqlite3 statistics.sqlite3然后本地执行与插件 collectSince 完全一致的 SQL统计游标之后还有多少条未推送事件SELECT b.md5, b.title, b.authors, p.page, p.start_time, p.duration, p.total_pages FROM page_stat_data p JOIN book b ON b.id p.id_book WHERE p.start_time stats_push_cursor ORDER BY p.start_time ASC;把stats_push_cursor换成 3.2 中读到的stats_push_cursor值。如果返回行数很大说明积压确实存在。这背后还有一层实现细节值得知道推送是分块进行的readest_syncstats.lua 定义PUSH_CHUNK 500每次请求最多 500 条 page 事件拉取侧PULL_PAGE 1000见 readest_syncstats.lua这是为了适配 sync client 的 10 秒总超时。若单次推送超过 chunk 大小插件会通过UIManager:nextTick串行续推见 readest_syncstats.lua但前提是每一块都要在超时内成功——所以一次 push 卡死、游标永远不推进正是要优先怀疑大积压的理由。3.4 网络探测先于怀疑插件的一步配方特别强调Check this FIRST before suspecting the plugin.在动插件之前先从设备端发一条网络探针curl -sS -o /dev/null -w %{http_code} https://web.readest.com/api/sync预期结果是401未认证——这恰恰说明网络链路通畅、服务器可达401 表示到达了应用层并被鉴权拦截而不是连接失败。2026-08-14 的真实案例正是如此当时 DNS 能解析但 curl 报curl: (7) Couldnt connect说明 Kindle 的 WiFi 已经丢失了 WAN 出口而 LAN/SSH 仍然可用——局域网内 SSH 正常不代表设备能出公网。用户重新连接 WiFi 后push 立即成功。这类设备看着在线、同步却失败的假象只有靠这条探测命令才能第一时间戳穿。3.5 把排障链条接到源码push 与 pull 的游标推进逻辑最后用源码把整个链路串起来。推送入口 SyncStats:push 的逻辑是读settings.stats_push_cursor秒调用collectSince(cursor)收集start_time cursor的书籍与 page 事件按PUSH_CHUNK分块通过client:pushChanges({ books {}, notes {}, configs {}, statBooks ..., statPages ... })发送回调successtrue时把stats_push_cursor推进到本块最后一条事件的start_time并保存readest_syncstats.luasuccessfalse时游标保持不变、重试会从原地继续readest_syncstats.lua。对照这张图Failed to push reading statistics 且游标不动的诊断顺序就清晰了先做 3.4 的网络探测排除 WAN 丢失再核对 3.2 的access_token/expires_at排除鉴权失效最后用 3.3 的 SQL 确认是否存在大积压。三者都正常而问题依旧才轮到检查插件版本与日志级别。四、已知注意事项与易踩的坑4.1_meta.lua的弃用警告无害但要知道KOReader 2026.07 在加载插件时会输出PluginLoader: readest name in _meta.lua, is deprecated and will be ignored这是无害警告插件照常加载。原因可以从 _meta.lua 的源码看出来——该文件目前只返回name、fullname、description三字段而新版 KOReader 已弃用_meta.lua里的name字段插件标识以目录名为准。插件自身在 main.lua 中读取_meta.lua只是为了拿到version字段用于检查更新菜单不受该警告影响。4.2 构建残留 zip 会被吞进新包从工作目录插件源码目录本地构建发布 zip 时如果源码目录里残留着历史构建产物如Readest-dev-sha-1.koplugin.zip它们会被卷进下一次打包导致包体膨胀甚至嵌套。构建脚本 build-koplugin.mjs 已经用-x ${PLUGIN_NAME}/*.koplugin.zip做了排除注释明确写着这是为了避免megabytes of stale nested copies。但配方仍建议双保险安装到设备时排除这类文件同时定期清理源码目录——因为脚本排除只在构建侧生效历史遗留产物仍可能通过其他打包路径混入。五、源码索引与延伸阅读本次排障涉及的核心文件均可在仓库中继续深挖主题文件插件入口、默认设置、菜单与同步编排main.lua统计推送/拉取、游标与分块策略readest_syncstats.lua阅读进度/元数据指纹同步readest_syncconfig.luaSupabase 登录、token 刷新与 Spore 客户端readest_syncauth.luaLocalSend 接收服务与 helper 进程管理readest_localsend.lua插件元信息_meta.lua的 name/fullname/description_meta.lua本地打包脚本版本注入与排除规则build-koplugin.mjs插件整体设计文档图书馆视图、schema、同步流程library-design.md同步 API 契约Spore 规格readest-sync-api.json需要说明的是本指南中的设备 IP、版本号与日期来自记录本身属于特定时刻的实测环境root192.168.2.180:2222是当时的局域网地址实际部署时请替换为你自己的设备地址并确认 KOReader SSH 插件已启用、设备与电脑处于同一局域网。掌握这套空密码 SSH 管道 状态表核查 数据库积压分析 网络探针的组合拳后Kindle 上的 KOReader 插件部署与同步排障就不再依赖图形界面或反复重启实验了。赞分享桌面应用跨平台前端【免费下载链接】readestReadest is a modern, feature-rich ebook reader designed for avid readers offering seamless cross-platform access, powerful tools, and an intuitive interface to elevate your reading experience.项目地址https://gitcode.com/gh_mirrors/re/readest点击查看免费下载相关推荐ToolJet GitSync SSH 配置实战在 GitHub、GitLab 与 Gitea 上部署 ToolJet 生成的 SSH 密钥ToolJet GitSync SSH 配置实战在 GitHub、GitLab 与 Gitea 上部署 ToolJet 生成的 SSH 密钥 本文围绕 Too低代码后端前端AI 应用MCP 服务Algorithm-Implementations 数学算法宝典阶乘、斐波那契、欧几里得算法详解Algorithm Implementations 数学算法宝典阶乘、斐波那契、欧几里得算法详解 Algorithm Implementations 是一个专kubeasz 实战基于 KubeBlocks 在 Kubernetes 上部署 MySQL 半同步主备集群与故障演练kubeasz 实战基于 KubeBlocks 在 Kubernetes 上部署 MySQL 半同步主备集群与故障演练 本文以 kubeasz 仓库中 rol云原生集群管理运维上一篇RuboCop 1.16.1 补丁版解析五个关键 Bug 修复的源码级原理与配置实战下一篇如何在Tuist中集成Unity Shader Graph创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考