Spoolman v0.27.0 API 使用详解

详解 Spoolman v0.27.0 REST API:厂商/耗材/丝盘三级模型端点、设置项特殊 JSON 编码写法、备份导出搜索、WebSocket 事件流与分页过滤排序约定,全部 curl/Python 实测回显。

Spoolman自托管库存管理3D打印2026/10/25

Spoolman v0.27.0 API 使用详解

目标:全面介绍 Spoolman v0.27.0 REST API 的认证方式、核心端点、请求示例与实测回显,覆盖厂商/耗材/丝盘/设置/备份/导出/搜索及 WebSocket 事件流。已在真实服务器环境实测通过(Spoolman v0.27.0,SQLite 模式,systemd 托管于 7912 端口)。

一、环境信息

  • 目标软件:Spoolman v0.27.0(commit bed3f14)
  • API 协议:HTTP/1.1 + WebSocket(HTTP Upgrade)
  • API 规范:OpenAPI 3.1.0,文档自动生成于 /api/v1/openapi.json
  • 认证方式:无内置认证,仅支持基于网络位置的访问控制;面向内网或反向代理使用
  • 请求工具:curl / Python urllib.request / 浏览器开发者工具
  • 测试环境:Standalone,systemd 托管,监听 http://localhost:7912

二、前置条件

  1. Spoolman 服务已运行:

    systemctl is-active Spoolman
    
  2. 确认 API 可访问:

    curl -s http://localhost:7912/api/v1/info | python3 -m json.tool
    
  3. 基础概念:Spoolman 的数据模型是 Vendor → Filament → Spool 三级。API 围绕这三个实体展开,另有 Setting、Backup、Export、Search 等管理端点。

三、API 总览

域端点方法说明
信息/api/v1/infoGET版本、构建信息、数据目录
健康/api/v1/healthGET服务健康状态
厂商/api/v1/vendorGET / POST列表 / 创建
厂商/api/v1/vendor/{id}GET / PATCH / DELETE详情 / 修改 / 删除
耗材/api/v1/filamentGET / POST列表 / 创建
耗材/api/v1/filament/{id}GET / PATCH / DELETE详情 / 修改 / 删除
耗材标签/api/v1/filament/{id}/tagPOST添加标签
耗材标签/api/v1/filament/{id}/tag/{uid}DELETE移除标签
丝盘/api/v1/spoolGET / POST列表 / 创建
丝盘/api/v1/spool/{id}GET / PATCH / DELETE详情 / 修改 / 删除
丝盘分组/api/v1/spool/groupGET按指定字段分组统计
丝盘批量/api/v1/spool/field/{field}PATCH批量修改指定字段
丝盘消耗/api/v1/spool/{id}/usePUT按长度扣减消耗
丝盘称重/api/v1/spool/{id}/measurePUT按整卷称重校准
丝盘标签/api/v1/spool/{id}/tagPOST / DELETE添加 / 移除 NFC 标签
设置/api/v1/setting/GET读取全部设置项
设置/api/v1/setting/{key}GET / POST读取 / 修改单项设置
自定义字段/api/v1/field/{entity_type}GET获取某实体的自定义字段
字段取值/api/v1/field/{entity_type}/{key}/valuesGET获取自定义字段已有值
外部目录/api/v1/external/filamentGET外部目录耗材列表
外部搜索/api/v1/external/filament/searchGET外部目录搜索
导出/api/v1/export/{spools|filaments|vendors}GETCSV / JSON 导出
搜索/api/v1/searchGET全文搜索(丝盘 + 耗材 + 厂商)
备份/api/v1/backupPOST触发 SQLite 备份
标签扫描/api/v1/tag/scanPOSTNFC 标签扫描登记
标签读取器/api/v1/tag/readerGET读取器状态
WebSocket/api/v1/spoolWS监听丝盘变更事件

四、认证与首次调用

Spoolman 没有 API Token 或 Basic Auth。所有端点公开可访问,依赖网络层安全(内网 / VPN / 反向代理)。首次调用无需额外 headers:

curl -s http://localhost:7912/api/v1/info

