Isso 0.14.0 常用 CLI 命令与工具详解
Isso 0.14.0 CLI 两个子命令全景:run 的 gevent/werkzeug/unix socket 选择逻辑,import 的 dry-run 临时库实现、非交互 EOFError、--empty-id ElementTree 陷阱源码级解释,Disqus/WordPress/Generic 三格式迁移实测与 12 条坑清单,附 sqlite3 库结构直查、curl API 日常与备份工具。
Isso 0.14.0 常用 CLI 命令与工具详解
本文以 2026-11-02 在 Ubuntu 22.04 上实测的 Isso 0.14.0 为对象,把命令行侧能力一次讲透:
isso入口的全部子命令与参数、run的服务器选择逻辑、import三种迁移格式(Disqus/WordPress/Generic)的逐字段实测(含 dry-run、非交互确认、--empty-id等真实踩坑),以及围绕 SQLite 库、curl API 与 systemd 的配套运维工具。部署上下文见 《Isso 0.14.0 pip + systemd 部署指南》,配置参数语义见 《Isso 0.14.0 配置参数详解》,HTTP 接口细节见 《Isso 0.14.0 API 使用详解》。
一、CLI 全景
1.1 唯一入口与帮助总览
pip install isso 只注册一个控制台脚本 isso(venv 里另有 misaka、mistune 等属于依赖包,与 Isso 运维无关)。总帮助实测:
$ isso --help
usage: isso [-h] [--version] [-c /etc/isso.cfg] {import,run} ...
a blog comment hosting service
positional arguments:
{import,run} commands
import import Disqus XML export
run run server
options:
-h, --help show this program's help message and exit
--version show program's version number and exit
-c /etc/isso.cfg set configuration file
也就是说 0.14.0 的 CLI 面非常小:只有 import 与 run 两个子命令,没有子命令之外的任何动作。
1.2 配置定位:-c 与 ISSO_SETTINGS
-c缺省值就是/etc/isso.cfg(帮助里的 metavar 直接写死)。- 环境变量
ISSO_SETTINGS优先级高于-c(源码isso/__init__.py:conf_file = os.environ.get("ISSO_SETTINGS") or args.conf)。实测:
$ ISSO_SETTINGS=/etc/isso.cfg timeout 3 isso 2>&1 | head -2
2026-11-02 06:36:45,298:INFO: Using configuration file '/etc/isso.cfg'
2026-11-02 06:36:45,306:INFO: Loading Mistune with plugins: ['strikethrough', 'subscript', 'superscript'] and parameters: ['escape', 'hard_wrap']
- 指定的配置不存在时快速失败,日志明确:
$ isso -c /nope.cfg run 2>&1 | tail -1
2026-11-02 06:36:44,990:ERROR: Specified config '/nope.cfg' does not exist! Exiting.
1.3 无子命令 = run
位置参数不强制,裸敲 isso 等价 isso run。systemd 实例占用端口时手工再启动,能同时看到这两件事:
$ isso -c /etc/isso.cfg 2>&1 | tail -2
Address already in use
Port 8080 is in use by another program. Either identify and stop the program or start the server with a different port.
1.4 版本与不存在的命令
$ isso --version
isso 0.14.0
$ isso gen-passwd 2>&1 | tail -1
isso: error: argument command: invalid choice: 'gen-passwd' (choose from 'import', 'run')
没有 gen-passwd/gen-key 之类的凭据子命令——管理密码只能在 [admin] password 明文配置(详见部署篇第八节)。
二、run:内置服务器
isso run --help 实测无任何自有选项(只有 -h):
usage: isso run [-h]
options:
-h, --help show this help message and exit
行为完全由 [server] listen 与 [server] reload 驱动。源码 isso/__init__.py 的三分支逻辑(实测环境未装 gevent,走第二分支):
listen = http://host:port且 gevent 可导入 →geevent.pywsgi.WSGIServer(...).serve_forever();- gevent 不可用 → werkzeug
run_simple(host, port, app, threaded=True, use_reloader=reload)——响应头可见Server: Werkzeug/3.1.9 Python/3.10.12,生产建议pip install gevent或按官方 Init scripts 思路套 gunicorn/uwsgi; listen = unix:///path.sock→ 自建SocketHTTPServer,先 unlink 残留 socket 再监听(profile = true会打印慢函数统计,仅限调试)。
reload = true 时改源码自动重启,仅对内置服务器有效——开发用,生产保持 false。
三、import:迁移导入总规则
$ isso import --help
usage: isso import [-h] [-n] [-t {disqus,wordpress,generic}] [--empty-id] FILE
positional arguments:
FILE
options:
-h, --help show this help message and exit
-n, --dry-run perform a trial run with no changes made
-t {disqus,wordpress,generic}, --type {disqus,wordpress,generic}
export type
--empty-id workaround for weird Disqus XML exports, #135
源码层面有四条硬规则,全部实测验证过:
- dry-run 的实现是"导入到临时库":
args.dryrun时新建tempfile.NamedTemporaryFile()作为 dbpath,正式库纹丝不动——所以试跑输出与真实导入一致,可放心先跑-n。 - 导入前自动关闭 guard:
conf.set("guard", "enabled", "off"),批量写入不触发限流。 - 非空库要交互确认,且非交互环境直接炸:
$ isso -c /etc/isso.cfg import -t disqus /tmp/disqus.xml 2>&1 | tail -3
migrate.dispatch(args.type, mydb, args.dump, args.empty_id)
File "/opt/isso/lib/python3.10/site-packages/isso/migrate.py", line 337, in dispatch
if input("Isso DB is not empty! Continue? [y/N]: ") not in ("y", "Y"):
EOFError: EOF when reading a line
脚本化场景用 echo y | isso ... import ... 管道喂确认(实测可用)。注意确认提示在 stderr 的 INFO 行之前打印(input 先于日志 flush 的观感),拼管道时留意。
4. import 分支收尾是 sys.exit(0):即使中途解析异常被吞,退出码也恒为 0,验收要靠查库而不是看返回码。
四、Disqus XML 导入实测
4.1 格式硬要求
解析器 isso/migrate.py::Disqus 决定了格式底线:
- 根元素
<disqus>,命名空间xmlns="http://disqus.com"、xmlns:dsq="http://disqus.com/disqus-internals"; <thread>与<post>都是根级平铺元素(tree.findall(ns + "post")只扫直接子节点,把 post 嵌在 thread 里会解析到 0 条);post用<thread dsq:id="..."/>关联线程,thread用自身dsq:id属性对上号;createdAt必须匹配%Y-%m-%dT%H:%M:%SZ(结尾的 Z 不能少);isDeleted决定 mode:false→1(发布),否则 4(删除但保留引用)。
实测样本 /tmp/disqus.xml:
<disqus xmlns="http://disqus.com" xmlns:dsq="http://disqus.com/disqus-internals">
<thread dsq:id="1">
<id>id-1</id>
<title>Test Post</title>
<link>http://10.89.72.10/test/1</link>
<createdAt>2026-01-01T00:00:00Z</createdAt>
</thread>
<post dsq:id="10">
<thread dsq:id="1"/>
<parent/>
<message>imported comment via CLI</message>
<author><name>carol</name><email>c@d.e</email><url/></author>
<createdAt>2026-01-01T00:00:00Z</createdAt>
<isDeleted>false</isDeleted>
</post>
</disqus>
4.2 踩坑实录:couldn't import any thread
先跑 dry-run:
$ isso -c /etc/isso.cfg import -n -t disqus /tmp/disqus.xml
2026-11-02 06:36:37,114:INFO: Using configuration file '/etc/isso.cfg'
Isso couldn't import any thread, try again with --empty-id
真实导入(管道喂 y)同样 0 条,查库确认无新增:
$ echo y | isso -c /etc/isso.cfg import -t disqus /tmp/disqus.xml 2>&1 | tail -1
Isso DB is not empty! Continue? [y/N]: Isso couldn't import any thread, try again with --empty-id
$ sqlite3 /var/lib/isso/comments.db "select count(*) from comments where id>2;"
0
根因在源码这几行:
thread_id = thread.find(Disqus.ns + "id")
if not thread_id: # ← 无子节点的 Element 布尔值为假
thread_id = dict(text="<empty thread id>", empty=True)
...
if thread_id.get("empty") and not self.empty_id:
continue # ← 线程被整体跳过
<id> 元素只要没有子节点,not thread_id 即为真(ElementTree 元素真值看子节点数的经典陷阱),于是被当作"空 id 线程"跳过,并给出 --empty-id 提示。这正是 --empty-id 的用途(帮助文案也写明 "workaround for weird Disqus XML exports, #135")。
4.3 加 --empty-id 成功导入
$ echo y | isso -c /etc/isso.cfg import -t disqus --empty-id /tmp/disqus.xml 2>&1 | tail -1
Isso DB is not empty! Continue? [y/N]: 3|1|carol|imported comment via CLI
(行尾是导入后立即用 sqlite3 ... "select id,tid,author,substr(text,1,40) from comments where id>2;" 的验证查询)新评论 id=3、挂到线程 tid=1、作者 carol,正文原样入库。线程归属按 <link> 的 path 部分(urlparse(...).path → /test/1),域名被丢弃——多站点迁移时 path 冲突要人工规划。
五、WordPress WXR 导入实测
-t wordpress 解析 WXR(xmlns:wp="http://wordpress.org/export/1.0/",命名空间版本号自动探测)。评论挂在 <item> 下的 <wp:comment> 子元素:
<rss version="2.0" xmlns:wp="http://wordpress.org/export/1.0/">
<channel>
<item>
<title>WP Demo Post</title>
<link>http://10.89.72.10/wp/1</link>
<wp:comment>
<wp:comment_id>101</wp:comment_id>
<wp:comment_parent>0</wp:comment_parent>
<wp:comment_author>dave</wp:comment_author>
<wp:comment_author_email>d@e.f</wp:comment_author_email>
<wp:comment_author_url></wp:comment_author_url>
<wp:comment_author_IP>10.0.0.1</wp:comment_author_IP>
<wp:comment_date_gmt>2026-02-02 03:04:05</wp:comment_date_gmt>
<wp:comment_content>wordpress imported via CLI</wp:comment_content>
<wp:comment_approved>1</wp:comment_approved>
</wp:comment>
</item>
</channel>
</rss>
$ echo y | isso -c /etc/isso.cfg import -t wordpress /tmp/wordpress.xml
$ sqlite3 /var/lib/isso/comments.db "select id,tid,author,substr(text,1,40) from comments where id=4;" "select id,title,uri from threads;"
4|2|dave|wordpress imported via CLI
2|WP Demo Post|/wp/1
源码映射规则:comment_approved == "1" → mode 1,否则 mode 2(进审核队列);comment_date_gmt 格式 %Y-%m-%d %H:%M:%S(与 Disqus 不同,没有 T 和 Z);正文里"两段之间只有一个换行"会被补成 Markdown 硬换行( \n),避免导入后挤成一行;wp:comment_parent 递归重映射为 Isso 的 parent id。
六、Generic JSON 导入实测
-t generic 吃一个线程数组 JSON。字段要求(源码 _build_comment 逐键下标访问,缺任何键直接 KeyError):线程 id/title/comments;评论 id/author/email/website/text/created/remote_addr。第一次实测就踩中了缺 website:
$ echo y | isso -c /etc/isso.cfg import -t generic /tmp/generic.json 2>&1 | tail -1
KeyError: 'website'
补全字段后成功:
[{"id":"/gen/2","title":"Generic Demo 2","comments":[{"id":1,"author":"erin","email":"e@f.g","website":"","text":"generic json imported","created":"2026-03-03 10:00:00","remote_addr":"10.0.0.2"}]}]
$ echo y | isso -c /etc/isso.cfg import -t generic /tmp/generic.json
$ sqlite3 /var/lib/isso/comments.db "select id,tid,author,substr(text,1,40) from comments where id=5;"
5|4|erin|generic json imported
Generic 的线程 id 直接作为 Isso 的 uri(不像 Disqus 走 urlparse 取 path),created 格式同 WordPress;mode 恒为 1、parent 恒为 None——不支持层级回复,深层嵌套的源数据请先拍平。
七、不传 -t 的自动探测与重复导入
不带 -t 时按文件头 512 字节探测:含 xmlns="http://disqus.com → Disqus;匹配 http://wordpress.org/export/1.x → WordPress;以 [{ 开头 → Generic。实测对 WordPress 样本自动探测与显式 -t 行为一致——但对已导入过的同一份文件重复导入(无论哪种方式)会撞唯一约束:
$ echo y | isso -c /etc/isso.cfg import /tmp/wordpress.xml 2>&1 | tail -1
sqlite3.IntegrityError: UNIQUE constraint failed: threads.uri
threads.uri 有 UNIQUE 索引,import 不幂等:重跑前先 sqlite3 查库或干脆从备份恢复(备份流程见部署篇 9.1),别指望它像 upsert。
八、配套运维工具
8.1 sqlite3 直查与库结构
$ sqlite3 /var/lib/isso/comments.db ".tables"
comments preferences threads
comments 表关键列(节选自 .schema 实测输出):
CREATE TABLE comments (
tid REFERENCES threads(id),
id INTEGER PRIMARY KEY,
parent INTEGER,
created FLOAT NOT NULL,
modified FLOAT,
mode INTEGER,
remote_addr VARCHAR,
text VARCHAR NOT NULL,
author VARCHAR,
email VARCHAR,
website VARCHAR,
likes INTEGER DEFAULT 0,
dislikes INTEGER DEFAULT 0,
voters BLOB NOT NULL,
notification INTEGER DEFAULT 0
);
CREATE TABLE threads ( id INTEGER PRIMARY KEY, uri VARCHAR(256) UNIQUE, title VARCHAR(256));
CREATE TRIGGER remove_stale_threads AFTER DELETE ON comments BEGIN DELETE FROM threads WHERE id NOT IN (SELECT tid FROM comments); END;
日常口径:mode 1=已发布、2=待审核、4=已删除但被回复引用;created 是 Unix 浮点秒;remote_addr 存 /24 匿名化后的值(所以排障时看到 127.0.0.0 不是 bug);voters BLOB 记录已投票地址,对应"同段 IP 一票"限制。preferences 存 CSRF/会话类键值。删评论不用手工清线程——触发器会自动回收孤儿 thread。
8.2 curl 驱动 API 日常操作
服务起来后,CLI 没提供的动作全部走 HTTP(完整路由表见 API 篇),运维高频四件套实测可用:
# 读线程评论(uri 走查询参数)
curl -sS "http://127.0.0.1:8080/?uri=/test/1" -H "Origin: http://10.89.72.10"
# 批量计数(请求体是 URI 字符串数组,写成对象恒返 0)
curl -sS http://127.0.0.1:8080/count -H "Origin: http://10.89.72.10" \
-H "Content-Type: application/json" -d '["/test/1","/nonexist"]'
[2, 0]
# 管理登录(只吃表单)+ 带 cookie 访问后台
curl -sS -i -c /tmp/cookie.txt -X POST http://127.0.0.1:8080/login/ \
-H "Origin: http://10.89.72.10" -d "password=TestPw2026!"
curl -sS -b /tmp/cookie.txt "http://127.0.0.1:8080/admin/" -H "Origin: http://10.89.72.10"
# 冒烟发帖(注意 guard 每分钟限额,连发第三条起 403)
curl -sS -X POST "http://127.0.0.1:8080/new?uri=/test/1" \
-H "Content-Type: application/json" -H "Origin: http://10.89.72.10" \
-d '{"text":"hello **world**","author":"alice","email":"a@b.c","title":"Test Post"}'
发帖成功回显(Markdown 已渲染、mode=1):
{"id": 1, "parent": null, "created": 1793601366.3152127, "modified": null, "mode": 1, "text": "<p>hello <strong>world</strong></p>", "author": "alice", "website": null, "likes": 0, "dislikes": 0, "notification": null, "hash": "b539304e6ef9"}
8.3 systemd/journalctl 与备份
服务纳管、状态与日志口径见部署篇第五节(journalctl -u isso -o cat 取干净行);备份用 sqlite3 ".backup" 而非裸 cp,实测校验行数一致:
$ systemctl stop isso
$ sqlite3 /var/lib/isso/comments.db ".backup /var/lib/isso/comments.backup.db"
$ systemctl start isso && sqlite3 /var/lib/isso/comments.backup.db "select count(*) from comments;"
5
九、坑清单(全部实测复现)
| # | 场景 | 现象 | 结论 |
|---|---|---|---|
| 1 | isso gen-passwd | invalid choice: 'gen-passwd' | 0.14.0 只有 import/run,凭据无 CLI |
| 2 | 裸敲 isso | 直接起服务器 | 无子命令默认 run,脚本里请显式写 run |
| 3 | ISSO_SETTINGS 与 -c 并存 | env 生效 | 排查"改了 -c 没生效"先看 env |
| 4 | 配置路径不存在 | Specified config ... does not exist! Exiting. | 快速失败,不会静默用默认值 |
| 5 | 非空库 + 无 stdin | EOFError: EOF when reading a line | 管道 echo y | 或先清库 |
| 6 | Disqus XML 里 post 嵌在 thread 内 | couldn't import any thread | post/thread 必须根级平铺 |
| 7 | <id> 无子节点被误判空 id | 提示 try again with --empty-id | 加 --empty-id 即通(见 4.2 源码) |
| 8 | createdAt 少结尾 Z | strptime 解析失败 | 格式 %Y-%m-%dT%H:%M:%SZ |
| 9 | generic JSON 缺 website | KeyError: 'website' | 所有键必填,可空串 |
| 10 | 同一文件重复 import | UNIQUE constraint failed: threads.uri | 不幂等,重跑先恢复备份 |
| 11 | import 中途报错 | 退出码仍 0 | 验收查库,不看返回码 |
| 12 | 未装 gevent 跑 run | 响应头 Server: Werkzeug/3.1.9 | 内置 werkzeug threaded 模式,生产装 gevent 或套 gunicorn |
十、小结
Isso 的 CLI 哲学和它的架构一致:命令行只管"跑起来"和"搬进来"两件事,其余全部交给 HTTP API 与 SQLite 文件本身。迁移前先 -n 试跑、管道喂确认、按格式底线准备源文件,导入后用 sqlite3 验收而不是退出码。更多参数语义见 《Isso 0.14.0 配置参数详解》,API 全路由实测见 《Isso 0.14.0 API 使用详解》,部署与 systemd 纳管见 《Isso 0.14.0 pip + systemd 部署指南》。