跳转到正文

远程 EP 与投影 ​

配套版本

本页远程 EP 与可恢复投影接口使用 1.1.0 Host(Host API 1.10)与配套 SDK。1.0.0 不包含这些接口;升级时须同时核对 Host、SDK 和权限,不能只更换插件代码。下载见工具包,升级步骤见版本页。

AP 是持有游戏连接与执行权的接入端,EP 是通过 Host 调用能力的执行端。只有 AP、一个 AP 配一个 EP、一个 AP 配多个 EP,都由同一 Host 协调受管资源与动作。EP 可以保留自己的观察缓存,影响实际操作的资源授予、权限检查和证据读取仍由 Host 完成。

连接与权限 ​

远程客户端需要管理员提供 Host 地址与绑定当前调用身份的凭据。Host 必须已配置凭据到主体的认证映射;客户端不能自行填写 installation 或 generation 获得身份。地址使用不带路径的 HTTP(S) origin;非数字回环地址必须使用 HTTPS,http://localhost 也不满足数字回环限制。

python
from neomega_runtime import RemoteBus, Resources, ScopeSpec

async def inspect_remote_inventory(origin, token):
    async with await RemoteBus.connect(origin, token) as bus:
        async with bus.scopes.open(
            ScopeSpec(resources=[Resources.inventory_session()])
        ) as scope:
            evidence = await scope.evidence("inventory")
        return evidence, scope.handle

本例需要Scope 权限,以及 scope.session_open、scope.session_renew、scope.session_close 三个精确 service 成员权限。普通业务成员和 View 仍各自授权;连接成功不等于能够调用所有目录成员。

async with bus 管理 EP 会话、续租、观察流与连接的关闭。会话上下文由当前异步任务及其内部创建的子任务继承,退出后恢复原上下文。显式管理连接时使用 bus = await RemoteBus.connect(...),并在 finally 中 await bus.close()。

单次动作也需要会话 ​

没有显式 Scope 时,普通 read、service 和单次 op 也可以通过 RemoteBus 调用;受管动作由 Host 按单次操作协调资源。跨多个动作持续占用时,再使用资源会话。

远程 OpIntent 必须保留创建时的 EP 会话。可在 async with bus 内构造;显式连接方式则使用:

python
intent = bus.intent(
    idempotency_key=business_action_id,
    session=action_session,
    deadline=original_deadline,
)

这里三个参数均由本次业务保留:幂等键、原动作会话、原固定期限。session 是游戏动作账本会话,与 RemoteBus 自动管理的 EP 会话不同,不能互换;在显式 Scope 内使用 scope.handle.action_session。调用真实成员时按其结构合同传入参数,见Bus 持久动作。

已经在远程上下文之外冻结、缺少 EP 会话的 OpIntent 会被拒绝,SDK 不会悄悄改写它。接纳返回的 operation_id 要保留,后续使用 await bus.operation(operation_id) 查询原动作回执;它不会重新提交动作。

API用途
await RemoteBus.connect(origin, token, timeout=5)建立受信 EP 会话。
await bus.call(...)调用有类型的 read/service/op;参数与合同见 Bus 文档。
bus.intent(...)在显式连接方式下冻结原 EP 会话与动作身份。
await bus.operation(operation_id)查询已保留的持久动作回执。
await bus.close()停止新调用并清理所属 Scope、观察流和连接。

失联后保留什么 ​

SDK 自动续租 EP 会话。续租失败会停止接纳新调用并发起所属 Scope 清理;Host 仍负责已经持久接纳的动作。连接断开、等待取消或关闭 socket 都不意味着游戏效果已撤销,也不证明提供方已经完成资源清理。

重新连接得到新的 EP 会话,不继承旧会话的占用。按原动作 ID 查询回执,按Scope 关闭与恢复规则核对资源状态。unknown 不能自动重放,用户回调也不会因重新连接自动执行第二次。

传输目前只支持内联小值,不支持远程二进制描述符或上传。它最多同时保留两个 SSE 流;普通请求与生命周期请求使用独立的并发额度。需要更多观察流或大载荷时,应先检查目标接口和传输支持范围。

可恢复投影 ​

普通 watch() 提供一次初始快照及连续增量,终止后不自行重开。projection() 在此基础上维护当前快照,并在可恢复的断流、提供者替换或游标失效后重新取得基线。本地 ctx.bus 与远程 bus 使用同一 Python 接口。

下面以计数示例的 stats_commands.count 为例。需要已安装并授权该提供者;它的增量是新的累计值,reducer 应替换旧值,不能相加。

