Atomic Server 0.38.0 API 使用详解

Atomic Server 0.38.0 HTTP API 全链路实测:JSON-AD/Turtle/JSON-LD/N-Triples 内容协商、Collections 集合查询、Search 全文检索、POST /commit 签名写入与错误码、WebSocket 订阅推送、@tomic/lib 0.39 客户端建库,含搜索索引丢失与重建等实测行为边界与 8 项常见问题。

Atomic ServerAtomic Server2026/11/01

Atomic Server 0.38.0 API 使用详解

目标:逐域讲清 Atomic Server 0.38.0 对外暴露的 HTTP API——资源读取与序列化协商、Collections 查询、Search 全文检索、Commit 签名写入、WebSocket 实时订阅,以及官方 JS 客户端 @tomic/lib 的用法。本文所有请求与响应均为 2026-11-01 在 http://localhost:8139 实测实例上逐条执行、逐行照抄,无转述官方示例。

一、环境信息

项目值
服务版本Atomic Server 0.38.0(--version 实测)
服务地址http://localhost:8139(server_url,subject 前缀,见配置篇 5.1)
库内测试数据自定义本体 classes/Mood、properties/mood 与实例 mood/1(stormy)、mood/2(windy)
HTTP 客户端curl 8.18.0(服务端访问日志 http.user_agent=curl/8.18.0 实测)
JS 客户端Node v24.22.1 + @tomic/lib@0.39.0
签名凭据首启 config.toml 中的默认根 Agent(agent/private_key)

💡 选题说明:Atomic Server 生态的命令行客户端 atomic-cli 未随 0.38.0 发布物分发(GitHub Releases 无二进制资产、发布仓库构建物入口仅服务端二进制),因此本专栏第四篇按「有 REST/gRPC 管理/数据 API → D5」决策树定为 API 使用详解;服务端自带的 export/import/show-config 等子命令已在部署篇运维章节实测覆盖。

二、前置条件

  1. 已按 《Atomic Server Docker Compose 部署指南》 启动服务并确认健康:
curl -s -o /dev/null -w 'GET / -> HTTP:%{http_code} TYPE:%{content_type}\n' http://localhost:8139/
GET / -> HTTP:200 TYPE:text/html
  1. 读取本文示例需库内已有 Mood 本体(本文 5.6 节演示了从零创建它们的全过程);
  2. 需要写入时,从服务器配置目录取得根 Agent 凭据(config.toml 的 agent 与 private_key 两行,见配置篇 5.7)。

三、API 总览

0.38.0 的 API 无版本前缀、无独立鉴权握手接口,全部围绕「资源即 URL」的 REST 模型:

端点方法用途
/<subject路径>GET读取任意资源(Class/Property/Instance/Drive/Endpoint…)
/collections?property=&value=GET属性值查询(过滤集合)
/search?q=GET全文检索
/commitPOST提交签名 Commit(唯一写入通道)
/wsWebSocketSUBSCRIBE 订阅资源变更,服务端推 COMMIT
/setupGET读取一次性管理邀请 Invite
  • 认证方式:读操作匿名开放(实测匿名 GET 返回 200);写操作不走 Token/Session,而是要求请求体本身是一条由 Agent 私钥签名、且通过公钥校验的 Commit 资源(见 5.4)。
  • 序列化:由 Accept 头协商,默认 JSON-AD,另支持 JSON-LD、Turtle、N-Triples;application/rdf+xml 在 0.38 未实现(见 5.1)。
  • 限流:实测响应头无任何限流/配额字段,官方文档亦未声明限流约定;生产建议在反向代理层自行限速。
  • 错误形态:所有错误返回统一的 Error 资源(isA: classes/Error + description 文本)并伴随 HTTP 500/404,见 5.4 与 FAQ。

四、认证与首次调用

4.1 匿名读取(读开放)

curl -s -o /dev/null -w 'GET /mood/1 -> HTTP:%{http_code}\n' http://localhost:8139/mood/1
curl -s -o /dev/null -w 'GET agent -> HTTP:%{http_code}\n' 'http://localhost:8139/agents/uuIgCqY79Wq1ZLU7y1tHIisGWmdSkHC2oAsGaioFANg='
GET /mood/1 -> HTTP:200
GET agent -> HTTP:200

资源与 Agent 均可匿名 GET。Agent 资源里发布的是公钥,私钥只存在于服务端 config.toml(或复制到客户端),这就是「读开放、写签名」模型的体现。

4.2 管理入口 /setup 邀请

