2026-07-02 03:22:12 +08:00
|
|
|
|
"""流式 DTO。
|
|
|
|
|
|
|
|
|
|
|
|
定义流式输出相关的不可变值对象,包括分块模式枚举、流式分块、分块
|
|
|
|
|
|
结果、流式完成结果与流式能力。所有 DTO 均为
|
|
|
|
|
|
``dataclass(frozen=True)``,仅依赖标准库与契约层内部类型,用于
|
|
|
|
|
|
流式输出的分块投递、结果同步与能力声明(FR-13)。
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
from dataclasses import dataclass
|
|
|
|
|
|
from enum import StrEnum
|
|
|
|
|
|
|
2026-07-03 19:18:13 +08:00
|
|
|
|
from yuxi.channels.contract.errors import ValidationError
|
|
|
|
|
|
|
2026-07-02 03:22:12 +08:00
|
|
|
|
|
|
|
|
|
|
class ChunkMode(StrEnum):
|
|
|
|
|
|
"""分块模式。
|
|
|
|
|
|
|
|
|
|
|
|
标识流式输出的分块模式,用于适配器按模式渲染分块。继承
|
|
|
|
|
|
``str, Enum`` 以支持 JSON 序列化与字符串比较。
|
|
|
|
|
|
|
|
|
|
|
|
取值:
|
|
|
|
|
|
NONE: 无流式(走完整消息)。
|
|
|
|
|
|
TEXT: 文本追加模式。
|
|
|
|
|
|
BLOCK: 块模式(每分块作为新消息块)。
|
|
|
|
|
|
FULL_UPDATE: 卡片整体更新。
|
|
|
|
|
|
PARTIAL_UPDATE: 卡片局部更新。
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
NONE = "none"
|
|
|
|
|
|
TEXT = "text"
|
|
|
|
|
|
BLOCK = "block"
|
|
|
|
|
|
FULL_UPDATE = "full_update" # 卡片整体更新
|
|
|
|
|
|
PARTIAL_UPDATE = "partial_update" # 卡片局部更新
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class RenderStrategy(StrEnum):
|
|
|
|
|
|
"""卡片渲染策略。
|
|
|
|
|
|
|
|
|
|
|
|
继承 ``str, Enum`` 以支持 JSON 序列化。用于 ``updatePartialCard``
|
|
|
|
|
|
方法声明局部更新的渲染策略。
|
|
|
|
|
|
|
|
|
|
|
|
取值:
|
|
|
|
|
|
APPEND: 追加到现有内容。
|
|
|
|
|
|
REPLACE: 替换现有内容。
|
|
|
|
|
|
MERGE: 合并到现有内容。
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
APPEND = "append" # 追加到现有内容
|
|
|
|
|
|
REPLACE = "replace" # 替换现有内容
|
|
|
|
|
|
MERGE = "merge" # 合并到现有内容
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
|
|
|
|
class StreamChunk:
|
|
|
|
|
|
"""流式分块。
|
|
|
|
|
|
|
|
|
|
|
|
描述流式输出的单个分块,包括内容、序号与是否末块标记,用于
|
|
|
|
|
|
适配器按序投递分块。
|
|
|
|
|
|
|
|
|
|
|
|
字段:
|
|
|
|
|
|
content: 分块内容。
|
|
|
|
|
|
sequence: 序号。
|
|
|
|
|
|
is_final: 是否末块(默认 False)。
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
content: str
|
|
|
|
|
|
sequence: int
|
|
|
|
|
|
is_final: bool = False
|
|
|
|
|
|
|
2026-07-03 19:18:13 +08:00
|
|
|
|
def __post_init__(self) -> None:
|
|
|
|
|
|
"""校验 content 非空与 sequence 非负。
|
|
|
|
|
|
|
|
|
|
|
|
``content`` 必须非空,``sequence`` 必须非负,在构造时即抛出
|
|
|
|
|
|
``ValidationError``,避免空分块或负序号破坏流式分块顺序(INV-8)。
|
|
|
|
|
|
"""
|
|
|
|
|
|
if not self.content:
|
|
|
|
|
|
raise ValidationError("content", "must not be empty")
|
|
|
|
|
|
if self.sequence < 0:
|
|
|
|
|
|
raise ValidationError("sequence", "must not be negative")
|
|
|
|
|
|
|
2026-07-02 03:22:12 +08:00
|
|
|
|
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
|
|
|
|
class ChunkResult:
|
|
|
|
|
|
"""分块结果。
|
|
|
|
|
|
|
|
|
|
|
|
描述单个分块投递的结果,包括分块本身、是否成功与可选错误信息,
|
|
|
|
|
|
用于流式投递的状态同步与错误处理。
|
|
|
|
|
|
|
|
|
|
|
|
字段:
|
|
|
|
|
|
chunk: 流式分块。
|
|
|
|
|
|
success: 是否成功。
|
|
|
|
|
|
error: 错误信息(可选)。
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
chunk: StreamChunk
|
|
|
|
|
|
success: bool
|
|
|
|
|
|
error: str | None = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
|
|
|
|
class StreamingCompleted:
|
|
|
|
|
|
"""流式完成结果。
|
|
|
|
|
|
|
|
|
|
|
|
描述流式输出完成后的汇总结果,包括总分块数与耗时(毫秒),用于
|
|
|
|
|
|
流式投递的统计与审计。
|
|
|
|
|
|
|
|
|
|
|
|
字段:
|
|
|
|
|
|
total_chunks: 总分块数。
|
|
|
|
|
|
duration_ms: 耗时(毫秒)。
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
total_chunks: int
|
|
|
|
|
|
duration_ms: int
|
|
|
|
|
|
|
2026-07-03 19:18:13 +08:00
|
|
|
|
def __post_init__(self) -> None:
|
|
|
|
|
|
"""校验 total_chunks 为正整数与 duration_ms 非负。
|
|
|
|
|
|
|
|
|
|
|
|
``total_chunks`` 必须为正整数,``duration_ms`` 必须非负,在构造时
|
|
|
|
|
|
即抛出 ``ValidationError``,避免非正分块数或负耗时导致统计失真(INV-8)。
|
|
|
|
|
|
"""
|
|
|
|
|
|
if self.total_chunks <= 0:
|
|
|
|
|
|
raise ValidationError("total_chunks", "must be a positive integer")
|
|
|
|
|
|
if self.duration_ms < 0:
|
|
|
|
|
|
raise ValidationError("duration_ms", "must not be negative")
|
|
|
|
|
|
|
2026-07-02 03:22:12 +08:00
|
|
|
|
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
|
|
|
|
class StreamingCapability:
|
|
|
|
|
|
"""流式能力。
|
|
|
|
|
|
|
|
|
|
|
|
描述渠道的流式能力声明,包括是否支持流式、是否支持输入指示、
|
|
|
|
|
|
分块模式、最小分块间隔与最大分块长度,用于 FR-13 流式能力协商
|
|
|
|
|
|
与降级决策。
|
|
|
|
|
|
|
|
|
|
|
|
字段:
|
|
|
|
|
|
supports_streaming: 是否支持流式(默认 False)。
|
|
|
|
|
|
supports_typing_indicator: 是否支持输入指示(默认 False)。
|
|
|
|
|
|
chunk_mode: 分块模式(默认 NONE)。
|
|
|
|
|
|
min_chunk_interval_ms: 最小分块间隔(毫秒,默认 0)。
|
|
|
|
|
|
max_chunk_length: 最大分块长度(默认 0)。
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
supports_streaming: bool = False
|
|
|
|
|
|
supports_typing_indicator: bool = False
|
|
|
|
|
|
chunk_mode: ChunkMode = ChunkMode.NONE
|
|
|
|
|
|
min_chunk_interval_ms: int = 0
|
|
|
|
|
|
max_chunk_length: int = 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
|
|
|
|
class StreamingConfig:
|
|
|
|
|
|
"""流式配置。
|
|
|
|
|
|
|
|
|
|
|
|
描述流式管道的运行时配置,由 ConfigPort.getStreamingConfig 读取,
|
|
|
|
|
|
用于控制流式管道是否激活、输入指示开关、最小分块间隔与 TTL 安全阀
|
|
|
|
|
|
(FR-13)。
|
|
|
|
|
|
|
|
|
|
|
|
字段:
|
|
|
|
|
|
streaming_enabled: 是否启用流式管道(默认 False)。启用时
|
|
|
|
|
|
``delivery_mode`` 设为 ``streaming``,激活 stream-chunk /
|
|
|
|
|
|
truncation-check 等流式阶段。
|
|
|
|
|
|
enable_typing: 是否启用输入指示(默认 True)。typing-indicator 阶段
|
|
|
|
|
|
据此决定是否调用 ``startTypingIndicator`` / ``stopTypingIndicator``
|
|
|
|
|
|
(FR-13)。关闭时跳过输入指示动画,不影响流式分块投递。
|
|
|
|
|
|
min_chunk_interval_ms: 最小分块间隔(毫秒,默认 200)。stream-chunk
|
|
|
|
|
|
阶段据此在分批发送之间强制等待,避免对渠道侧造成压力(AC-22)。
|
|
|
|
|
|
streaming_ttl_ms: 流式 TTL 安全阀(毫秒,默认 60000)。stream-chunk
|
|
|
|
|
|
阶段累计耗时超过该值时停止发送分块,由 typing-stop 阶段强制
|
|
|
|
|
|
结束流式会话并记录告警日志(AC-62)。
|
|
|
|
|
|
typing_ttl_ms: 输入指示 TTL(毫秒,默认 10000)。插件可据此控制
|
|
|
|
|
|
输入指示动画的刷新周期,避免动画超时停留(FR-13)。
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
streaming_enabled: bool = False
|
|
|
|
|
|
enable_typing: bool = True
|
|
|
|
|
|
min_chunk_interval_ms: int = 200
|
|
|
|
|
|
streaming_ttl_ms: int = 60000
|
|
|
|
|
|
typing_ttl_ms: int = 10000
|