python
from neomega_runtime import u64

async def observe_count(bus, provider):
    async with await bus.projection(
        "stats_commands.count",
        snapshot_type=u64,
        delta_type=u64,
        reducer=lambda current, delta: delta,
        provider=provider,
    ) as projection:
        initial = projection.snapshot
        print(initial.snapshot, initial.complete, initial.reason)
        async for update in projection:
            print(update.snapshot, update.complete, update.reason)

provider 是目录中选择的实际提供者,不是任意名称。reducer 必须同步且无副作用,接收旧快照的独立副本与当前增量;返回新的快照值。Python 投影在 read() 或异步迭代时推进,读取 snapshot 属性本身不会消费后续增量。

接口或字段含义
await bus.projection(member, reducer=..., snapshot_type=..., delta_type=..., provider=...)建立原子初始基线,返回投影。
projection.snapshot当前快照的独立副本,不应据此假定游戏状态全局最新。
await projection.read() / async for update in projection应用下一增量,或在恢复后返回替换的新基线。
complete / reason来源完整性与失效原因;丢失连续性时旧快照标为不完整。
await projection.close()关闭所属 Watch,并等待正在进行的重开结束。

恢复采用有界退避并原子替换基线,不虚构漏掉的事件,也不重放任何业务动作。来源本身不完整时,重新订阅仍不能把它变成完整来源。例如 players.online 只描述当前来源观察到的玩家,不能据此断言全服完整名单。reducer 或结构合同出错会向调用方暴露,不能靠不断重连掩盖。

投影恢复解决的是观察流连续性;联合资源决策应使用 Scope 权威证据。Go 同样提供公开的远程客户端与投影,见下节。

Go 远程客户端 ​

1.1.0 工具包内的独立 Go SDK 提供 bushttp、scopes、busapi 与 busview 包。bushttp.New 创建传输配置,scopes.NewRemote 才会向 Host 申请 EP 会话;地址和权限限制与 Python 相同。

go
package pluginexample

import (
    "context"
    "errors"

    "github.com/OmineDev/neomega-sdk-go/bushttp"
    "github.com/OmineDev/neomega-sdk-go/scopes"
)

func inspectRemote(ctx context.Context, origin, token string) (result scopes.Result, err error) {
    transport, err := bushttp.New(bushttp.Config{BaseURL: origin, Token: token})
    if err != nil {
        return result, err
    }
    remote, err := scopes.NewRemote(ctx, transport)
    if err != nil {
        return result, err
    }
    defer func() { err = errors.Join(err, remote.Close()) }()
    result, err = remote.Run(ctx, scopes.Spec{
        Resources: scopes.All(scopes.InventorySession()),
    }, func(session *scopes.Session) error {
        _, err := session.Evidence(session.Context(), "inventory")
        return err
    })
    // 返回 err 后仍须核对 result.Handle 的关闭状态和未决动作。
    return result, err
}

普通远程调用使用 remote.Bus() 并传递 remote.Context(ctx);Scope 内改用 session.Context()。通过 FreezeOp(ctx, request) 保留创建时的 Scope/EP 上下文,busapi.CallRequest.Intent 包含原 IdempotencyKey、游戏账本 Session 和固定 Deadline。不能因重连重新冻结成新会话后重发;原动作回执使用 transport.Operation(ctx, operationID) 查询。

bushttp.Config.HTTPClient 可提供 TLS 根证书或传输;长流的 http.Client.Timeout 保持为零,由每次调用的 context 限定期限。重定向被禁用,二进制大载荷不走本页远程传输。

Go 可恢复投影 ​

通过目录取得实际 View 的 busapi.WatchRequest,其中 Member、Contract 和可选 Target 必须对应同一个成员。计数示例的累计值合同可以使用:

go
projection, err := busview.OpenProjection[uint64, uint64](
    remote.Context(ctx), remote.Bus(), request,
    func(current, delta uint64) (uint64, error) { return delta, nil },
)

从 github.com/OmineDev/neomega-sdk-go/busview 导入该包。创建成功后由调用方关闭 projection.Close();Snapshot() 返回当前副本,Read(ctx) 等待一次更新或失效并返回最新快照。检查 Value、Complete、Reason 及返回错误。

Go 投影在后台持续消费并恢复,慢读者可能只看到合并后的最新更新;Python 投影在 read() 或异步迭代时推进。两者都不提供每个历史事件的持久投递保证。reducer 同步且无副作用,必须及时返回,也不能在自身回调中关闭该投影;关闭会等待正在执行的 reducer 和观察流真正结束。

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