Isso 0.14.0 API 使用详解

Isso 0.14.0 JSON API 逐路由实测详解:18 个路由总览与认证形态,评论提交、列表读取、投票、评论者自管、管理员审核全流程 curl 实测回显逐行照抄,含 403 拦截文案排障比对与分页、过滤、排序通用约定。

IssoAPI自托管2026/10/30

Isso 0.14.0 API 使用详解

本文完整梳理 Isso 0.14.0 的 JSON API:18 个路由逐一列出,公开读接口、评论提交、投票、评论者自管、管理员审核四大域均有 curl 实测回显(逐行照抄,JSON 内的 unicode 转义保留原样——客户端解析后即为中文)。全部实测在真实服务器完成(实测日期 2026-10-30),拦截类响应的 403 原因文案可直接用于排障比对。

组件定位见《Isso 0.14.0 介绍与选型指南》,部署见《Isso Docker Compose 部署指南》,配置项与行为对应关系见《Isso 0.14.0 配置参数详解》。

一、环境信息

项目内容
组件版本Isso 0.14.0(/info 实测确认)
服务地址http://127.0.0.1:8141(API 与管理后台)
API 风格JSON over HTTP;除登录为表单提交外,请求体均为 application/json
认证形态无全局认证。两类会话凭据:评论者 cookie(<id>=签名,15 分钟)与管理员 cookie(admin-session,24 小时)
时间字段created / modified 为 Unix 时间戳(浮点秒);响应头 Date 为 UTC
实测工具curl(含 -i 观察响应头与 Set-Cookie)
实测日期2026-10-30

二、前置条件

  1. 运行中的 Isso 0.14.0:
curl -s http://127.0.0.1:8141/info

实测回显:

{"version": "0.14.0", "host": "http://127.0.0.1:8141", "origin": "http://10.89.71.1:8142", "moderation": true}
  1. 浏览器侧调用须满足跨域白名单:请求 Origin(或 Referer)必须命中配置的站点白名单,服务端才会回显 Access-Control-Allow-Origin 响应头;本文 curl 实测均显式携带 -H 'Origin: http://127.0.0.1:8142'。
  2. 管理后台已开启(审核域接口需要)。

三、API 总览

以下 18 个路由来自源码 isso/views/comments.py 的路由表,与实测一一对应:

域方法与路径作用
公开读GET /拉取指定线程的公开评论列表(支持分页/排序/取回复)
公开读GET /id/<id>单条评论(仅评论者本人携 cookie 或无限制场景)
公开读GET /feed线程评论的 Atom 订阅
公开读GET /latest跨线程最新已审核评论(需配置开启)
公开读GET /config公开开关集合
公开读GET /demo/内置演示页
写入POST /new发表评论(建线程/回复)
写入POST /count批量统计线程可见评论数
写入POST /previewMarkdown 渲染预览
互动POST /id/<id>/like点赞
互动POST /id/<id>/dislike点踩
作者自管PUT /id/<id>编辑自己的评论(15 分钟窗口 + cookie)
作者自管DELETE /id/<id>删除自己的评论(同上)
审核POST /login/管理员登录(表单)
审核GET /admin/管理后台页面(含待审列表与签名 key)
审核`GETPOST /id/<id>/<action>/<key>`
退订GET /id/<id>/unsubscribe/<email>/<key>评论回复通知退订
服务信息GET /info版本与运行开关(本表之外的源码级端点)

四、认证与首次调用

  • 公开调用无需任何凭据:/info、/config、GET /?uri=...、POST /count、POST /preview 等;
  • 评论者凭据:POST /new 成功后响应下发两个头——Set-Cookie: <id>=<签名>(标准 cookie)与 X-Set-Cookie: isso-<id>=<签名>(供嵌入脚本写入存储)。签名内容为「评论 id + 文本摘要」,有效期即 [general] max-age(本文 15 分钟,实测 Max-Age=900)。编辑/删除自己评论、单条查看都要靠它;编辑后签名轮换,须使用新 cookie;
  • 管理员凭据:POST /login/ 提交表单口令,成功 302 并下发 admin-session cookie(24 小时)。

