Traggo 0.8.3 API 使用详解

Traggo 0.8.3 GraphQL API 全量实测:设备令牌认证、标签/时间段/计时器/统计/仪表盘/用户/设备八大域 curl 示例与真实响应、分页约定与错误处理。

TraggoTraggo时间追踪GraphQL2026/10/25

Traggo 0.8.3 API 使用详解

目标:用 curl 实测打通 Traggo 0.8.3 的 GraphQL API:设备令牌认证、标签/时间段/计时器/统计/仪表盘/用户/设备全量操作与错误处理。本文所有请求与响应均为真实环境实测照抄(Traggo 无任何 CLI 子命令,API 是其唯一的可编程接口)。

一、环境信息

  • 服务端:Traggo v0.8.3(commit 6321119c3c2d55f04e2e4967f6492aabd6067b76),Ubuntu 22.04.5 LTS,监听 3030
  • API 端点:POST http://<服务器>:3030/graphql
  • 演示账号:admin / q1w2e3r4
  • 客户端工具:curl(任何能发 HTTP POST 的工具均可)

二、前置条件

已按本专栏《Ubuntu 22.04 安装 Traggo 0.8.3 指南》完成部署并能 curl http://localhost:3030/ 返回 200;本文示例均在本机(localhost:3030)执行,换服务器时替换地址即可。

三、API 总览

  • 协议:GraphQL over HTTP,端点固定 /graphql,一律 POST + Content-Type: application/json,请求体 {"query": "..."}。
  • Schema 规模(内落实测):14 个查询(Query)+ 27 个变更(Mutation),覆盖标签、时间段、计时器、统计、仪表盘、用户、设备、用户设置八大域。
  • 认证:基于「设备(Device)令牌」——login 变更换取 token,之后每个请求用 Cookie traggo=<token> 携带;设备令牌可随时吊销(removeDevice)。
  • 授权角色:绝大多数操作要求 USER 角色;用户管理(users/createUser 等)要求 ADMIN。
  • 免认证接口:version 查询无需登录(探测服务存活与版本很方便)。
  • 时间类型:Time 标量接受 RFC3339(如 2026-10-24T09:00:00+08:00 或 UTC Z 形式);相对时间表达式(now-7d)仅用于仪表盘的 RelativeOrStaticRange。

四、认证与首次调用

4.1 免登录探测版本

curl -s -X POST http://localhost:3030/graphql \
  -H 'Content-Type: application/json' \
  -d '{"query":"{ version { name commit buildDate } }"}'

实测响应:

{"data": {"version": {"name": "0.8.3", "commit": "6321119c3c2d55f04e2e4967f6492aabd6067b76", "buildDate": "2026-03-01T10:30:56Z"}}}

4.2 登录换取设备令牌

login 的签名为 login(username: String!, pass: String!, deviceName: String!, type: DeviceType!, cookie: Boolean!): Login。DeviceType 枚举:NoExpiry(永不过期)/ LongExpiry(长有效期)/ ShortExpiry(短有效期);cookie: false 表示不种 Set-Cookie、只取 token(适合脚本)。

curl -s -X POST http://localhost:3030/graphql \
  -H 'Content-Type: application/json' \
  -d '{"query":"mutation { login(username: \"admin\", pass: \"q1w2e3r4\", deviceName: \"curl-script\", type: NoExpiry, cookie: false) { token user { name admin } device { id name type } } }"}'

实测响应:

{"data": {"login": {"token": "c0NqO9VhFH7PHfli6gtl", "user": {"name": "admin", "admin": true}, "device": {"id": 3, "name": "curl-script", "type": "NoExpiry"}}}}

4.3 携带令牌调用

令牌通过 Cookie 头 traggo=<token> 传递(不是 Authorization: Bearer,实测 Bearer 不生效):

TOKEN=c0NqO9VhFH7PHfli6gtl
curl -s -X POST http://localhost:3030/graphql \
  -H 'Content-Type: application/json' \
  -H "Cookie: traggo=$TOKEN" \
  -d '{"query":"{ currentUser { id name admin } }"}'

实测响应:

{"data": {"currentUser": {"id": 1, "name": "admin", "admin": true}}}

对照实测:不带令牌时同一查询返回 {"data": {"currentUser": null}};带 Authorization: Bearer <token> 也返回 null。

五、标签域(tags)

标签分两层:先定义标签键(TagDefinition,含颜色),再在时间段上贴 键:值。

5.1 创建标签键

curl -s -X POST http://localhost:3030/graphql -H 'Content-Type: application/json' -H "Cookie: traggo=$TOKEN" \
  -d '{"query":"mutation { createTag(key: \"project\", color: \"#4caf50\") { key color usages } }"}'