实测回显:

{"version":"0.27.0","debug_mode":false,"automatic_backups":true,"data_dir":"/root/.local/share/spoolman","logs_dir":"/root/.local/share/spoolman","backups_dir":"/root/.local/share/spoolman/backups","db_type":"sqlite","external_db_name":"SpoolmanDB","git_commit":"bed3f14","build_date":null}

健康检查:

curl -s http://localhost:7912/api/v1/health

实测回显:

{"status":"healthy"}

⚠️ 安全提醒:由于无认证,切勿将 Spoolman 端口直接暴露公网。应置于带认证的反向代理(如 Nginx + Basic Auth、Authelia 等)之后,或仅在内网可访问。

五、核心 API 域详解

5.1 Vendor(厂商)

创建厂商(POST /api/v1/vendor):

curl -s -X POST http://localhost:7912/api/v1/vendor \
  -H 'Content-Type: application/json' \
  -d '{"name":"eSUN"}'

实测回显:

{"id":1,"registered":"2026-10-25T00:53:47Z","name":"eSUN","extra":{}}

查询厂商详情(GET /api/v1/vendor/1):

curl -s http://localhost:7912/api/v1/vendor/1

实测回显:

{"id":1,"registered":"2026-10-25T00:53:47Z","name":"eSUN","extra":{}}

5.2 Filament(耗材型号)

创建耗材(POST /api/v1/filament):

curl -s -X POST http://localhost:7912/api/v1/filament \
  -H 'Content-Type: application/json' \
  -d '{
    "name":"PLA+ 白色",
    "vendor_id":1,
    "material":"PLA",
    "density":1.24,
    "diameter":1.75,
    "weight":1000,
    "spool_weight":140,
    "price":89.0
  }'

实测回显(关键字段节选):

{"id":1,"registered":"2026-10-25T00:53:47Z","name":"PLA+ 白色","vendor":{"id":1,"name":"eSUN","extra":{}},"material":"PLA","density":1.24,"diameter":1.75,"weight":1000,"spool_weight":140,"price":89.0,"extra":{}}

5.3 Spool(实物丝盘)

创建丝盘(POST /api/v1/spool):

curl -s -X POST http://localhost:7912/api/v1/spool \
  -H 'Content-Type: application/json' \
  -d '{"filament_id":1,"initial_weight":1000,"spool_weight":140,"price":89.0}'

实测回显(关键字段节选):

{"id":1,"registered":"2026-10-25T00:53:55Z","remaining_weight":1000.0,"used_weight":0.0,"remaining_length":335283.6194167644,"used_length":0.0,"archived":false,"filament":{"id":1,"name":"PLA+ 白色"}}

💡 说明:创建时只需提供 filament_id、initial_weight、spool_weight 与可选 price;remaining_weight、remaining_length 由后端根据密度/直径自动计算。

扣减消耗(PUT /api/v1/spool/{id}/use):

curl -s -X PUT http://localhost:7912/api/v1/spool/1/use \
  -H 'Content-Type: application/json' \
  -d '{"use_length":5.5}'

实测回显(关键字段节选):

{"id":1,"used_length":5.5,"used_weight":0.016404022390259954,"remaining_weight":999.9835959776098,"remaining_length":335278.1152374521}

💡 说明:扣减后 used_length 累加,used_weight 按密度/直径自动换算,first_used/last_used 自动更新。

称重校准(PUT /api/v1/spool/{id}/measure):

curl -s -X PUT http://localhost:7912/api/v1/spool/1/measure \
  -H 'Content-Type: application/json' \
  -d '{"weight":950}'

实测回显(关键字段节选):

{"id":1,"remaining_weight":810.0000000000001,"used_weight":189.9999999999999,"remaining_length":271579.73172757926}

💡 说明:weight 为整卷含空轴的称重值,后端自动扣除 spool_weight(皮重 140g)反推 remaining_weight。

修改丝盘属性(PATCH /api/v1/spool/{id}):

curl -s -X PATCH http://localhost:7912/api/v1/spool/1 \
  -H 'Content-Type: application/json' \
  -d '{"location":"一号干燥箱","comment":"上机打印中"}'

