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 日常与备份工具。

IssoIsso自托管CLISQLite2026/11/02

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,走第二分支):

  1. listen = http://host:port 且 gevent 可导入 → geevent.pywsgi.WSGIServer(...).serve_forever();
  2. 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;
  3. 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

源码层面有四条硬规则,全部实测验证过:

  1. dry-run 的实现是"导入到临时库":args.dryrun 时新建 tempfile.NamedTemporaryFile() 作为 dbpath,正式库纹丝不动——所以试跑输出与真实导入一致,可放心先跑 -n。
  2. 导入前自动关闭 guard:conf.set("guard", "enabled", "off"),批量写入不触发限流。
  3. 非空库要交互确认,且非交互环境直接炸:
$ 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

九、坑清单(全部实测复现)

#场景现象结论
1isso gen-passwdinvalid choice: 'gen-passwd'0.14.0 只有 import/run,凭据无 CLI
2裸敲 isso直接起服务器无子命令默认 run,脚本里请显式写 run
3ISSO_SETTINGS 与 -c 并存env 生效排查"改了 -c 没生效"先看 env
4配置路径不存在Specified config ... does not exist! Exiting.快速失败,不会静默用默认值
5非空库 + 无 stdinEOFError: EOF when reading a line管道 echo y | 或先清库
6Disqus XML 里 post 嵌在 thread 内couldn't import any threadpost/thread 必须根级平铺
7<id> 无子节点被误判空 id提示 try again with --empty-id加 --empty-id 即通(见 4.2 源码)
8createdAt 少结尾 Zstrptime 解析失败格式 %Y-%m-%dT%H:%M:%SZ
9generic JSON 缺 websiteKeyError: 'website'所有键必填,可空串
10同一文件重复 importUNIQUE constraint failed: threads.uri不幂等,重跑先恢复备份
11import 中途报错退出码仍 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 部署指南》。