Isso 0.14.0 API 使用详解
Isso 0.14.0 JSON API 逐路由实测详解:18 个路由总览与认证形态,评论提交、列表读取、投票、评论者自管、管理员审核全流程 curl 实测回显逐行照抄,含 403 拦截文案排障比对与分页、过滤、排序通用约定。
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 |
二、前置条件
- 运行中的 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}
- 浏览器侧调用须满足跨域白名单:请求
Origin(或Referer)必须命中配置的站点白名单,服务端才会回显Access-Control-Allow-Origin响应头;本文 curl 实测均显式携带-H 'Origin: http://127.0.0.1:8142'。 - 管理后台已开启(审核域接口需要)。
三、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 /preview | Markdown 渲染预览 |
| 互动 | POST /id/<id>/like | 点赞 |
| 互动 | POST /id/<id>/dislike | 点踩 |
| 作者自管 | PUT /id/<id> | 编辑自己的评论(15 分钟窗口 + cookie) |
| 作者自管 | DELETE /id/<id> | 删除自己的评论(同上) |
| 审核 | POST /login/ | 管理员登录(表单) |
| 审核 | GET /admin/ | 管理后台页面(含待审列表与签名 key) |
| 审核 | `GET | POST /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-sessioncookie(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 provided | require-author=true 且缺昵称 |
email address required but not provided | require-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: 'newest', 'oldest', 'upvotes';缺 uri → 400 <p>missing uri query</p>。注意错误响应是 HTML 片段而非 JSON。
5.3 单条查看与作者自管:GET / PUT / DELETE /id/<id>
- 单条查看:无 cookie 返回 403(回显
You don'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"><p>楼上说的对,补充一点:留了邮箱才能收到回复通知。</p></content></entry>...
[rss] base 为空时整体禁用,实测 404。
- 最新评论:
[general] latest-enabled开启后GET /latest?limit=5返回跨线程已审核评论列表(含 uri 字段);未开启时 404,实测原文Unavailable because 'latest-enabled' not set by site admin;缺limit时 400,实测原文Query parameter 'limit' is mandatory (integer, >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 走通全程):
- 登录(表单提交,非 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 并渲染登录页(不跳转)。
- 取待审条目的签名 key:
GET /admin/?mode=2页面内嵌每条的操作 key(send_edit(<id>, '<key>'),实测 id 27 的 key:
send_edit(27, 'Mjc.auSK2w.7BxEQfFC77epYPOq_GeK-b0ZjoU'
- 放行(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
- 验证公开可见:
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,属预期 |
七、常见问题
- 400
missing uri query:GET /未带uri参数(线程路径需 URL 编码,如%2Findex.html)。 - 400
Cannot create new thread: URI ... is not accessible and no title was provided:新线程且未传title,服务端抓页失败。传title或保证白名单首项从服务侧可达。 - 403 带 guard 文案:对照 5.1 的拦截原因表,调
[guard]/[server] trusted-proxies(详见配置篇)。 - 403
You don't have the permission...:访问单条/编辑/删除缺少对应评论的作者 cookie,或 cookie 已过期(15 分钟)、内容签名不匹配(编辑后必须用轮换的新 cookie)。 - 已删评论再删返回 500:0.14.0 已知缺陷(源码空值检查次序问题),前端正常流程不会触发。
GET /count返回 405:计数只接受 POST 且请求体为 JSON 数组。- 响应里中文显示为
\uXXXX:JSON 规范转义,解析后即原文,无需处理。 - 浏览器直调无
Access-Control-Allow-Origin头:请求来源不在[general] host白名单,补白名单后重试。
参考链接
- 官方文档:https://isso-comments.de/docs/
- API 实现源码(路由表与行为口径):https://github.com/posativ/isso/blob/0.14.0/isso/views/comments.py
- 0.14.0 Release:https://github.com/posativ/isso/releases/tag/0.14.0
- GitHub 仓库:https://github.com/posativ/isso