Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .agents/skills/readerx-data/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ description: 修改 ReaderX 本地书库存储、稳定身份与旧数据迁移

| 功能 | 实现入口 | 按需资料 |
| --- | --- | --- |
| 本地书库存储、迁移、稳定 ID | src-tauri/src/book_store.rs、storage.rs、models.rs、sync/identity.rs、sync/book_ids.rs、sync/data_ids.rs;src/lib/books.ts、booksTypes.ts、dataIds.ts | [存储与身份约束](references/contracts.md#书库与稳定身份) |
| 本地书库存储、迁移、稳定 ID | src-tauri/src/book_store.rs、book_store/sqlite.rs、book_store/backup.rs、book_store/state.rs、storage.rs、models.rs、sync/identity.rs、sync/book_ids.rs、sync/data_ids.rs;src/lib/books.ts、booksTypes.ts、dataIds.ts | [存储与身份约束](references/contracts.md#书库与稳定身份) |
| 备份与恢复 | src-tauri/src/data_transfer/;src/lib/backup.ts、src/pages/DataBackup.tsx | [备份约束](references/contracts.md#备份与恢复)、[备份文档](../../../docs/backup.md) |
| 同步协议、合并与传输 | src-tauri/crates/readerx-sync/ | [同步约束](references/contracts.md#局域网同步)、[同步文档](../../../docs/sync.md) 对应章节 |
| 同步 App 接线与落库 | src-tauri/src/sync/;src/lib/sync.ts、src/components/SyncProgress.tsx | 同步文档 App 接线、实时落库与停止部分 |
Expand Down
10 changes: 5 additions & 5 deletions .agents/skills/readerx-data/references/contracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,19 @@

## 书库与稳定身份

- book_store.rs 按 books/<id>/ 拆分 bookdetail.json、content.json、bookmarks.json、annotations.json;digest.json 是派生指纹缓存。元信息补丁不读写整本正文,逐章回写与书签各走对应文件。新增字段核对 models.rs、BookDetail 与前端 booksTypes.ts,保留 schemaVersion 信封及字段往返测试。
- 注释走 readerx_annotations_get/put,与正文/书签文件独立,信封 schemaVersion=1,记录按段落聚合 notes;只在读取及写入成功后发布内存变化,读失败不得作为空数组覆盖。锚点不含 bookId,身份迁移随整个书目录移动;删除/恢复失效缓存并阻止在飞读取回填。备份包含文件并在同段合并 note id,覆盖旧备份的缺失文件清空该书注释;导出/导入/删除前等待注释写队列。同步按 note 独立实体,保存只发布差集且与落库串行;同段新增聚合、正文并发进冲突、删书级联,启动只补缺失实体并恢复引擎已有值。备份导入显式发布改写/删除/恢复,落库通过 AppliedChanges.annotations 按书刷新。契约见 [阅读注释](../../../../docs/annotations.md),检查为 book_store、data_transfer、readerx-sync 与 sync_bridge 测试。
- book_store.rs / book_store/sqlite.rs / book_store/backup.rs 将书籍数据存入 data_root 下的 books.sqlite3(books、chapters、bookmarks、annotations、groups、rules;进度直接在 books.detail.progress);正文按章增量更新,章节头、指纹和图片引用同事务提交。TTS、书源、其他状态 JSON 和图片字节不入库。数据库 user_version 与旧 JSON schemaVersion 分别维护,新增字段核对 models.rs、BookDetail 与前端 booksTypes.ts。见 [书籍存储](../../../../docs/book-storage.md)。
- 注释走 readerx_annotations_get/put,与正文/书签独立存表,旧 JSON 信封 schemaVersion=1,记录按段落聚合 notes;只在读取及写入成功后发布内存变化,读失败不得作为空数组覆盖。锚点不含 bookId,身份迁移由外键级联更新归属;删除/恢复失效缓存并阻止在飞读取回填。备份包含注释表并在同段合并 note id,覆盖旧备份的缺失文件清空该书注释;导出/导入/删除前等待注释写队列。同步按 note 独立实体,保存只发布差集且与落库串行;同段新增聚合、正文并发进冲突、删书级联,启动只补缺失实体并恢复引擎已有值。备份导入显式发布改写/删除/恢复,落库通过 AppliedChanges.annotations 按书刷新。契约见 [阅读注释](../../../../docs/annotations.md),检查为 book_store、data_transfer、readerx-sync 与 sync_bridge 测试。
- book_store 的读写入口在进程级书库事务锁内完成迁移、读取、修改、落盘及指纹更新;持锁时不调用同步引擎。单章补丁以 chapter.cid 定位,index 仅兼容旧载荷;先验证整批 cid 存在且唯一,再只改正文,保留当前目录标题/地址。
- 普通状态写入使用同目录临时文件,先 sync_all 再重命名替换,Unix(含 Android)随后同步父目录;提交前失败保留旧文件并清理临时文件,目录同步失败返回“已替换”错误(不可假定已回滚)。Windows 仅同步文件并替换,不承诺断电后的目录持久性。普通状态读写/删除与 update_state 共用事务锁;读改写必须使用 update_state 闭包,闭包内不重入状态存储或同步引擎。阅读进度经 readerx_shelf_patch 按书提交增量,ensure 仅补缺失项,reset 在后端重置当前全部记录;不要恢复成前端整份旧书架快照覆盖。
- 旧 books/<id>.json 布局迁移成功后才删旧文件,失败仍可回退读取;全库旧书签完成迁移后留 .migrated 备份。不要通过删旧数据让新布局检查通过。
- 分组 / 书源分组 / 文本替换 / 分章规则经 book_store/state.rs 存库,书架 API 是 books.detail.progress 的视图;整书及元信息补丁不得清除进度。这些状态与书库共用事务锁,旧 JSON 和回执同事务迁入,成功才归档;数据库 user_version=2,恢复兼容 v1。其他普通状态写入使用同目录临时文件,先 sync_all 再重命名替换,Unix(含 Android)随后同步父目录;提交前失败保留旧文件并清理临时文件,目录同步失败返回“已替换”错误(不可假定已回滚)。Windows 仅同步文件并替换,不承诺断电后的目录持久性。普通状态读写/删除与 update_state 共用事务锁;读改写必须使用 update_state 闭包,闭包内不重入状态存储或同步引擎。阅读进度经 readerx_shelf_patch 按书提交增量,ensure 仅补缺失项,reset 在后端重置当前全部记录;不要恢复成前端整份旧书架快照覆盖。
- 旧整书 / 目录 JSON 逐本在事务内导入并保存迁移回执,成功才改名 .migrated 留底;失败保留读回退和重试,不影响其他书。归档中断不能重复导入旧数据覆盖新编辑。数据库身份改动和状态 / TTS 目录迁移保持可重跑。
- 书库与同步共用稳定 ID;已有规范 ID 沿用,不能在每次编辑后按新名字/地址重新计算。身份算法以 sync/identity.rs 为准:在线书首次按书源地址与详情页地址派生,导入书按归一文件名与字节数派生。分组与书源首次按名称/地址派生后编辑沿用 ID;规则仍按内容派生。前端 dataIds.ts 与 Rust 种子、大小写和归一化口径保持一致。
- 迁移顺序与启动入口检查 src-tauri/src/lib.rs:书籍迁移在 UI/同步访问前完成,data_ids 再处理分组、书源、规则及引用。迁移待办先落盘,每步可重跑;目录或身份冲突保留原数据并返回失败,不能覆盖目标。
- 改 ID 时检查书架进度、书签 bookId、按书规则、阅读时长及 tts-audio 目录。现有书籍迁移保留图片名,因为正文持有旧引用;不能只改目录或强行按新 ID 重命名图片。
- 隐藏分组身份为 __hidden__,不写进普通 readerx.groups;发布与落地都需识别,不能生成同名普通组。

## 备份与恢复

- 归档入口按 data_transfer/archive.rs 分类与校验条目名,拒绝越界路径;不绕过 is_safe_entry 直接解压。plan.rs 只读规划,import.rs 写盘;书籍导入先落 bookdetail.json,复用 write_bytes 创建父目录,再处理书签/注释,不单独重复建目录或依赖条目字母顺序;流式处理正文/图片,避免为整库复制字节到内存。
- 归档入口按 data_transfer/archive.rs 分类与校验条目名,拒绝越界路径;不绕过 is_safe_entry 直接解压。plan.rs 只读规划,import.rs 写盘;书籍导入先经 import_book_json 在事务内写元信息和正文,再处理书签/注释;导出 readerx-backup/2,在书库锁内用 VACUUM INTO 生成一致的 books.sqlite3 快照并流式写 ZIP,书源仍导出 JSON,分组、规则与进度只在数据库快照中导出,事务日志不进归档;导入先只读校验数据库,再逐书复用身份规划与合并入口,不整库替换或导入迁移回执;兼容 readerx-backup/1 JSON,图片流式处理。
- 合并与覆盖语义不同:合并保留本机额外数据并按身份映射引用,偏好已有值保留;覆盖先写后删并清空归档缺失的内容类状态。新增 state 键时明确分类、合并规则、缺失语义与 ID 换算,不能直接全量 JSON 覆盖。
- 书源会话与 WebDAV 密码默认过滤,显式包含才导出;sync/、tts-audio/、logs/ 和本机 reading-time-origin 不作为换机数据搬走。阅读时长 contributions 应保留,恢复后本机新增时间使用独立来源。
- 覆盖删除书或书源前先调用同步删除钩子(定位身份仍需要旧数据),批量导入后调用 on_data_imported 全量对账。前端 import 完成后按 backup.ts 依赖顺序失效书签并重载分组、书库、进度、统计、规则、书源。
Expand Down
1 change: 1 addition & 0 deletions .agents/skills/readerx-pr/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ description: 按 pr: 请求把 issue 或需求做成 Pull Request:读取需求
- 按 worktree 技能创建 `.worktree/<name>` 与分支 `codex/<name>`,名称取任务关键词;已存在同名目录或分支时先确认,不覆盖已有工作。
- 默认从最新 main 起分支;用户指定其他目标分支时尊重该选择,并在 PR 中使用对应 base。
- 在 worktree 内完成改动,运行适用检查:`pnpm exec tsc --noEmit`、`pnpm run i18n:check`、`pnpm build`;涉及 Rust 时在 src-tauri 运行对应 crate 的检查。
- PR 自动检查、手动签名 arm64 APK、签名 Secrets 与产物验收见 [CI 与构建](../../../docs/ci.md);CI 文件变动检查 Actions 语法,产物收集脚本变动运行 `node --test scripts/collect-artifacts.test.mjs`。
- 同步更新受影响的文档、中英文文案与项目 skill;只提交本次任务文件,提交信息用中文 `feat/fix/docs: 内容`,改动较大时按完整改动分批提交。

## 推送与创建 PR
Expand Down
1 change: 1 addition & 0 deletions .agents/skills/readerx-source-engine/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ description: 修改 ReaderX 书源引擎、Boa 宿主 API、浏览器认证后
- 桌面登录看 src-tauri/plugins/tauri-plugin-webview-login;Linux 用 WebKitGTK CookieManager 读取含 httpOnly 的 Cookie;页面存储能力以 src/desktop.rs 的实测表为准,不用宿主 eval 读隔离的页面 localStorage。探针生成逻辑在引擎 storage.rs,后端不解析其内部结构。桌面正常收尾与 Android 一样不要求 Cookie 非空,仍采集并透传存储探针;等待标记按求值字符串解码后的完整值比较,不匹配快照子串。
- 数据根目录由宿主 store::init_data_root 指定。修改格式前检查 store.rs 布局和旧数据,保持 App/CLI 共用数据兼容,明确迁移方式。
- 书源稳定 ID 与迁移查看 src-tauri/crates/readerx-source/src/id_migration.rs;涉及在线书引用、分组或同步身份时同时读 [数据技能](../readerx-data/SKILL.md),检查 src-tauri/src/sync/data_ids.rs 与 src/lib/dataIds.ts,不因编辑书源地址重建已有稳定 ID。
- App 保存书源通过 sync/data_ids.rs 的 save_source;文件仍由共享书源 crate 写入,数据库中的 bookSourceId 引用与迁移映射由 App 维护。旧元信息损坏时保留映射并延后引用迁移。
- 新 App command 同步 src-tauri/src/lib.rs 注册与 capability;书源变动同步相应 docs/。

## 验证与交付
Expand Down
46 changes: 46 additions & 0 deletions .github/actions/setup-android/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: Setup Android
description: 准备 JDK、Gradle 缓存与 Android SDK / NDK
runs:
using: composite
steps:
- name: Setup JDK 17
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: '17'

# Gradle 依赖下载占 Android 构建的大头,缓存下来能省几分钟;
# key 带上 gen/android 下的构建脚本 hash,脚本变了就重新拉依赖。
- name: Cache Gradle
uses: actions/cache@v5
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: gradle-${{ runner.os }}-${{ hashFiles('src-tauri/gen/android/**/*.gradle', 'src-tauri/gen/android/**/*.gradle.kts', 'src-tauri/gen/android/gradle/wrapper/gradle-wrapper.properties') }}
restore-keys: gradle-${{ runner.os }}-

# GitHub 托管的 runner 预装了 Android SDK/NDK;选最高版本的 NDK 并导出,
# tauri CLI 的 ensure_ndk 会优先读取 NDK_HOME(CI 下不会自行下载 NDK)。
- name: Resolve Android SDK / NDK
shell: bash
run: |
ANDROID_HOME="${ANDROID_HOME:-/usr/local/lib/android/sdk}"
echo "ANDROID_HOME=${ANDROID_HOME}" >> "$GITHUB_ENV"
echo "ANDROID_SDK_ROOT=${ANDROID_HOME}" >> "$GITHUB_ENV"
NDK_DIR="$(find "${ANDROID_HOME}/ndk" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | sort -V | tail -n 1)"
if [ -z "$NDK_DIR" ]; then
echo "::error::未在 ${ANDROID_HOME}/ndk 下找到 NDK,请先安装 NDK 或在 Actions runner 中配置。"
exit 1
fi
echo "NDK_HOME=${NDK_DIR}" >> "$GITHUB_ENV"
echo "ANDROID_NDK_HOME=${NDK_DIR}" >> "$GITHUB_ENV"
echo "Using NDK: ${NDK_DIR}"

# 仓库里 gradle.properties 提交了本机路径 org.gradle.java.home,替换为 runner 的 JDK。
- name: Point Gradle at runner JDK
shell: bash
run: |
sed -i "s|^org\.gradle\.java\.home=.*|org.gradle.java.home=${JAVA_HOME}|" src-tauri/gen/android/gradle.properties
grep '^org.gradle.java.home' src-tauri/gen/android/gradle.properties

66 changes: 66 additions & 0 deletions .github/workflows/build-android-signed.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
name: Build Android (signed arm64, no publish)
on:
workflow_dispatch:
permissions:
contents: read
concurrency:
group: build-android-signed-${{ github.ref }}
cancel-in-progress: true
jobs:
signed-arm64:
name: Signed release APK (arm64-v8a)
runs-on: ubuntu-latest
timeout-minutes: 120
steps:
- uses: actions/checkout@v5
- uses: ./.github/actions/setup-build
with:
rust-targets: aarch64-linux-android
- uses: ./.github/actions/setup-android
- name: Configure Android signing (secrets)
env:
KEYSTORE_B64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
run: |
set -euo pipefail
umask 077
if [ -z "$KEYSTORE_B64" ] || [ -z "$KEYSTORE_PASSWORD" ] || [ -z "$KEY_ALIAS" ] || [ -z "$KEY_PASSWORD" ]; then
echo "::error::缺少签名 secrets。请在仓库 Settings -> Secrets and variables -> Actions 配置"
echo "::error::ANDROID_KEYSTORE_BASE64 / ANDROID_KEYSTORE_PASSWORD / ANDROID_KEY_ALIAS / ANDROID_KEY_PASSWORD。"
exit 1
fi
echo "$KEYSTORE_B64" | base64 -d > "$RUNNER_TEMP/android-release.keystore"
python3 - <<'PYTHON'
import os
from pathlib import Path
# Encode UTF-16 code units for java.util.Properties, preserving special characters.
def prop(value):
raw = value.encode('utf-16-be')
return ''.join('\\u%04x' % int.from_bytes(raw[i:i+2], 'big') for i in range(0, len(raw), 2))
values = {'keyAlias': os.environ['KEY_ALIAS'], 'password': os.environ['KEY_PASSWORD'],
'storePassword': os.environ['KEYSTORE_PASSWORD'],
'storeFile': os.environ['RUNNER_TEMP'] + '/android-release.keystore'}
Path('src-tauri/gen/android/keystore.properties').write_text(
''.join(f'{key}={prop(value)}\n' for key, value in values.items()))
PYTHON

- name: Build signed arm64 APK
run: pnpm tauri android build --apk --target aarch64 --split-per-abi --ci
timeout-minutes: 90
- name: Collect and verify signed arm64 APK
run: |
node scripts/collect-artifacts.mjs android --abi arm64-v8a --out "$RUNNER_TEMP/artifacts"
BUILD_TOOLS="$(find "$ANDROID_HOME/build-tools" -mindepth 1 -maxdepth 1 -type d | sort -V | tail -n 1)"
"$BUILD_TOOLS/apksigner" verify --verbose "$RUNNER_TEMP"/artifacts/*.apk
"$BUILD_TOOLS/aapt" dump badging "$RUNNER_TEMP"/artifacts/*.apk | grep -Fx "native-code: 'arm64-v8a'"
- name: Remove signing files
if: always()
run: rm -f "$RUNNER_TEMP/android-release.keystore" src-tauri/gen/android/keystore.properties
- uses: actions/upload-artifact@v6
with:
name: readerx-android-arm64-signed
path: ${{ runner.temp }}/artifacts/*.apk
if-no-files-found: error
retention-days: 14
43 changes: 43 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: PR CI
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
workflow_dispatch:
permissions:
contents: read
concurrency:
group: pr-ci-${{ github.ref }}
cancel-in-progress: true
jobs:
checks:
name: Frontend and workflow checks
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v5
- uses: ./.github/actions/setup-build
with:
system-deps: 'false'
- name: TypeScript types (no emit)
run: pnpm exec tsc --noEmit
- name: i18n keys and user-facing text
run: pnpm run i18n:check
- name: Artifact collection regression (fixtures only)
run: node --test scripts/collect-artifacts.test.mjs
- name: GitHub Actions syntax
run: |
curl --fail --silent --show-error --location \
https://github.com/rhysd/actionlint/releases/download/v1.7.12/actionlint_1.7.12_linux_amd64.tar.gz \
--output "$RUNNER_TEMP/actionlint.tar.gz"
tar -xzf "$RUNNER_TEMP/actionlint.tar.gz" -C "$RUNNER_TEMP" actionlint
"$RUNNER_TEMP/actionlint" -color
rust:
name: Rust workspace and test target checks
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@v5
- uses: ./.github/actions/setup-build
- name: Check Rust code and test targets (no application bundle)
working-directory: src-tauri
run: cargo check --locked --workspace --all-targets
43 changes: 43 additions & 0 deletions .github/workflows/test-data.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: Test book storage and data compatibility

on:
workflow_dispatch:

permissions:
contents: read

concurrency:
group: test-data-${{ github.ref }}
cancel-in-progress: true

jobs:
data-tests:
name: Linux storage, backup and sync regression
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@v5
- uses: ./.github/actions/setup-build
- name: Prepare frontend assets
run: pnpm build
- name: SQLite book storage and state
working-directory: src-tauri
run: cargo test --locked -p readerx --lib book_store
- name: Book identity migrations
working-directory: src-tauri
run: cargo test --locked -p readerx --lib sync::book_ids
- name: Group and rule identity migrations
working-directory: src-tauri
run: cargo test --locked -p readerx --lib sync::data_ids
- name: Backup export, import and compatibility
working-directory: src-tauri
run: cargo test --locked -p readerx --lib data_transfer
- name: App sync persistence integration
working-directory: src-tauri
run: cargo test --locked -p readerx --test sync_bridge
- name: Sync protocol regression
working-directory: src-tauri
run: cargo test --locked -p readerx-sync
- name: Source engine regression (serial Boa tests)
working-directory: src-tauri
run: cargo test --locked -p readerx-source -- --test-threads=1
Loading
Loading