多看阅读没有批量导出?把这段提示词交给你的 AI 助手

多看阅读里攒了几千条笔记,想拿出来却只能一本书一本书点「导出」:没有批量功能,没有公开 API,导入书的笔记也不存在本地数据库里。

能走通的路是让 AI 助手替你点。它在电脑上用 adb 读屏幕、点按钮,每本书进「想法详情」→「导出」→ 分享给 Termux 存成文件,约 40 秒一本,放后台跑。我用这条路导出了 217 本书的 3172 条笔记,与 app 里显示的总数一条不差(多看阅读 7.12.10,Android 9 平板)。

两个坑决定成败:默认文件名是「《书名》的笔记.txt」,书名一长就超过 255 字节,Termux 报 File name too long,所以要在保存框里改成编号名;另外每次返回列表别多按 BACK,否则会退到书架。

不需要 root:让 Termux 的 ~/downloads 链接到 /sdcard/Download/duokan,导出文件就落在 adb 能直接取的地方。这一步我是在平板上手动授予存储权限、建同样的链接验证的,没有经过 termux-setup-storage 本身。

准备好电脑(装 adb,有能执行命令的 agent,比如 Claude Code)、打开 USB 调试的安卓设备和 Termux,把下面整段发给你的助手:

请帮我把安卓设备上「多看阅读」里所有书的笔记(想法)批量导出到这台电脑。下面是已实测走通的路,照此做;与我的设备不符时自行调整,并告诉我改了什么。

【已知事实】
- 多看没有公开 API,导入书的笔记也不存在本地数据库里,不必去抓包或翻数据库。唯一可靠的路是 app 内每本书「想法详情」页的「导出」按钮:它通过安卓分享菜单发出一个文本文件。
- 分享目标选 Termux:会弹出「Save file in ~/downloads/」对话框,里面有可编辑的文件名框(EditText)和「OPEN DIRECTORY」按钮,点 OPEN DIRECTORY 即保存。
- 导出文件格式:第一行书名,空一行,然后每条笔记先是一行「 YYYY-MM-DD」(行首有一个空格),下面是笔记正文,可多行。划线与我的评论不做区分。

【准备】
1. `adb devices` 确认设备已连上。
2. 让 Termux 把文件存到共享存储,这样无需 root 就能用 adb 取:请我在设备上打开 Termux,依次输入
   termux-setup-storage            (在弹窗里允许存储权限)
   mkdir -p /storage/emulated/0/Download/duokan
   mv ~/downloads ~/downloads.bak 2>/dev/null; ln -s /storage/emulated/0/Download/duokan ~/downloads
   此后导出的文件会落在 /sdcard/Download/duokan/。
3. 记下 `adb shell settings get system screen_off_timeout` 的原值,临时设为 1800000,全部完成后还原。
4. 请我在多看里打开「我的」→「想法」:这一页列出所有有笔记的书,每项有书名、条数、上次记录日期。「我的」页上的「想法」总数记下来,最后用来核对。

【逐本导出:写一个脚本,用 `adb shell uiautomator dump` 读界面,用 `adb shell input tap / swipe / keyevent / text` 操作】
- 列表项节点的 resource-id 是 com.duokan.reader:id/personal__reading_note_item_view__title,另有 __count 和 __date,可读出书名、条数、日期和坐标。笔记正文是自绘的,dump 读不到,不必尝试。
- 每本书:点书名 → 等出现文字为「导出」的节点再点它(不同书的位置不同,按文字找,别写死坐标)→ 在分享菜单里找文字「Termux」并点它 → 对话框出现后,点 EditText,发 `input keyevent 123`(光标移到末尾),再发约 250 个 67(退格)清空,`input text 001.txt` 换成短编号名 → 重新 dump,核对文件名框确为 001.txt → 点「OPEN DIRECTORY」→ 确认 /sdcard/Download/duokan/001.txt 已出现。
- 一定要改文件名:默认名「《书名》的笔记.txt」在书名很长时超过 255 字节,Termux 会报 Error saving file … (File name too long);同名的书也会互相覆盖。用三位编号命名,并把 编号→书名、条数、日期 追加写入一个 jsonl 进度文件,中断后可续跑。
- 返回列表:保存后 Termux 在前台。用 `adb shell am start -n com.duokan.reader/.DkMainActivity` 拉回多看(通常回到想法详情页);只有 dump 里看不到列表项时才再按一次 BACK,多按会退到书架。
- 自愈:每轮开始先确认在想法列表;在「想法详情」页就按 BACK;在别处就点底部「我的」,再点「想法」那一行。dump 里会混有被遮住的页面层的节点,判断当前页要看它独有的文字。
- 当前屏的书都导完就上滑(从屏高 83% 处滑到 34% 处);连续两次滑动后列表不变,就是到底了。
- 同一本书失败两次就记为失败,继续下一本;出现错误对话框先点「OK」关掉。每 25 本执行一次 `adb shell am force-stop com.termux`,免得 Termux 会话越积越多。
- 脚本放后台跑并写日志,定时看进度,不要守着。

