Jelu 0.88.2 常用 CLI 命令与工具详解
Jelu 0.88.2 日常命令与工具实测手册——systemd 服务管理、java 前台启动与工作目录要点、REST API 三种认证与书目/阅读事件/统计/导入导出全链路 curl 实测、sqlite3 库内检查、calibre fetch-ebook-metadata 在线抓取、在线备份与恢复演练,全部命令附真实回显。
Jelu 0.88.2 常用 CLI 命令与工具详解
Jelu 本体是一个 Spring Boot fat jar(jelu-0.88.2.jar),没有自带的「jelu start」这类多子命令 CLI。日常运维实际围绕六类工具展开:systemd 服务管理、java 直接启动、REST API(curl)、sqlite3 数据库检查、calibre 的 fetch-ebook-metadata 元数据抓取,以及基于 SQLite 备份的恢复流程。本文在 Ubuntu 24.04 + Jelu 0.88.2 环境中把六类工具逐条实测,所有命令与回显均为真实执行结果,可直接照做。
一、环境信息
本章列出实测环境与关键路径,文中所有命令均在此环境验证。先确认 Jelu 服务与 Java 版本:
java -version
实测输出:
openjdk version "21.0.12.1" 2026-08-18
OpenJDK Runtime Environment (build 21.0.12.1+1-1-24.04.4-Ubuntu)
OpenJDK 64-Bit Server VM (build 21.0.12.1+1-1-24.04.4-Ubuntu, mixed mode, sharing)
systemctl is-active jelu
实测输出:
active
| 项目 | 值 |
|---|---|
| 操作系统 | Ubuntu 24.04 |
| Jelu 版本 | 0.88.2(jar:/opt/jelu/jelu-0.88.2.jar,108216402 字节) |
| Java | OpenJDK 21.0.12.1(Ubuntu 仓库 openjdk-21) |
| SQLite CLI | 3.45.1(Ubuntu 仓库 sqlite3 3.45.1-1ubuntu2.8) |
| calibre | 7.6.0+ds-1build1(提供 fetch-ebook-metadata) |
| 工作目录 | /var/lib/jelu(与 systemd 单元 WorkingDirectory 一致) |
| 数据目录 | /var/lib/jelu(数据库 jelu.db、日志 jelu.log、导入导出 files/imports/) |
| 服务端口 | 11111 |
| 演示管理员账号 | admin / q1w2e3r4 |
| 服务单元 | /etc/systemd/system/jelu.service |
/var/lib/jelu 实际内容(后续各节会反复用到这些文件):
ls -la /var/lib/jelu/
实测输出(节选):
drwxr-xr-x 7 jelu jelu 4096 Oct 28 11:24 .
-rw-r--r-- 1 jelu jelu 301 Oct 28 11:09 application.yml
-rw-r--r-- 1 jelu jelu 417792 Oct 28 11:24 jelu.db
-rw-r--r-- 1 jelu jelu 263908 Oct 28 11:24 jelu.log
drwxr-xr-x 4 jelu jelu 4096 Oct 28 08:14 files
drwxr-xr-x 4 jelu jelu 4096 Oct 28 11:19 tmp
-rw-r--r-- 1 jelu jelu 0 Oct 28 08:18 write.lock
关键差异解读:application.yml 是外置配置文件(C1 篇详解其逐项参数);jelu.db 是 SQLite 数据库单文件;write.lock 是 Jelu 在当前工作目录创建的锁文件,这是后文「java 直接启动」排障的关键线索。
二、工具概览
Jelu 无自带 CLI 子命令,以下六类工具覆盖它的全部命令行运维场景:
| 工具 | 负责什么 | 典型场景 |
|---|---|---|
systemd(systemctl/journalctl) | 服务生命周期与日志 | 启停重启、开机自启、看运行日志 |
java -jar 直接启动 | 绕开 systemd 前台调试 | 验证参数、看完整控制台输出 |
| REST API(curl) | 全部业务操作 | 书目、阅读事件、统计、导入导出、账号管理 |
| sqlite3 | 库内直查与备份 | 查表结构、核对数据、在线备份 |
fetch-ebook-metadata(calibre) | ISBN/书名在线抓元数据 | 手动补全元数据、验证元数据源可用性 |
| SQLite 备份恢复 | 数据保护 | .backup 在线备份 + 停服回灌恢复 |
选型解读:日常操作走 systemd + API 两类;排查启动问题用 java -jar 前台跑;数据层面的核对与保护用 sqlite3 与备份恢复。fetch-ebook-metadata 是 calibre 套件工具,Jelu 的元数据抓取默认就调用它(配置见 C1 篇)。
三、前置条件
- Jelu 已按 B1 篇完成安装并由 systemd 托管(
systemctl is-active jelu为active)。 - 已安装 java(上文
java -version回显)与 curl。 - sqlite3 与 calibre 确认已装(本文环境实测回显):
dpkg -l sqlite3 | tail -1
实测输出:
ii sqlite3 3.45.1-1ubuntu2.8 amd64 Command line interface for SQLite 3
dpkg -l calibre | tail -1
实测输出:
ii calibre 7.6.0+ds-1build1 all powerful and easy to use e-book manager
which fetch-ebook-metadata
实测输出:
/usr/bin/fetch-ebook-metadata
若 which 无输出,先安装 calibre(apt install calibre)再继续。
四、命令与工具详解
4.1 systemd 服务管理
Jelu 的标准运行形态是 systemd 服务 jelu.service,单元文件内容如下(cat /etc/systemd/system/jelu.service 实测):
[Unit]
Description=Jelu self-hosted reading tracker
After=network.target
[Service]
Type=simple
User=jelu
Group=jelu
WorkingDirectory=/var/lib/jelu
ExecStart=/usr/bin/java -Xms256m -Xmx512m -Djava.io.tmpdir=/var/lib/jelu/tmp -jar /opt/jelu/jelu-0.88.2.jar
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
逐项说明:User=jelu 以专用低权用户运行;WorkingDirectory=/var/lib/jelu 决定了外部配置 application.yml、write.lock 的位置(4.2 节会看到它有多关键);-Xms256m -Xmx512m 把 JVM 堆限在 256–512MB;Restart=on-failure + RestartSec=5 表示异常退出 5 秒后自动拉起。
状态与自启检查:
systemctl is-active jelu
实测输出:
active
systemctl is-enabled jelu
实测输出:
enabled
systemctl status jelu --no-pager -l
实测输出(节选,省略与主机路径相关的 CGroup 一行):
● jelu.service - Jelu self-hosted reading tracker
Loaded: loaded (/etc/systemd/system/jelu.service; enabled; preset: enabled)
Active: active (running) since Wed 2026-10-28 11:09:21 UTC; 22min ago
Main PID: 6106 (java)
Tasks: 43 (limit: 2932)
Memory: 568.2M (peak: 637.7M)
CPU: 25.765s
└─6106 /usr/bin/java -Xms256m -Xmx512m -Djava.io.tmpdir=/var/lib/jelu/tmp -jar /opt/jelu/jelu-0.88.2.jar
服务重启与就绪等待(实测:重启后立即访问会拿到 HTTP 000——连接拒绝,服务尚未就绪;约 8 秒后恢复 200):
systemctl restart jelu
systemctl is-active jelu
curl -s -o /dev/null -w "=> HTTP %{http_code}\n" -m 3 http://127.0.0.1:11111/api/v1/setup/status
实测输出:
active
=> HTTP 000
等待约 8 秒后再测:
sleep 8
curl -s -o /dev/null -w "=> HTTP %{http_code}\n" http://127.0.0.1:11111/api/v1/setup/status
实测输出:
=> HTTP 200
停止与启动(停止后 is-active 变为 failed,端口探测 000;启动后恢复):
systemctl stop jelu
systemctl is-active jelu
curl -s -o /dev/null -w "=> HTTP %{http_code}\n" -m 3 http://127.0.0.1:11111/api/v1/setup/status
实测输出:
failed
=> HTTP 000
systemctl start jelu
systemctl is-active jelu
实测输出:
active
日志查看(jelu 的运行日志同时落在 /var/lib/jelu/jelu.log,journal 里也有同一份):
journalctl -u jelu -n 4 --no-pager
实测输出(节选,每行行尾截断):
Oct 28 11:44:35 jelu-lab java[7231]: 2026-10-28T11:44:35.669Z INFO 7231 --- [io-11111-exec-1] o.s.web.servlet.DispatcherServlet : Completed initializati
Oct 28 11:44:35 jelu-lab java[7231]: 2026-10-28T11:44:35.676Z DEBUG 7231 --- [io-11111-exec-1] o.s.security.web.FilterChainProxy : Securing GET /api/v1/s
Oct 28 11:44:35 jelu-lab java[7231]: 2026-10-28T11:44:35.681Z DEBUG 7231 --- [io-11111-exec-1] o.s.s.w.a.AnonymousAuthenticationFilter : Set SecurityContextHol
Oct 28 11:44:35 jelu-lab java[7231]: 2026-10-28T11:44:35.682Z DEBUG 7231 --- [io-11111-exec-1] o.s.security.web.FilterChainProxy : Secured GET /api/v1/se
跟踪实时日志用 journalctl -u jelu -f(Ctrl+C 退出)。
4.2 java 直接启动(前台调试)
绕开 systemd 前台运行 jar,可以看到完整控制台输出,是验证启动参数的第一手段。但直接启动有一个必须遵守的前提:必须先 cd /var/lib/jelu(与 systemd 的 WorkingDirectory 一致)。先看错误示范——在当前目录为 / 时直接启动:
java -Xms256m -Xmx512m -Djava.io.tmpdir=/var/lib/jelu/tmp -jar /opt/jelu/jelu-0.88.2.jar
实测输出(节选,保留关键行):
2026-10-28T11:40:59.046Z INFO 6675 --- [ main] i.github.bayang.jelu.JeluApplicationKt : Starting JeluApplicationKt v0.88.2 using Java 21.0.12.1 with PID 6675 (/opt/jelu/jelu-0.88.2.jar started by jelu in /)
2026-10-28T11:41:05.754Z INFO 6675 --- [ main] o.s.boot.tomcat.TomcatWebServer : Tomcat started on port 11111 (http) with context path '/'
2026-10-28T11:41:05.934Z WARN 6675 --- [ main] ConfigServletWebServerApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: java.lang.reflect.UndeclaredThrowable
2026-10-28T11:41:05.955Z ERROR 6675 --- [ main] o.s.b.c.p.m.PropertiesMigrationListener :
The use of configuration keys that are no longer supported was found in the environment:
Key: spring.mvc.throw-exception-if-no-handler-found
Line: 25
Reason: DispatcherServlet property is deprecated for removal and should no longer need to be configured.
2026-10-28T11:41:05.956Z ERROR 6675 --- [ main] o.s.boot.SpringApplication : Application run failed
Caused by: java.nio.file.NoSuchFileException: /write.lock
端口虽短暂监听(Tomcat started 一行已出现),但上下文随后初始化失败、应用退出,服务不可用:
curl -s -o /dev/null -w "=> HTTP %{http_code}\n" http://127.0.0.1:11111/api/v1/setup/status
实测输出:
=> HTTP 000
排障结论:Jelu 启动时要在当前工作目录创建 write.lock,从 / 启动时无权在该目录创建文件,抛出 NoSuchFileException: /write.lock 后整个应用启动失败;同时工作目录不对,Spring Boot 也加载不到 /var/lib/jelu/application.yml 外置配置。⚠️ 所以 java -jar 直接启动前,务必先切换到服务工作目录。
正确姿势(先 cd 再启动):
cd /var/lib/jelu
java -Xms256m -Xmx512m -Djava.io.tmpdir=/var/lib/jelu/tmp -jar /opt/jelu/jelu-0.88.2.jar
实测输出(节选):
2026-10-28T11:42:54.914Z INFO 6997 --- [ main] i.github.bayang.jelu.JeluApplicationKt : Starting JeluApplicationKt v0.88.2 using Java 21.0.12.1 with PID 6997 (/opt/jelu/jelu-0.88.2.jar started by jelu in /var/lib/jelu)
2026-10-28T11:43:00.452Z INFO 6997 --- [ main] o.s.boot.tomcat.TomcatWebServer : Tomcat started on port 11111 (http) with context path '/'
2026-10-28T11:43:00.631Z INFO 6997 --- [ main] i.github.bayang.jelu.JeluApplicationKt : Started JeluApplicationKt in 6.138 seconds (process running for 6.569)
验证可用后 Ctrl+C(或另开终端 kill <PID>)停止,再用指定端口参数验证参数生效:
cd /var/lib/jelu
java -Xms256m -Xmx512m -Djava.io.tmpdir=/var/lib/jelu/tmp -jar /opt/jelu/jelu-0.88.2.jar --server.port=11113
实测输出(节选):
2026-10-28T11:43:12.697Z INFO 7078 --- [ main] o.s.boot.tomcat.TomcatWebServer : Tomcat started on port 11113 (http) with context path '/'
2026-10-28T11:43:12.884Z INFO 7078 --- [ main] i.github.bayang.jelu.JeluApplicationKt : Started JeluApplicationKt in 6.252 seconds (process running for 6.655)
另开终端验证:11113 端口正常响应、默认端口 11111 已无服务:
curl -s -o /dev/null -w "11113 => HTTP %{http_code}\n" http://127.0.0.1:11113/api/v1/setup/status
curl -s -o /dev/null -w "11111 => HTTP %{http_code}\n" -m 3 http://127.0.0.1:11111/api/v1/setup/status
实测输出:
11113 => HTTP 200
11111 => HTTP 000
要点提示:前台调试与 systemd 服务二选一,同时运行会撞端口与 SQLite/Lucene 锁。-Xms256m -Xmx512m 与 jelu.service 保持一致;-Djava.io.tmpdir 指向可执行目录(若落在 noexec 挂载的 /tmp 上,sqlite-jdbc 原生库会加载失败,排障实录见 B1 篇常见问题)。完整的启动参数、配置覆盖顺序与环境变量(SERVER_PORT、JELU_DATABASE_PATH 等)实测见 C1 篇。
4.3 REST API(curl)
Jelu 全部业务能力都暴露在 /api/v1 前缀的 REST API 上,完整的接口清单可打开 OpenAPI 文档 http://127.0.0.1:11111/v3/api-docs(实测 HTTP 200,返回体开头为 {"openapi":"3.1.0","info":{"title":"Jelu API","license":{"name":"MIT","url":"https://github.com/bayang/jelu/blob/main/LICENSE"},"version":"v1.0"},...},输出节选)。本节按「认证 → 个人 API 令牌 → 书目与阅读 → 统计与书评 → 导入导出 → 账号管理 → 元数据抓取」顺序实测。
(1)三种认证形态:Basic、X-Auth-Token、Bearer 个人令牌
最直接的是 HTTP Basic 认证,用它换取会话令牌:
curl -su admin:q1w2e3r4 -w "\n=> HTTP %{http_code}\n" http://127.0.0.1:11111/api/v1/token
实测输出:
{"token":"004035b3-021b-458d-b946-17893cabfcae"}
=> HTTP 200
拿到的 token 放进 X-Auth-Token 请求头即可访问数据端点(以书目与统计为例):
TOK=$(curl -su admin:q1w2e3r4 http://127.0.0.1:11111/api/v1/token | python3 -c "import json,sys;print(json.load(sys.stdin)['token'])")
curl -s -H "X-Auth-Token: $TOK" http://127.0.0.1:11111/api/v1/books | head -c 200; echo
curl -s -w "\n=> HTTP %{http_code}\n" -H "X-Auth-Token: $TOK" http://127.0.0.1:11111/api/v1/stats
实测输出(书目为首行节选):
{"content":[{"id":"89c2642d-fc00-4244-ba1d-67c7273daa37","creationDate":"2026-10-28T10:52:34.295Z","title":"三体","isbn10":null,"isbn13":null,"summary":"","publisher":null,"pageCount":null,"publishe
[{"dropped":0,"finished":1,"year":2026,"pageCount":0,"price":0.0}]
=> HTTP 200
⚠️ 实测怪癖:同一令牌访问 /api/v1/users/current 会返回 400 Bad Request({"timestamp":"2026-10-28T11:37:58.251+0000","status":400,"error":"Bad Request","path":"/api/v1/users/current"}),该端点请改用 Basic 认证或用数据端点验证令牌。
无凭据与错误密码都会被 401 拒绝(实测回显):
{"timestamp":"2026-10-28T11:34:31.532+0000","status":401,"error":"Unauthorized","path":"/api/v1/users/current"}
{"timestamp":"2026-10-28T11:34:31.591+0000","status":401,"error":"Unauthorized","path":"/api/v1/users/current"}
(2)个人 API 令牌(Bearer,最小授权)
个人 API 令牌是给脚本/自动化用的长效凭据,支持 scope 粒度授权。先看平台支持的全部 scope:
curl -su admin:q1w2e3r4 http://127.0.0.1:11111/api/v1/api-tokens/scopes
实测输出:
[{"name":"books:read","description":"View books, authors, tags, series, publishers","category":"Books & Metadata"},{"name":"books:write","description":"Create/modify/delete books and metadata","category":"Books & Metadata"},{"name":"reading:read","description":"View reading events and statistics","category":"Reading Events"},{"name":"reading:write","description":"Create/modify/delete reading events","category":"Reading Events"},{"name":"reviews:read","description":"View reviews","category":"Reviews"},{"name":"reviews:write","description":"Create/modify/delete reviews","category":"Reviews"},{"name":"lists:read","description":"View custom lists, shelves, quotes","category":"Lists & Shelves"},{"name":"lists:write","description":"Create/modify/delete lists, shelves, quotes","category":"Lists & Shelves"},{"name":"import:write","description":"Import/export data, browse filesystem","category":"Import/Export"},{"name":"metadata:read","description":"Fetch external metadata from providers","category":"External Metadata"}]
创建令牌时 scopes 是必填字段,缺了会报 400(实测错误信息,用于排障对照):
{"message":"JSON parse error: Cannot construct instance of `io.github.bayang.jelu.dto.CreateApiTokenDto`, problem: Parameter specified as non-null is null: method io.github.bayang.jelu.dto.CreateApiTokenDto.<init>, parameter scopes","violations":[]}
带 name 与 scopes 正常创建:
curl -su admin:q1w2e3r4 -w "\n=> HTTP %{http_code}\n" -H "Content-Type: application/json" \
-X POST http://127.0.0.1:11111/api/v1/api-tokens \
-d '{"name":"cli-doc","scopes":["books:read"]}'
实测输出:
{"token":{"id":"9d2fb9d1-0d5f-4e8d-8953-c9b1e08434ca","userId":"8ab5893d-6f33-3ebd-0a80-539927125ff7","name":"cli-doc","scopes":["books:read"],"createdAt":"2026-10-28T11:40:32.079862737Z","lastUsedAt":null,"usageCount":0,"expiresAt":null,"active":true},"rawToken":"jelu_4f544e32b16a81189d90d483f541f3f1"}
=> HTTP 201
⚠️ rawToken 只在创建这一次响应里出现(jelu_ 前缀),请立即保存;之后列表接口只能看到令牌元数据,看不到原文。用 Authorization: Bearer <rawToken> 调用,scope 之外的端点会被 403 拒绝:
RAW=jelu_4f544e32b16a81189d90d483f541f3f1
curl -s -o /dev/null -w "books => HTTP %{http_code}\n" -H "Authorization: Bearer $RAW" http://127.0.0.1:11111/api/v1/books
curl -s -w "\nreviews => HTTP %{http_code}\n" -H "Authorization: Bearer $RAW" http://127.0.0.1:11111/api/v1/reviews
实测输出:
books => HTTP 200
{"timestamp":"2026-10-28T11:40:32.132+0000","status":403,"error":"Forbidden","path":"/api/v1/reviews"}
reviews => HTTP 403
令牌只授了 books:read,访问书评接口即被拒——scope 管控真实生效。查看令牌列表(含用量统计):
curl -su admin:q1w2e3r4 http://127.0.0.1:11111/api/v1/api-tokens
实测输出:
[{"id":"632a1dc6-d289-4579-bf6e-7a7bc9dba6f9","userId":"8ab5893d-6f33-3ebd-0a80-539927125ff7","name":"ci-script","scopes":["books:read"],"createdAt":"2026-10-28T11:15:44.424Z","lastUsedAt":"2026-10-28T11:16:19.924Z","usageCount":1,"expiresAt":null,"active":true}]
(3)书目与阅读事件
查书目与阅读状态(userbooks 是「某用户 × 某本书」的阅读维度视图):
curl -su admin:q1w2e3r4 http://127.0.0.1:11111/api/v1/books | python3 -m json.tool | head -30
实测输出(节选):
{
"content": [
{
"id": "89c2642d-fc00-4244-ba1d-67c7273daa37",
"title": "三体",
"isbn10": null,
"isbn13": null,
"authors": [
{
"name": "刘慈欣"
}
],
"userBookId": "bd40c32c-b368-4475-a441-e96d3859bbb0"
}
]
}
curl -su admin:q1w2e3r4 http://127.0.0.1:11111/api/v1/userbooks | python3 -m json.tool | grep -E '"(lastReadingEvent|percentRead|avgRating|userAvgRating)"'
实测输出(节选):
"lastReadingEvent": "FINISHED",
"percentRead": 100,
"avgRating": 5.0,
"userAvgRating": 5.0,
Lucene 全文搜索(按题名/作者模糊匹配):
curl -su admin:q1w2e3r4 -w "\n=> HTTP %{http_code}\n" "http://127.0.0.1:11111/api/v1/books?q=%E4%B8%89%E4%BD%93" | head -c 120; echo
实测输出:
{"content":[{"id":"89c2642d-fc00-4244-ba1d-67c7273daa37","creationDate":"2026-10-28T10:52:34.295Z","title":"三体","isbn10":null,
=> HTTP 200
记录阅读事件。⚠️ 先看错误示范:不带 bookId 会直接 500(实测错误信息):
{"message":"Missing bookId to create reading event","violations":[]}
带 bookId 后创建「读完」事件(已存在的 userBook 会被复用,不会重复建档):
curl -su admin:q1w2e3r4 -o /tmp/re.json -w "=> HTTP %{http_code}\n" -H "Content-Type: application/json" \
-X POST http://127.0.0.1:11111/api/v1/reading-events \
-d '{"bookId":"89c2642d-fc00-4244-ba1d-67c7273daa37","eventType":"FINISHED","startDate":"2026-10-28T12:00:00.000Z","endDate":"2026-10-28T12:00:00.000Z"}'
cat /tmp/re.json | python3 -m json.tool | grep -E '"(id|eventType|percentRead)"' | head -6
实测输出:
=> HTTP 200
"id": "7842bb47-8030-40ef-b199-60efd81e771e",
"id": "bd40c32c-b368-4475-a441-e96d3859bbb0",
"eventType": "FINISHED",
"percentRead": 100,
eventType 支持 FINISHED/CURRENTLY_READING/DROPPED 等阅读状态;事件创建后统计与 CSV 导出随即反映。
(4)阅读统计
curl -su admin:q1w2e3r4 -w "\n=> HTTP %{http_code}\n" http://127.0.0.1:11111/api/v1/stats
curl -su admin:q1w2e3r4 -w "\n=> HTTP %{http_code}\n" http://127.0.0.1:11111/api/v1/stats/2026
curl -su admin:q1w2e3r4 -w "\n=> HTTP %{http_code}\n" http://127.0.0.1:11111/api/v1/stats/years
实测输出:
[{"dropped":0,"finished":1,"year":2026,"pageCount":0,"price":0.0}]
=> HTTP 200
[{"dropped":0,"finished":1,"year":2026,"month":10,"pageCount":0,"price":0.0}]
=> HTTP 200
[2026]
=> HTTP 200
解读:/stats 按年聚合(2026 年读完 1 本),/stats/{年份} 细到月份(10 月读完 1 本),/stats/years 返回有统计数据的年份列表。
(5)书评
curl -su admin:q1w2e3r4 http://127.0.0.1:11111/api/v1/reviews
实测输出(节选):
{"content":[{"id":"5a78fe5d-bf77-450d-b088-649c8dcab3fa","reviewDate":"2026-10-28T11:15:44.587Z","text":"硬科幻里程碑","rating":5.0,"visibility":"PUBLIC","user":"8ab5893d-6f33-3ebd-0a80-539927125ff7","book":"89c2642d-fc00-4244-ba1d-67c7273daa37"}],"totalElements":1}
(6)导出 CSV 与导入
导出是 POST 触发(⚠️ GET /api/v1/exports 会返回 405 Method Not Allowed),CSV 落在 files/imports/ 下:
curl -su admin:q1w2e3r4 -o /dev/null -w "POST exports => HTTP %{http_code}\n" -X POST http://127.0.0.1:11111/api/v1/exports
ls -la /var/lib/jelu/files/imports/
实测输出:
POST exports => HTTP 201
-rw-r--r-- 1 jelu jelu 14 Oct 28 11:38 isbn-d1.txt.imported
-rw-r--r-- 1 jelu jelu 195 Oct 28 11:15 jelu-export-admin-20261028T111544.csv
-rw-r--r-- 1 jelu jelu 195 Oct 28 11:34 jelu-export-admin-20261028T113445.csv
head -2 /var/lib/jelu/files/imports/jelu-export-admin-20261028T111544.csv
实测输出:
Title,Author,ISBN,Publisher,Date Read,Shelves,Bookshelves,read_dates,tags,authors,isbn10,isbn13,owned,dropped_dates,currently_reading
三体,刘慈欣,,,2026/10/28,,,2026/10/28,,刘慈欣,,,,,
导入接口是 multipart 表单(file + importConfig 两个表单字段;⚠️ 直接发 JSON 会返回 415 Unsupported Media Type)。importSource 只接受四个枚举值,写错会得到列出全部合法值的 400(实测错误信息,排障时直接照抄修正):
{"message":"JSON parse error: Cannot deserialize value of type `io.github.bayang.jelu.dao.ImportSource` from String \"ISBN\": not one of the values accepted for Enum class: [GOODREADS, STORYGRAPH, LIBRARYTHING, ISBN_LIST]","violations":[]}
用 ISBN_LIST(每行一个 ISBN 的文本文件)导入:
printf "9780134685991\n" > /tmp/isbn-d1.txt
curl -su admin:q1w2e3r4 -o /dev/null -w "POST imports => HTTP %{http_code}\n" -X POST \
-F "file=@/tmp/isbn-d1.txt" \
-F 'importConfig={"importSource":"ISBN_LIST"};type=application/json' \
http://127.0.0.1:11111/api/v1/imports
ls /var/lib/jelu/files/imports/
curl -su admin:q1w2e3r4 "http://127.0.0.1:11111/api/v1/books?size=50" | python3 -c "import json,sys;d=json.load(sys.stdin);print('totalElements:',d['totalElements'],'titles:',[b['title'] for b in d['content']])"
实测输出:
POST imports => HTTP 201
isbn-d1.txt.imported
isbns.txt.imported
jelu-export-admin-20261028T111544.csv
jelu-export-admin-20261028T113445.csv
totalElements: 1 titles: ['三体']
现象解读(实测结论):导入请求受理返回 201,但书目不会立刻出现。Jelu 会把上传文件改名为 *.imported(表示已消费、不会重复导入),然后异步逐个 ISBN 调元数据提供方检索;本实测环境对外网元数据源不可达(4.3(8)节的 errors 日志可见),检索无结果时该书目不会创建。读者环境若能正常联网访问 Google/Amazon,导入后会自动建出带完整元数据的书目。
(7)账号管理(管理员)
管理员用 Basic 认证建户。⚠️ CreateUserDto 的 provider 是必填字段,缺了报 400(实测):
{"message":"JSON parse error: Cannot construct instance of `io.github.bayang.jelu.dto.CreateUserDto`, problem: Parameter specified as non-null is null: method io.github.bayang.jelu.dto.CreateUserDto.<init>, parameter provider","violations":[]}
带 provider 建户成功,响应里密码以 **** 掩码:
curl -su admin:q1w2e3r4 -w "\n=> HTTP %{http_code}\n" -H "Content-Type: application/json" \
-X POST http://127.0.0.1:11111/api/v1/users \
-d '{"login":"temp1","password":"q1w2e3r4","isAdmin":false,"provider":"JELU_DB"}'
实测输出:
{"id":"5fa6e1b2-61e1-49c9-8b04-f86226b5f0dc","creationDate":"2026-10-28T11:38:18.124029805Z","login":"temp1","password":"****","modificationDate":"2026-10-28T11:38:18.124029805Z","admin":false,"provider":"JELU_DB"}
=> HTTP 200
改密用 PUT /api/v1/users/{id}(完整传用户字段 + 新 password):
curl -su admin:q1w2e3r4 -o /tmp/upd.json -w "PUT user => HTTP %{http_code}\n" -H "Content-Type: application/json" \
-X PUT http://127.0.0.1:11111/api/v1/users/5fa6e1b2-61e1-49c9-8b04-f86226b5f0dc \
-d '{"login":"temp1","password":"newpass123","email":"","admin":false,"provider":"JELU_DB"}'
curl -su temp1:newpass123 -o /dev/null -w "login temp1 => HTTP %{http_code}\n" http://127.0.0.1:11111/api/v1/books
实测输出:
PUT user => HTTP 200
login temp1 => HTTP 200
新口令立即生效。删除用户:
curl -su admin:q1w2e3r4 -o /dev/null -w "DELETE user => HTTP %{http_code}\n" \
-X DELETE http://127.0.0.1:11111/api/v1/users/5fa6e1b2-61e1-49c9-8b04-f86226b5f0dc
实测输出:
DELETE user => HTTP 204
⚠️ 标签接口只支持查询与关联:GET /api/v1/tags 正常返回分页结构(本环境 totalElements:0),但 POST /api/v1/tags 返回 405 Method Not Allowed——标签的创建只能通过 Web 界面完成。
(8)元数据在线抓取
让 Jelu 按 ISBN 从元数据提供方抓取:
curl -su admin:q1w2e3r4 -m 90 -w "\n=> HTTP %{http_code} time %{time_total}s\n" \
"http://127.0.0.1:11111/api/v1/metadata?isbn=9780134685991"
实测输出(节选,errors 内的插件日志保留了首尾):
{"amazonId":null,"authors":[],"errors":[{"sourcePlugin":"calibre","errorType":"EXIT_CODE_NOT_ZERO","pluginErrorMessage":"Running identify query with parameters:\n{'title': None, 'authors': [], 'identifiers': {'isbn': '9780134685991'}, 'timeout': 30}\nUsing plugins: Google (1, 1, 1), Amazon.com (1, 3, 7)\n****************************** Google (1, 1, 1) ******************************\nFound 0 results\nDownloading from Google took 30.071556568145752\nTimeoutError: timed out\n****************************** Amazon.com (1, 3, 7) ******************************\nFound 0 results\nOSError: [Errno 101] Network is unreachable\nNo results found"}],"filled":false,"isbn13":null,"title":null}
=> HTTP 200 time 30.434841s
解读:端点本身返回 200,抓取结果在响应体里;errors 数组聚合每个提供方的失败详情。本实测环境访问 books.google.com 超时、google.com 不可达(Network is unreachable),calibre 子进程 30 秒超时后以非零码退出(EXIT_CODE_NOT_ZERO),filled:false 表示未取到元数据。读者环境联网正常时,同一端点会返回 filled:true 且 title/authors/publisher 等字段被填充。
4.4 sqlite3 直查与备份
Jelu 的数据都在单个 SQLite 文件 /var/lib/jelu/jelu.db 里,用 sqlite3 可以直接检查。查看全部表(实测 25 张,含 Liquibase 与会话表):
sqlite3 /var/lib/jelu/jelu.db ".tables"
实测输出:
DATABASECHANGELOG custom_list
DATABASECHANGELOGLOCK import_entity
SPRING_SESSION lifecycle
SPRING_SESSION_ATTRIBUTES reading_event
api_token review
author series
book series_rating
book_authors shelf
book_narrators tag
book_quote user
book_series user_book
book_tags user_message
book_translators
核心业务表:book(书目)、author(作者)、user_book(用户×书阅读状态)、reading_event(阅读事件)、review(书评)、tag(标签)、api_token(个人 API 令牌)、user(用户)。查看用户表结构与数据:
sqlite3 /var/lib/jelu/jelu.db ".schema user"
实测输出:
CREATE TABLE user (id BLOB(16) NOT NULL, creation_date TEXT NOT NULL, modification_date TEXT NOT NULL, login VARCHAR(50) NOT NULL, password VARCHAR(1000) NOT NULL, is_admin BOOLEAN NOT NULL, provider varchar(200), CONSTRAINT PK_USER PRIMARY KEY (id));
sqlite3 /var/lib/jelu/jelu.db "SELECT login,is_admin,provider FROM user;"
实测输出:
admin|1|JELU_DB
reader|0|JELU_DB
password 列存 bcrypt 哈希(SELECT 时排除该列即可);provider=JELU_DB 表示本地数据库认证账号。确认当前日志模式(C1 篇实测过 wal 切换,本环境当前为默认 delete):
sqlite3 /var/lib/jelu/jelu.db "PRAGMA journal_mode;"
实测输出:
delete
4.5 fetch-ebook-metadata(calibre 元数据抓取)
fetch-ebook-metadata 是 calibre 自带的在线元数据抓取 CLI,也是 Jelu 默认的元数据提供方(jelu.metadata.calibre.path 指向它)。版本与帮助:
fetch-ebook-metadata --version
实测输出:
fetch-ebook-metadata (calibre 7.6)
fetch-ebook-metadata --help
实测输出(全文):
Usage: fetch-ebook-metadata [options]
Fetch book metadata from online sources. You must specify at least one
of title, authors or ISBN.
Whenever you pass arguments to fetch-ebook-metadata that have spaces in them, enclose the arguments in quotation marks. For example: "/some path/with spaces"
Options:
--version show program's version number and exit
-h, --help show this help message and exit
-t TITLE, --title=TITLE
Book title
-a AUTHORS, --authors=AUTHORS
Book author(s)
-i ISBN, --isbn=ISBN Book ISBN
-I IDENTIFIER, --identifier=IDENTIFIER
Identifiers such as ASIN/Goodreads id etc. Can be
specified multiple times for multiple identifiers. For
example: --identifier asin:B0082BAJA0
-v, --verbose Print the log to the console (stderr)
-o, --opf Output the metadata in OPF format instead of human
readable text.
-c COVER, --cover=COVER
Specify a filename. The cover, if available, will be
saved to it. Without this option, no cover will be
downloaded.
-d TIMEOUT, --timeout=TIMEOUT
Timeout in seconds. Default is 30
-p ALLOWED_PLUGIN, --allowed-plugin=ALLOWED_PLUGIN
Specify the name of a metadata download plugin to use.
By default, all metadata plugins will be used. Can be
specified multiple times for multiple plugins. All
plugin names: Google, Google Images, Amazon.com,
Edelweiss, Open Library, Big Book Search
参数要点:-d 默认 30 秒超时(与 4.3(8)节 Jelu 实测的 30.4 秒总耗时吻合);内置插件为 Google、Google Images、Amazon.com、Edelweiss、Open Library、Big Book Search;-o 输出 OPF 格式(可喂给其他工具)。
按 ISBN 检索(本实测环境外网元数据源不可达,拿到真实的「无结果」输出——正好演示失败形态与退出码):
fetch-ebook-metadata --isbn 9787532776413; echo "exit=$?"
实测输出:
Running identify query with parameters:
{'title': None, 'authors': [], 'identifiers': {'isbn': '9787532776413'}, 'timeout': 30}
Using plugins: Google (1, 1, 1), Amazon.com (1, 3, 7)
The log from individual plugins is below
****************************** Google (1, 1, 1) ******************************
Found 0 results
Downloading from Google took 0.03614044189453125
Insufficient metadata to construct query
********************************************************************************
****************************** Amazon.com (1, 3, 7) ******************************
Found 0 results
Downloading from Amazon.com took 0.026116132736206055
User-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36
Server: auto
Insufficient metadata to construct query, none of title, ISBN or ASIN supplied
********************************************************************************
The identify phase took 0.20 seconds
The longest time (0.036140) was taken by: Google
Merging results from different sources
We have 0 merged results, merging took: 0.00 seconds
No results found
exit=1
解读:检索无结果时输出 No results found 且退出码为 1——脚本里可以用 $? 判断成败;联网正常的环境同样的命令会打印书名、作者、出版信息。Jelu 调用它时通过 identifiers 传入 ISBN(4.3(8)节 errors 日志里的 Running identify query 即它的工作日志)。
4.6 备份与恢复
Jelu 全部业务数据在 jelu.db 单文件里(封面/导入文件在 files/ 下),备份两件套:sqlite3 在线备份 + 文件复制。
在线备份(不用停服务;用 runuser -u jelu 执行保证备份文件属主为服务用户):
mkdir -p /var/lib/jelu/backup && chown jelu:jelu /var/lib/jelu/backup
runuser -u jelu -- sqlite3 /var/lib/jelu/jelu.db ".backup /var/lib/jelu/backup/jelu-20261028.db"
.backup 是 SQLite 在线备份 API,能拿到一致性快照(比直接 cp 正在写的库文件安全)。校验备份完整性:
sqlite3 /var/lib/jelu/backup/jelu-20261028.db "PRAGMA integrity_check;"
sqlite3 /var/lib/jelu/backup/jelu-20261028.db "SELECT count(*) FROM book;"
ls -la /var/lib/jelu/backup/
实测输出:
ok
1
-rw-r--r-- 1 jelu jelu 589824 Oct 28 11:44 jelu-20261028.db
integrity_check 返回 ok、书目行数与线上一致(1),备份可用。
恢复演练(把备份回灌为主库;当前备份与主库内容一致,可安全演练):
systemctl stop jelu
cp /var/lib/jelu/backup/jelu-20261028.db /var/lib/jelu/jelu.db && chown jelu:jelu /var/lib/jelu/jelu.db
systemctl start jelu
sleep 8
systemctl is-active jelu
curl -s -o /dev/null -w "curl => HTTP %{http_code}\n" http://127.0.0.1:11111/api/v1/setup/status
curl -su admin:q1w2e3r4 http://127.0.0.1:11111/api/v1/books | python3 -c "import json,sys;d=json.load(sys.stdin);print('books totalElements:',d['totalElements'])"
实测输出:
active
curl => HTTP 200
books totalElements: 1
恢复后服务正常、数据完整。要点提示:
- 若 C1 篇把
journalMode切到了wal,库目录会有jelu.db-wal/jelu.db-shm两个伴生文件,文件级备份必须三者一起拷,或先停服再拷; - CSV 导出(4.3(6)节)是跨系统迁移用的「逻辑备份」,与
jelu.db物理备份互为补充; - 长期方案建议 cron 定时执行
.backup并异地保存backup/目录。
五、注意事项
以下要点来自实测中踩过或验证过的行为,操作前过一遍可避免大部分坑:
- 工作目录敏感:
java -jar前台启动必须cd /var/lib/jelu,否则write.lock创建失败导致启动失败(4.2 节实录)。 - 端口与锁独占:systemd 服务与前台实例二选一;同时跑两个实例会撞 11111 端口并争抢 SQLite/Lucene 锁。
- 重启就绪窗口:
systemctl restart后立即访问会拿到HTTP 000,实测约 8 秒后就绪,脚本里重试再判定。 - API 令牌最小授权:
rawToken仅创建时可见;按需授予 scope(实测越权 403);令牌泄露后应在界面或 API 立即吊销。 - 导入是异步的:
POST /api/v1/imports201 只代表受理;书目出现依赖元数据提供方检索成功,源文件会被改名为*.imported不会重复导入。 - 元数据源可达性:calibre 默认走 Google/Amazon 等,本实测环境不可达时 30 秒超时返回
filled:false;自建环境若在内网,需给服务器配代理或换用可达的元数据插件。 - 直查数据库只读为主:sqlite3 直查适合核对与备份,业务写入请走 API/界面,避免绕过应用层校验。
六、常见问题
以下问题全部来自本文实测过程,每条给出真实错误信息与已验证的解决办法:
Q1:java -jar 直接启动报 NoSuchFileException: /write.lock 后退出?
工作目录不对。Jelu 启动时在当前目录创建 write.lock,从 / 等无权目录启动即失败(4.2 节完整实录)。解决:cd /var/lib/jelu 后再启动,与 systemd 的 WorkingDirectory 保持一致。
Q2:重启服务后立刻 curl 返回 HTTP 000?
服务尚未完成启动。实测重启后立即访问 000、约 8 秒后 200(4.1 节实录)。健康检查脚本加重试等待即可。
Q3:POST /api/v1/tags 或 GET /api/v1/exports 返回 405?
设计如此:标签创建只支持 Web 界面(API 仅查询/关联);导出必须用 POST 触发(4.3(6)节)。按正确方法调用即可。
Q4:POST /api/v1/reading-events 返回 500 Missing bookId to create reading event?
请求体缺少 bookId 字段。带上书目 id(如 {"bookId":"89c2642d-fc00-4244-ba1d-67c7273daa37","eventType":"FINISHED"})即可,实测 200(4.3(3)节)。
Q5:建户 400、创建令牌 400、导入 400?
三个 DTO 各有必填字段:建户必须带 "provider":"JELU_DB";创建 API 令牌必须带 scopes 数组;importSource 只接受 GOODREADS/STORYGRAPH/LIBRARYTHING/ISBN_LIST(实测错误信息见 4.3(2)(6)(7)节,报错信息会列出合法值)。
Q6:用 X-Auth-Token 访问 /api/v1/users/current 返回 400?
实测该端点对令牌认证返回 400(对 Basic 正常)。验证令牌请改用数据端点(如 /api/v1/books、/api/v1/stats,4.3(1)节)。
Q7:ISBN 导入返回 201 但书目没出现?
正常现象:导入异步执行,先把文件改名 *.imported,再逐个 ISBN 向元数据提供方检索;检索无结果(如本环境外网不可达)则不建书目(4.3(6)节实测)。可在 /api/v1/metadata?isbn=9780134685991 的 errors 里看具体失败原因。
Q8:Bearer 令牌访问部分接口返回 403?
该令牌未授予对应 scope。GET /api/v1/api-tokens/scopes 查全部 scope,重新创建令牌时按需授予(4.3(2)节实测:仅授 books:read 的令牌访问书评 403)。
七、参考链接
本文所属系列与官方资源汇总(站内链接可点击跳转):
- 站内系列:《Jelu 介绍与选型指南》(定位、架构与选型对比);《Ubuntu 24.04 安装 Jelu 0.88.2 指南》(从零安装与 systemd 托管);《Jelu 0.88.2 配置参数详解》(外置 application.yml 逐项参数与环境变量实测)
- 官方文档站:https://bayang.github.io/jelu-web/
- 源码仓库:https://github.com/bayang/jelu(MIT License)
- OpenAPI 接口清单:
http://127.0.0.1:11111/v3/api-docs(本机访问;实测返回 openapi 3.1.0 JSON)