首次调用建议顺序:/info 确认版本 → GET /?uri= 看线程 → POST /new 提交 → 按审核状态验证可见性。

五、各 API 域详解

5.1 评论提交:POST /new

请求形态:查询串携带 uri(目标页面路径,必填)与 title(新线程标题,可选但强烈建议);请求体为 JSON,可提交字段共 7 个(源码 API.ACCEPT):

字段类型说明
text字符串评论内容,Markdown 语法
author字符串昵称
email字符串邮箱(Gravatar 与回复通知的依据)
website字符串个人站点
parent整数父评论 id,回复时携带
title字符串线程标题(嵌入页从 data-title 读取后提交)
notification整数1 表示订阅该评论的回复通知

实测(回复 id 25,moderation 开启 → 202 待审):

curl -s -i -X POST 'http://127.0.0.1:8141/new?uri=%2Findex.html&title=Isso%20%E6%BC%94%E7%A4%BA%E6%96%87%E7%AB%A0%EF%BC%9A%E8%87%AA%E6%89%98%E7%AE%A1%E8%AF%84%E8%AE%BA%E7%B3%BB%E7%BB%9F%E5%AE%9E%E6%B5%8B' \
  -H 'Origin: http://127.0.0.1:8142' -H 'Referer: http://127.0.0.1:8142/index.html' \
  -H 'Content-Type: application/json' \
  -d '{"text":"部署验收冒烟回复:审核通过后对外可见。","author":"周八","email":"zhouba@example.com","parent":25}'

实测回显:

HTTP/1.1 202 ACCEPTED
Server: gunicorn
Date: Fri, 30 Oct 2026 12:03:00 GMT
Connection: close
Content-Type: application/json
Content-Length: 436
Set-Cookie: 26=WzI2LCI1NjYzMDdiZjZmZjk4MzJhYTM1NWU5MWJiZGNiMmE2ZmViMmI1M2E2Il0.auSHdA.xgJp2MVDiFr3NaoGRSrhxBSw6JI; Expires=Fri, 30 Oct 2026 12:18:00 GMT; Max-Age=900; Path=/; SameSite=Lax
X-Set-Cookie: isso-26=WzI2LCI1NjYzMDdiZjZmZjk4MzJhYTM1NWU5MWJiZGNiMmE2ZmViMmI1M2E2Il0.auSHdA.xgJp2MVDiFr3NaoGRSrhxBSw6JI; Expires=Fri, 30 Oct 2026 12:18:00 GMT; Max-Age=900; Path=/; SameSite=Lax
Access-Control-Allow-Origin: http://127.0.0.1:8142
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: HEAD, GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Origin, Referer, Content-Type
Access-Control-Expose-Headers: X-Set-Cookie, Date

{"id": 26, "parent": 25, "created": 1793361780.5222783, "modified": null, "mode": 2, "text": "<p>\u90e8\u7f72\u9a8c\u6536\u5192\u70df\u56de\u590d\uff1a\u5ba1\u6838\u901a\u8fc7\u540e\u5bf9\u5916\u53ef\u89c1\u3002</p>", "author": "\u5468\u516b", "website": null, "likes": 0, "dislikes": 0, "notification": null, "hash": "c1905c0f8037", "gravatar_image": "https://www.gravatar.com/avatar/9034ce632c0b6990cfdef207fbdb8149?d=identicon&s=55"}

状态码语义:202 + mode=2 待审;201 + mode=1 直接可见(审核关闭,或邮箱此前获审核通过被自动放行——两种触发均有实测)。响应 text 字段已是渲染后的 HTML。

