如何调用 JumpServer Runtime Store API 提交 Kael journal 记录并处理 revision 冲突?
【免费下载链接】jumpserverJumpServer is an open-source Privileged Access Management (PAM) platform that provides DevOps and IT teams with on-demand and secure access to SSH, RDP, Kubernetes, Database and RemoteApp endpoints through a web browser.项目地址: https://gitcode.com/GitHub_Trending/ju/jumpserver
当你在为 JumpServer 的 Chat AI 运行时(Go 服务 Kael)做二次开发或联调时,需要把 Kael 产生的对话状态落到 JumpServer Core 的持久化存储里。这一步走的是 Core 的内部 Runtime Store API:Kael 通过POST /api/v1/chat-ai/runtime-store/追加一条 journal record,Core 在事务内做 revision 比对(CAS)并落库;当多个调用方并发写入、或你上一次写入因为网络抖动没有确认结果时,就会收到 revision 冲突,需要按返回的current_revision重算后重提。本文给出从准备凭据、构造 record 与签名、提交写入,到正确处理 409 冲突的完整路径。适用前提是:你已经有一个绑定到type=kael的 Terminal 的服务账号,并且能拿到该服务账号的 Access Key;接口只在 Core 的/api/v1/chat-ai/路由下可访问,不单独暴露端口。
一、前置条件:服务账号与 Access Key
Runtime Store 是"内部接口",认证和授权都有硬性约束,先核对清楚再动手:
- 认证方式为Access Key HTTP Signature(
SignatureAuthentication)。请求需携带Signature形式的Authorization头,包含keyid、algorithm、signature三个字段,且签名至少覆盖(request-target)和date两个头。keyid对应你的 Access Key。 - 授权上要求认证用户必须是服务账号,并且该账号绑定的 Terminal 类型必须是
type=kael。普通用户、其他组件的服务账号、以及用户委托票据(X-JMS-AI-Delegation)都不能访问这个接口。 - 接口实现位于 RuntimeStoreView,路由定义在 api/urls.py。
确认要点:你的 Access Key 属于一个服务账号、该账号挂了kael类型的 Terminal、Key 处于有效状态(过期或被 IP 组限制会直接鉴权失败)。这一步不满足时,后续任何请求都会在鉴权层失败,与本文后续的 record/签名无关。
二、先读一次:拿到当前 revision 起点
写入前需要先知道 journal 当前的 revision。读接口:
GET /api/v1/chat-ai/runtime-store/?nonce=<uuid>&after=0&limit=1000nonce:本次请求生成的 UUID,必填。after:默认0,表示调用方已持久化到的 revision;limit:默认1000,范围 1 到 1000。- 若
after早于最新 snapshot,响应会从该 snapshot 开始。 - 单页 record 总量达到 64 MiB 时,即使未达
limit也会截断并返回has_more=true。
响应结构(数值为文档示例,仅作格式参考):
{ "nonce": "<uuid>", "revision": 12, "results": [ { "revision": 12, "commit_id": "<uuid>", "snapshot": false, "record": "<jsonl-record>" } ], "has_more": false, "receipt": "<hmac-sha256>" }revision字段即当前 head。把它作为下一次写入的expected_revision起点。响应带Cache-Control: no-store;has_more=true时用本页最后一条 record 的revision继续翻页。receipt用当前请求 Access Key 的 secret 对 canonical 内容做 HMAC-SHA256,调用方应校验它,以确认响应未被篡改。
三、构造写入请求:record 结构与 integrity 签名
写入请求体只有五个字段,多一个都会被拒绝(Unknown field(s): ...):
{ "commit_id": "<uuid>", "expected_revision": 11, "snapshot": false, "record": "<jsonl-record>", "integrity": "<hmac-sha256>" }各字段含义与约束:
commit_id:本次提交的 UUID,用于幂等(见第四节)。expected_revision:你预期当前 head 的值(非负整数),是 CAS 的比对基准。snapshot:布尔。写入 snapshot 时,Core 会在同一事务中保存新 snapshot,并删除它之前的 journal records。record:必须恰好一行的 JSON 字符串,且只能包含version、created_at、payload、checksum四个字段。当前version为1;payload是 Base64 字符串,checksum是 payload 解码后字节的 SHA-256。Core 会把record当不透明字符串保存,但会验证 JSONL envelope、payload checksum、提交完整性和 revision。单条 record 上限 64 MiB。integrity:用当前请求 Access Key 的 secret 对如下 canonical 内容(各部分以换行拼接、无末尾换行)执行 HMAC-SHA256:
kael-runtime-store-commit-v1 default <commit_id> <expected_revision> <0|1 snapshot> <sha256(record)>其中<0|1 snapshot>是 snapshot 的位表示(false→0,true→1),<sha256(record)>是record字符串的 SHA-256。下面这段 Python 仅演示"如何按文档规则算出payload的checksum以及integrity",其中的 UUID、时间戳、payload 都是占位值,需要你替换成真实数据:
import base64, hashlib, hmac, json secret = b"<你的 Access Key secret>" # 替换:请求所用 Access Key 的 secret commit_id = "<uuid>" # 替换:本次提交的 UUID expected_revision = 11 # 替换:预期的当前 head snapshot = False payload_bytes = b"<你的 payload 原始字节>" # 替换:要持久化的业务数据 payload_b64 = base64.b64encode(payload_bytes).decode() record = json.dumps({ "version": 1, "created_at": "<ISO8601 时间字符串>", # 替换:如 2026-09-11T20:00:00Z "payload": payload_b64, "checksum": hashlib.sha256(payload_bytes).hexdigest(), }, separators=(',', ':'), ensure_ascii=False) def sign(parts): msg = "\n".join(str(p) for p in parts).encode("utf-8") return hmac.new(secret, msg, hashlib.sha256).hexdigest() record_hash = hashlib.sha256(record.encode("utf-8")).hexdigest() integrity = sign([ "kael-runtime-store-commit-v1", "default", commit_id, expected_revision, "1" if snapshot else "0", record_hash, ])注意:record内层只允许那四个字段,外层请求体也严格只允许那五个字段,任何多余键都会触发校验失败。
四、提交写入并处理 revision 冲突
提交:
POST /api/v1/chat-ai/runtime-store/Core 在事务内锁定 revision 并执行 CAS,两种结果:
成功(HTTP 201),返回:
{ "revision": 12, "commit_id": "<uuid>", "receipt": "<hmac-sha256>" }成功receipt的 canonical 内容同样用 Access Key secret 做 HMAC-SHA256:
kael-runtime-store-receipt-v1 default <commit_id> <expected_revision> <revision> <0|1 snapshot> <sha256(record)>调用方应校验该 receipt。
revision 冲突(HTTP 409),返回:
{ "code": "runtime_store_revision_conflict", "detail": "The runtime store revision has changed.", "current_revision": 13 }冲突意味着在你读到expected_revision之后、真正落库之前,head 已经被别的写入推进了。处理路径:
- 读取返回体里的
current_revision,这是服务端给出的最新 head,不要自己猜。 - 基于这个新 head 重新计算你的业务状态与 record(Kael 负责对话状态的恢复、压缩和演进,Core 不解析 payload 语义),得到新的
record与对应的sha256(record)。 - 用新的
expected_revision = current_revision、并重新生成commit_id和integrity后重新POST。
关于幂等重试(这是冲突处理里最容易踩坑的一点):commit_id支持网络失败后的重试,但只有当 revision、snapshot、record 均相同,且该提交仍是当前 head 时,才会返回与首次相同的结果。也就是说,"网络超时但实际已写入"的场景,用同一个commit_id重发能拿回原结果;一旦你在冲突后换成了新的 record 或 revision,就必须用新的commit_id。判断是否可以直接重试的关键,就是这三项(revision/snapshot/record)是否与上次完全一致。
五、边界与限制
以下限制直接影响调用能否成功,联调前确认一遍:
- 反向代理的请求体限制必须高于 64 MiB,以容纳 record 外层 JSON 和字符串转义开销;否则会先被代理拦下,到不了 Core。
- Runtime Store API 只能在 Core 的
/api/v1/chat-ai/路由下访问,不应单独暴露服务端口。 - Core 只把
record作为不透明字符串保存,不解析 Kael payload 的业务语义;恢复、压缩和对话状态演进由 Kael 负责。 - Access Key 轮换不会重写历史 record;读取和写入的 receipt 始终使用当前请求的 Access Key。
- 落库表为
chat_ai_runtime_store(head)和chat_ai_runtime_store_record(record),见 models.py。
完成一次成功的POST后,用GET重新读取确认新 revision 与 record 已可见,即视为这条 journal 记录已正确落入 Runtime Store;遇到 409 时按第四节的current_revision重算-重提流程收敛。更多接口细节可参考 Chat AI:Kael 与 JumpServer Core 桥接。
【免费下载链接】jumpserverJumpServer is an open-source Privileged Access Management (PAM) platform that provides DevOps and IT teams with on-demand and secure access to SSH, RDP, Kubernetes, Database and RemoteApp endpoints through a web browser.项目地址: https://gitcode.com/GitHub_Trending/ju/jumpserver
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考