feat: 实现 Auth 全局状态机与 401 统一处理机制

- 新增 AuthSessionInvalidated 事件处理 token 失效场景
- ApiInterceptor 新增 authFailureCallback 单飞机制
- AuthBloc 区分 manual logout 与 auto expiry 语义
- 新增 startup recovery fallback 防止启动卡死

feat: 重构 Calendar DayWeek 视图事件布局引擎

- 新增 DayEventLayoutEngine 解耦事件计算与渲染
- 新增 DayTimelineMetrics 统一时间轴常量
- 新增 DayViewScale 支持捏合缩放

feat: 新增 Settings 页面共享 UI 组件

- 新增 BackTitlePageHeader 统一页面 header
- 新增 DetailHeaderActionMenu 统一操作菜单
- 新增 DestructiveActionSheet 统一删除确认
- 新增 AppToggleSwitch 统一开关组件

feat: Chat UI Schema 支持导航操作

- 支持 navigation 类型 action 触发内部路由跳转
- 新增路径验证与参数处理

chore: 更新相关测试覆盖 auth 失效路径
This commit is contained in:
qzl
2026-03-18 13:35:25 +08:00
parent 19981964fb
commit b34697660d
56 changed files with 2602 additions and 784 deletions
@@ -0,0 +1,76 @@
from __future__ import annotations
from functools import lru_cache
from pathlib import Path
from typing import Any, ClassVar
import yaml
from pydantic import BaseModel, ConfigDict, Field, ValidationError
class FrontendRoute(BaseModel):
model_config: ClassVar[ConfigDict] = ConfigDict(extra="forbid")
route_id: str
path: str
description: str
category: str
auth_required: bool
path_params: list[str] = Field(default_factory=list)
query_params: list[str] = Field(default_factory=list)
class FrontendRouteCatalog(BaseModel):
model_config: ClassVar[ConfigDict] = ConfigDict(extra="forbid")
version: str
routes: list[FrontendRoute]
def _default_catalog_path() -> Path:
return (
Path(__file__).resolve().parents[2]
/ "config"
/ "static"
/ "route"
/ "frontend_routes.yaml"
)
@lru_cache(maxsize=1)
def load_frontend_routes_catalog() -> FrontendRouteCatalog:
path = _default_catalog_path()
with path.open("r", encoding="utf-8") as file:
loaded: Any = yaml.safe_load(file) or {}
if not isinstance(loaded, dict):
raise ValueError(f"Invalid frontend routes catalog format: {path}")
try:
return FrontendRouteCatalog.model_validate(loaded)
except ValidationError as exc:
raise ValueError(f"Invalid frontend routes catalog data: {path}") from exc
def build_frontend_route_prompt() -> str:
catalog = load_frontend_routes_catalog()
lines = [
"[Frontend Route Catalog]",
f"version={catalog.version}",
"rules: use listed route_id only; output concrete path; no placeholders; no query in path; put query in params; params scalar only.",
"ROUTES:",
]
for route in catalog.routes:
path_params = ", ".join(route.path_params) if route.path_params else "none"
query_params = ", ".join(route.query_params) if route.query_params else "none"
lines.append(
"- "
f"route_id={route.route_id}; "
f"path={route.path}; "
f"path_params={path_params}; "
f"query_params={query_params}"
)
return "\n".join(lines)
@@ -9,6 +9,7 @@ from ag_ui.core.types import Tool
from core.agentscope.prompts.agent_prompt import (
build_agent_prompt,
)
from core.agentscope.prompts.route_prompt import build_frontend_route_prompt
from core.agentscope.prompts.tool_prompt import build_tools_prompt
from schemas.agent.system_agent import AgentType
from schemas.agent.forwarded_props import ClientTimeContext
@@ -19,6 +20,7 @@ def _wrap_section(section: str, content: str) -> str:
marker_map = {
"env": ("<!-- ENV_START -->", "<!-- ENV_END -->"),
"identity": ("<!-- IDENTITY_START -->", "<!-- IDENTITY_END -->"),
"route": ("<!-- ROUTE_START -->", "<!-- ROUTE_END -->"),
"schema": ("<!-- SCHEMA_START -->", "<!-- SCHEMA_END -->"),
"safety": ("<!-- SAFETY_START -->", "<!-- SAFETY_END -->"),
"output": ("<!-- OUTPUT_START -->", "<!-- OUTPUT_END -->"),
@@ -193,6 +195,10 @@ def _build_output_rules() -> str:
)
def _build_route_section() -> str:
return _wrap_section("route", build_frontend_route_prompt())
def build_system_prompt(
*,
agent_type: AgentType,
@@ -202,7 +208,7 @@ def build_system_prompt(
extra_context: str | None = None,
tools: Sequence[Tool | dict[str, Any]] | None = None,
) -> str:
sections = [
sections: list[str | None] = [
_build_identity_section(),
_build_env_section(
user_context=user_context,
@@ -210,6 +216,7 @@ def build_system_prompt(
runtime_client_time=runtime_client_time,
extra_context=extra_context,
),
_build_route_section(),
_build_safety_section(),
build_agent_prompt(
agent_type=agent_type,