myTinyTodo 1.8.6 API 使用详解

myTinyTodo 1.8.6 JSON API 全路由实测详解:MTT-Token 认证模型、session/login/logout 令牌流转、清单与任务的 action 分发语义、标签查询、ext-settings 扩展路由、CSV/iCal/RSS 导出订阅与 403/404 排障。

myTinyTodoPHP 应用自托管任务管理2026/10/24

myTinyTodo 1.8.6 API 使用详解

目标:完整讲解 myTinyTodo 1.8.6 内置 JSON API(api.php)的认证模型、全部路由、请求/响应格式与实测示例,并覆盖 CSV/iCalendar 导出与 RSS 订阅两个数据出口。myTinyTodo 没有官方 CLI 工具,API 是脚本化与第三方集成的唯一官方途径;本文所有请求与回显均在 Ubuntu 22.04.5 + nginx + PHP-FPM + myTinyTodo 1.8.6(SQLite 与 MariaDB 双后端)环境实测。

一、环境信息

项目值
应用版本myTinyTodo v1.8.6(includes/version.php 实测 Version::VERSION='1.8.6')
运行环境Ubuntu 22.04.5 LTS,nginx 1.18.0 + php8.1-fpm;Apache 2.4.52 + mod_php8.1 亦验证通过
数据库SQLite(db/todolist.db)为主,关键流程在 MariaDB 10.6.23 后端复测
API 入口http://<服务器地址>/api.php?_path=<路由>
测试口令示例口令 q1w2e3r4(存储形式 sha256:13a5c202...,实测 `printf 'q1w2e3r4'
测试数据初始清单 Todo(id=1)与内置虚拟清单 All tasks(id=-1)

二、前置条件

  • 已完成安装并设置访问口令(见 《Ubuntu 22.04 安装 myTinyTodo 1.8.6 指南》 第四章);
  • 明确 API 的三种鉴权级别(源码 api.php 实证):
    1. 会话令牌级(读写):登录用户的读写操作,请求头 MTT-Token 必须等于当前会话令牌;
    2. 匿名读级:未登录时,仅对「已发布」清单的只读查询放行(如 GET /tasks?list=N,代码检查 published=1);
    3. 无鉴权:POST /session 与 POST /login 本身(它们也是创建令牌的入口)。
  • 请求约定:请求体 Content-Type: application/json;每个需要鉴权的请求都必须携带 MTT-Token 请求头(实测缺头即 403 Access denied! No token provided.)。

三、API 总览(全部路由)

api.php 的路由注册表(源码 api.php $endpoints 数组,与实测一一对应):

路由(_path=)方法用途
/sessionPOST创建会话,取得初始令牌
/loginPOST口令登录,换发正式令牌
/logoutPOST登出并作废令牌
/listsGET / POST / PUT清单查询 / 新建 / 批量操作
/lists/{id}GET / PUT / DELETE(POST 兼容 PUT)单清单查询 / 操作(带 action)/ 删除
/tasksGET / POST / PUT任务查询 / 新建 / 排序操作
/tasks/{id}PUT / DELETE(POST 兼容 PUT)单任务操作(带 action)/ 删除
/tasks/parseTitlePOST预解析智能语法(不落库)
/tasks/newCounterPOST未读新任务计数
/suggestTagsGET标签自动补全
/tagCloud/{listId}GET标签云(含计数与字重)
/ext-settings/{bundleId}GET / PUT / POST扩展设置读写(仅已启用扩展)

通用响应形态:任务/清单类返回 {"total": N, "list": [...], "time": <unix时间戳>};操作类返回 {"total": 0|1};错误返回裸文本(403/404)或 {"msg": "..."}。

四、认证与令牌管理

4.1 三步获得可用令牌(实测)

第一步,创建会话(无需鉴权):

===== POST /session -> 200
{
 "token": "ecd868a1-72d3-4f93-9b14-0e3dd7474bfc",
 "session": "8jkgbqrqk9epnec33lj26fugn3"
}

返回的 token 用于携带 MTT-Token 调用 login(否则 login 一步就会 403);session 是 PHP 会话 ID,后续请求需带着会话 Cookie(mtt-session)才能维持同一会话。

第二步,口令登录(携带第一步的 token):

===== POST /login -> 200
{
 "logged": 1,
 "token": "324a5fc9-34be-4176-aa49-97bd45df17cf"
}

登录成功后必须改用响应中的新 token——服务器端已轮换会话令牌(源码 update_token()),旧令牌立即失效。口令错误时返回 {"logged": 0}(HTTP 仍为 200,实测回显)。

第三步,用新 token 调用业务接口(请求头 MTT-Token: <token> + 会话 Cookie)。

4.2 令牌机制要点(源码实证)

  • 设有口令的应用:令牌存于服务端会话($_SESSION['token']),MTT-Token 请求头与其逐字符比对,任何不匹配即 403;
  • 未设口令的应用:令牌存于 mtt-token Cookie,MTT-Token 头与之比对;
  • POST /logout 返回 {"logged":0} 并作废当前令牌(实测回显);
  • 实测坑位:测试脚本中最常见的 403 原因就是「换了会话仍用旧令牌」或「某次请求漏带 tok 头」——403 的 Access denied! No token provided. 永远意味着头与当前会话不匹配,请逐请求核对。

五、清单管理 API

5.1 查询清单

===== GET /lists -> 200
{
 "total": 2,
 "list": [
  {
   "id": -1,
   "name": "All tasks",
   "sort": 3,
   "published": 0,
   "showCompl": 1,
   "showNotes": 0,
   "hidden": 1,
   "feedKey": ""
  },
  {
   "id": 1,
   "name": "Todo",
   "sort": 0,
   "published": 0,
   "showCompl": 1,
   "showNotes": 0,
   "hidden": 0,
   "feedKey": ""
  }
 ],
 "time": 1792803861
}

字段含义:id=-1 是内置虚拟清单「All tasks」(跨清单视图,hidden=1,不可删除);sort 为排序方式编号;published 决定 RSS 是否可访问;showCompl/showNotes 是清单显示开关;feedKey 是发布后生成的 RSS 访问键。

5.2 新建清单

===== POST /lists(body: {"name": "收件箱"}) -> 200
{
 "total": 1,
 "list": [
  {
   "id": 4,
   "name": "收件箱",
   "sort": 0,
   "published": 0,
   "showCompl": 1,
   "showNotes": 0,
   "hidden": 0,
   "feedKey": ""
  }
 ]
}

5.3 操作单清单:必须带 action(重要实测发现)

PUT /lists/{id} 的请求体必须包含 action 字段,服务器按 action 分发(源码 ListsController::putId() 的 switch:rename / sort / publish / enableFeedKey / showNotes / hide / clearCompleted / delete)。实测验证:不带 action 时返回 {"total":0} 且什么都不改——

===== PUT /lists/2(body: {"name": "工作任务", "sort": 1},无 action) -> 200
{
 "total": 0
}
(随后 GET /lists 确认清单 2 仍叫「工作」——静默无操作)

正确写法与实测回显:

===== PUT /lists/2(body: {"action": "rename", "name": "工作任务"}) -> 200
{
 "total": 1,
 "list": [
  {
   "id": 2,
   "name": "工作任务",
   "sort": 0,
   "published": 0,
   "showCompl": 1,
   "showNotes": 0,
   "hidden": 0,
   "feedKey": ""
  }
 ]
}
===== PUT /lists/2(body: {"action": "sort", "sort": 1}) -> 200
{
 "total": 1
}

发布清单(RSS 前置条件)同样是 action:

===== PUT /lists/1(body: {"action": "publish", "publish": 1}) -> 200
{
 "total": 1
}

5.4 删除清单

===== DELETE /lists/3 -> 200
{
 "total": 1
}

六、任务管理 API

6.1 创建任务:智能语法一投递

智能语法开启(设置 smartsyntax=1)时,title 字段可直接写 ±priority 标题 #标签 @截止日 一行式:

===== POST /tasks(body: {"list": 1, "title": "+2 整理季度周报 #work @2026-10-28"}) -> 200(节选)
{
 "total": 1,
 "list": [
  {
   "id": 3,
   "title": "整理季度周报",
   "titleText": "整理季度周报",
   "listId": 1,
   "compl": 0,
   "prio": 1,
   "note": "",
   "tags": "work",
   "tags_ids": "2",
   "duedate": "10/26/2026",
   "dueClass": "soon",
   "dueStr": "in 2 days",
   "dueInt": 20261026,
   "dueTitle": "Due 26 Oct 2026"
  }
 ]
}

实测解读:标题剥掉了语法前缀(剩余「整理季度周报」),prio、tags、duedate 三项全部结构化落库;dueClass/dueStr/dueTitle 是服务端计算的到期提示。

6.2 创建任务:newFull 结构化投递

===== POST /tasks(body: {"newFull": {...}}) -> 200(节选)
{
 "total": 1,
 "list": [
  {
   "id": 4,
   "title": "部署验证 myTinyTodo",
   "listId": 1,
   "compl": 0,
   "prio": 2,
   "note": "<p><strong>要点</strong>:检查 PHP 扩展</p>",
   "noteText": "**要点**:检查 PHP 扩展",
   "tags": "ops,urgent",
   "duedate": "10/25/2026",
   "dueClass": "today",
   "dueStr": "tomorrow"
  }
 ]
}

注意 note 存的是渲染后的 HTML、noteText 是 Markdown 原文——两者都随任务返回。

6.3 预解析智能语法(不落库)

===== POST /tasks/parseTitle(body: {"title": "+2 买牛奶 #home @2026-10-25"}) -> 200
{
 "title": "买牛奶",
 "prio": 2,
 "tags": "home",
 "duedate": "10/25/2026"
}

6.4 查询任务

调用实测结果
GET /tasks?list=1返回清单 1 未完成任务
GET /tasks?list=1&compl=compl 留空 = 含已完成任务(实测 total: 4)
GET /tasks?list=1&s=周报(URL 编码 s=%E5%91%A8%E6%8A%A5)关键词搜索,实测命中 1 条
GET /tasks?list=1&t=ops按标签过滤,实测命中带 ops 标签的任务
GET /tasks?list=-1跨全部清单(响应带 listName 字段标注归属,实测 total: 4)

6.5 操作单任务:同样必须带 action

PUT /tasks/{id} 的 action 全集(源码 TasksController::putId()):complete / priority / note / edit / move / order 等。实测逐个验证:

===== PUT /tasks/4(body: {"action": "complete", "compl": 1}) -> 200(节选)
{
 "total": 1,
 "list": [
  { "id": 4, "compl": 1, "dateCompleted": "24 Oct 2026",
    "dateCompletedFull": "24 Oct 2026 09:04" }
 ]
}
===== PUT /tasks/3(body: {"action": "priority", "prio": -1}) -> 200
{
 "total": 1,
 "list": [ { "id": 3, "prio": -1 } ]
}
===== PUT /tasks/3(body: {"action": "note", "note": "周五下班前提交"}) -> 200
{
 "total": 1,
 "list": [
  { "id": 3,
    "note": "<p>周五下班前提交</p>",
    "noteText": "周五下班前提交" }
 ]
}
===== PUT /tasks/3(body: {"action": "edit", "title": "整理季度周报(改)",
      "note": "含三张图", "prio": 1, "duedate": "2026-10-30", "tags": "work,report"}) -> 200
{
 "total": 1,
 "list": [ { "id": 3, "title": "整理季度周报(改)", "listName": "Todo", ... } ]
}
===== PUT /tasks(body: {"action": "order", "order": [{"id": 3, "diff": 10}]}) -> 200
{
 "total": 1
}
===== PUT /tasks/4(body: {"action": "move", "to": 4}) -> 200
{
 "total": 1
}

说明:edit 是一次提交全字段的整单编辑(等价于界面的编辑表单);order 走 PUT /tasks(集合路由)按 diff 偏移手动排序;move 的 to 是目标清单 id。

6.6 删除任务

===== DELETE /tasks/{id} -> 200
{
 "total": 1
}

6.7 未读计数

===== POST /tasks/newCounter(body: {"lists": [{"listId": 1, "later": 0}]}) -> 200
{
 "ok": true,
 "total": 1,
 "tasks": [],
 "lists": [ { "listId": 1, "counter": 3 } ],
 "time": 179280...
}

later 传上次查询的时间戳即可统计增量新任务(界面角标的数据源)。

七、标签查询 API

===== GET /suggestTags?q=w&list=1 -> 200
[
 "work"
]
===== GET /tagCloud/1 -> 200
{
 "total": 3,
 "items": [
  { "tag": "home",   "tagText": "home",   "id": 1, "count": 2, "w": 2 },
  { "tag": "report", "tagText": "report", "id": 5, "count": 1, "w": 0 },
  { "tag": "work",   "tagText": "work",   "id": 2, "count": 1, "w": 0 }
 ]
}

suggestTags 服务输入框自动补全(q 前缀匹配);tagCloud 返回标签云,count 是任务数、w 是字重等级。

八、扩展设置 API

路由 /ext-settings/{bundleId} 只为已启用的扩展注册。实测两步验证:

未启用扩展时(应用默认 extensions: []):

===== GET /ext-settings/CustomCSS -> 404
{"msg":"Unknown extension"}

扩展生效的前提(实测链路):应用自带的扩展打包在 ext/extensions.tar.gz 里,需先解包出目录——

tar -C /var/www/mytinytodo-app/ext -xzf /var/www/mytinytodo-app/ext/extensions.tar.gz
ls /var/www/mytinytodo-app/ext/
CustomCSS
backup
extensions.tar.gz
index.html
notifications
updater

随后在 Web 界面 settings.php 扩展区启用(写入 config.json 的 extensions 数组),API 路由即出现:

===== GET /ext-settings/CustomCSS -> 200
<h3 class="page-title">... Custom CSS </h3>
<form id='ext_settings_form' data-ext=...
===== PUT /ext-settings/CustomCSS(body: {"css": "body{background:#fafafa}"}) -> 200
{"saved":1,"msg":"Saved"}

内置扩展共 4 个(extension.json 实测):backup(备份/恢复 v1.2.1)、CustomCSS(自定义 CSS v1.0.1)、notifications(任务通知,内置 Telegram 通道)、updater(在线检查更新)。

九、数据导出与订阅

这两个出口不在 api.php 路由内,是独立端点,走浏览器会话 Cookie 鉴权:

9.1 导出 CSV

===== GET /export.php?list=1 -> 200  Content-Type: text/csv; charset=utf-8
Completed;Priority;Task;Notes;Tags;Due;DateCreated;DateCompleted
0;1;"采购牛奶";"";"home";2026-10-26;2026-10-24 08:55:29 +0800;
0;1;"整理季度周报(改)";"含三张图";"work,report";2026-10-30;2026-10-24 09:04:21 +0800;

9.2 导出 iCalendar

===== GET /export.php?list=1&format=ical -> 200  Content-Type: text/calendar; charset=utf-8
BEGIN:VCALENDAR
VERSION:2.0
METHOD:PUBLISH
CALSCALE:GREGORIAN
PRODID:-//myTinyTodo//iCalendar Export v1.4//EN
X-WR-CALNAME:Todo
X-MTT-TIMEZONE:Asia/Shanghai
BEGIN:VTODO
UID:00a5230b-bc5f-4c25-a8c8-8eaec50f7cef
CREATED:20261024T005529Z
DTSTAMP:20261024T005529Z
SUMMARY:采购牛奶
DUE;VALUE=DATE:20261026
PRIORITY:5
X-MTT-PRIORITY:1
DESCRIPTION:Tags: home
...

可直接把该 URL 订阅进支持 iCal 的日历客户端。注意时区行 X-MTT-TIMEZONE 跟随应用设置。

9.3 RSS 订阅(发布前置)

未发布清单访问 RSS 的实测回显(HTTP 200,内容为错误页文本):

Access denied!<br> List is not published.

按 5.3 节先 PUT /lists/1 {"action":"publish","publish":1}(或界面清单右键勾选「发布」)后:

===== GET /feed.php?list=1 -> 200  Content-Type: text/xml; charset=utf-8
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>Todo - New tasks</title>
<link>http://<服务器地址>/?list=1</link>
<description>New tasks</description>
<lastBuildDate>Sat, 24 ...

RSS 输出的是清单的「新任务流」,需要长期订阅时把 feed.php?list=1(或带 feedKey 的地址)交给 RSS 阅读器。

十、常见问题(FAQ)

Q1:请求返回 403 Access denied! No token provided.? 三选一原因:请求根本没带 MTT-Token 头;带了但与当前会话令牌不匹配(最常见:login 后没换用响应里的新 token,或换了会话 Cookie 仍用旧 token);GET 只读请求访问了未发布清单且未登录(匿名只读仅限已发布清单)。实测缺头/错 token 都是同一句 403 裸文本。

Q2:PUT /lists/{id} 或 PUT /tasks/{id} 返回 {"total":0} 但数据没变? 请求体少了 action 字段——路由按 action 分发,没有 action 就静默无操作(5.3 节实测:连 total 都是 200 包着的假成功)。清单 action:rename/sort/publish/enableFeedKey/showNotes/hide/clearCompleted/delete;任务 action:complete/priority/note/edit/move 等。

Q3:_path 里带查询参数得到 404? 路由只认 _path 的路径部分,查询参数必须拆到 URL 上。实测对比:api.php?_path=tasks?list=1 → 404 Unknown endpoint;正确写法 api.php?_path=tasks&list=1 → 200。

Q4:/ext-settings/xxx 返回 {"msg":"Unknown extension"}? 该扩展未启用或未解包。先 tar -C <应用目录>/ext -xzf <应用目录>/ext/extensions.tar.gz,再在设置页启用(写入 extensions 数组),路由才会注册(8 节实测全链路)。

Q5:中文标题/备注经 API 读写正常吗? 正常。实测创建 整理季度周报、部署验证 myTinyTodo、备注 要点:检查 PHP 扩展 等均完整往返;唯一注意点是 GET 查询参数里的中文要 URL 编码(s=周报 → s=%E5%91%A8%E6%8A%A5),且响应里备注是双轨的(note 为 HTML、noteText 为 Markdown 原文)。

Q6:想在第三方脚本里长期轮询任务,怎么设计? 用「session → login → 保存 token+Cookie」的会话对轮询所有请求复用;不要每请求都重新 login(服务器会反复轮换令牌)。退出时调一次 POST /logout 作废令牌。

十一、参考链接