feat(gateway): add core gateway protocol implementation

新建了完整的网关协议模块,包含版本协商、基础类型定义、信道请求模型、错误码规范、通信帧结构以及握手流程,实现了完整的客户端-服务端通信协议基础框架
This commit is contained in:
Kris 2026-05-12 00:54:15 +08:00
parent 7a972055b7
commit fee71576a2
8 changed files with 260 additions and 0 deletions

View File

@ -0,0 +1,39 @@
from yuxi.gateway.protocol import (
ChannelLogoutRequest,
ChannelStartRequest,
ChannelStatusRequest,
ConnectParams,
ErrorCode,
ErrorDetail,
EventFrame,
GatewayFrame,
HelloFeatures,
HelloOk,
NonEmptyString,
PositiveInt,
PROTOCOL_VERSION,
RequestFrame,
ResponseFrame,
handle_handshake,
negotiate_version,
)
__all__ = [
"RequestFrame",
"ResponseFrame",
"EventFrame",
"GatewayFrame",
"ErrorCode",
"ErrorDetail",
"PROTOCOL_VERSION",
"negotiate_version",
"ConnectParams",
"HelloFeatures",
"HelloOk",
"handle_handshake",
"NonEmptyString",
"PositiveInt",
"ChannelStatusRequest",
"ChannelStartRequest",
"ChannelLogoutRequest",
]

View File

@ -0,0 +1,37 @@
from __future__ import annotations
from typing import Annotated
from pydantic import Field
from yuxi.gateway.protocol.channels import ChannelLogoutRequest, ChannelStartRequest, ChannelStatusRequest
from yuxi.gateway.protocol.errors import ErrorCode, ErrorDetail
from yuxi.gateway.protocol.frames import EventFrame, RequestFrame, ResponseFrame
from yuxi.gateway.protocol.handshake import ConnectParams, HelloFeatures, HelloOk, handle_handshake
from yuxi.gateway.protocol.primitives import NonEmptyString, PositiveInt
from yuxi.gateway.protocol.version import PROTOCOL_VERSION, negotiate_version
GatewayFrame = Annotated[
RequestFrame | ResponseFrame | EventFrame,
Field(discriminator="type"),
]
__all__ = [
"RequestFrame",
"ResponseFrame",
"EventFrame",
"GatewayFrame",
"ErrorCode",
"ErrorDetail",
"PROTOCOL_VERSION",
"negotiate_version",
"ConnectParams",
"HelloFeatures",
"HelloOk",
"handle_handshake",
"NonEmptyString",
"PositiveInt",
"ChannelStatusRequest",
"ChannelStartRequest",
"ChannelLogoutRequest",
]

View File

@ -0,0 +1,19 @@
from __future__ import annotations
from pydantic import BaseModel
class ChannelStatusRequest(BaseModel):
channel_id: str | None = None
account_id: str | None = None
class ChannelStartRequest(BaseModel):
channel_id: str
account_id: str | None = None
class ChannelLogoutRequest(BaseModel):
channel_id: str
account_id: str | None = None
logged_out: bool = True

View File

@ -0,0 +1,27 @@
from __future__ import annotations
from enum import IntEnum
from pydantic import BaseModel
class ErrorCode(IntEnum):
INVALID_REQUEST = -32600
METHOD_NOT_FOUND = -32601
INVALID_PARAMS = -32602
INTERNAL_ERROR = -32603
RATE_LIMITED = -32000
AUTH_REQUIRED = -32001
NOT_LINKED = -32002
NOT_PAIRED = -32003
AGENT_TIMEOUT = -32004
APPROVAL_NOT_FOUND = -32005
UNAVAILABLE = -32006
class ErrorDetail(BaseModel):
code: ErrorCode
message: str
details: str | None = None
retryable: bool = False
retry_after_ms: int | None = None

View File

@ -0,0 +1,36 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Any, Literal
from pydantic import BaseModel, Field
if TYPE_CHECKING:
from yuxi.gateway.protocol.errors import ErrorDetail
class RequestFrame(BaseModel):
type: Literal["req"] = "req"
id: str
method: str
params: dict[str, Any] = Field(default_factory=dict)
class ResponseFrame(BaseModel):
type: Literal["res"] = "res"
id: str
ok: bool
payload: Any = None
error: ErrorDetail | None = None
class EventFrame(BaseModel):
type: Literal["event"] = "event"
event: str
payload: Any = None
seq: int = 0
state_version: int = 0
from yuxi.gateway.protocol.errors import ErrorDetail # noqa: E402
ResponseFrame.model_rebuild()

View File

@ -0,0 +1,75 @@
from __future__ import annotations
import uuid
from typing import Literal
from pydantic import BaseModel, Field
from yuxi.gateway.protocol.version import negotiate_version
class ConnectParams(BaseModel):
min_protocol: int = 1
max_protocol: int = 1
client_id: str = ""
client_version: str = ""
client_platform: str = ""
auth_token: str | None = None
caps: list[str] = Field(default_factory=list)
client_display_name: str = ""
client_device_family: str = ""
client_model_identifier: str = ""
client_mode: str = "default"
client_instance_id: str = ""
commands: list[str] = Field(default_factory=list)
permissions: dict[str, bool] = Field(default_factory=dict)
path_env: str = ""
role: str = ""
scopes: list[str] = Field(default_factory=list)
locale: str = ""
user_agent: str = ""
class HelloFeatures(BaseModel):
methods: list[str] = Field(default_factory=list)
events: list[str] = Field(default_factory=list)
class HelloOk(BaseModel):
type: Literal["hello-ok"] = "hello-ok"
protocol: int
server_version: str
server_id: str
session_id: str
features: HelloFeatures = Field(default_factory=HelloFeatures)
snapshot: dict | None = None
dm_policy: str = "pairing"
group_policy: str = "allowlist"
conn_id: str = ""
max_payload: int = 1048576
max_buffered_bytes: int = 10485760
tick_interval_ms: int = 5000
async def handle_handshake(params: ConnectParams) -> HelloOk:
protocol = negotiate_version(params.min_protocol, params.max_protocol)
if protocol is None:
raise ValueError("Protocol version negotiation failed")
session_id = str(uuid.uuid4())
conn_id = f"conn-{session_id[:8]}"
return HelloOk(
protocol=protocol,
server_version="1.0.0",
server_id="forcepilot-gateway",
session_id=session_id,
conn_id=conn_id,
features=HelloFeatures(
methods=["channel.status", "channel.start", "channel.stop", "chat.send", "chat.abort"],
events=["channel.status_change", "tick", "chat", "channel.logout", "config.reload"],
),
)

View File

@ -0,0 +1,14 @@
from __future__ import annotations
from typing import Annotated
from pydantic import AfterValidator, StringConstraints
NonEmptyString = Annotated[str, StringConstraints(min_length=1)]
def _ensure_positive(v: int) -> int:
return v if v >= 1 else 1
PositiveInt = Annotated[int, AfterValidator(_ensure_positive)]

View File

@ -0,0 +1,13 @@
from __future__ import annotations
PROTOCOL_VERSION: int = 1
def negotiate_version(client_min: int, client_max: int) -> int | None:
server_max = PROTOCOL_VERSION
server_min = 1
negotiated = min(client_max, server_max)
if negotiated >= max(client_min, server_min):
return negotiated
return None