实测回显:200 OK,返回全量丝盘对象(节选):

{"id":1,"location":"一号干燥箱","comment":"上机打印中"}

错误示例:访问不存在的 ID:

curl -s http://localhost:7912/api/v1/spool/999

实测回显(HTTP 404):

{"message":"No spool with ID 999 found."}

缺少必填字段:

curl -s -X POST http://localhost:7912/api/v1/spool \
  -H 'Content-Type: application/json' \
  -d '{}'

实测回显(HTTP 422):

{"detail":[{"type":"missing","loc":["body","filament_id"],"msg":"Field required","input":{}}]}

5.4 设置(Setting)

读取全部设置:

curl -s http://localhost:7912/api/v1/setting/

实测回显(节选):

{"currency":{"value":"\"EUR\"","is_set":false,"type":"string"},"round_prices":{"value":null,"is_set":false,"type":"boolean"},"locations":{"value":[],"is_set":true,"type":"array"}}

修改单项设置(POST /api/v1/setting/{key}):

OpenAPI schema 显示该端点 body 类型为 string,但实测发现:字符串本身必须是一个 JSON 字面量(即对 Spoolman 而言,其存储的是「以字符串形式存在的 JSON 编码值」)。直接 curl -d '"CNY"' 会因 shell 转义问题导致空请求体返回 400。

正确做法(Python 实测):

import json, urllib.request

# body 需要是 JSON 字符串,其内容又是一个 JSON 字符串
body = json.dumps(json.dumps("CNY")).encode()
req = urllib.request.Request(
    "http://localhost:7912/api/v1/setting/currency",
    data=body,
    headers={"Content-Type": "application/json"},
    method="POST")
with urllib.request.urlopen(req) as r:
    print(r.read().decode())

实测回显:

{"value":"\"CNY\"","is_set":true,"type":"string"}

重置为未设置状态(设 is_set 回 false):

body = json.dumps(json.dumps("null")).encode()

实测回显:

{"value":"\"EUR\"","is_set":false,"type":"string"}

💡 说明:此设计的目的是让 setting 值能承载任意 JSON 类型(字符串、布尔、数组、对象)。修改后无需重启服务,UI 立即生效。

5.5 备份(Backup)

触发 SQLite 手动备份:

curl -s -X POST http://localhost:7912/api/v1/backup

实测回显(HTTP 200):

{"path":"/root/.local/share/spoolman/backups/spoolman.db","created":true}

备份文件落在数据目录 backups/ 下,文件名带时间戳(如 spoolman_2026-10-25_011800.db)。

5.6 导出(Export)

导出丝盘 CSV(必须显式指定 fmt 参数):

curl -s "http://localhost:7912/api/v1/export/spools?fmt=csv" | head -3

实测回显(表头 + 第一行):

archived,comment,filament.article_number,filament.color_hex,filament.comment,filament.density,filament.diameter,filament.external_id,filament.id,filament.material,filament.multi_color_direction,filament.multi_color_hexes,filament.name,filament.price,filament.registered,filament.settings_bed_temp,filament.settings_extruder_temp,filament.spool_weight,filament.tags,filament.vendor.comment,filament.vendor.empty_spool_weight,filament.vendor.external_id,filament.vendor.id,filament.vendor.name,filament.vendor.registered,filament.vendor_id,filament.weight,filament_id,first_used,id,initial_weight,last_used,location,lot_nr,price,registered,spool_weight,tags,used_weight
False,上机打印中,,,,1.24,1.75,,1,PLA,,,PLA+ 白色,89.0,2026-10-25 00:53:47,,,140.0,,,,,1,eSUN,2026-10-25 00:53:47,1,1000.0,1,2026-10-25 00:53:55,1,1000.0,2026-10-25 00:54:06,一号干燥箱,,89.0,2026-10-25 00:53:55,140.0,,189.9999999999999

错误示例:不传 fmt 参数:

curl -s "http://localhost:7912/api/v1/export/spools"

实测回显(HTTP 422):

