进度台 API 完整使用指南
依据 app/main.py 的路由、app/models.py 的请求模型和 app/repository.py 的实际行为编写。涵盖登录、创建任务、写入进度、读取统计、日程、复盘、备份和账户管理。
1. 地址与工具
网站根地址记为BASE,不要预先加 /api。当前本地测试为 http://127.0.0.1:8001,默认Uvicorn端口为8000,服务器可使用实际部署域名 https://todolist.qingruxu.xyz。任务列表完整地址是 BASE + /api/tasks。
JSON请求发送 Content-Type: application/json。日期为 YYYY-MM-DD,月份为 YYYY-MM;省略参考日期时以北京时间当天为准。可在 /docs 查看Swagger,或读取 /openapi.json;部分账户接口的dict请求体字段需对照本文。本文网页版为 /manual/api.html。
2. 认证和CSRF
| 方式 |
读取内容 |
写入内容 |
| 普通账户会话(推荐) |
登录返回的Cookie |
Cookie + X-CSRF-Token |
| 普通账户Bearer |
Authorization: Bearer <账户完整token> |
Bearer + ppm_csrf Cookie + 同值 X-CSRF-Token |
| 专用Agent |
部署配置的 PPM_API_TOKEN |
仅两个Agent接口,不要求CSRF |
账户token与PPM_API_TOKEN是两套凭证。 账户token用于普通内容接口;Agent单独校验部署级token,不能拿Agent token去新增普通任务或填写每日记录。
账户模式中,每次密码登录、验证码登录或token轮换都会替换账户唯一token,使旧token及使用它的Cookie失效。脚本登录同一账号可能导致先前网页会话需要重新登录;当前没有独立的长期API key。
账户资料、TOTP设置、管理员账号管理的处理函数直接读取 ppm_account Cookie,这些接口请使用Cookie会话,仅Bearer会失败。同时携带两者时中间件优先读取账户Cookie,过期Cookie会遮盖有效Bearer。
旧单密码会话模式使用 ppm_session,登录可只提交password,账户Bearer不适用。当前默认账户模式需要username/password;PPM_ALLOW_NO_AUTH=true是开发模式开关,不用于服务器公开部署。
2.1 密码登录
POST /api/auth/login
Content-Type: application/json
{"username":"你的用户名","password":"你的密码"}
成功响应包含authenticated、csrfToken,账户模式还包含username/theme/完整token,设置ppm_account和ppm_csrf Cookie。保存Cookie,在普通POST/PUT/DELETE中发送 X-CSRF-Token: <csrfToken>;GET无需此头。登录、注册、验证码登录无需先提供CSRF。
浏览器同源fetch自动管理Cookie;Python示例使用CookieJar;curl以 -c 保存Cookie、-b 发送Cookie。不要打印或公开登录响应。
2.2 Bearer方式(curl)
下方为Bash写法,占位符需替换。Windows使用 curl.exe,避免PowerShell同名别名;JSON写入UTF-8文件后以 --data-binary @文件 发送,避免中文和引号转义问题。
# 读取任务,无需CSRF
curl -H "Authorization: Bearer <账户完整token>" \
"http://127.0.0.1:8001/api/tasks"
# 获取CSRF,同时保存ppm_csrf Cookie
curl -c csrf-cookies.txt "http://127.0.0.1:8001/api/auth/status"
# task.json使用第4节请求字段;csrf值取上一步的csrfToken
curl -b csrf-cookies.txt -H "Authorization: Bearer <账户完整token>" \
-H "X-CSRF-Token: <csrfToken>" -H "Content-Type: application/json" \
--data-binary @task.json "http://127.0.0.1:8001/api/tasks"
账户模式的auth/status只根据Cookie判断authenticated,仅有Bearer时显示false不代表Bearer失效。CSRF Cookie与请求头必须来自同一次获取,Cookie文件视为凭证保存。
3. 可直接运行的完整示例
examples/api_quickstart.py只依赖Python标准库,密码/验证码交互输入,不写入源码或命令历史。它会实际创建示例任务、当天记录、日程和图表,不是只读测试。重复执行会创建新的示例资源。
python examples/api_quickstart.py --base http://127.0.0.1:8001 --username 你的用户名
python examples/api_quickstart.py --base https://todolist.qingruxu.xyz --username 你的用户名
# 已绑定验证器也可使用验证码登录
python examples/api_quickstart.py --base http://127.0.0.1:8001 --username 你的用户名 --totp
输出新建资源ID、记录版本、指标汇总和复盘数值,不输出认证token。刷新网页即可看到内容;示例任务、日程和图表可以在界面管理。
以下Python片段共享client,创建自己的脚本并从项目根目录运行。先初始化,再复制所需片段:
from getpass import getpass
from urllib.parse import urlencode
from examples.api_quickstart import ApiClient
client = ApiClient("http://127.0.0.1:8001")
client.login("你的用户名", getpass("密码:"))
request(method, path, body)自动携带Cookie及CSRF;失败抛出包含HTTP状态及detail的异常。
4. 新增、读取和修改任务
task = client.request("POST", "/api/tasks", {
"name": "阅读计划", "description": "每周阅读并记录学习时间",
"startDate": None, "endDate": None, "priority": 3, "state": "active",
"metrics": [
{"name": "阅读打卡", "kind": "checkin", "period": "day", "target": 1},
{"name": "阅读页数", "kind": "quantity", "period": "week", "target": 100, "unit": "页"},
{"name": "学习时长", "kind": "duration", "period": "week", "target": 7200, "durationUnit": "hour"}
]
})
task_id = task["taskId"]
checkin_id, quantity_id, duration_id = [m["metricId"] for m in task["metrics"]]
也可将请求保存为task.json供curl调用,Python的None改成JSON的null。创建返回201,直接返回任务对象,不包在items内;保存taskId与每个metricId。
| 字段 |
约束/含义 |
| name / description |
名称1–120字符;说明最多2000字符 |
| startDate / endDate |
可省略或null;都有值时截止不能早于开始;无日期显示开放边界 |
| priority / state |
优先级1–5默认3;状态active或paused |
| metrics |
1–20个指标,指标名称1–60字符 |
| kind |
checkin打卡 / quantity数量 / duration时长 |
| period |
day / week / month / lifetime,默认day |
| target |
正整数或null;打卡未设目标自动为1 |
| unit |
数量单位,最多20字符;时长自动清空此字段 |
| durationUnit |
second / minute / hour,仅输入与显示口径,默认minute |
时长target与记录value始终是整数秒。 durationUnit为hour不会让API自动乘3600;2小时目标填写7200,1小时30分记录填写5400。数量为非负整数,打卡只能记录0或1。
listing = client.request("GET", "/api/tasks") # {version, items}
task = client.request("GET", f"/api/tasks/{task_id}")
fields = ("name", "description", "startDate", "endDate", "priority", "state", "metrics")
update = {key: task[key] for key in fields}
update["name"] = "阅读与笔记计划"
update["expectedVersion"] = task["version"]
task = client.request("PUT", f"/api/tasks/{task_id}", update)
PUT是完整属性更新,不能只发送新名称,也不要提交taskId/revision/时间戳等只读字段,否则未知字段校验失败。已有指标保留metricId;新指标省略ID。省略旧指标会移除其定义,不是保持原值。
5. 填写进度、补录与读取统计
5.1 写入一天的完整记录
day = "2026-10-01" # 换成需要记录或补录的日期
path = f"/api/tasks/{task_id}/daily/{day}"
old = client.request("GET", path)
record = client.request("PUT", path, {
"expectedVersion": old["version"], "note": "读了18页,学习1小时30分。",
"completedAt": f"{day}T20:30:00+08:00",
"values": [
{"metricId": checkin_id, "value": 1},
{"metricId": quantity_id, "value": 18},
{"metricId": duration_id, "value": 5400}
]
})
换成过去日期就是补录。不存在的日记录GET仍返回200、version为0、values为空;首次写入expectedVersion为0,后续使用该日期记录的版本。
PUT替换当天记录,不追加增量。 先记录18页,再读5页,应提交23页。values、note、completedAt都替换原内容;省略其他指标会移除其该天数值,省略心得/时间会使用空字符串/null。只改一个指标时先读并合并:
old = client.request("GET", path)
values = dict(old["values"])
values[quantity_id] = 23
record = client.request("PUT", path, {
"expectedVersion": old["version"], "note": old["note"], "completedAt": old["completedAt"],
"values": [{"metricId": key, "value": value} for key, value in values.items()]
})
expectedVersion虽可省略,建议始终提供以防覆盖并发修改。填写0是已保存零投入,与未记录不同。保存后日记录version增加;心得最多4000字符;completedAt为ISO日期时间或null,建议明确+08:00。没有单独删除某日记录的API。
5.2 读取进度与历史
all_progress = client.request("GET", "/api/summary?" + urlencode({"date": day}))
progress = client.request("GET", f"/api/tasks/{task_id}/summary?" + urlencode({"date": day}))
for metric in progress["tasks"][task_id]["metrics"]:
print(metric["name"], metric["value"], metric["target"], metric["percent"],
metric["windowStart"], metric["windowEnd"])
history = client.request("GET", f"/api/tasks/{task_id}/history?start=2026-09-01&end=2026-10-01")
汇总为 {date, tasks: {taskId: {...}}},单任务也是这个结构。metrics分别给出值、目标、百分比、窗口,时长仍为秒。日/周/月按各自窗口计算,周从周一开始;累计以任务开始日为下界,没有开始日期视为不限起始。晚于参考日期的事实不计入。
无目标percent为null,有目标时最大100%,真实超额仍在value中。recentValue/previousValue是参考日期结束的近7天/前7天投入。任务级progress是兼容字段(有目标指标百分比平均值),当前UI使用独立指标,不建议跨指标比较平均值。
today.recorded表示参考日是否已保存,不表示所有目标完成。history含records及任务属性revisions。查询跨度最多730天(end-start),无分页。汇总读取原始事实,补录后无需手动归档来更新统计。
6. 日程的新增、读取、完成与修改
schedule = client.request("POST", "/api/schedules", {
"title": "整理阅读笔记", "description": "提炼三个要点", "date": day,
"time": "20:30", "recurrence": "daily", "priority": 3, "state": "active"
})
schedule_id = schedule["scheduleId"]
daily_schedules = client.request("GET", f"/api/schedules/today?date={day}")
calendar = client.request("GET", f"/api/schedules/calendar?month={day[:7]}")
schedule = client.request("POST", f"/api/schedules/{schedule_id}/complete", {
"date": day, "completed": True, "expectedVersion": schedule["version"]
})
schedule = client.request("POST", f"/api/schedules/{schedule_id}/complete", {
"date": day, "completed": False, "expectedVersion": schedule["version"]
}) # 取消该天完成
名称1–120字符,说明最多2000字符;date必填,time为HH:MM或null(全天)。recurrence为none/daily/weekly/monthly,state为active/archived。只有active日程进入日历;每周按开始日星期重复,每月按同一日号重复,缺少该日号的月份不出现。
GET schedules是定义,today/calendar按日期展开。完成记录按日期保存,不会完成所有重复实例;完成接口返回原始日程和新version,completed字段由展开接口提供。PUT修改需提交全部输入字段与expectedVersion,不能包含scheduleId/completions等只读字段。日程完成与任务打卡独立,不会自动写入任务进度。
7. 图表和复盘
chart = client.request("POST", "/api/charts", {
"title": "学习时长", "taskId": task_id, "metricId": duration_id, "days": 30, "yMax": None
})
chart_id = chart["chartId"]
chart_data = client.request("GET", f"/api/charts/{chart_id}/data?start=2026-09-01&end=2026-10-01")
review = client.request("GET", "/api/review/data?" + urlencode({
"start": "2026-09-01", "end": "2026-10-01", "taskId": task_id, "metricId": duration_id
}))
notes = client.request("GET", "/api/review/notes?start=2026-09-01&end=2026-10-01")
图表days为7–365,yMax为显示单位的正整数参考上限或null;图表存配置,删除图表不删除事实。PUT提交title/taskId/metricId/days/yMax及expectedVersion。图表DELETE直接删除,无图表回收站或版本参数。
图表数据默认以ref或今天结束、按配置days取范围,start/end可覆盖,end优先于ref。review/data不需要先创建图表,start/end必填,taskId/metricId可选;具体指标建议同时提供所属taskId。
连续数据点含date/value/recorded/recordedCount;无记录value为0,recorded区分缺失与零投入。指定metricId时是原始指标量,时长仍为秒;未指定时是当天有非零指标投入的任务数,不是不同指标相加,也不表示所有目标完成。recordedCount为筛选范围内当天有事实记录的任务数。
心得按日期倒序返回days[].records[],默认近30天,不接受taskId或文本搜索参数,这些筛选由客户端处理。历史/心得/复盘跨度上限730天(差值),图表配置days上限365天。页面筛选不会修改图表配置。
8. 版本号、排序、删除与恢复
| 操作 |
expectedVersion来源 |
传在哪里 |
| 任务/日程/图表PUT |
对应资源version |
JSON |
| 任务/日程软删除、恢复 |
对应资源最新version |
查询参数 |
| 日记录PUT |
对应日期记录version,首次0 |
JSON |
| 日程完成/取消 |
日程version |
JSON |
| 任务排序 |
GET tasks的顶层列表version |
JSON |
每次更新使用响应的新版本,409时重新读取并核对,不要只替换版本号强行覆盖。
listing = client.request("GET", "/api/tasks")
client.request("PUT", "/api/tasks/reorder/all", {
"taskIds": [t["taskId"] for t in listing["items"]][::-1], "expectedVersion": listing["version"]
}) # 必须包含全部未删除任务,含挂起任务
task = client.request("GET", f"/api/tasks/{task_id}")
deleted = client.request("DELETE", f"/api/tasks/{task_id}?expectedVersion={task['version']}")
task = client.request("POST", f"/api/tasks/{task_id}/restore?expectedVersion={deleted['version']}", {})
任务/日程DELETE软删除,读取回收站带includeDeleted=true。永久删除须先软删除,再POST /api/tasks/{id}/purge 或 /api/schedules/{id}/purge,请求 {"confirmation":"完整资源ID"}。任务永久删除会清理其事实与修订,不可恢复。
9. 备份、导入、统计与归档
import json
from pathlib import Path
backup = client.request("GET", "/api/export")
Path("progress-export.json").write_text(json.dumps(backup, ensure_ascii=False, indent=2), encoding="utf-8")
stats = client.request("GET", "/api/admin/stats")
archive = client.request("POST", "/api/admin/archive", {})
导出含meta/tasks/revisions/daily/archives/schedules/scheduleRevisions/charts等业务字段,不包含账户数据库、密码、token或TOTP密钥。旧备份缺少图表字段仍可导入。admin/stats、archive、import操作当前账户自己的数据,不要求账户管理员身份。
导入整体替换当前账户数据,不追加或合并。先导出目标备份,再明确执行下方代码。接口不是文件上传表单,直接把完整备份JSON作为请求体:
# 确认要覆盖当前账户的数据后执行
payload = json.loads(Path("progress-export.json").read_text(encoding="utf-8"))
result = client.request("POST", "/api/admin/import", payload)
10. 注册、账户资料与验证码
注册先GET /api/auth/challenge,获取challengeId/question;用户解答后5分钟内POST /api/auth/register:
{"username":"new-user","password":"至少12个字符的密码","challengeId":"返回的ID","challengeAnswer":"计算结果字符串"}
挑战一次性,失败后重新获取;用户名至少3字符,密码至少12字符。成功返回完整token,但不会设置登录Cookie,即使返回authenticated=true也不代表已有浏览器会话;继续调用登录接口。
| 方法与路径 |
字段或用途 |
GET /api/auth/status |
启用状态、Cookie认证状态、csrfToken;账户模式设置CSRF Cookie |
POST /api/auth/logout |
Cookie+CSRF;账户模式清除客户端Cookie,不撤销已发出的Bearer token |
GET /api/account/me |
userId、username、tokenPrefix、isAdmin、theme,不返回完整token |
PUT /api/account/me |
username/password/theme可选;密码至少12字符;不用expectedVersion |
POST /api/account/token/rotate |
返回新完整token,旧token立即失效;当前不更新登录Cookie,需重新处理会话 |
POST /api/auth/totp/login |
username/code,code为6位字符串;设置Cookie并返回csrfToken,不返回完整token |
POST /api/account/totp/setup |
password为当前密码,返回secret/otpauth URI/PNG二维码Data URI |
POST /api/account/totp/confirm |
code为6位字符串,5分钟内确认 |
POST /api/account/totp/disable |
password为当前密码,解除绑定 |
theme支持studio/pixel/cartoon/rick-morty/rose。TOTP采用SHA-1、6位、30秒;确认绑定的验证码已消费,不可立即用同一码登录,已使用的验证码禁止重放。错误有频率限制,secret/uri/二维码均为凭证,勿记录日志。详见动态验证码登录。
11. 管理其他账户(仅管理员)
要求管理员账户ppm_account Cookie,写操作还需CSRF:
| 方法与路径 |
用途 |
GET /api/admin/accounts |
列表中user_id用作路径参数 |
PUT /api/admin/accounts/{user_id} |
username和/或password,密码至少12字符 |
POST /api/admin/accounts/{user_id}/token/rotate |
轮换指定用户token,返回完整新token |
GET /api/admin/accounts/{user_id}/resources |
指定用户资源统计 |
DELETE /api/admin/accounts/{user_id} |
永久删除用户和其目录数据;不能删除当前管理员或admin;无版本号/回收站 |
12. 专用Agent API
服务启动环境设置独立PPM_API_TOKEN,再重启进程/容器。Compose需把变量传入容器,只写.env但Compose未引用不会生效;未配置返回503。
curl -H "Authorization: Bearer <PPM_API_TOKEN配置值>" \
"http://127.0.0.1:8001/api/agent/overview?date=2026-10-01"
curl -H "Authorization: Bearer <PPM_API_TOKEN配置值>" \
-H "Content-Type: application/json" --data-binary @schedule.json \
"http://127.0.0.1:8001/api/agent/schedules"
schedule.json使用第6节日程字段。overview返回指定日期的日程和任务进度;Agent没有创建任务或填写进度接口,这些使用普通账户API。
独立Agent客户端不发送账户Cookie,默认操作根仓库(当前默认管理员数据)。携带有效账户Cookie时中间件会切换该账户仓库,但仍须部署级Agent token。不要把部署级token分发给普通用户。
13. 内容接口速查与错误处理
| 方法与路径 |
功能 / 参数 |
GET /api/health |
公开健康检查 |
GET /api/layouts/{page} |
home/task-detail/review,只读布局,无布局写接口 |
GET / POST /api/tasks |
列表/创建;GET支持includeDeleted |
GET / PUT / DELETE /api/tasks/{id} |
读/完整更新/软删;GET支持includeDeleted |
POST /api/tasks/{id}/restore、/purge |
恢复/永久删除 |
PUT /api/tasks/reorder/all |
全量ID排序 |
GET / PUT /api/tasks/{id}/daily/{date} |
读/替换当天事实 |
GET /api/tasks/{id}/history |
start/end,默认近365天,含修订 |
GET /api/summary、/api/tasks/{id}/summary |
date参考日期,全部/单任务 |
GET / POST /api/schedules |
定义列表/创建;GET支持includeDeleted |
GET /api/schedules/today、/calendar |
date展开/month月历 |
GET / PUT / DELETE /api/schedules/{id} |
读/完整更新/软删;GET支持includeDeleted |
POST /api/schedules/{id}/complete、/restore、/purge |
完成/恢复/永久删除 |
GET / POST /api/charts |
列表/创建配置 |
GET / PUT / DELETE /api/charts/{id} |
读/完整更新/直接删除 |
GET /api/charts/{id}/data |
ref/start/end |
GET /api/review/data |
start/end必填,taskId/metricId可选 |
GET /api/review/notes |
start/end,默认近30天,任务/文本客户端过滤 |
GET /api/export |
业务JSON备份 |
GET /api/admin/stats |
当前账户统计 |
POST /api/admin/archive、/api/admin/import |
补归档/整体替换 |
创建任务/日程/图表返回201,读取/更新一般200。错误通常 {"detail":"说明"},模型校验detail可能为数组,要保留字段定位。
| 状态 |
处理 |
| 401 |
检查凭证/过期Cookie/登录或轮换导致旧token失效 |
| 403 |
检查CSRF同值Cookie和头,或管理员权限/绑定密码 |
| 404 |
资源不存在、软删未includeDeleted,或当前模式未启用功能 |
| 409 |
版本/用户名/绑定状态冲突,先读最新状态 |
| 422 |
日期、必填/未知字段、整数值、确认ID不合法 |
| 429 |
登录/注册/绑定失败频率限制,按说明等待 |
| 503 |
Agent token未配置 |
成功后才使用返回值;有副作用POST不要无条件重试,避免重复创建。列表无分页、服务器文本搜索或批量事实写入接口。生产调用使用HTTPS;导出虽然不含账户凭证,仍可能包含私人说明与心得。