feat(gateway): add core gateway protocol implementation
新建了完整的网关协议模块,包含版本协商、基础类型定义、信道请求模型、错误码规范、通信帧结构以及握手流程,实现了完整的客户端-服务端通信协议基础框架
This commit is contained in:
parent
7a972055b7
commit
fee71576a2
39
backend/package/yuxi/gateway/__init__.py
Normal file
39
backend/package/yuxi/gateway/__init__.py
Normal 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",
|
||||
]
|
||||
37
backend/package/yuxi/gateway/protocol/__init__.py
Normal file
37
backend/package/yuxi/gateway/protocol/__init__.py
Normal 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",
|
||||
]
|
||||
19
backend/package/yuxi/gateway/protocol/channels.py
Normal file
19
backend/package/yuxi/gateway/protocol/channels.py
Normal 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
|
||||
27
backend/package/yuxi/gateway/protocol/errors.py
Normal file
27
backend/package/yuxi/gateway/protocol/errors.py
Normal 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
|
||||
36
backend/package/yuxi/gateway/protocol/frames.py
Normal file
36
backend/package/yuxi/gateway/protocol/frames.py
Normal 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()
|
||||
75
backend/package/yuxi/gateway/protocol/handshake.py
Normal file
75
backend/package/yuxi/gateway/protocol/handshake.py
Normal 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"],
|
||||
),
|
||||
)
|
||||
14
backend/package/yuxi/gateway/protocol/primitives.py
Normal file
14
backend/package/yuxi/gateway/protocol/primitives.py
Normal 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)]
|
||||
13
backend/package/yuxi/gateway/protocol/version.py
Normal file
13
backend/package/yuxi/gateway/protocol/version.py
Normal 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
|
||||
Loading…
Reference in New Issue
Block a user