{"detail":[{"type":"missing","loc":["query","fmt"],"msg":"Field required","input":null}]}

5.7 搜索(Search)

全文搜索(同时命中丝盘、耗材、厂商):

curl -s "http://localhost:7912/api/v1/search?q=PLA"

实测回显(节选):

{"spools":[],"filaments":[{"filament":{"id":1,"name":"PLA+ 白色","material":"PLA",...}}],"vendors":[]}

💡 说明:搜索按名称模糊匹配,结果按三类实体分组返回。

5.8 WebSocket 事件流

部分列表端点(如 /api/v1/spool)同时提供 WebSocket 升级,用于订阅实时变更事件:

import asyncio, json, websockets

async def main():
    async with websockets.connect("ws://localhost:7912/api/v1/spool") as ws:
        # 在另一个线程触发一次修改(如 PATCH /spool/1)
        # ...
        msg = await asyncio.wait_for(ws.recv(), timeout=5)
        print(msg)

asyncio.run(main())

实测回显(触发 PATCH 后收到的事件):

{"type":"updated","resource":"spool","date":"2026-10-25T01:21:46.957626Z","payload":{"id":1,"registered":"2026-10-25T00:53:55Z","first_used":"2026-10-25T00:53:55Z","last_used":"2026-10-25T00:54:06Z",...}}

六、分页、过滤、排序通用约定

丝盘/耗材/厂商列表端点支持以下通用查询参数(以 /api/v1/spool 为例,参数来自 OpenAPI 3.1.0):

参数说明示例
filament.name按耗材名模糊搜索(不区分大小写)filament.name=PLA
filament.material按材料模糊搜索filament.material=PLA
filament.vendor.name按厂商名模糊搜索filament.vendor.name=eSUN
filament_id / filament.id按精确 ID(支持逗号分隔多值)filament_id=1,2,3
vendor_id / filament.vendor.id按厂商 IDvendor_id=1
location按存放位置模糊搜索location=一号干燥箱
lot_nr按批次号模糊搜索lot_nr=LOT001
tag按 NFC 标签 UID 精确匹配tag=ABC123
allow_archived是否包含已归档丝盘(默认 false)allow_archived=true
first_used / last_used / registered时间范围过滤,格式 start,end(含两端)first_used=2026-10-01,2026-10-31
sort排序字段,格式 field:asc 或 field:desc,可多字段逗号分隔sort=remaining_weight:asc
limit每页最大条数limit=10
offset分页偏移量(需配合 limit)offset=0

实测示例:

curl -s "http://localhost:7912/api/v1/spool?filament.material=PLA&sort=remaining_weight:asc&limit=10&offset=0"

实测回显(数组,节选首条关键字段):

[{"id":1,"registered":"2026-10-25T00:53:55Z","first_used":"2026-10-25T00:53:55Z","last_used":"2026-10-25T00:54:06Z","filament":{"name":"PLA+ 白色","material":"PLA"},"remaining_weight":999.9835959776098,...}]

七、常见问题

Q1:POST 返回 422 Field required?

请求体缺少必填字段。对照 OpenAPI 文档(/api/v1/openapi.json)或本文示例补齐字段。如 POST /spool 必须有 filament_id。

Q2:POST /setting/{key} 一直报 400 / 422?

该端点 body 必须是「JSON 字符串,其内容又是一个 JSON 字面量」。curl 直接写容易因 shell 转义出错,建议用 Python 按 5.4 节示例构造,或先用 GET /api/v1/setting/{key} 观察当前值的 JSON 编码形态再构造。

Q3:导出返回 422?

/export/* 端点必须带 fmt 查询参数(csv 或 json),如 ?fmt=csv。

Q4:WebSocket 连不上?

确认地址是 ws:// 而非 http://,路径为 /api/v1/spool(或其他支持 WS 的列表端点)。连接建立后若无事件,会处于空闲等待状态;触发一次数据变更(如 PATCH)即可收到推送。

Q5:返回 404 No spool with ID ... found.?

资源 ID 不存在。列表接口先查全量确认 ID 再操作。

八、参考链接