【收尾】
- 核对:每本文件里「 YYYY-MM-DD」行数应等于列表上的条数,总和应等于「想法」总数。不符的书单独重导。
- `adb pull /sdcard/Download/duokan/` 到电脑;按进度文件把 001.txt 改名为「001-书名.txt」(书名截到 80 字节以内,去掉 / \ : * ? " < > | 和空白),再合成一份按书分节、带日期的 Markdown 汇总。
- 还原屏幕超时;把 Termux 的 ~/downloads 恢复原状(删掉链接,把 ~/downloads.bak 改回来)。最后告诉我:导出了几本书、几条笔记、哪些失败了。

多看改版后界面元素可能变化,提示词里已让助手按文字找按钮、自行调整,并在最后核对条数。

Let xrdp repair stuck keyboard and mouse state on reconnect

An xrdp reconnect can leave a virtual mouse button or key down. A stuck mouse button creates an X11 pointer grab, so every window appears unable to receive clicks even though XFCE and the taskbar are still alive. The old workaround was to click the stuck button once; a reconnect hook can repair both devices automatically without destroying the desktop session.

Keep the session alive in /etc/xrdp/sesman.ini:

KillDisconnected=false
DisconnectedTimeLimit=0
ReconnectScript=reconnectwm.sh

ReconnectScript is the important part. It runs when xrdp-sesman reconnects a client to an existing session. Put the repair command in /etc/xrdp/reconnectwm.sh:

#!/bin/sh

/usr/local/bin/rdp-input-reset >/dev/null 2>&1 || true

The helper only resets a device when X reports a key or button still down:

#!/bin/sh
set -eu

for name in xrdpMouse xrdpKeyboard; do
    id=$(xinput list --id-only "$name" 2>/dev/null || true)
    [ -n "$id" ] || continue

    if xinput query-state "$id" | grep -Eq '(key|button)\[[0-9]+\]=down'; then
        xinput disable "$id"
        xinput enable "$id"
    fi
done

Run the helper manually when the connection has not dropped:

DISPLAY=:10.0 XAUTHORITY="$HOME/.Xauthority" rdp-input-reset

This is safer than restarting xfwm4, the panel, or the whole desktop: it clears the stale XRDP input devices while preserving windows, terminals, and the session. It is not magic—the hook only runs for a reconnect to an existing session. If the connection remains open while input gets stuck, run the helper manually. KillDisconnected=true is a stronger isolation workaround, but it eventually kills the disconnected desktop, so it is the wrong trade-off for a persistent RDP session.

GitHub Pages 404.html Can Read the Deep Link

GitHub Pages can serve a custom 404.html for a path such as /note/123 without changing the address bar. The fallback page can read the requested path and pass the note ID to the single-page app:

const match = window.location.pathname.match(/^\/note\/([^/]+)\/?$/);
const noteId = match?.[1] ?? null;

This lets a static app display the requested note, but the original HTTP response is still 404. A host or edge rewrite is needed for the same clean URL to return HTTP 200.

Your API returns 403 to Python but 200 to curl? It's Cloudflare's Browser Integrity Check

An AI agent posting to my API through Python's urllib kept getting 403 Forbidden. The same request sent with curl succeeded. The request never reached the API at all: Cloudflare rejected it at the edge.

The difference is the User-Agent header. Compare the two:

curl -s -o /dev/null -w '%{http_code} %header{server}\n' https://api.example.com/health
# 200 cloudflare
curl -s -A 'Python-urllib/3.12' https://api.example.com/health
# error code: 1010

error code: 1010 together with server: cloudflare means Cloudflare refused the client's signature. On a zone with no Bot Fight Mode and no WAF rules, the usual cause is Browser Integrity Check (BIC). It is on by default, and it blocks requests whose headers look like those of abusive bots. Against my zone I tried six User-Agents. Python-urllib/3.12 got 403. python-requests, curl, Go-http-client, node, and no UA at all all got 200. So one Python script fails while its neighbour using requests works, and the failure looks random.

You can confirm BIC is the cause with the zone settings API:

curl -s -H "X-Auth-Key: $CF_API_KEY" -H "X-Auth-Email: $CF_API_EMAIL" \
  "https://api.cloudflare.com/client/v4/zones/$ZONE/settings/browser_check"
# "value": "on"

The quick fix is to make every client send a browser-ish UA. That fixes your own scripts, but every other agent and SDK still hits the wall, and an API that only answers browsers is an odd thing to publish. A better fix is to turn BIC off only for the API hosts, and keep it on for the rest of the zone. A Configuration Rule does exactly that:

curl -s -X PUT -H "X-Auth-Key: $CF_API_KEY" -H "X-Auth-Email: $CF_API_EMAIL" \
  -H "Content-Type: application/json" \
  "https://api.cloudflare.com/client/v4/zones/$ZONE/rulesets/phases/http_config_settings/entrypoint" \
  -d '{"rules":[{
        "description": "API hosts: no Browser Integrity Check",
        "expression": "(http.host in {\"api.example.com\" \"upload.example.com\"})",
        "action": "set_config",
        "action_parameters": {"bic": false}
      }]}'

A PUT on the phase entrypoint replaces the whole rule list. If the zone already has Configuration Rules, fetch the entrypoint first and send the existing rules back along with the new one. On a zone with none, the GET returns error 10003 ("could not find entrypoint ruleset") and the PUT creates the entrypoint. The change took effect within seconds: the Python-urllib request went from 403 to the origin's own response.

Don't forget sibling hosts. The API worked after the change, but the image upload host behind it still returned 403 to the same agent until I added it to the rule.

gmail-cli Terms of Service

gmail-cli is a personal command-line tool run by David Wei (shukebeta) for Google accounts he owns or operates. It is not offered to the public.

Use. Only the account owner, or an assistant acting for him, may use gmail-cli, and only on accounts that authorized it through Google OAuth. Use must follow Google's terms and the law: no spam, no unsolicited bulk mail, no access to accounts you do not own.

No warranty. gmail-cli is provided as is, without warranty of any kind. The operator is not liable for lost mail, missed messages or any damage arising from its use.

Access. Authorization can be revoked at any time at myaccount.google.com/permissions. The operator may stop or change the tool at any time.

Privacy. See the gmail-cli Privacy Policy on this blog.

Contact. [email protected]

Last updated: 2026-11-04.