TimeTagger v26.1.3 API 使用详解

TimeTagger v26.1.3 REST API 全端点实测:authtoken 头与 webtoken/apitoken 双令牌体系及 seed 吊销机制、records 增改查与 timerange/tag/running 过滤、无 DELETE 的 HIDDEN 隐藏语义、settings 与 updates/forcereset 同步机制,含错误路径(401/403/400/500)实测回显与脚本集成排障实录。

TimeTagger时间追踪自托管项目管理2026/10/24

TimeTagger v26.1.3 API 使用详解

目标:完整实测 TimeTagger v26.1.3 的 REST API——/api/v2/ 下全部 7 类端点的认证方式、参数语义、请求示例与真实响应回显,覆盖令牌体系(webtoken/apitoken)、记录 records 的增改查与过滤、同步机制 updates/forcereset,以及令牌吊销、非法请求等异常路径的实测行为。全部请求与响应已在 Ubuntu 22.04 服务器(TimeTagger v26.1.3,Compose 部署,端口 8137)逐条 curl 实测通过。

一、环境信息

  • 操作系统:Ubuntu 22.04.5 LTS
  • TimeTagger:v26.1.3(GET /api/v2/version 实测返回 26.1.3;依赖组件 asgineer 0.9.5 / itemdb 1.3.0 / pscript 0.8.0)
  • 服务地址:http://localhost:8137(Compose 部署,应用前缀 /timetagger/,部署过程见《TimeTagger v26.1.3 Docker Compose 部署指南》)
  • API 基路径:http://localhost:8137/timetagger/api/v2/
  • 演示账号:admin / q1w2e3r4
  • 客户端工具:curl 7.81+、python3(requests/json 标准库即可)