实测响应:

{"data": {"createTag": {"key": "project", "color": "#4caf50", "usages": 0}}}

usages 是该键被时间段使用的次数。同法创建 type 键(#2196f3)。

5.2 标签列表与建议

curl -s -X POST http://localhost:3030/graphql -H 'Content-Type: application/json' -H "Cookie: traggo=$TOKEN" \
  -d '{"query":"{ tags { key color usages } }"}'

实测响应(已有时间段使用后 usages 会计数):

{"data": {"tags": [{"key": "project", "color": "#4caf50", "usages": 2}, {"key": "type", "color": "#2196f3", "usages": 2}]}}

输入建议(Web UI 自动补全用的就是它们):

# 键建议:按前缀模糊匹配
'{"query":"{ suggestTag(query: \"pro\") { key color usages } }"}'
# {"data": {"suggestTag": [{"key": "project", "color": "#4caf50", "usages": 0}]}}

# 值建议:指定键下按前缀匹配历史值
'{"query":"{ suggestTagValue(key: \"project\", query: \"tra\") }"}'
# {"data": {"suggestTagValue": ["traggo-docs"]}}

5.3 修改与删除标签键

  • updateTag(key: "project", newKey: "proj", color: "#4caf50"):改键名/颜色(newKey 可空表示不改名);
  • removeTag(key: "proj"):删除键定义。

⚠️ removeTag 只删键定义与配色,不会删历史时间段上的 键:值 记录。

六、时间段域(timeSpans)

时间段(TimeSpan)是 Traggo 的核心实体:{id, start, end, oldStart, note, tags};end 为 null 表示正在计时。

6.1 补录一段已完成时间

curl -s -X POST http://localhost:3030/graphql -H 'Content-Type: application/json' -H "Cookie: traggo=$TOKEN" \
  -d '{"query":"mutation { createTimeSpan(start: \"2026-10-24T09:00:00+08:00\", end: \"2026-10-24T10:30:00+08:00\", tags: [{key: \"project\", value: \"traggo-docs\"}, {key: \"type\", value: \"writing\"}], note: \"write traggo docs\") { id start end note tags { key value } } }"}'

实测响应:

{"data": {"createTimeSpan": {"id": 1, "start": "2026-10-24T09:00:00+08:00", "end": "2026-10-24T10:30:00+08:00", "note": "write traggo docs", "tags": [{"key": "project", "value": "traggo-docs"}, {"key": "type", "value": "writing"}]}}}

6.2 开启一个计时器(省略 end)

'{"query":"mutation { createTimeSpan(start: \"2026-10-25T08:30:00+08:00\", tags: [{key: \"project\", value: \"traggo-docs\"}, {key: \"type\", value: \"review\"}], note: \"\") { id start end note } }"}'

实测响应(end: null 即运行中的计时器):

{"data": {"createTimeSpan": {"id": 2, "start": "2026-10-25T08:30:00+08:00", "end": null, "note": ""}}}

6.3 按区间查询(含游标分页)

签名:timeSpans(fromInclusive: Time, toInclusive: Time, cursor: InputCursor): PagedTimeSpans!,InputCursor = {offset, startId, pageSize}。

'{"query":"{ timeSpans(fromInclusive: \"2026-10-24T00:00:00+08:00\", toInclusive: \"2026-10-25T23:59:59+08:00\") { timeSpans { id start end note tags { key value } } cursor { hasMore offset startId pageSize } } }"}'

实测响应:

{"data": {"timeSpans": {"timeSpans": [{"id": 1, "start": "2026-10-24T09:00:00+08:00", "end": "2026-10-24T10:30:00+08:00", "note": "write traggo docs", "tags": [{"key": "project", "value": "traggo-docs"}, {"key": "type", "value": "writing"}]}], "cursor": {"hasMore": true, "offset": 1, "startId": 2, "pageSize": 100}}}}

注意返回的 cursor.hasMore: true、startId: 2——区间内还有 id=2 的运行中计时器未在本页展开,翻页时把 cursor 原样回传(cursor: {offset: 1, startId: 2, pageSize: 100})继续取。

6.4 当前计时器与停止

'{"query":"{ timers { id start note tags { key value } } }"}'
{"data": {"timers": [{"id": 2, "start": "2026-10-25T08:30:00+08:00", "note": "", "tags": [{"key": "project", "value": "traggo-docs"}, {"key": "type", "value": "review"}]}]}}

停止计时器:

'{"query":"mutation { stopTimeSpan(id: 2, end: \"2026-10-25T10:45:00+08:00\") { id start end note } }"}'
{"data": {"stopTimeSpan": {"id": 2, "start": "2026-10-25T08:30:00+08:00", "end": "2026-10-25T10:45:00+08:00", "note": ""}}}

6.5 更新、复制、删除

  • updateTimeSpan(id, start, end, tags, oldStart, note):全量更新(tags 不传或传 null 会清空标签,务必回传原标签):
'{"query":"mutation { updateTimeSpan(id: 2, start: \"2026-10-25T08:30:00+08:00\", end: \"2026-10-25T10:45:00+08:00\", note: \"review traggo docs\", tags: [{key: \"project\", value: \"traggo-docs\"}, {key: \"type\", value: \"review\"}]) { id note } }"}'
# {"data": {"updateTimeSpan": {"id": 2, "note": "review traggo docs"}}}
  • copyTimeSpan(id, start, end):按原标签/备注复制到新时间段(实测返回 id=3 副本,tags 完整复制);
  • removeTimeSpan(id):删除(实测返回被删对象 {"id": 3});
  • replaceTimeSpanTags(from: {key,value}, to: {key,value}, opt: {override: Override|Discard}):批量把某标签值替换为另一个(Override 覆盖冲突、Discard 丢弃冲突)。

七、统计域(stats / stats2)

7.1 按标签键聚合时长

stats(ranges: [Range!], tags: [String!], excludeTags: [InputTimeSpanTag!], requireTags: [InputTimeSpanTag!]):tags 是要聚合的标签键列表,requireTags 过滤「必须带某标签」,excludeTags 过滤「不得带某标签」。

'{"query":"{ stats(ranges: [{start: \"2026-10-24T01:00:00Z\", end: \"2026-10-25T15:59:59Z\"}], tags: [\"project\", \"type\"]) { start end entries { key value timeSpendInSeconds } } }"}'

实测响应:

{"data": {"stats": [{"start": "2026-10-24T01:00:00Z", "end": "2026-10-25T15:59:59Z", "entries": [{"key": "project", "value": "traggo-docs", "timeSpendInSeconds": 13500}, {"key": "type", "value": "review", "timeSpendInSeconds": 8100}, {"key": "type", "value": "writing", "timeSpendInSeconds": 5400}]}]}}

对照手工核算:writing 段 1.5h(5400s)+ review 段 2.25h(8100s),project 合计 13500s,完全吻合。

7.2 stats2:仪表盘同款统计

stats2(now: Time!, stats: InputStatsSelection!),InputStatsSelection = {interval: Hourly|Daily|Weekly|Monthly|Yearly|Single, tags, excludeTags, includeTags, rangeId, range: {from, to}}。range 支持 RFC3339 绝对时间或 now-7d 相对表达式:

'{"query":"{ stats2(now: \"2026-10-25T12:00:00Z\", stats: {interval: Single, tags: [\"project\"], range: {from: \"2026-10-24T01:00:00Z\", to: \"2026-10-25T15:59:59Z\"}}) { start end entries { key value timeSpendInSeconds } } }"}'
# 实测与 stats 同结果:project/traggo-docs = 13500

⚠️ 排障实录:统计区间与时间段时区表达不一致会导致 entries 为空。实测用 +08:00 创建时间段、再用 +08:00 写区间时,统计内部先把区间归一化到 UTC 再去掉时区做文本比对,与时间段存储值错位,结果为空数组;改用 UTC Z 形式的区间后即正常。建议全链路统一用 UTC Z 形式传时间。错误写法会静默返回空,不报错,排障时先核对这个点。

八、仪表盘域(dashboards)

Dashboard = 若干命名区间(ranges)+ 若干图表条目(entries)。图表类型枚举:PieChart | BarChart | StackedBarChart | LineChart | VerticalTable | HorizontalTable。

# 创建仪表盘
'{"query":"mutation { createDashboard(name: \"weekly\") { id name ranges { id name } } }"}'
# {"data": {"createDashboard": {"id": 1, "name": "weekly", "ranges": []}}}

# 加命名区间(绝对时间)
'{"query":"mutation { addDashboardRange(dashboardId: 1, range: {name: \"this week\", editable: true, range: {from: \"2026-10-19T00:00:00+08:00\", to: \"2026-10-25T23:59:59+08:00\"}}) { id name editable range { from to } } }"}'
# {"data": {"addDashboardRange": {"id": 1, "name": "this week", "editable": true, "range": {"from": "2026-10-19T00:00:00+08:00", "to": "2026-10-25T23:59:59+08:00"}}}}

# 加相对区间
'{"query":"mutation { addDashboardRange(dashboardId: 1, range: {name: \"last 7 days\", editable: true, range: {from: \"now-7d\", to: \"now\"}}) { id name } }"}'
# 正常返回(实测)

# 加饼图条目
'{"query":"mutation { addDashboardEntry(dashboardId: 1, entryType: PieChart, title: \"by project\", total: true, stats: {interval: Single, tags: [\"project\"], range: {from: \"2026-10-24T01:00:00Z\", to: \"2026-10-25T15:59:59Z\"}}) { id title entryType } }"}'
# {"data": {"addDashboardEntry": {"id": 1, "title": "by project", "entryType": "PieChart"}}}

⚠️ range.from/to 只接受 RFC3339 完整时间或 now 开头的相对表达式;传 2026-10-24 这种纯日期会报错:value must be a valid rfc3339 date or start with 'now'(实测)。

配套操作:dashboards(列表)、updateDashboard、removeDashboard(实测 {"id": 1} 删除成功)、updateDashboardRange、removeDashboardRange、updateDashboardEntry、removeDashboardEntry。

九、用户与设备域(users / devices)

9.1 用户管理(需 admin)

# 建普通用户
'{"query":"mutation { createUser(name: \"viewer\", pass: \"q1w2e3r4\", admin: false) { id name admin } }"}'
# {"data": {"createUser": {"id": 2, "name": "viewer", "admin": false}}}

# 用户列表
'{"query":"{ users { id name admin } }"}'
# {"data": {"users": [{"id": 1, "name": "admin", "admin": true}, {"id": 2, "name": "viewer", "admin": false}]}}

# 删除用户
'{"query":"mutation { removeUser(id: 2) { id name } }"}'
# {"data": {"removeUser": {"id": 2, "name": "viewer"}}}

改密码/改名用 updateUser(id, name, pass?, admin)。

9.2 设备管理(令牌生命周期)

每次 login 都登记一个设备;devices 列出当前用户全部设备令牌:

{"data": {"devices": [{"id": 3, "name": "curl-script", "type": "NoExpiry", "createdAt": "2026-10-25T01:04:56Z", "activeAt": "2026-10-25T01:04:56Z"}, ...]}}
  • currentDevice:查当前令牌对应设备;
  • removeDevice(id):吊销指定设备令牌(吊销后该 token 立即失效,实测后续请求返回 you need to login)——这就是「登出/踢下线」;
  • removeCurrentDevice():吊销当前令牌;
  • createDevice(name, type):不登录也能为自己名下创建设备(需已认证)。

9.3 用户设置(userSettings)

userSettings 查询返回 {theme, dateLocale, firstDayOfTheWeek, dateTimeInputStyle};实测默认 GruvboxDark / American / Monday / Fancy。修改:

'{"query":"mutation { setUserSettings(settings: {theme: MaterialDark, dateLocale: British, firstDayOfTheWeek: Monday, dateTimeInputStyle: Fancy}) { theme } }"}'
# {"data": {"setUserSettings": {"theme": "MaterialDark"}}}

枚举取值:Theme = GruvboxDark/GruvboxLight/MaterialDark/MaterialLight;DateLocale = American/American24h/German/Australian/British;WeekDay = Monday..Sunday;DateTimeInputStyle = Fancy/Native。

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

  • 分页仅 timeSpans 使用,游标对象 InputCursor{offset, startId, pageSize},响应回传 {hasMore, offset, startId, pageSize},把回传值原样带入下一请求即可翻页;默认 pageSize=100。
  • 过滤:timeSpans 按 fromInclusive/toInclusive 时间区间;stats/stats2 用 requireTags/includeTags/excludeTags 做标签过滤。
  • 排序:timeSpans 按开始时间倒序返回(实测 id 大者/new 先出现),无自定义排序参数。

十一、常见问题

Q1:请求返回 {"errors":[{"message":"you need to login",...}]}?

A:没带令牌或令牌已吊销。确认请求头有 Cookie: traggo=<token>(不是 Authorization Bearer);设备被 removeDevice 后需重新 login。

Q2:登录报 username/password combination does not exist?

A:用户名/密码错误(实测错误响应原文如此)。确认默认账号是否已被改动。

Q3:报 Cannot query field "xxx" / Unknown argument "yyy"?

A:GraphQL 校验错误(GRAPHQL_VALIDATION_FAILED),字段名/参数名写错了。如 login 的密码参数是 pass 不是 password。完整 schema 可用内省查询获取:

curl -s -X POST http://localhost:3030/graphql -H 'Content-Type: application/json' \
  -d '{"query":"{ __schema { queryType { fields { name } } mutationType { fields { name } } } }"}'

Q4:stats 返回空 entries 但明明有数据?

A:时区口径问题(见 7.2 排障实录)。统一改用 UTC Z 形式的区间重试。

Q5:普通用户调用 users 被拒?

A:users/createUser/removeUser/updateUser 要求 ADMIN 角色,普通用户令牌调用会被拒。

十二、参考链接