chore: 迁移到 social-app 架构,集成 Supabase 和 taskiq worker

This commit is contained in:
qzl
2026-04-02 16:36:35 +08:00
parent 695adb7d6f
commit 92cdfd9fca
132 changed files with 5802 additions and 759 deletions
+68
View File
@@ -0,0 +1,68 @@
from schemas.agent.forwarded_props import (
ClientTimeContext,
ForwardedPropsPayload,
parse_forwarded_props_client_time,
parse_forwarded_props_runtime_mode,
)
from schemas.agent.forwarded_props import RuntimeMode
from schemas.agent.runtime_models import (
AgentOutput,
ConstraintItem,
ExecutionMode,
KeyEntity,
NormalizedTaskInput,
ResultTyping,
ResultType,
RouterAgentOutput,
RunStatus,
TaskType,
TaskTyping,
ToolAgentOutput,
ToolStatus,
WorkerAgentOutputLite,
WorkerAgentOutputRich,
resolve_worker_output_model,
)
from schemas.agent.system_agent import AgentType, SystemAgentLLMConfig
from schemas.agent.visibility import SystemVisibilityBit, VisibilityMask, bit_mask
from schemas.agent.ui_hints import (
UiHintAction,
UiHintIntent,
UiHintSection,
UiHintStatus,
UiHintsPayload,
)
__all__ = [
"AgentType",
"AgentOutput",
"ConstraintItem",
"ExecutionMode",
"ForwardedPropsPayload",
"KeyEntity",
"NormalizedTaskInput",
"ResultTyping",
"ClientTimeContext",
"ResultType",
"RouterAgentOutput",
"RunStatus",
"RuntimeMode",
"TaskType",
"TaskTyping",
"SystemAgentLLMConfig",
"SystemVisibilityBit",
"ToolAgentOutput",
"ToolStatus",
"UiHintAction",
"UiHintIntent",
"UiHintSection",
"UiHintStatus",
"UiHintsPayload",
"VisibilityMask",
"WorkerAgentOutputLite",
"WorkerAgentOutputRich",
"bit_mask",
"parse_forwarded_props_client_time",
"parse_forwarded_props_runtime_mode",
"resolve_worker_output_model",
]
@@ -0,0 +1,93 @@
from __future__ import annotations
from datetime import datetime
from enum import Enum
import re
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
from pydantic import (
BaseModel,
ConfigDict,
Field,
StrictInt,
ValidationError,
field_validator,
)
_RFC3339_WITH_TZ_PATTERN = re.compile(
r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$"
)
class ClientTimeContext(BaseModel):
model_config = ConfigDict(extra="forbid")
device_timezone: str = Field(
...,
description="IANA timezone from client device, e.g. America/Los_Angeles.",
)
client_now_iso: str = Field(
...,
description="RFC3339 datetime with timezone offset from client device.",
)
client_epoch_ms: StrictInt = Field(
...,
ge=0,
description="Unix epoch milliseconds from client device.",
)
@field_validator("device_timezone")
@classmethod
def validate_device_timezone(cls, value: str) -> str:
try:
ZoneInfo(value)
except ZoneInfoNotFoundError as exc:
raise ValueError("invalid client_time.device_timezone") from exc
return value
@field_validator("client_now_iso")
@classmethod
def validate_client_now_iso(cls, value: str) -> str:
if not _RFC3339_WITH_TZ_PATTERN.fullmatch(value):
raise ValueError("invalid client_time.client_now_iso")
normalized = value.replace("Z", "+00:00")
try:
parsed = datetime.fromisoformat(normalized)
except ValueError as exc:
raise ValueError("invalid client_time.client_now_iso") from exc
if parsed.tzinfo is None:
raise ValueError("invalid client_time.client_now_iso")
return value
class RuntimeMode(str, Enum):
CHAT = "chat"
AUTOMATION = "automation"
class ForwardedPropsPayload(BaseModel):
model_config = ConfigDict(extra="forbid")
runtime_mode: RuntimeMode
client_time: ClientTimeContext | None = None
def parse_forwarded_props(forwarded_props: object) -> ForwardedPropsPayload:
if not isinstance(forwarded_props, dict):
raise ValueError("invalid RunAgentInput.forwardedProps")
try:
return ForwardedPropsPayload.model_validate(forwarded_props)
except ValidationError as exc:
raise ValueError("invalid RunAgentInput.forwardedProps") from exc
def parse_forwarded_props_client_time(
forwarded_props: object,
) -> ClientTimeContext | None:
payload = parse_forwarded_props(forwarded_props)
return payload.client_time
def parse_forwarded_props_runtime_mode(forwarded_props: object) -> RuntimeMode:
payload = parse_forwarded_props(forwarded_props)
return payload.runtime_mode
+177
View File
@@ -0,0 +1,177 @@
from __future__ import annotations
from enum import Enum
from typing import Any
from pydantic import BaseModel, ConfigDict, Field, field_validator
from schemas.agent.ui_hints import UiHintsPayload
class TaskType(str, Enum):
KNOWLEDGE = "knowledge"
RECOMMENDATION = "recommendation"
PLANNING = "planning"
SCHEDULING = "scheduling"
REMINDER_MANAGEMENT = "reminder_management"
TODO_MANAGEMENT = "todo_management"
COMMUNICATION_DRAFTING = "communication_drafting"
INFORMATION_ORGANIZATION = "information_organization"
STATUS_TRACKING = "status_tracking"
TRANSACTION_ASSIST = "transaction_assist"
ACTION_EXECUTION = "action_execution"
TROUBLESHOOTING = "troubleshooting"
UNKNOWN = "unknown"
class ResultType(str, Enum):
DIRECT_ANSWER = "direct_answer"
OPTIONS_WITH_RECOMMENDATION = "options_with_recommendation"
ACTION_PLAN = "action_plan"
SCHEDULE_PROPOSAL = "schedule_proposal"
TODO_LIST = "todo_list"
DRAFT_MESSAGE = "draft_message"
SUMMARY = "summary"
PROGRESS_SUMMARY = "progress_summary"
DIAGNOSIS_REPORT = "diagnosis_report"
STRUCTURED_PAYLOAD = "structured_payload"
EXECUTION_REPORT = "execution_report"
CLARIFICATION_REQUEST = "clarification_request"
SAFETY_BLOCK = "safety_block"
ERROR_REPORT = "error_report"
UNKNOWN = "unknown"
class TaskTyping(BaseModel):
model_config = ConfigDict(extra="forbid")
primary: TaskType
secondary: list[TaskType] = Field(default_factory=list, max_length=3)
class ResultTyping(BaseModel):
model_config = ConfigDict(extra="forbid")
primary: ResultType
secondary: list[ResultType] = Field(default_factory=list, max_length=3)
class ExecutionMode(str, Enum):
ONESTEP = "onestep"
TOOL_ASSISTED = "tool_assisted"
MULTISTEP = "multistep"
class RunStatus(str, Enum):
SUCCESS = "success"
PARTIAL_SUCCESS = "partial_success"
FAILED = "failed"
class ToolStatus(str, Enum):
SUCCESS = "success"
FAILURE = "failure"
PARTIAL = "partial"
class KeyEntity(BaseModel):
model_config = ConfigDict(extra="forbid")
name: str
type: str
value: str | None = None
@field_validator("value", mode="before")
@classmethod
def normalize_value(cls, value: object) -> object:
if value is None:
return None
if isinstance(value, str):
return value
if isinstance(value, bool | int | float):
return str(value)
return value
class ConstraintItem(BaseModel):
model_config = ConfigDict(extra="forbid")
key: str
value: str
required: bool = True
@field_validator("value", mode="before")
@classmethod
def normalize_value(cls, value: object) -> object:
if isinstance(value, bool | int | float):
return str(value)
return value
class NormalizedTaskInput(BaseModel):
model_config = ConfigDict(extra="forbid")
user_text: str
multimodal_summary: list[str] = Field(default_factory=list)
context_summary: str = Field(default="", max_length=2000)
class RouterAgentOutput(BaseModel):
model_config = ConfigDict(extra="forbid")
normalized_task_input: NormalizedTaskInput
key_entities: list[KeyEntity] = Field(default_factory=list)
constraints: list[ConstraintItem] = Field(default_factory=list)
task_typing: TaskTyping
execution_mode: ExecutionMode
result_typing: ResultTyping
class ErrorInfo(BaseModel):
model_config = ConfigDict(extra="forbid")
code: str
message: str
retryable: bool = False
details: dict[str, Any] | None = None
class ToolAgentOutput(BaseModel):
model_config = ConfigDict(extra="forbid")
tool_name: str
tool_call_id: str
tool_call_args: dict[str, Any] | None = None
status: ToolStatus
result: str
error: ErrorInfo | None = None
class WorkerAgentOutputLite(BaseModel):
model_config = ConfigDict(extra="forbid")
status: RunStatus = RunStatus.SUCCESS
answer: str
key_points: list[str] = Field(default_factory=list)
result_type: ResultType = ResultType.UNKNOWN
suggested_actions: list[str] = Field(default_factory=list)
error: ErrorInfo | None = None
class WorkerAgentOutputRich(WorkerAgentOutputLite):
ui_hints: UiHintsPayload | None = None
class AgentOutput(WorkerAgentOutputRich):
model_config = ConfigDict(extra="forbid")
WorkerAgentOutput = WorkerAgentOutputLite | WorkerAgentOutputRich
def resolve_worker_output_model(
execution_mode: ExecutionMode,
) -> type[WorkerAgentOutputLite]:
if execution_mode == ExecutionMode.ONESTEP:
return WorkerAgentOutputLite
return WorkerAgentOutputRich
+30
View File
@@ -0,0 +1,30 @@
from __future__ import annotations
from enum import Enum
from pydantic import BaseModel, Field
class AgentType(str, Enum):
ROUTER = "router"
WORKER = "worker"
class ContextBuildStrategy(str, Enum):
DAY = "day"
NUMBER = "number"
class ContextMessagesConfig(BaseModel):
mode: ContextBuildStrategy = ContextBuildStrategy.NUMBER
count: int = Field(default=20, ge=1, le=200)
class SystemAgentLLMConfig(BaseModel):
temperature: float | None = Field(default=None, ge=0.0, le=2.0)
max_tokens: int | None = Field(default=None, ge=1)
timeout_seconds: float | None = Field(default=30.0, gt=0.0, le=300.0)
context_messages: ContextMessagesConfig = Field(
default_factory=ContextMessagesConfig
)
enabled_tools: list[str] = Field(default_factory=list, max_length=32)
+349
View File
@@ -0,0 +1,349 @@
"""
UiHints - 描述性 UI 提示
设计原则:
- 描述性而非渲染性: 告诉编译器“要展示什么”,而不是“如何渲染”
- 最小化 token: 保持字段简洁
- 可编译: 可机械转换为 UiSchemaRenderer
- 尽量无损: hints 中的主要内容字段应尽量被保留到 renderer 中
Version: 2.1
"""
from __future__ import annotations
from enum import Enum
import re
from typing import Any, ClassVar, Literal
from pydantic import BaseModel, ConfigDict, Field
from pydantic import field_validator
_NAVIGATION_PATH_PATTERN = re.compile(r"^/[A-Za-z0-9/_-]*$")
_NAVIGATION_PARAM_KEY_PATTERN = re.compile(r"^[A-Za-z][A-Za-z0-9_]{0,31}$")
_MAX_NAVIGATION_PARAMS = 8
# ============================================================
# Enums
# ============================================================
class UiHintStatus(str, Enum):
INFO = "info"
SUCCESS = "success"
WARNING = "warning"
ERROR = "error"
PENDING = "pending"
class UiHintIntent(str, Enum):
"""主要展示意图(弱提示,不应决定字段生死)"""
MESSAGE = "message" # 普通消息/说明
DATA = "data" # 数据/结果摘要
LIST = "list" # 列表为主
STATUS = "status" # 状态结果为主
FORM = "form" # 结构化内容(当前不表示真实输入表单)
MIXED = "mixed" # 混合内容
class UiHintActionStyle(str, Enum):
PRIMARY = "primary"
SECONDARY = "secondary"
GHOST = "ghost"
DANGER = "danger"
class UiHintTextFormat(str, Enum):
PLAIN = "plain"
MARKDOWN = "markdown"
class UiHintActionType(str, Enum):
NAVIGATION = "navigation"
URL = "url"
EVENT = "event"
TOOL = "tool"
COPY = "copy"
PAYLOAD = "payload"
class UiHintIconSource(str, Enum):
ICON = "icon"
EMOJI = "emoji"
URL = "url"
# ============================================================
# Base Config
# ============================================================
class UiHintBaseModel(BaseModel):
model_config: ClassVar[ConfigDict] = ConfigDict(
extra="forbid",
populate_by_name=True,
)
# ============================================================
# Action Targets
# ============================================================
class UiHintActionNavigation(UiHintBaseModel):
type: Literal["navigation"]
path: str = Field(..., description="Internal route path.")
params: dict[str, Any] | None = Field(default=None, description="Route params.")
@field_validator("path")
@classmethod
def validate_navigation_path(cls, value: str) -> str:
path = value.strip()
if not path:
raise ValueError("navigation path must not be empty")
if len(path) > 256:
raise ValueError("navigation path is too long")
if path.startswith("//") or "://" in path:
raise ValueError("navigation path must be internal")
if "?" in path or "#" in path:
raise ValueError("navigation path must not contain query or fragment")
if ":" in path:
raise ValueError("navigation path must be concrete without placeholders")
if _NAVIGATION_PATH_PATTERN.fullmatch(path) is None:
raise ValueError("navigation path contains unsupported characters")
return path
@field_validator("params")
@classmethod
def validate_navigation_params(
cls, value: dict[str, Any] | None
) -> dict[str, Any] | None:
if value is None:
return None
if len(value) > _MAX_NAVIGATION_PARAMS:
raise ValueError("navigation params exceed limit")
normalized: dict[str, Any] = {}
for key, param_value in value.items():
if _NAVIGATION_PARAM_KEY_PATTERN.fullmatch(key) is None:
raise ValueError("navigation param key is invalid")
if isinstance(param_value, (str, int, float, bool)):
normalized[key] = param_value
continue
raise ValueError("navigation params must be scalar")
return normalized
class UiHintActionUrl(UiHintBaseModel):
type: Literal["url"]
url: str = Field(..., description="External URL.")
target: Literal["_self", "_blank"] | None = Field(default=None)
class UiHintActionEvent(UiHintBaseModel):
type: Literal["event"]
event: str = Field(..., description="Frontend event name.")
payload: dict[str, Any] | None = Field(default=None)
class UiHintActionTool(UiHintBaseModel):
type: Literal["tool"]
tool_id: str = Field(alias="toolId", description="Tool identifier.")
params: dict[str, Any] | None = Field(default=None)
class UiHintActionCopy(UiHintBaseModel):
type: Literal["copy"]
content: str = Field(..., description="Content to copy.")
success_message: str | None = Field(alias="successMessage", default=None)
class UiHintActionPayload(UiHintBaseModel):
type: Literal["payload"]
payload: dict[str, Any] = Field(..., description="Structured payload.")
submit_to: str | None = Field(alias="submitTo", default=None)
UiHintActionTarget = (
UiHintActionNavigation
| UiHintActionUrl
| UiHintActionEvent
| UiHintActionTool
| UiHintActionCopy
| UiHintActionPayload
)
class UiHintAction(UiHintBaseModel):
label: str = Field(..., description="Button label.")
style: UiHintActionStyle | None = Field(default=None, description="Button style.")
disabled: bool = Field(default=False, description="Disabled state.")
action: UiHintActionTarget = Field(..., description="Action to execute.")
# ============================================================
# Small Descriptive Models
# ============================================================
class UiHintIcon(UiHintBaseModel):
source: UiHintIconSource = Field(default=UiHintIconSource.ICON)
value: str = Field(..., description="Icon identifier / emoji / url.")
color: str | None = Field(default=None)
size: int | None = Field(default=None)
class UiHintKvItem(UiHintBaseModel):
key: str = Field(..., description="Key identifier.")
label: str | None = Field(default=None, description="Display label.")
value: Any = Field(default=None, description="Value.")
copyable: bool = Field(default=False, description="Allow copy.")
class UiHintListItem(UiHintBaseModel):
id: str | None = Field(default=None)
title: str = Field(..., description="Item title.")
subtitle: str | None = Field(default=None)
description: str | None = Field(default=None)
icon: UiHintIcon | None = Field(default=None)
status: UiHintStatus | None = Field(default=None)
actions: list[UiHintAction] = Field(default_factory=list)
@field_validator("status", mode="before")
@classmethod
def normalize_status(cls, value: object) -> object:
if value is None:
return None
if isinstance(value, dict):
status_type = value.get("type")
if isinstance(status_type, str):
return status_type
status_value = value.get("status")
if isinstance(status_value, str):
return status_value
return value
class UiHintSection(UiHintBaseModel):
title: str | None = Field(default=None, description="Section title.")
description: str | None = Field(default=None, description="Section description.")
icon: UiHintIcon | None = Field(default=None, description="Section icon.")
content: str | None = Field(default=None, description="Main text content.")
content_format: UiHintTextFormat = Field(
default=UiHintTextFormat.PLAIN,
alias="contentFormat",
description="Section content text format.",
)
items: list[UiHintKvItem] = Field(default_factory=list, description="KV items.")
list_items: list[UiHintListItem] = Field(
default_factory=list,
alias="listItems",
description="List items.",
)
actions: list[UiHintAction] = Field(default_factory=list, description="Actions.")
# ============================================================
# Root Payload
# ============================================================
class UiHintsPayload(UiHintBaseModel):
"""
描述性 UI 提示
设计目标:
- agent 输出尽可能短
- 不表达布局细节
- 编译器负责转换为完整 UiSchemaRenderer
"""
model_config: ClassVar[ConfigDict] = ConfigDict(
extra="forbid",
populate_by_name=True,
json_schema_extra={
"examples": [
{
"intent": "status",
"status": "success",
"title": "日程已创建",
"body": "本次创建已成功完成。",
"items": [
{"key": "title", "label": "主题", "value": "Q1 规划会议"},
{"key": "time", "label": "时间", "value": "2026-03-15 14:00"},
],
"actions": [
{
"label": "查看详情",
"style": "primary",
"action": {
"type": "navigation",
"path": "/calendar/evt_123",
},
},
{
"label": "删除",
"style": "danger",
"action": {
"type": "tool",
"toolId": "calendar.delete",
"params": {"eventId": "evt_123"},
},
},
],
}
]
},
)
version: str = Field(default="2.1")
intent: UiHintIntent = Field(
default=UiHintIntent.MESSAGE,
description="Primary display intent.",
)
status: UiHintStatus = Field(
default=UiHintStatus.INFO,
description="Overall status.",
)
title: str | None = Field(default=None, description="Top-level title.")
description: str | None = Field(default=None, description="Top-level description.")
body: str | None = Field(default=None, description="Top-level main body text.")
body_format: UiHintTextFormat = Field(
default=UiHintTextFormat.PLAIN,
alias="bodyFormat",
description="Body text format.",
)
items: list[UiHintKvItem] = Field(
default_factory=list,
description="Top-level key-value items.",
)
list_items: list[UiHintListItem] = Field(
default_factory=list,
alias="listItems",
description="Top-level list items.",
)
sections: list[UiHintSection] = Field(
default_factory=list,
description="Grouped sections.",
)
actions: list[UiHintAction] = Field(
default_factory=list,
description="Top-level actions.",
)
icon: UiHintIcon | None = Field(
default=None,
description="Top-level icon.",
)
meta: dict[str, Any] = Field(
default_factory=dict,
description="Extra meta, e.g. requestId/toolId/traceId/userId.",
)
+628
View File
@@ -0,0 +1,628 @@
"""
UI Schema Renderer Protocol
目标:
- 只保留“基础组件 + 布局容器”
- 最终返回一个 UiSchemaRenderer
- 前端只需要递归渲染 root 布局树即可
Version: 2.0
"""
from __future__ import annotations
from enum import Enum
from typing import Any, Literal, TypedDict, Union
# ============================================================
# Enums
# ============================================================
class UiStatus(str, Enum):
INFO = "info"
SUCCESS = "success"
WARNING = "warning"
ERROR = "error"
PENDING = "pending"
class IconSource(str, Enum):
ICON = "icon"
EMOJI = "emoji"
URL = "url"
class TextFormat(str, Enum):
PLAIN = "plain"
MARKDOWN = "markdown"
class TextRole(str, Enum):
TITLE = "title"
SUBTITLE = "subtitle"
BODY = "body"
CAPTION = "caption"
CODE = "code"
class ButtonStyle(str, Enum):
PRIMARY = "primary"
SECONDARY = "secondary"
GHOST = "ghost"
DANGER = "danger"
class LayoutDirection(str, Enum):
VERTICAL = "vertical"
HORIZONTAL = "horizontal"
class LayoutAppearance(str, Enum):
PLAIN = "plain"
CARD = "card"
SECTION = "section"
class LayoutAlign(str, Enum):
START = "start"
CENTER = "center"
END = "end"
STRETCH = "stretch"
class LayoutJustify(str, Enum):
START = "start"
CENTER = "center"
END = "end"
SPACE_BETWEEN = "space-between"
class RendererTheme(str, Enum):
DEFAULT = "default"
LIGHT = "light"
DARK = "dark"
# ============================================================
# Meta
# ============================================================
class UiMeta(TypedDict, total=False):
requestId: str
toolId: str
traceId: str
userId: str
# ============================================================
# Action Payloads
# ============================================================
class NavigateAction(TypedDict, total=False):
type: Literal["navigation"]
path: str
params: dict[str, Any]
class UrlAction(TypedDict, total=False):
type: Literal["url"]
url: str
target: Literal["_self", "_blank"]
class EventAction(TypedDict, total=False):
type: Literal["event"]
event: str
payload: dict[str, Any]
class ToolAction(TypedDict, total=False):
type: Literal["tool"]
toolId: str
params: dict[str, Any]
class CopyAction(TypedDict, total=False):
type: Literal["copy"]
content: str
successMessage: str
class PayloadAction(TypedDict, total=False):
type: Literal["payload"]
payload: dict[str, Any]
submitTo: str
UiActionPayload = Union[
NavigateAction,
UrlAction,
EventAction,
ToolAction,
CopyAction,
PayloadAction,
]
# ============================================================
# Shared Small Types
# ============================================================
class UiIconSpec(TypedDict, total=False):
source: str
value: str
color: str
size: int
class UiKvItem(TypedDict, total=False):
key: str
label: str
value: Any
copyable: bool
class UiBaseNode(TypedDict, total=False):
id: str
visible: bool
# ============================================================
# Primitive Components
# ============================================================
class UiTextNode(UiBaseNode, total=False):
type: Literal["text"]
content: str
format: str # TextFormat
role: str # TextRole
status: str # UiStatus
maxLines: int
class UiIconNode(UiBaseNode, total=False):
type: Literal["icon"]
source: str # IconSource
value: str
color: str
size: int
class UiBadgeNode(UiBaseNode, total=False):
type: Literal["badge"]
label: str
status: str # UiStatus
class UiButtonNode(UiBaseNode, total=False):
type: Literal["button"]
label: str
style: str # ButtonStyle
disabled: bool
icon: UiIconSpec
action: UiActionPayload
class UiKvNode(UiBaseNode, total=False):
type: Literal["kv"]
items: list[UiKvItem]
columns: int
class UiDividerNode(UiBaseNode, total=False):
type: Literal["divider"]
inset: int
# ============================================================
# Layout Containers
# ============================================================
class UiStackNode(UiBaseNode, total=False):
type: Literal["stack"]
direction: str # LayoutDirection
gap: int
appearance: str # LayoutAppearance
status: str # UiStatus
align: str # LayoutAlign
justify: str # LayoutJustify
wrap: bool
children: list["UiNode"]
class UiGridNode(UiBaseNode, total=False):
type: Literal["grid"]
columns: int
gap: int
appearance: str # LayoutAppearance
status: str # UiStatus
children: list["UiNode"]
UiNode = Union[
UiTextNode,
UiIconNode,
UiBadgeNode,
UiButtonNode,
UiKvNode,
UiDividerNode,
UiStackNode,
UiGridNode,
]
UiLayoutNode = Union[UiStackNode, UiGridNode]
# ============================================================
# Root Renderer
# ============================================================
class UiSchemaRenderer(TypedDict, total=False):
version: str
locale: str
status: str # UiStatus
theme: str # RendererTheme
meta: UiMeta
root: UiLayoutNode
# ============================================================
# Root Builder
# ============================================================
def build_renderer(
root: UiLayoutNode,
*,
version: str = "2.0",
locale: str = "zh-CN",
status: UiStatus = UiStatus.INFO,
theme: RendererTheme = RendererTheme.DEFAULT,
meta: UiMeta | None = None,
) -> UiSchemaRenderer:
renderer: UiSchemaRenderer = {
"version": version,
"locale": locale,
"status": status.value,
"theme": theme.value,
"root": root,
}
if meta:
renderer["meta"] = meta
return renderer
# ============================================================
# Primitive Builders
# ============================================================
def build_text(
content: str,
*,
node_id: str | None = None,
format: TextFormat = TextFormat.PLAIN,
role: TextRole = TextRole.BODY,
status: UiStatus | None = None,
max_lines: int | None = None,
visible: bool = True,
) -> UiTextNode:
node: UiTextNode = {
"type": "text",
"content": content,
"format": format.value,
"role": role.value,
"visible": visible,
}
if node_id:
node["id"] = node_id
if status:
node["status"] = status.value
if max_lines is not None:
node["maxLines"] = max_lines
return node
def build_icon(
source: IconSource,
value: str,
*,
node_id: str | None = None,
color: str | None = None,
size: int | None = None,
visible: bool = True,
) -> UiIconNode:
node: UiIconNode = {
"type": "icon",
"source": source.value,
"value": value,
"visible": visible,
}
if node_id:
node["id"] = node_id
if color:
node["color"] = color
if size is not None:
node["size"] = size
return node
def build_badge(
label: str,
*,
node_id: str | None = None,
status: UiStatus = UiStatus.INFO,
visible: bool = True,
) -> UiBadgeNode:
node: UiBadgeNode = {
"type": "badge",
"label": label,
"status": status.value,
"visible": visible,
}
if node_id:
node["id"] = node_id
return node
def build_button(
label: str,
action: UiActionPayload,
*,
node_id: str | None = None,
style: ButtonStyle = ButtonStyle.PRIMARY,
disabled: bool = False,
icon: UiIconSpec | None = None,
visible: bool = True,
) -> UiButtonNode:
node: UiButtonNode = {
"type": "button",
"label": label,
"style": style.value,
"disabled": disabled,
"action": action,
"visible": visible,
}
if node_id:
node["id"] = node_id
if icon:
node["icon"] = icon
return node
def build_kv(
items: list[UiKvItem],
*,
node_id: str | None = None,
columns: int = 1,
visible: bool = True,
) -> UiKvNode:
node: UiKvNode = {
"type": "kv",
"items": items,
"columns": columns,
"visible": visible,
}
if node_id:
node["id"] = node_id
return node
def build_divider(
*,
node_id: str | None = None,
inset: int = 0,
visible: bool = True,
) -> UiDividerNode:
node: UiDividerNode = {
"type": "divider",
"inset": inset,
"visible": visible,
}
if node_id:
node["id"] = node_id
return node
# ============================================================
# Layout Builders
# ============================================================
def build_stack(
children: list[UiNode],
*,
node_id: str | None = None,
direction: LayoutDirection = LayoutDirection.VERTICAL,
gap: int = 12,
appearance: LayoutAppearance = LayoutAppearance.PLAIN,
status: UiStatus | None = None,
align: LayoutAlign = LayoutAlign.START,
justify: LayoutJustify = LayoutJustify.START,
wrap: bool = False,
visible: bool = True,
) -> UiStackNode:
node: UiStackNode = {
"type": "stack",
"direction": direction.value,
"gap": gap,
"appearance": appearance.value,
"align": align.value,
"justify": justify.value,
"wrap": wrap,
"children": children,
"visible": visible,
}
if node_id:
node["id"] = node_id
if status:
node["status"] = status.value
return node
def build_grid(
children: list[UiNode],
*,
columns: int,
node_id: str | None = None,
gap: int = 12,
appearance: LayoutAppearance = LayoutAppearance.PLAIN,
status: UiStatus | None = None,
visible: bool = True,
) -> UiGridNode:
node: UiGridNode = {
"type": "grid",
"columns": columns,
"gap": gap,
"appearance": appearance.value,
"children": children,
"visible": visible,
}
if node_id:
node["id"] = node_id
if status:
node["status"] = status.value
return node
# ============================================================
# Small Action Builders
# ============================================================
def action_navigation(
path: str, params: dict[str, Any] | None = None
) -> NavigateAction:
action: NavigateAction = {"type": "navigation", "path": path}
if params:
action["params"] = params
return action
def action_url(url: str, target: Literal["_self", "_blank"] = "_blank") -> UrlAction:
return {"type": "url", "url": url, "target": target}
def action_event(event: str, payload: dict[str, Any] | None = None) -> EventAction:
action: EventAction = {"type": "event", "event": event}
if payload:
action["payload"] = payload
return action
def action_tool(tool_id: str, params: dict[str, Any] | None = None) -> ToolAction:
action: ToolAction = {"type": "tool", "toolId": tool_id}
if params:
action["params"] = params
return action
def action_copy(content: str, success_message: str | None = None) -> CopyAction:
action: CopyAction = {"type": "copy", "content": content}
if success_message:
action["successMessage"] = success_message
return action
def action_payload(
payload: dict[str, Any], submit_to: str | None = None
) -> PayloadAction:
action: PayloadAction = {"type": "payload", "payload": payload}
if submit_to:
action["submitTo"] = submit_to
return action
# ============================================================
# Derived Helpers (协议外的便捷封装,不是基础原语)
# ============================================================
def build_card(
children: list[UiNode],
*,
node_id: str | None = None,
gap: int = 12,
status: UiStatus | None = None,
) -> UiStackNode:
return build_stack(
children,
node_id=node_id,
direction=LayoutDirection.VERTICAL,
gap=gap,
appearance=LayoutAppearance.CARD,
status=status,
)
def build_section(
title: str,
children: list[UiNode],
*,
node_id: str | None = None,
description: str | None = None,
status: UiStatus | None = None,
gap: int = 12,
) -> UiStackNode:
header_nodes: list[UiNode] = [build_text(title, role=TextRole.TITLE)]
if description:
header_nodes.append(build_text(description, role=TextRole.CAPTION))
all_children = header_nodes + children
return build_stack(
all_children,
node_id=node_id,
direction=LayoutDirection.VERTICAL,
gap=gap,
appearance=LayoutAppearance.SECTION,
status=status,
)
def build_status_panel(
title: str,
message: str,
*,
status: UiStatus,
primary_button: UiButtonNode | None = None,
secondary_button: UiButtonNode | None = None,
node_id: str | None = None,
) -> UiStackNode:
status_label = f"ui.status.{status.value}"
children: list[UiNode] = [
build_stack(
[
build_text(title, role=TextRole.TITLE),
build_badge(label=status_label, status=status),
],
direction=LayoutDirection.HORIZONTAL,
gap=8,
align=LayoutAlign.CENTER,
justify=LayoutJustify.SPACE_BETWEEN,
),
build_text(message, role=TextRole.BODY, status=status),
]
actions: list[UiNode] = []
if primary_button:
actions.append(primary_button)
if secondary_button:
actions.append(secondary_button)
if actions:
children.append(
build_stack(
actions,
direction=LayoutDirection.HORIZONTAL,
gap=8,
)
)
return build_card(children, node_id=node_id, status=status)
+50
View File
@@ -0,0 +1,50 @@
from __future__ import annotations
from enum import IntEnum
from pydantic import BaseModel, ConfigDict, Field, field_validator
class SystemVisibilityBit(IntEnum):
UI_HISTORY = 0
CONTEXT_ASSEMBLY = 1
class VisibilityMask(BaseModel):
model_config = ConfigDict(extra="forbid")
value: int = Field(..., ge=0, le=(1 << 63) - 1)
@classmethod
def from_bits(cls, *, bits: list[int]) -> "VisibilityMask":
mask = 0
for bit in bits:
validate_visibility_bit(bit=bit)
mask |= 1 << bit
return cls(value=mask)
def contains(self, *, bit: int) -> bool:
validate_visibility_bit(bit=bit)
return bool(self.value & (1 << bit))
class VisibilityBitRef(BaseModel):
model_config = ConfigDict(extra="forbid")
bit: int = Field(..., ge=0, le=63)
@field_validator("bit")
@classmethod
def _validate_bit(cls, value: int) -> int:
validate_visibility_bit(bit=value)
return value
def validate_visibility_bit(*, bit: int) -> None:
if bit < 0 or bit > 63:
raise ValueError("visibility bit must be in range [0, 63]")
def bit_mask(*, bit: int) -> int:
validate_visibility_bit(bit=bit)
return 1 << bit