curl -s -H 'Accept: application/ad+json' http://localhost:8139/setup

实测响应(节选,实测首尾):

{
  "@id": "http://localhost:8139/setup",
  "https://atomicdata.dev/properties/description": "Use this Invite to create an Agent, or use an existing one. Accepting will grant your Agent the necessary rights to edit the data in your Atomic Server. This can only be used once. If you, for whatever reason, need a new `/setup` invite, you can pass the `--initialize` flag to `atomic-server`.",
  "https://atomicdata.dev/properties/invite/target": "http://localhost:8139",
  "https://atomicdata.dev/properties/invite/usagesLeft": 1,
  "https://atomicdata.dev/properties/invite/write": true,
  "https://atomicdata.dev/properties/isA": [
    "https://atomicdata.dev/classes/Invite"
  ],
  "https://atomicdata.dev/properties/name": "Setup"
}

usagesLeft=1 一次性、write=true 授予写权。浏览器访问 http://localhost:8139/setup 可让前端生成新 Agent 并接受邀请;本次实测直接使用首启默认 Agent 凭据(config.toml)作为写入身份。用 @tomic/lib 程序化接受邀请的两次尝试均告失败(实测回显):

ERR: Agent requires a private key
NEW agent subject: http://localhost:8139/agents/8CMhKpM+dCN5cpSSMMcQQaToyMa+6Kw+3SaFPjYTOao=
ERR: acceptInvite is not a function

⚠️ 排障实录:@tomic/lib@0.39.0 对「无凭据 Agent 接受邀请」路径要求携带私钥,且未导出 acceptInvite API——0.38 时代程序化注册新管理 Agent 应走浏览器 /setup 页面,或直接复用 config.toml 默认凭据。

4.3 Drive 的真实 subject

直觉上根目录是 /drives/root,实测 404:

curl -s -H 'Accept: application/ad+json' http://localhost:8139/drives/root -w '\nHTTP:%{http_code}\n'
{
  "@id": "unknown_subject",
  "https://atomicdata.dev/properties/description": "Resource not found. Failed to retrieve locally: 'http://localhost:8139/drives/root'",
  "https://atomicdata.dev/properties/isA": [
    "https://atomicdata.dev/classes/Error"
  ]
}
HTTP:404

改用 Collections 查询 isA=Drive 才看到真相(响应节选,实测首尾):

curl -s -H 'Accept: application/ad+json' 'http://localhost:8139/collections?property=https%3A%2F%2Fatomicdata.dev%2Fproperties%2FisA&value=https%3A%2F%2Fatomicdata.dev%2Fclasses%2FDrive'
{
  "@id": "http://localhost:8139/collections?property=...",
  "https://atomicdata.dev/properties/collection/members": [
    {
      "@id": "http://localhost:8139",
      "https://atomicdata.dev/properties/isA": ["https://atomicdata.dev/classes/Drive"],
      "https://atomicdata.dev/properties/name": "localhost",
      "https://atomicdata.dev/properties/read": [
        "http://localhost:8139/agents/uuIgCqY79Wq1ZLU7y1tHIisGWmdSkHC2oAsGaioFANg=",
        "https://atomicdata.dev/agents/publicAgent"
      ],
      "https://atomicdata.dev/properties/write": [
        "http://localhost:8139/agents/uuIgCqY79Wq1ZLU7y1tHIisGWmdSkHC2oAsGaioFANg="
      ],
      "https://atomicdata.dev/properties/subresources": [
        "http://localhost:8139/setup",
        "http://localhost:8139/import",
        "http://localhost:8139/collections"
      ]
    }
  ],
  "https://atomicdata.dev/properties/collection/totalMembers": 1
}