不带 title 的建线程行为:服务端会按请求来源抓取页面标题建线程;页面不可达时返回 400(实测原文):Cannot create new thread: URI /index.html is not accessible and no title was provided. Please provide a title parameter in your request.

guard 拦截(403,原因即响应体)实测汇总:

拦截原因(响应原文)触发条件
10.89.71.0: ratelimit exceeded (5, 6)同来源 60 秒窗口内评论超限(默认 2)
3 direct responses to /index.html同来源对该线程直评累计超限(默认 3,无时间窗)
edit time frame is still open编辑窗口内回复自己(reply-to-self=false 时)
author address required but not providedrequire-author=true 且缺昵称
email address required but not providedrequire-email=true 且缺邮箱

参数含义与调优见配置参数详解篇。

5.2 评论列表:GET /

查询参数:uri(必填)、sort(newest / oldest / upvotes,默认 oldest)、limit、offset、parent(只取某条的回复)、nested_limit(嵌套层数)、plain(1 = 返回原始 Markdown 而非渲染 HTML)、after(时间游标)。

实测(取 id 25 的回复,最新在前):

curl -s 'http://127.0.0.1:8141/?uri=%2Findex.html&parent=25&sort=newest'

实测回显:

{"id": 25, "total_replies": 1, "hidden_replies": 0, "replies": [{"id": 27, "parent": 25, "created": 1793362637.7663841, "modified": null, "mode": 1, "text": "<p>API \u8be6\u89e3\u7bc7\u5b9e\u6d4b\uff1a\u8fd9\u6761\u8bc4\u8bba\u8d70\u5b8c\u5ba1\u6838\u95ed\u73af\u3002</p>", "author": "\u6d4b\u8bd5\u5458", "website": null, "likes": 0, "dislikes": 0, "notification": null, "hash": "74737ca8db59", "gravatar_image": "https://www.gravatar.com/avatar/5ede5f0e167e4d7fa86dbbaf335e9c0e?d=identicon&s=55"}], "config": {"reply-to-self": true, "require-email": false, "require-author": false, "reply-notifications": true, "gravatar": true, "avatar": false, "feed": true}}

口径说明:total_replies 为该范围内对外可见的评论数(待审不计入);顶层请求时 hidden_replies = 顶层可见数 − 本次返回数 − offset(分页未展示的部分);replies 内每条含二级回复计数与嵌套列表。

错误形态实测:sort=foo → 400 Invalid sort option. Must be one of: &#39;newest&#39;, &#39;oldest&#39;, &#39;upvotes&#39;;缺 uri → 400 <p>missing uri query</p>。注意错误响应是 HTML 片段而非 JSON。

