外观
外部 HTTP Client
neomega_runtime.Client 让 Host 进程外的 Python 程序通过已开放的本机 HTTP API 查询能力、提交动作、读写自身业务状态及消费持久事件。它是同步、阻塞、无自动重试的客户端,不负责启动 Host,也不提供所有 ctx.* 插件方法的 HTTP 映射。
在 Host 插件中使用 PluginContext 和异步 ctx.operations、ctx.storage 等 IPC 接口。不要让插件另建 HTTP Client 来绕过安装身份或 manifest 权限。外部 HTTP 身份由 Host 鉴权配置决定,和插件 Worker 的身份不是同一个概念。
工具包提供公开 Client。HTTP 路径版本 /runtime/v1、HTTP 能力响应中的 api_version 与插件 host_api 分别定义对应接口。使用前由实例管理员开启 HTTP,并配置事件来源和客户端授权。
1.1.0 的异步 RemoteBus 使用另一套 Bus/SSE 接口,见远程 EP 与投影。本页同步 Client 的地址、传输与方法合同保持不变。
连接与鉴权
python
import os
from pathlib import Path
from neomega_runtime import Client
# 由管理员提供本机 origin 和凭据文件;不要把 token 放进代码或日志。
client = Client(
os.environ["NEOMEGA_RUNTIME_ORIGIN"],
Path(os.environ["NEOMEGA_RUNTIME_TOKEN_FILE"]).read_text().strip(),
timeout=5,
max_response_bytes=1 << 20,
)
capabilities = client.capabilities()Client(base_url, token, timeout=5, max_response_bytes=1 << 20) 的构造条件:
| 参数 | 合同 |
|---|---|
base_url | 只接受没有路径、尾斜杠、查询、片段或用户信息的 HTTP loopback origin;主机为 127.0.0.1、localhost 或 [::1],端口为 1–65535。localhost 固定连接到 127.0.0.1。不接受公网域名或 HTTPS。 |
token | 非空 Bearer token,最多 8192 字符,只允许不含空白的可见 ASCII;不需要自行加 Bearer 前缀。 |
timeout | 有限正数,单位秒;默认 socket I/O 超时为 5 秒,不是任意阻塞系统调用的硬中断保证。 |
max_response_bytes | 正整数,默认 1 MiB;约束普通 JSON 响应及 SSE 单帧。读取较大自有状态时按业务配额显式调整。 |
客户端自动添加 /runtime/v1/ 路径,设置 Authorization: Bearer ...、随机 X-Request-ID 和相应 Content-Type/Accept。每次普通请求建立一个连接并关闭;不跟随重定向。凭据不得发给 Wiki 域名或浏览器页面,API origin 也不是 Wiki 网址。
请求 JSON 上限为 256 KiB;禁止非有限数字、非字符串对象键和不受支持的 Python 对象。客户端核对响应 request_id、JSON 类型及相关操作/订阅身份;重复 JSON 键、截断响应和越界响应会报协议错误。
所有 Client 方法
以下方法都同步执行;events() 返回同步迭代器。URL 均相对 /runtime/v1/。
| 分组 | 方法 | HTTP 路径 / 行为 |
|---|---|---|
| 能力 | capabilities() | GET capabilities,读取能力目录与可用状态。 |
| 状态 | state() | GET state,读取当前身份完整 KV 快照与 session。 |
| 状态 | commit_state(*, commit_id, expected_state_revision, mutations, actions, session=None) | POST state/commit,原子提交 KV 修改与动作意图。 |
| 动作 | submit(operation, arguments, *, idempotency_key, deadline) | POST operations,持久受理一次动作,返回受理对象。 |
| 动作 | get(operation_id) | GET operations/{id},读取自己拥有的动作状态及证据。 |
| 动作 | cancel(operation_id) | POST operations/{id}/cancel,显式请求取消。 |
| 动作 | wait(operation_id, *, timeout=30, poll_interval=.1) | 有界轮询 get 直到任意终态;不自动取消。 |
| 订阅 | create_subscription(subscription_id, *, filter_version, kinds) | POST subscriptions,从日志开头创建固定过滤器。 |
| 订阅 | subscription(subscription_id) | GET subscriptions/{id},读取订阅元数据。 |
| 事件 | events(subscription_id, *, cursor=None) | GET subscriptions/{id}/events,一次 SSE 连接。 |
| 事件 | acknowledge(subscription_id, *, delivery_token, commit_id) | POST subscriptions/{id}/ack,只确认该次投递。 |
| 事件 | commit_event(subscription_id, *, delivery_token, event_id, commit_id, expected_state_revision, mutations, actions, session=None) | POST subscriptions/{id}/commit,原子确认投递、修改状态及登记动作。 |
Client 不提供插件安装、管理控制台、文件下载、packet codec 或原始包观察的方法。对应能力使用管理员工具或已授权的插件 SDK;不要推导不存在的 /runtime/v1/packets/* 路由。
能力查询与单次动作
capabilities() 返回对象,capabilities 字段为能力列表;当前响应还包含 api_version、available 和 request_id。能力描述说明方法与参数形状,最终受理及分发仍核对身份、当前 session、授权和实例可用状态。
下面的函数只受理调用方已经保存的一个意图。operation 和 arguments 应来自当前能力目录;不要让 AI 猜操作名。
python
def submit_saved_intent(client, intent):
# intent 必须已经由业务保存在私有持久存储中。
accepted = client.submit(
intent["operation"], intent["arguments"],
idempotency_key=intent["idempotency_key"],
deadline=intent["deadline"],
)
return accepted["operation_id"]operation 是非空字符串,arguments 是对象,idempotency_key 是调用方固定的非空字符串。deadline 是带时区的 RFC3339 绝对时间,例如由当前时间加业务允许的执行窗口生成。HTTP 直接提交时 session 由 Host 绑定,调用方不能在 JSON 中指定安装身份、代际或 session。
受理响应包含 operation_id、state、duplicate、scheduling_pending、expires_at、request_id。HTTP 202 或 accepted 只说明动作意图已持久受理,不表示已经在游戏执行。
相同身份内,同一个幂等键必须对应同一个操作、参数和固定 deadline;变更内容会冲突。客户端不会自动重发。提交期间断网或响应异常时,保留原意图,先核对 Host 的原有记录;不能换一个键或延长 deadline 再发。unknown 结果更不能自动重放。
查询、等待和取消
python
receipt = client.get(operation_id)
if receipt["state"] in {"accepted", "dispatching", "running"}:
try:
receipt = client.wait(operation_id, timeout=10, poll_interval=0.2)
except TimeoutError:
# 只结束本次等待,operation_id 继续用于稍后核对。
receipt = None| 返回字段 / 状态 | 含义 |
|---|---|
accepted、dispatching、running | 非终态,继续查询原操作。 |
succeeded、failed、cancelled、unknown | 全部都是终态;wait 遇到任何一种都会返回,不只返回成功。 |
result | 动作结果,含 state,按动作可能有 evidence、reason、result、partial_effects、recovery_required;保留字段原意,不推断缺失证据。 |
can_retry | 当前查询返回 false;不是可自动重试的接口。 |
wait 默认最多等待 30 秒,每次查询使用剩余时间与连接超时中较小的预算,轮询间隔默认 0.1 秒。TimeoutError("operation wait timed out; action was not cancelled") 表示等待期限结束;HTTP 传输失败仍可能是 TransportError。
cancel 返回 operation_id、state、cancellation_requested 和 request_id。对尚未分发的动作可以形成取消终态;对在途动作只是提出取消请求,已经产生的世界效果不会回滚。收到取消回执后仍用 get 核对原操作。
业务状态与原子提交
state() 返回 revision、values、session、request_id;KV 只属于认证身份,不能读取其他插件或安装的状态。状态读、写分别需要 Host 授权。客户端默认 1 MiB 响应限制可能小于 Host 允许的全部状态,要按实际数据量配置,不能将响应超限解释为空状态。
python
import uuid
snapshot = client.state()
request = {
"commit_id": uuid.uuid4().hex,
"expected_state_revision": snapshot["revision"],
"mutations": [{"key": "welcome_enabled", "value": True}],
"actions": [],
}
# 业务持久保存 request 后提交;无 actions 时不要添加 session。
receipt = client.commit_state(**request)| 参数 | 规则 |
|---|---|
commit_id | 1–128 个 ASCII 字母、数字、下划线或连字符;与 ack 共用当前安装的提交 ID 命名空间。 |
expected_state_revision | 非布尔整数,0 至 2**63 - 1;用读取的 revision 做 CAS,不自动 rebase。 |
mutations | 必须为列表,可空;每项恰为 {"key": ..., "value": ...} 或 {"key": ..., "delete": True}。key 为 1–256 UTF-8 bytes,不允许同一提交重复 key。JSON null 是值,不是删除。 |
actions | 必须为列表,可空;每项恰含 operation、arguments、idempotency_key、deadline。各意图键不可重复。 |
session | actions 非空时必须来自 state() 的当前 session;actions 为空时必须省略。它用于防止跨游戏会话执行,不替代身份。 |
提交成功返回 commit_id、新 revision、按动作顺序排列的 operation_ids、duplicate 和 request_id。这些是业务事务及动作意图的受理结果,游戏结果需逐个查询 operation ID。
状态、事件确认和动作登记可以原子提交,动作的游戏效果不是该数据库事务的一部分。收到 revision_conflict 时先读最新业务状态,决定是否重新计算;不要仅替换 revision 或 commit ID 重交旧业务意图。若丢失回执,保留完整原始请求,交由恢复逻辑核对同一提交;SDK 不自动重试或变更 session。
持久订阅与 SSE
python
metadata = client.create_subscription(
"chat-audit", filter_version="1", kinds=["chat.received"],
)subscription_id 遵循 1–128 字符标识规则;filter_version 为 1–256 UTF-8 bytes 非空字符串;kinds 为 1–32 项不重复的非空字符串,各项最多 256 UTF-8 bytes。创建固定从 beginning 开始,SDK 不提供从当前时间或数字位置开始的参数。
相同 ID 和相同过滤器返回当前元数据,不重置进度;修改原订阅过滤器会冲突。返回 subscription_id、filter_version、kinds、delivered_count、acknowledged_prefix_count、request_id。两个 count 是诊断值,不是恢复游标,也不是“所有已确认事件数”。
Host 必须为当前外部身份授权每个事件 kind,并存在相应持久事件来源。能够提交动作并不意味着能够读事件;实例未装配来源或授权时会拒绝。按 kind 授权不能隔离他人私有动作或服务结果,不能据此假设所有内部事件都可订阅。
读取一条投递后原子确认
python
import uuid
with client.events("chat-audit", cursor=None) as stream:
for frame in stream:
if frame["event"] != "delivery":
# gap / runtime.error 是终态控制帧,不能当成业务投递确认。
break
delivery = frame["data"]
state = client.state()
request = {
"delivery_token": delivery["delivery_token"],
"event_id": delivery["event_id"],
"commit_id": uuid.uuid4().hex,
"expected_state_revision": state["revision"],
"mutations": [{"key": "last_event", "value": delivery["event_id"]}],
"actions": [],
}
# 保存原始 request,再提交。不要额外调用 acknowledge。
receipt = client.commit_event("chat-audit", **request)
# 成功后可持久保存 frame["cursor"],后续仍须处理重复投递。
breakEventStream 是上下文管理器和同步迭代器;使用 with 确保提前退出时关闭 socket,也可以显式 close()。events() 不自动确认,不自动重连,也不会默默跳过未确认事件。同步迭代会阻塞至收到事件或触发传输超时,不要放在插件事件循环里直接调用。
每次返回的 frame 为 {"event": kind, "cursor": cursor, "data": object}:
event | 返回内容与处理 |
|---|---|
delivery | 有 cursor,data 包括 subscription_id、delivery_token、event_id、boot_id、session、ingress_seq、kind、payload 和 request_id。 |
gap | 无 cursor,data 含 reason、resync_required: true;流关闭,需要明确重新同步,不能伪造连续进度。 |
runtime.error | 无 cursor,data 含结构化 code;流关闭,保留原恢复游标。 |
heartbeat 在客户端内部消化,不交给业务迭代器。正常连接结束触发 StopIteration,不自动重新连接。cursor 如提供,必须是服务端返回的 d1. 加 32 位小写十六进制不透明字符串;它证明订阅所有权,不是偏移量。即使重连带上 Last-Event-ID,Host 仍会投递全部未确认事件,包括游标之前的确认空洞。
仅确认投递
acknowledge(subscription_id, *, delivery_token, commit_id) 只确认一次投递,不修改业务 KV 或 state revision。返回 subscription_id、commit_id、duplicate、提交当时的 acknowledged_prefix_count 和 request_id。要读取当前进度,另调 subscription()。
相同 commit ID 和相同内容可识别重复确认;同 ID 改内容会冲突。不要对已确认 token 换一个新 commit ID 再确认,也不要用数字游标代替 token。需要同时保存业务状态或登记动作时用 commit_event,避免先确认后崩溃造成业务处理丢失。
错误处理与超时
python
from neomega_runtime import APIError, ProtocolError, TransportError
try:
current = client.get(operation_id)
except APIError as error:
# 可记录这三个结构化字段;不记录 token、完整请求或未经筛选的包体。
diagnostic = {"status": error.status, "code": error.code,
"request_id": error.request_id}
except (ProtocolError, TransportError):
# 此例是读取失败;保留 operation_id,稍后继续核对。
current = None| 异常 | 发生位置与处理 |
|---|---|
ValueError | 本地参数验证失败,如非法 origin、ID、时间、游标、JSON 或额度。修正输入后再决定是否提交。 |
APIError(status, code, request_id) | HTTP API 返回可验证的结构化拒绝;可读 .status、.code、.request_id。客户端不保留不可信 message 文本。 |
ProtocolError | 响应不是约定 JSON/SSE、身份不匹配、重定向、截断、超限或结构错误。若发生在写请求之后,不能由异常推断“未受理”。 |
TransportError | socket / HTTP 传输失败,包括 I/O 超时。动作或事务可能已经受理;保留原意图、原提交 ID 和已知 operation ID。 |
TimeoutError | wait 的整体等待预算到期;动作未被自动取消。 |
常见 API code 与处理方向:
| code | 处理 |
|---|---|
permission_denied、identity_revoked | 核对当前身份和真实授权,不靠换参数绕过。 |
invalid_request、invalid_arguments、invalid_deadline | 对照能力 schema、本页参数及固定 deadline 修正。 |
revision_conflict、state_conflict、invalid_delivery、session_expired | 核对原提交、业务 revision、投递 token 和游戏 session,不自动重算后重放。 |
quota_exceeded、admission_paused、runtime_unavailable | 检查配额或 Host 状态;它们不是客户端自动重试指令。 |
state_unavailable、subscriptions_unavailable、not_found | 核对实例是否提供该存储/功能以及请求对象是否属于当前身份。 |
不要使用 except Exception: client.submit(...) 一类自动重放。succeeded 也要结合动作的 result.evidence 判断已证明的范围;例如包发送的 dispatched 只证明本地分发,不等于服务器接受或玩家效果。参见协议包与回执及错误与恢复。