二、前置条件

  • 一个可访问的 TimeTagger 服务(本文用 localhost:8137,公网域名场景把基路径换成 https://<域名>/<path_prefix>api/v2/,前缀参数语义见《TimeTagger v26.1.3 配置参数详解》5.5);
  • 一组有效凭据(配置在服务端 TIMETAGGER_CREDENTIALS 里),用于换取令牌;仅 localhost 单用户模式例外(见第四章 4.5);
  • curl 或任意 HTTP 客户端。TimeTagger 无官方 SDK,社区 CLI(timetagger_cli 仓库)与 VSCode 扩展走的也是同一套 API。

三、API 总览

协议与约定(2026-10-24 对 v26.1.3 实测归纳):

  • 协议:HTTP/1.1 JSON API;请求体与响应体均为 UTF-8 JSON(认证引导端点除外,收 base64 编码的 JSON);
  • 基路径:<path_prefix>api/v2/,默认即 /timetagger/api/v2/;
  • 认证:自定义请求头 authtoken 携带 JWT(不是 Authorization 头,实测用错头一律 401);
  • 限流:无服务端限流(源码 # todo: rate limiting 注释,v26.1.3 未实现),高频调用请自控;
  • 错误风格:认证失败 401 + 纯文本 unauthorized: <原因>;权限不符 403 + forbidden: <原因>;参数错误 400 + bad request: <原因>;服务端异常 500。

端点全表(7 类,M 为 HTTP 方法,全部路径相对基路径):

端点方法认证用途
``(根)GET无返回文档站指引
bootstrap_authenticationPOST按方式用凭据/localhost/代理头换取 webtoken
versionGET需要查询服务版本
updatesGET需要增量同步(自某时间戳后的变更)
recordsGET / PUT需要时间记录的查询 / 批量增改
settingsGET / PUT需要用户设置(工时目标等)的读写
forceresetPUT需要强制客户端全量重同步
webtokenGETwebtoken刷新 webtoken(官方建议每日一次)
apitokenGETwebtoken换取长效 apitoken(脚本集成用)

💡 说明:端点数量少是设计而非缺失——TimeTagger 的 API 是「同步存储」模型:客户端本地有一份全量数据,records/settings 负责读写条目,updates/forcereset 负责多端之间把这份对齐。理解了「API 是给同步客户端用的」这一层,端点语义与字段设计就都顺了(mt/st 两个时间戳的分工在 5.1 展开)。

四、认证与首次调用

4.1 换取第一个 webtoken(bootstrap_authentication)

服务端凭据认证的入口。请求体为 base64 编码的 JSON(方法 usernamepassword),实测先用 python3 造请求体,再 POST:

B=http://localhost:8137/timetagger
BODY=$(python3 -c "
import base64, json
print(base64.b64encode(json.dumps({'method':'usernamepassword','username':'admin','password':'q1w2e3r4'}).encode()).decode())")
curl -s -w '\n[%{http_code}]' -X POST --data-binary "$BODY" $B/api/v2/bootstrap_authentication

实测输出(令牌为示意格式,实际值每次会话不同):

{"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiZXhwaXJlcyI6MTc5NDA0NDc3Miwic2VlZCI6ImlUUVJKWXlPLWJzIn0.pCIW7M_nVao4S0zIWsMxH0e9NAWqOI2afkwASjWQOxE"}
[200]

错误路径实测——错误口令(password: wrong):

Invalid credentials [403]

💡 说明:bootstrap_authentication 支持三种 method:usernamepassword(本文所用)、localhost(服务绑定 127.0.0.1 且请求来自本机时免凭据,见 4.5)、proxy(反代头认证,需服务端启用 TIMETAGGER_PROXY_AUTH_* 三件套)。

4.2 authtoken 头:唯一正确的携带方式

后续所有需要认证的请求都用自定义头 authtoken。实测错误头(Authorization: Basic)被明确拒绝:

WT=$(cat token.txt)
curl -s -w ' [%{http_code}]' -H "Authorization: Basic $WT" $B/api/v2/version

实测输出:

unauthorized: Missing jwt 'authtoken' in header. [401]

完全不带头的输出相同(同一句 401)。正确用法:

curl -s -w ' [%{http_code}]' -H "authtoken: $WT" $B/api/v2/version

实测输出:

{"version": "26.1.3"} [200]

4.3 webtoken 与 apitoken:两种令牌的生命周期

两种令牌都是 HS256 JWT,payload 含 username/expires/seed 三字段,由服务端数据目录的 jwt.key 签发与校验(密钥文件语义见配置篇 5.2)。实测生命周期对照:

令牌有效期获取方式适用场景
webtoken14 天bootstrap_authentication 换取;GET /webtoken 刷新浏览器/Web App 会话
apitoken至公元 3000 年(非过期型)GET /apitoken(webtoken 换取)脚本、CLI 等长期集成

刷新 webtoken 实测(官方建议客户端每日调一次保持会话):

curl -s -H "authtoken: $WT" $B/api/v2/webtoken

实测输出(新 expires、seed 不变,旧令牌仍有效到自然过期):

{"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiZXhwaXJlcyI6MTc5NDA0NDkwOSwic2VlZCI6ImlUUVJKWXlPLWJzIn0.cWfh60ePd_Yr7VwXEAJs-IOcOlepFjaGxC89NXgxjAc"}

换取 apitoken 实测:

AP=$(curl -s -H "authtoken: $WT" $B/api/v2/apitoken | python3 -c 'import json,sys; print(json.load(sys.stdin)["token"])')
echo "apitoken 前缀: ${AP:0:60}..."

实测输出:

apitoken 前缀: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWl...

apitoken 可以直接调数据端点(用它查 records 返回 200 与完整记录列表,实测通过);但不能反向换 webtoken:

curl -s -w ' [%{http_code}]' -H "authtoken: $AP" $B/api/v2/webtoken

实测输出:

forbidden: /webtoken needs auth with a web-token [403]

4.4 令牌吊销(seed 机制)与重置

每个令牌 payload 里的 seed 对应服务端按用户按令牌种类存的一份「当前种子」:校验时比对 seed,不一致即判吊销。实测触发路径——服务端重新走一次凭据引导并请求 ?reset=true(GET /webtoken?reset=true 或 GET /apitoken?reset=true)会生成新 seed,此前的旧令牌随即全部失效:

curl -s -w ' [%{http_code}]' -H "authtoken: <旧webtoken>" $B/api/v2/version

实测输出:

unauthorized: The webtoken is revoked (seed does not match) [401]

排障含义:见到 revoked 就重新走 4.1 换新令牌(客户端扫到 revoked 字样应与「过期」区分处理——过期是自然重登,吊销往往意味着他人做了重置);jwt.key 被删除重建时,签名校验直接失败,效果等同全员吊销。

4.5 localhost 单用户模式(免凭据路径)

服务绑定 127.0.0.1:* 且请求 Host 为 localhost/127.0.0.1 时,可用 method: localhost 无凭据换取默认用户 defaultuser 的 webtoken(源码 get_webtoken_localhost 逻辑)。这是 PyPI 单机形态「装完即用无需配置」的 API 侧支撑;绑定改为 0.0.0.0 或启用反代认证后该路径即被禁(实测返回 403,防止认证旁路)。

五、records:时间记录域详解

5.1 记录模型与字段

一条 record 的完整字段(v26.1.3 实测值):

{
  "key": "1792832216-a",
  "mt": 1792832216,
  "t1": 1792828616,
  "t2": 1792830416,
  "ds": "#部署 #TimeTagger 撰写部署指南",
  "st": 1792832216.5274923
}

逐字段说明:

  • key:记录唯一键,字符串。官方客户端生成 8 位随机串;服务端只要求字符串唯一,脚本集成可自选键名(如 <起始时间戳>-<序号>);
  • t1 / t2:起止 Unix 时间戳(秒)。t1 == t2 表示正在计时的未结束记录(running timer);
  • ds:描述文本,标签以 #标签 形式内嵌在文本里(如上例的 #部署、#TimeTagger),上限 256 字符;
  • mt:客户端修改时间(秒),写入时必填,服务器用它做「最后写入者胜」的冲突裁决;
  • st:服务器时间戳,服务端自动维护,客户端不可控——它保证同步的最终一致(每次写入严格递增)。

5.2 GET records:按时间范围与条件查询

必填参数 timerange(两个时间戳以 - 连接,闭区间重叠即命中;未结束记录只要 t1 落在区间内即命中)。缺参实测:

curl -s -w ' [%{http_code}]' -H "authtoken: $WT" "$B/api/v2/records"

实测输出:

bad request: /records needs timerange (2 timestamps) [400]

正常查询(全量,时间范围给足够大):

curl -s -H "authtoken: $WT" "$B/api/v2/records?timerange=0-1999999999"

实测输出:

{"records": [{"key": "1792832216-a", "mt": 1792832216, "t1": 1792828616, "t2": 1792830416, "ds": "#部署 #TimeTagger 撰写部署指南", "st": 1792832216.5274923}, {"key": "1792832216-b", "mt": 1792832221, "t1": 1792831616, "t2": 1792832216, "ds": "#测试 API 联调", "st": 1792832227.22625}, {"key": "1792835300-c", "mt": 1792835300, "t1": 1792832400, "t2": 1792833000, "ds": "#API #实测 设置条目验证", "st": 1792835309.1}]}

(输出节选,实测共 3 条。)可选过滤参数三个,可叠加:

  • tag=标签1,标签2:按 ds 内的 #标签 过滤(多个 tag 为 AND 关系;参数值里的 # 可带可不带,服务端忽略)。实测:
curl -s -H "authtoken: $WT" "$B/api/v2/records?timerange=0-1999999999&tag=TimeTagger"

实测输出(只余含 #TimeTagger 的那一条):

{"records": [{"key": "1792832216-a", "mt": 1792832216, "t1": 1792828616, "t2": 1792830416, "ds": "#部署 #TimeTagger 撰写部署指南", "st": 1792832216.5274923}]}
  • running=true/false:只看计时中(t1==t2)或已结束的记录;
  • hidden=true/false:ds 以 HIDDEN 前缀开头的记录视为「隐藏」(客户端删除语义,见 5.4),该参数控制查询是否纳入隐藏项。

5.3 PUT records:批量增改(upsert)

请求体为记录数组,命中已有 key 即更新,否则新建;响应恒为 200,逐条给出接受/失败结果:

curl -s -w '\n[%{http_code}]' -X PUT -H "authtoken: $WT" -H 'Content-Type: application/json' \
  --data-binary '[{"key":"1792835300-c","mt":1792835300,"t1":1792832400,"t2":1792833000,"ds":"#API #实测 设置条目验证"}]' \
  "$B/api/v2/records"

实测输出:

{"accepted": ["1792835300-c"], "failed": [], "errors": []}
[200]

非法条目不会连坐整批——缺必填字段的条目进 failed 并附原因,其余条目照常入库:

curl -s -w '\n[%{http_code}]' -X PUT -H "authtoken: $WT" -H 'Content-Type: application/json' \
  --data-binary '[{"key":"baditem","mt":1792835300}]' "$B/api/v2/records"

实测输出:

{"accepted": [], "failed": ["baditem"], "errors": ["A records is missing required fields: frozenset({'t1', 't2'})"]}
[200]

必填字段为 key/mt/t1/t2 四个(ds 可省,省略时视为空描述);单字段上限:ds 等字符串 256 字符,value 类 JSON 序列化后 8192 字符。整体不是合法 JSON 时服务端直接 500(实测 --data-binary 'notjson' 返回 JSONDecodeError in request handler: Expecting value: line 1 column 1 (char 0) [500])——客户端要自己保证请求体合法,这类错误不会转成 400。

💡 说明:PUT 是「按 mt 新者胜」而非「按到达顺序覆盖」——若服务端已有同 key 条目的 mt 更大,传入条目被忽略(源码 _push_items 逻辑)。多端并发写同一记录时,谁最后修改(mt 大)谁生效,这正是同步模型的冲突裁决规则。

5.4 「删除」的语义:HIDDEN 前缀

API 没有 DELETE 端点。官方客户端的删除是把 ds 改写为 HIDDEN <原文> 前缀再 PUT(源码 make_hidden),配合查询参数 hidden=false(默认排除隐藏项)实现「删了看不见、数据可追溯」。脚本集成删除一条记录等价于:

curl -s -X PUT -H "authtoken: $WT" -H 'Content-Type: application/json' \
  --data-binary '[{"key":"1792835300-c","mt":1792835400,"t1":1792832400,"t2":1792833000,"ds":"HIDDEN #API #实测 设置条目验证"}]' \
  "$B/api/v2/records"

实测输出:

{"accepted": ["1792835300-c"], "failed": [], "errors": []}

之后默认查询不再返回该条,带 hidden=true 可查回。注意 mt 要更新,否则被「新者胜」规则挡回。

5.5 计时器(running record)与时间戳语义

正在计时的记录 t1 == t2。开始计时 = PUT 一条 t1=t2=当前秒 的记录;停止计时 = PUT 同 key 记录把 t2 改成当前秒。时间戳均为秒级整数,timerange 边界按「区间重叠」判定(5.2 所述服务端查询条件),跨天/跨周的报表窗口由客户端拼 timerange 完成。

六、settings / updates / forcereset:设置与同步域

6.1 settings:用户设置

结构与 records 同构(key/mt/value/st),value 为任意 JSON(序列化后 ≤8192 字符)。实测读取:

curl -s -w ' [%{http_code}]' -H "authtoken: $WT" "$B/api/v2/settings"

实测输出:

{"settings": [{"key": "targets", "mt": 1792832226, "value": {"daily": 4.5}, "st": 1792835309.2}]} [200]

实测写入(每日工时目标 4.5 小时,这是官方客户端存「daily target」的真实 key):

curl -s -w '\n[%{http_code}]' -X PUT -H "authtoken: $WT" -H 'Content-Type: application/json' \
  --data-binary '[{"key":"targets","mt":1792835300,"value":{"daily":4.5}}]' "$B/api/v2/settings"

实测输出:

{"accepted": ["targets"], "failed": [], "errors": []}
[200]

settings 的 PUT 语义(mt 新者胜、failed 逐条报错)与 records 完全一致。

6.2 updates:增量同步

轮询端点:客户端带着上次同步时间戳问「有什么新的吗」。缺 since 实测:

bad request: /updates needs since [400]

正常轮询实测:

curl -s -H "authtoken: $WT" "$B/api/v2/updates?since=1792830000"

实测输出(节选,含记录与设置两类变更):

{"server_time": 1792835225.67227, "reset": false, "records": [{"key": "1792832216-a", ...}, {"key": "1792832216-b", ...}], "settings": [{"key": "targets", "mt": 1792832226, "value": {"daily": 4.5}, "st": 1792832244.2981749}]}

语义要点:响应恒含 server_time(作为下一轮 since)、reset 布尔与两类变更数组;reset=true 时数组是全量数据且客户端必须清空本地重建(服务端发生了 forcereset 或客户端落后于重置点);数据库无变化时走快速路径,返回空数组(源码按库文件 mtime 加余量提前返回)。

6.3 forcereset:强制全量重同步

PUT /forcereset 把服务端 reset_time 拨到当前时刻,效果有二:其一,所有客户端下一轮 updates 轮询拿到 reset=true 触发全量重建;其二,此后写入 mt 早于该时刻的条目被拒(实测错误文案 Item was modified after a reset,防旧客户端拿陈旧数据覆写)。适用场景:服务端做过数据修复/批量脚本治理后,把所有端拉回同一基线。该端点无请求体、无逐条响应,成功即 200。

七、分页/过滤/排序通用约定

  • 分页:无。数据端点按时间范围一次返回全部命中项,数据量大时用 timerange 切片(如按月查询)替代分页;
  • 过滤:records 支持 timerange/running/hidden/tag 四参数(可叠加,5.2);updates 仅 since;settings 无过滤参数;
  • 排序:服务端不承诺排序,客户端按 t1/key 自行排序。官方客户端以时间线视图自行组织;
  • 增量约定:updates 的 server_time 是唯一游标来源,客户端不得用本地时钟拼 since(时钟偏差会漏变更);
  • 错误约定汇总:400 bad request: <原因>(参数缺失/非法)、401 unauthorized: <原因>(缺头/吊销/过期)、403 forbidden: <原因>(令牌种类不符/代理认证未启用或不受信)、500(请求体非法 JSON 等,客户端自查)。

八、常见问题

Q1:昨天还能用的脚本今天 401 revoked (seed does not match)?

令牌被吊销了。两个常见触发源:有人在界面上重新登录时带了 ?reset=true(或手工调过该参数),该用户此类令牌的 seed 被轮换;jwt.key 文件被删后服务重建了密钥,效果等同全员吊销。处理:重新走 bootstrap_authentication 换新令牌(第四章 4.1、4.4)。

Q2:请求头带了令牌,还是 unauthorized: Missing jwt 'authtoken' in header.?

头名字写错了。TimeTagger 认自定义头 authtoken,不认 Authorization(Bearer/Basic 都不行,4.2 实测同一句 401)。

Q3:PUT 请求返回 500 而不是 400?

请求体不是合法 JSON(实测 notjson 返回 JSONDecodeError in request handler [500])。TimeTagger 对「能解析但内容不合规」的条目走 failed+errors 逐条报错(200),对「根本解析不了」的请求体直接 500——客户端先把 JSON.stringify 对,再谈字段校验。

Q4:tag 过滤查不到刚写的记录?

tag 参数匹配的是 ds 文本里的 #标签 片段(前缀匹配 #标签 或行尾 #标签)。检查三点:ds 里确实写了 #TimeTagger 形式的标签;参数值多余的 # 可带但不影响;标签后必须跟空格或位于文本末尾(#部署xx 不会被 tag=部署 命中)。

Q5:写入的记录「消失」了?

三种可能按概率排:mt 比服务端已有同 key 条目小,被「新者胜」规则忽略(用 hidden=true 与全量 timerange 查证);mt 早于最近一次 forcereset 的时刻,被拒(failed 里有 Item was modified after a reset);ds 被 HIDDEN 前缀覆盖后走了默认隐藏路径(5.4)。

Q6:多用户各自看到各自的数据吗?

是。每用户一个独立 SQLite 库(users/<用户名>~base64.db),令牌 payload 里的用户名决定读写哪个库;API 层没有跨用户查询能力,也没有角色权限概念(工具定位为个人/小团队,见《TimeTagger v26.1.3 介绍与选型指南》的限制章节)。

九、参考链接