5.3 单条查看与作者自管:GET / PUT / DELETE /id/<id>

  • 单条查看:无 cookie 返回 403(回显 You don&#39;t have the permission to access the requested resource.);携该评论的作者 cookie 返回 200,实测可见 modified 字段随编辑刷新:
{"id": 28, "parent": 25, "created": 1793362672.786666, "modified": 1793362695.521434, "mode": 1, "text": "<p>API \u8be6\u89e3\u7bc7\u5b9e\u6d4b\uff1a15 \u5206\u949f\u7f16\u8f91\u7a97\u53e3\u6f14\u793a\u3002\uff08\u5df2\u7f16\u8f91\uff09</p>", "author": "\u6d4b\u8bd5\u5458", "website": null, "likes": 0, "dislikes": 0, "notification": null}
  • 编辑(窗口内 + cookie):PUT /id/<id>,JSON 体 {"text": "...修改后内容..."},实测 200,响应同时下发轮换后的新 cookie(签名含文本摘要,内容变则签名变);
  • 无凭据编辑:403(实测);
  • 删除:DELETE /id/<id> 携作者 cookie 返回 200(响应体 null)。⚠️ 已知缺陷实录:对已删除的评论再次持有效 cookie 调 DELETE,返回 500(0.14.0 源码在空值检查前取 text 字段导致),服务端日志有 traceback,可忽略但不建议复现;
  • 窗口:全部受 [general] max-age(默认 15 分钟)约束,超窗或 cookie 过期一律 403。

5.4 投票:POST /id/<id>/like 与 /dislike

投票按来源地址去重(布隆过滤器),评论作者的 IP 在评论创建时已预置,因此作者给自己投票会被拒绝。实测序列(对同一评论跨源操作):

{"likes": 1, "dislikes": 0}                                              # 跨源首次点赞
{"likes": 2, "dislikes": 0}                                              # 第三方来源再赞,累计
{"likes": 2, "dislikes": 1}                                              # 第三方来源点踩
{"likes": 1, "dislikes": 0, "message": "Upvote denied because a vote has already been registered for this remote address: 10.89.71.0"}          # 同源重复点赞被拒
{"likes": 2, "dislikes": 1, "message": "Downvote denied because a vote has already been registered for this remote address: 203.0.113.0"}       # 同源先赞后踩被拒

规律:来源匿名化到 /24 网段聚合(message 中的地址即匿名化结果);同一来源「赞/踩」二选一,先到先得。

5.5 计数与预览:POST /count、POST /preview

  • 计数:请求体为 uri 数组,响应为同序数组(页面「N 条评论」角标的取数来源):
curl -s -X POST http://127.0.0.1:8141/count -H 'Content-Type: application/json' -d '["/index.html", "/demo/"]'

实测回显:

[12, 2]
  • 预览:把 Markdown 交给服务端按白名单渲染,请求体 {"text": "**加粗**与*斜体*预览"},实测回显:
{"text": "<p><strong>\u52a0\u7c97</strong>\u4e0e<em>\u659c\u4f53</em>\u9884\u89c8</p>"}

5.6 订阅与最新:GET /feed、GET /latest

  • Feed:GET /feed?uri=%2Findex.html 返回 Atom XML,实测节选:
<?xml version='1.0' encoding='utf-8'?>
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:thr="http://purl.org/syndication/thread/1.0"><updated>2026-10-30T11:25:21.049397Z</updated><id>tag:127.0.0.1:8142,2018:/isso/thread/index.html</id><title>Comments for 127.0.0.1:8142/index.html</title><entry><id>tag:127.0.0.1:8142,2018:/isso/2/7</id><title>Comment #7</title><updated>2026-10-30T11:25:21.049397Z</updated><author><name>李四</name></author><link href="http://127.0.0.1:8142/index.html#isso-7" /><content type="html">&lt;p&gt;楼上说的对,补充一点:留了邮箱才能收到回复通知。&lt;/p&gt;</content></entry>...

[rss] base 为空时整体禁用,实测 404。

  • 最新评论:[general] latest-enabled 开启后 GET /latest?limit=5 返回跨线程已审核评论列表(含 uri 字段);未开启时 404,实测原文 Unavailable because &#39;latest-enabled&#39; not set by site admin;缺 limit 时 400,实测原文 Query parameter &#39;limit&#39; is mandatory (integer, &gt;0)。实测列表(id / uri / 作者):
1 /demo/ 验证员
2 /demo/ 审核测试员
5 /index.html 张三
7 /index.html 李四
11 /index.html 赵六

5.7 管理审核:POST /login/、GET /admin/、POST /id/<id>/<action>/<key>

完整审核闭环实测(待审评论 id 27 走通全程):

  1. 登录(表单提交,非 JSON):
curl -s -i -X POST http://127.0.0.1:8141/login/ --data-urlencode 'password=q1w2e3r4'

实测回显(节选):

HTTP/1.1 302 FOUND
Location: http://127.0.0.1:8141/admin/
Set-Cookie: admin-session=eyJsb2dnZWQiOnRydWV9.auSKzQ.mSlcRBlzVgjfyIEbl0WLuQdm2_g; Expires=Sat, 31 Oct 2026 12:17:17 GMT; Path=/; SameSite=Lax

错误口令实测返回 200 并渲染登录页(不跳转)。

  1. 取待审条目的签名 key:GET /admin/?mode=2 页面内嵌每条的操作 key(send_edit(<id>, '<key>'),实测 id 27 的 key:
send_edit(27, 'Mjc.auSK2w.7BxEQfFC77epYPOq_GeK-b0ZjoU'
  1. 放行(key 走路径,携管理员 cookie):
curl -s -i -b "<admin-session>" -X POST 'http://127.0.0.1:8141/id/27/activate/Mjc.auSK2w.7BxEQfFC77epYPOq_GeK-b0ZjoU'

实测回显:

HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Comment has been activated
  1. 验证公开可见:GET /?uri=%2Findex.html&parent=25 中即出现 id 27(见 5.2 回显)。delete 动作同形(返回 Comment has been deleted);重复 activate 返回 Already activated。GET 与 POST 均可路由到审核动作——GET 出确认页、POST 真执行;新评论的 SMTP 通知邮件正文带同形链接(实测直接还原邮件文本访问返回 403,系 quoted-printable 还原不精确,请用邮件原链)。

5.8 退订:GET /id/<id>/unsubscribe/<email>/<key>

回复通知邮件的 List-Unsubscribe 头携带该链接(quoted-printable 形态,实测样例 http://127.0.0.1:8141/id/16/unsubscrib...)。链接形态已从邮件头确认;完整退订闭环本文未实测。

5.9 服务信息:GET /info、GET /config、GET /demo/

  • /info:版本、host、origin、moderation 开关(见第二节回显);
  • /config:嵌入页公开开关(七个键,见配置篇 3.2 回显);
  • /demo/:内置演示页(验证服务存活可用,返回完整 HTML)。

六、分页、过滤与排序通用约定

约定说明
分页limit(条数)+ offset(偏移),用于 GET / 与 GET /latest;GET /latest 强制要求 limit
排序sort 仅三值:newest / oldest / upvotes,非法值 400
过滤parent 取回复链;after 时间游标;nested_limit 控制嵌套展开层数
渲染默认返回渲染后 HTML;plain=1 返回原始 Markdown
错误形态参数错误为 400 + HTML 片段;拦截为 403 + 原因文案;业务成功为 JSON
速率POST /new 受 guard 限流约束(60 秒窗口),脚本化压测会触发 403,属预期

七、常见问题

  1. 400 missing uri query:GET / 未带 uri 参数(线程路径需 URL 编码,如 %2Findex.html)。
  2. 400 Cannot create new thread: URI ... is not accessible and no title was provided:新线程且未传 title,服务端抓页失败。传 title 或保证白名单首项从服务侧可达。
  3. 403 带 guard 文案:对照 5.1 的拦截原因表,调 [guard] / [server] trusted-proxies(详见配置篇)。
  4. 403 You don&#39;t have the permission...:访问单条/编辑/删除缺少对应评论的作者 cookie,或 cookie 已过期(15 分钟)、内容签名不匹配(编辑后必须用轮换的新 cookie)。
  5. 已删评论再删返回 500:0.14.0 已知缺陷(源码空值检查次序问题),前端正常流程不会触发。
  6. GET /count 返回 405:计数只接受 POST 且请求体为 JSON 数组。
  7. 响应里中文显示为 \uXXXX:JSON 规范转义,解析后即原文,无需处理。
  8. 浏览器直调无 Access-Control-Allow-Origin 头:请求来源不在 [general] host 白名单,补白名单后重试。

参考链接

  1. 官方文档:https://isso-comments.de/docs/
  2. API 实现源码(路由表与行为口径):https://github.com/posativ/isso/blob/0.14.0/isso/views/comments.py
  3. 0.14.0 Release:https://github.com/posativ/isso/releases/tag/0.14.0
  4. GitHub 仓库:https://github.com/posativ/isso