外观
远程 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 和观察流真正结束。