根 Drive 的 subject 就是 server_url 本身(http://localhost:8139),其 read 含 publicAgent(匿名可读的来源)、write 仅根 Agent——层级权限(Hierarchy)在 API 层的投影即这两个属性。

五、各 API 域详解

5.1 资源读取与序列化协商

同一资源 /mood/1,Accept 头决定序列化格式。五种请求全部实测:

JSON-AD(默认业务格式):

curl -s -H 'Accept: application/ad+json' http://localhost:8139/mood/1
{
  "@id": "http://localhost:8139/mood/1",
  "http://localhost:8139/properties/mood": "stormy",
  "https://atomicdata.dev/properties/isA": [
    "http://localhost:8139/classes/Mood"
  ],
  "https://atomicdata.dev/properties/lastCommit": "http://localhost:8139/commits/vCeoXaynoicEx9zqhl8x6WTs2bh2oNsMw2U2R1pnT3qsupPLAv2+xfmpNBQHEsw4oQTYOWQlCURLyqInQ++bDg==",
  "https://atomicdata.dev/properties/parent": "http://localhost:8139"
}

Turtle:

curl -s -H 'Accept: text/turtle' http://localhost:8139/mood/1
<http://localhost:8139/mood/1> <https://atomicdata.dev/properties/isA> "http://localhost:8139/classes/Mood"^^<https://atomicdata.dev/datatypes/resourceArray> .
<http://localhost:8139/mood/1> <https://atomicdata.dev/properties/lastCommit> <http://localhost:8139/commits/vCeoXaynoicEx9zqhl8x6WTs2bh2oNsMw2U2R1pnT3qsupPLAv2+xfmpNBQHEsw4oQTYOWQlCURLyqInQ++bDg==> .
<http://localhost:8139/mood/1> <https://atomicdata.dev/properties/parent> <http://localhost:8139> .
<http://localhost:8139/mood/1> <http://localhost:8139/properties/mood> "stormy" .

JSON-LD:

curl -s -H 'Accept: application/ld+json' http://localhost:8139/mood/1
{
  "@context": {
    "is-a": {
      "@container": "@list",
      "@id": "https://atomicdata.dev/properties/isA"
    },
    "last-commit": {
      "@id": "https://atomicdata.dev/properties/lastCommit",
      "@type": "@id"
    },
    "mood": "http://localhost:8139/properties/mood",
    "parent": {
      "@id": "https://atomicdata.dev/properties/parent",
      "@type": "@id"
    }
  },
  "@id": "http://localhost:8139/mood/1",
  "is-a": [
    "http://localhost:8139/classes/Mood"
  ],
  "last-commit": "http://localhost:8139/commits/vCeoXaynoicEx9zqhl8x6WTs2bh2oNsMw2U2R1pnT3qsupPLAv2+xfmpNBQHEsw4oQTYOWQlCURLyqInQ++bDg==",
  "mood": "stormy",
  "parent": "http://localhost:8139"
}

N-Triples(对 mood/2 实测):

curl -s -H 'Accept: application/n-triples' http://localhost:8139/mood/2
<http://localhost:8139/mood/2> <https://atomicdata.dev/properties/isA> "http://localhost:8139/classes/Mood"^^<https://atomicdata.dev/datatypes/resourceArray> .
<http://localhost:8139/mood/2> <https://atomicdata.dev/properties/lastCommit> <http://localhost:8139/commits/51YQSt5S5uqdaAnDzH3XS5ZQq/vsnxhfS+PGINzZHf0TtTPSTBXON6WWsXlTRG3NoM0OC4hjEEr5dtW4EyqACw==> .
<http://localhost:8139/mood/2> <https://atomicdata.dev/properties/parent> <http://localhost:8139> .
<http://localhost:8139/mood/2> <http://localhost:8139/properties/mood> "windy" .

RDF/XML(0.38 未实现,静默降级 HTML):

curl -s -o /dev/null -w 'HTTP:%{http_code} TYPE:%{content_type}\n' -H 'Accept: application/rdf+xml' http://localhost:8139/mood/1
HTTP:200 TYPE:text/html

服务端日志同步给出降级原因(原文照抄,含官方拼写错误 defaut):

atomic_server::content_types: Unknown Accept header, defaut to HTML: application/rdf+xml

不带 Accept:同样回 HTTP:200 TYPE:text/html(浏览器页面)。

⚠️ 注意:README 宣称的 RDF/XML 序列化在 0.38 实测不可用且不报错(200 + HTML),对接 RDF 工具链时务必校验响应 Content-Type,勿以状态码判断。

5.2 Collections 集合查询

Collections 是「按 属性=值 过滤」的查询端点,参数值需 URL 编码。实测查询「所有 isA=Mood 的资源」:

curl -s -H 'Accept: application/ad+json' \
  'http://localhost:8139/collections?property=https%3A%2F%2Fatomicdata.dev%2Fproperties%2FisA&value=http%3A%2F%2Flocalhost%3A8139%2Fclasses%2FMood'

实测响应全文:

{
  "@id": "http://localhost:8139/collections?property=https%3A%2F%2Fatomicdata.dev%2Fproperties%2FisA&value=http%3A%2F%2Flocalhost%3A8139%2Fclasses%2FMood",
  "https://atomicdata.dev/properties/collection/currentPage": 0,
  "https://atomicdata.dev/properties/collection/includeNested": true,
  "https://atomicdata.dev/properties/collection/members": [
    {
      "@id": "http://localhost:8139/mood/1",
      "http://localhost:8139/properties/mood": "stormy",
      "https://atomicdata.dev/properties/isA": [
        "http://localhost:8139/classes/Mood"
      ],
      "https://atomicdata.dev/properties/lastCommit": "http://localhost:8139/commits/vCeoXaynoicEx9zqhl8x6WTs2bh2oNsMw2U2R1pnT3qsupPLAv2+xfmpNBQHEsw4oQTYOWQlCURLyqInQ++bDg==",
      "https://atomicdata.dev/properties/parent": "http://localhost:8139"
    },
    {
      "@id": "http://localhost:8139/mood/2",
      "http://localhost:8139/properties/mood": "windy",
      "https://atomicdata.dev/properties/isA": [
        "http://localhost:8139/classes/Mood"
      ],
      "https://atomicdata.dev/properties/lastCommit": "http://localhost:8139/commits/51YQSt5S5uqdaAnDzH3XS5ZQq/vsnxhfS+PGINzZHf0TtTPSTBXON6WWsXlTRG3NoM0OC4hjEEr5dtW4EyqACw==",
      "https://atomicdata.dev/properties/parent": "http://localhost:8139"
    }
  ],
  "https://atomicdata.dev/properties/collection/pageSize": 30,
  "https://atomicdata.dev/properties/collection/property": "https://atomicdata.dev/properties/isA",
  "https://atomicdata.dev/properties/collection/sortBy": "https://atomicdata.dev/properties/collection/value",
  "https://atomicdata.dev/properties/collection/totalMembers": 2,
  "https://atomicdata.dev/properties/collection/totalPages": 1,
  "https://atomicdata.dev/properties/collection/value": "http://localhost:8139/classes/Mood",
  "https://atomicdata.dev/properties/isA": [
    "https://atomicdata.dev/classes/Collection"
  ],
  "https://atomicdata.dev/properties/lastCommit": "http://localhost:8139/commits/MXgd/iTjRleBKqd6Qrv/GgEyWMNTBA5wRB+MD2oIL2XAQMXHn38JNEA43GPv7wkfAxtSPYzR10jUw5XrzTnvBw==",
  "https://atomicdata.dev/properties/name": "collections",
  "https://atomicdata.dev/properties/parent": "http://localhost:8139"
}

响应本身就是一个 Collection 资源:members 内嵌完整资源(includeNested=true),并携带 pageSize/currentPage/totalMembers/totalPages/sortBy 元数据。

非法参数直接 500(实测两种):

curl -s -H 'Accept: application/ad+json' \
  'http://localhost:8139/collections?property=https%3A%2F%2Fatomicdata.dev%2Fproperties%2FisA&value=http%3A%2F%2Flocalhost%3A8139%2Fclasses%2FMood&class=https%3A%2F%2Fatomicdata.dev%2Fclasses%2FMood' -w '\nHTTP:%{http_code}\n'
{
  "@id": "unknown_subject",
  "https://atomicdata.dev/properties/description": "Invalid query param: class",
  "https://atomicdata.dev/properties/isA": [
    "https://atomicdata.dev/classes/Error"
  ]
}
HTTP:500
curl -s -H 'Accept: application/ad+json' \
  'http://localhost:8139/collections?property=https%3A%2F%2Fatomicdata.dev%2Fproperties%2FisA&value=http%3A%2F%2Flocalhost%3A8139%2Fclasses%2FMood&pageSize=5' -w '\nHTTP:%{http_code}\n'
{
  "@id": "unknown_subject",
  "https://atomicdata.dev/properties/description": "Invalid query param: pageSize",
  "https://atomicdata.dev/properties/isA": [
    "https://atomicdata.dev/classes/Error"
  ]
}
HTTP:500

⚠️ 0.38 实测边界:class、pageSize、page、current-page 均报 Invalid query param(HTTP 500 而非 400);响应里的 pageSize:30 是服务端固定默认值。翻页参数在 0.38 未开放,大结果集需靠 property/value 组合收窄。

另一个实测坑:核心本体类不在 store。查询「所有 Class」返回空:

curl -s -H 'Accept: application/ad+json' 'http://localhost:8139/collections?property=https%3A%2F%2Fatomicdata.dev%2Fproperties%2FisA&value=https%3A%2F%2Fatomicdata.dev%2Fclasses%2FClass'

响应节选(实测首尾):

{
  "@id": "http://localhost:8139/collections?property=...",
  "https://atomicdata.dev/properties/collection/members": [],
  "https://atomicdata.dev/properties/collection/totalMembers": 0,
  "https://atomicdata.dev/properties/collection/totalPages": 0,
  "https://atomicdata.dev/properties/collection/value": "https://atomicdata.dev/classes/Class",
  "https://atomicdata.dev/properties/isA": [
    "https://atomicdata.dev/classes/Collection"
  ],
  "https://atomicdata.dev/properties/name": "collections"
}

totalMembers=0——内置 Class/Property 本体(https://atomicdata.dev/... 前缀)只是被引用的外部定义,不作为本地资源入库,因此 Collections 查不到它们;本地自建的本体(http://localhost:8139/classes/Mood)才能被枚举(上一条查询 2 members 即证)。

5.3 Search 全文检索

curl -s -H 'Accept: application/ad+json' 'http://localhost:8139/search?q=Mood'

实测命中 4 条(响应节选,实测首尾;results 数组含 4 个完整资源):

{
  "@id": "http://localhost:8139/search?q=Mood",
  "https://atomicdata.dev/properties/description": "Full text-search endpoint. You can use the keyword `AND` and `OR`, or use `\"` for advanced searches. ",
  "https://atomicdata.dev/properties/endpoint/parameters": [
    "https://atomicdata.dev/properties/search/query",
    "https://atomicdata.dev/properties/search/limit",
    "https://atomicdata.dev/properties/search/property"
  ],
  "https://atomicdata.dev/properties/endpoint/results": [
    {
      "@id": "http://localhost:8139/classes/Mood",
      "https://atomicdata.dev/properties/description": "A record of someone mood",
      "https://atomicdata.dev/properties/isA": [
        "https://atomicdata.dev/classes/Class"
      ],
      "https://atomicdata.dev/properties/name": "Mood",
      "https://atomicdata.dev/properties/parent": "http://localhost:8139",
      "https://atomicdata.dev/properties/recommends": [
        "http://localhost:8139/properties/mood"
      ],
      "https://atomicdata.dev/properties/shortname": "mood"
    },
    {
      "@id": "http://localhost:8139/properties/mood",
      "https://atomicdata.dev/properties/datatype": "https://atomicdata.dev/datatypes/string",
      "https://atomicdata.dev/properties/description": "How the subject feels",
      "https://atomicdata.dev/properties/isA": [
        "https://atomicdata.dev/classes/Property"
      ],
      "https://atomicdata.dev/properties/name": "Mood",
      "https://atomicdata.dev/properties/parent": "http://localhost:8139",
      "https://atomicdata.dev/properties/shortname": "mood"
    },
    {
      "@id": "http://localhost:8139/mood/1",
      "http://localhost:8139/properties/mood": "stormy",
      "https://atomicdata.dev/properties/isA": ["http://localhost:8139/classes/Mood"],
      "https://atomicdata.dev/properties/parent": "http://localhost:8139"
    },
    {
      "@id": "http://localhost:8139/mood/2",
      "http://localhost:8139/properties/mood": "windy",
      "https://atomicdata.dev/properties/isA": ["http://localhost:8139/classes/Mood"],
      "https://atomicdata.dev/properties/parent": "http://localhost:8139"
    }
  ],
  "https://atomicdata.dev/properties/isA": [
    "https://atomicdata.dev/classes/Endpoint"
  ],
  "https://atomicdata.dev/properties/parent": "http://localhost:8139/endpoints",
  "https://atomicdata.dev/properties/shortname": "search"
}

命中构成:类定义 1 + 属性定义 1 + 实例 2(name/description 等被索引字段含 "Mood")。宽口径验证:q=Commit 命中 18 条(实测计数),前 5 个 subject 为:

https://atomicdata.dev/classes/Commit
https://atomicdata.dev/properties/lastCommit
https://atomicdata.dev/commits
https://atomicdata.dev/properties/previousCommit
http://localhost:8139/commits

⚠️ 三个重要实测边界:

  1. 自定义属性的值不进全文索引:q=stormy 实测 results: [](0 命中)——尽管 mood/1 的 http://localhost:8139/properties/mood 值就是 stormy。索引覆盖的是 name/description 等标准字段;按自定义属性值过滤应改用 Collections(5.2)。
  2. limit 有效、property 被忽略:Endpoint 资源声明支持 search/limit 与 search/property。实测 q=Mood&limit=2 返回 2 条(截断:classes/Mood、mood/1),q=Mood&limit=10 与 limit=0 均返回全部 4 条;而 q=Mood&property=https://atomicdata.dev/properties/name 返回集与不带 property 时完全一致(4 条)——0.38 的 property 参数不参与过滤,只当装饰。
  3. Search 结果依赖独立的索引目录,索引丢了就全查不到:本文曾出现「带 limit/property 一律 0 条」的现象,根因不是参数失效,而是服务重建后搜索索引被清空——索引不在数据目录,而在 search_index_path(0.38 默认 /root/.cache/atomic-data/search_index,见配置篇 5.5),服务实例整体重建即丢;且重启不会自动全量重建,只有新提交会被增量索引(实测:索引空时提交 mood/3 后 q=Mood 仅返回这 1 条新资源,存量 4 条不回来;直读 GET /mood/1 始终正常)。恢复办法:以 --rebuild-indexes(或环境变量 ATOMIC_REBUILD_INDEX=true)启动一次,启动日志出现 Removing existing search index... → Building search index... → Search index finished! 后即恢复全量(实测 q=Mood 回到 4 条、q=Commit 回到 18 条)。

5.4 Commit 写入(POST /commit)

写入的唯一通道是 POST /commit,请求体是一条签名 Commit。先看服务器对两类非法请求的实测应答。

空体提交:

curl -s -X POST -H 'Content-Type: application/json' -d '{}' \
  -w '\nHTTP:%{http_code}\n' http://localhost:8139/commit
{
  "@id": "unknown_subject",
  "https://atomicdata.dev/properties/description": "No subject field in Commit.",
  "https://atomicdata.dev/properties/isA": [
    "https://atomicdata.dev/classes/Error"
  ]
}
HTTP:500

重放已存在的 Commit(把库内已有提交原样再 POST):

curl -s -X POST -H 'Content-Type: application/json' --data @commit-body.json \
  -w '\nHTTP:%{http_code}\n' http://localhost:8139/commit
{
  "@id": "unknown_subject",
  "https://atomicdata.dev/properties/description": "Failed to add: 'http://localhost:8139/commits/vCeoXaynoicEx9zqhl8x6WTs2bh2oNsMw2U2R1pnT3qsupPLAv2+xfmpNBQHEsw4oQTYOWQlCURLyqInQ++bDg==', already exists, should not be overwritten.",
  "https://atomicdata.dev/properties/isA": [
    "https://atomicdata.dev/classes/Error"
  ]
}
HTTP:500

一条合法 Commit 的真实形态——新建 mood/2(windy)的提交体(实测落库内容原文照抄):

{"https://atomicdata.dev/properties/createdAt":1793530544536,"https://atomicdata.dev/properties/isA":["https://atomicdata.dev/classes/Commit"],"https://atomicdata.dev/properties/set":{"http://localhost:8139/properties/mood":"windy","https://atomicdata.dev/properties/isA":["http://localhost:8139/classes/Mood"],"https://atomicdata.dev/properties/parent":"http://localhost:8139"},"https://atomicdata.dev/properties/signature":"51YQSt5S5uqdaAnDzH3XS5ZQq/vsnxhfS+PGINzZHf0TtTPSTBXON6WWsXlTRG3NoM0OC4hjEEr5dtW4EyqACw==","https://atomicdata.dev/properties/signer":"http://localhost:8139/agents/uuIgCqY79Wq1ZLU7y1tHIisGWmdSkHC2oAsGaioFANg=","https://atomicdata.dev/properties/subject":"http://localhost:8139/mood/2"}

字段解读:subject 目标资源、set 要写入的属性集、signer 签名 Agent(其公钥可从 4.1 的 GET 取得)、signature 对排序后载荷的 Ed25519 签名、createdAt 毫秒时间戳。修改已有资源时还携带 previousCommit 形成版本链——mood/1 的当前提交体(实测落库原文):

{"https://atomicdata.dev/properties/createdAt":1793530506629,"https://atomicdata.dev/properties/isA":["https://atomicdata.dev/classes/Commit"],"https://atomicdata.dev/properties/previousCommit":"http://localhost:8139/commits/Xznf1o6Bk/JYmlCIeFMJav4MwjkaPYqOWrdfGAoOv1vGCYa/qjqYRloPJDFqENKCk7CJqcsbSqaqSKl0DsSQBw==","https://atomicdata.dev/properties/set":{"http://localhost:8139/properties/mood":"stormy"},"https://atomicdata.dev/properties/signature":"vCeoXaynoicEx9zqhl8x6WTs2bh2oNsMw2U2R1pnT3qsupPLAv2+xfmpNBQHEsw4oQTYOWQlCURLyqInQ++bDg==","https://atomicdata.dev/properties/signer":"http://localhost:8139/agents/uuIgCqY79Wq1ZLU7y1tHIisGWmdSkHC2oAsGaioFANg=","https://atomicdata.dev/properties/subject":"http://localhost:8139/mood/1"}

注意 signature 值同时就是资源 lastCommit 的 URL 尾段(5.1 响应可见)——Commit 的 subject 由签名派生,天然防重放(上一条 500 即证)。

手工构造签名体极易出错,实际写入请交给客户端库(5.6)。另一个实测的写入失败场景:给资源挂 isA=https://atomicdata.dev/classes/Task(核心本体类)时服务器 500,日志给出根因(原文照抄):

Error response: Failed getting class https://atomicdata.dev/classes/Task. Error when server tried fetching https://atomicdata.dev/classes/Task : https://atomicdata.dev/classes/Task: status code 404

服务器为校验类型会回源抓取外部本体定义,而上游 atomicdata.dev 演示实例已升级 0.41,旧 URL 失效。解法:0.38 建自定义本体时直接内嵌 Class/Property 定义(5.6 的 t3.js 即此路径),避免依赖已失效的外部类。

5.5 WebSocket 实时订阅

订阅通道 ws://<server>/ws,协议为文本帧 SUBSCRIBE <subject>,此后该资源的每次落库 Commit 都会推送给订阅者。实测脚本(Node v24 全局 WebSocket,无需依赖):

const ws = new WebSocket('ws://localhost:8139/ws');
ws.onopen = () => { console.log('[WS] open, sending SUBSCRIBE'); ws.send('SUBSCRIBE http://localhost:8139/mood/1'); };
ws.onmessage = (ev) => { console.log('[WS msg]', String(ev.data).slice(0, 300)); };

随后用 @tomic/lib 对 mood/1 保存一次变更,订阅端实测收到推送(输出原文照抄):

[WS] open, sending SUBSCRIBE
[WS msg 1] COMMIT {
  "@id": "http://localhost:8139/commits/Xznf1o6Bk/JYmlCIeFMJav4MwjkaPYqOWrdfGAoOv1vGCYa/qjqYRloPJDFqENKCk7CJqcsbSqaqSKl0DsSQBw==",
  "https://atomicdata.dev/properties/createdAt": 1793530435509,
  "https://atomicdata.dev/properties/isA": [
    "https://atomicdata.dev/classes/Commit"
  ],
  
[lib] saved mood=cloudy, waiting push...

推送帧以 COMMIT 前缀开头,载荷即完整 Commit 资源(节选,实测首尾)。这就是官方宣称「前端一行代码实时同步」的服务端机制;匿名即可订阅公开资源的变更。

5.6 @tomic/lib 客户端实测(建库全流程)

手写 curl 签名不现实,官方 JS 库 @tomic/lib 承担「构造 Commit→签名→POST→重放校验」全套。环境准备:

mkdir atomic-client && cd atomic-client
npm init -y >/dev/null
npm install @tomic/lib@0.39.0

完整脚本(实测通过;凭据取自 config.toml):

const { Store, Agent } = require('@tomic/lib');
const agent = new Agent('aKyy/8Ij6aOibh6LhozEweJELzmo6seGLjxtFBQaw5U=', 'http://localhost:8139/agents/uuIgCqY79Wq1ZLU7y1tHIisGWmdSkHC2oAsGaioFANg=');
const store = new Store({ serverUrl: 'http://localhost:8139', agent });
(async () => {
  const prop = await store.newResource({ subject: 'http://localhost:8139/properties/mood', isA: 'https://atomicdata.dev/classes/Property' });
  await prop.set('https://atomicdata.dev/properties/name', 'Mood');
  await prop.set('https://atomicdata.dev/properties/shortname', 'mood');
  await prop.set('https://atomicdata.dev/properties/datatype', 'https://atomicdata.dev/datatypes/string');
  await prop.set('https://atomicdata.dev/properties/description', 'How the subject feels');
  await prop.save();
  console.log('PROPERTY saved:', prop.getSubject());

  const cls = await store.newResource({ subject: 'http://localhost:8139/classes/Mood', isA: 'https://atomicdata.dev/classes/Class' });
  await cls.set('https://atomicdata.dev/properties/name', 'Mood');
  await cls.set('https://atomicdata.dev/properties/shortname', 'mood');
  await cls.set('https://atomicdata.dev/properties/description', 'A record of someone mood');
  await cls.set('https://atomicdata.dev/properties/recommends', ['http://localhost:8139/properties/mood']);
  await cls.save();
  console.log('CLASS saved:', cls.getSubject());

  const inst = await store.newResource({ subject: 'http://localhost:8139/mood/1', isA: 'http://localhost:8139/classes/Mood' });
  await inst.set('http://localhost:8139/properties/mood', 'sunny');
  await inst.save();
  console.log('INSTANCE saved:', inst.getSubject());
})().catch(e => { console.log('ERR:', String(e.message).slice(0, 400)); process.exit(1); });

实测输出:

PROPERTY saved: http://localhost:8139/properties/mood
CLASS saved: http://localhost:8139/classes/Mood
INSTANCE saved: http://localhost:8139/mood/1

三步分别落库为 Property/Class/Instance 资源(其后续读取即 5.1/5.2/5.3 的查询对象;实例值后来被改为 stormy,故前文响应显示 stormy)。newResource + set + save 内部就是「按 previousCommit 链构造签名 Commit 并 POST /commit」。

⚠️ 版本配对实测:@tomic/lib@0.39.0 与 server 0.38.0 工作正常;但实验 0.41.0-beta.8 服务器时,lib 0.40/0.41-beta 签名均被服务端判「DID 公钥校验失败」——beta 线与服务端签名格式存在断裂,生产配对请锁定稳定线相邻版本。

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

  • Collections:过滤仅 property+value(值需 URL 编码);响应内嵌成员(includeNested=true);分页元数据 pageSize=30、currentPage=0(0 起)、totalPages 固定返回,但 0.38 实测无可用翻页参数(page/current-page/pageSize 均 500,见 5.2),>30 条结果需拆分查询条件;排序字段 sortBy 默认按 collection/value。
  • Search:q 为核心参数;limit 实测有效(截断返回条数,0/超总量视为不限),property 实测被忽略(见 5.3);结果依赖搜索索引,索引丢失时一律 0 条(5.3 ⚠️ 第 3 条);支持 AND/OR/"引号" 布尔语法(Endpoint description 原文声明)。
  • 读取:无批量端点,多资源逐个 GET;序列化统一走 Accept 头(5.1)。

七、常见问题

Q1:POST /commit 一律 500,怎么定位? 看 Error 资源 description:No subject field in Commit.=载荷缺 subject;already exists, should not be overwritten.=重放/签名冲突(同载荷同签名);服务端日志(--log-level debug)会打印完整原因(如 5.4 的回源 404)。

Q2:为什么 search 搜不到我写入的属性值,甚至所有查询都 0 条? 两类原因:(a) 0.38 索引口径不含自定义属性值(q=stormy 实测 0 命中),按值查改用 Collections;(b) 搜索索引丢失——索引在缓存目录(search_index_path),服务实例重建即丢,现象是所有查询 0 条但直读资源正常,且重启不自愈(5.3 ⚠️ 第 3 条实测)。对策:以 --rebuild-indexes(或 ATOMIC_REBUILD_INDEX=true)启动一次,日志出现 Search index finished! 即恢复(配置篇 5.5)。

Q3:Accept: application/rdf+xml 拿回的是 HTML? 0.38 未实现该格式,静默降级(日志 Unknown Accept header, defaut to HTML)。RDF 需求用 Turtle/N-Triples/JSON-LD 三选一。

Q4:Collections 查内置本体(如所有 Class)为空? 核心本体是外部引用不入库(5.2 实测 totalMembers=0),只能枚举本地自建资源。

Q5:创建 isA=Task 等核心类实例报 500? 服务器回源 https://atomicdata.dev/classes/Task 已 404(上游升级 0.41)。0.38 下请内嵌定义自建 Class/Property(5.6)。

Q6:能匿名写数据吗? 不能。匿名 POST /commit 无法构造合法签名;根 Agent 私钥在服务端 config.toml,泄露即等于交出全部写权(配置篇 5.7)。

Q7:WebSocket 连上后没有任何推送? 确认已发送 SUBSCRIBE 资源完整subject 文本帧(写法见 5.5 实测命令),且订阅的是本服务器 subject 前缀的资源;订阅后变更才推,历史变更不补发。

Q8:@tomic/lib 保存报 DID/签名校验失败? 检查 lib 与 server 版本配对(5.6 ⚠️),稳定线相邻版本可用,beta 混用已实测断裂。

八、参考链接

Atomic Server 0.38.0 API 使用详解 - Quzuo