跳转到正文

外部 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_id1–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。各意图键不可重复。
sessionactions 非空时必须来自 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"],后续仍须处理重复投递。
        break

EventStream 是上下文管理器和同步迭代器;使用 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、身份不匹配、重定向、截断、超限或结构错误。若发生在写请求之后,不能由异常推断“未受理”。
TransportErrorsocket / HTTP 传输失败,包括 I/O 超时。动作或事务可能已经受理;保留原意图、原提交 ID 和已知 operation ID。
TimeoutErrorwait 的整体等待预算到期;动作未被自动取消。

常见 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 只证明本地分发,不等于服务器接受或玩家效果。参见协议包与回执及错误与恢复。

Neomega · 使用、开发与参考文档