bugfix:修复一些bug

This commit is contained in:
2026-10-02 02:36:11 -07:00
parent 68588d1f92
commit 5b1e6c5654
14 changed files with 37 additions and 81 deletions
+3 -13
View File
@@ -7,7 +7,7 @@ from copy import deepcopy
from app.actions.registry import apply_action
from app.actions.update import fit_existing_path
from app.resources.material_fields import check_update_field
from app.resources.material_fields import check_update_field, find_material
from app.schemas.actions import Action, AddAction, UpdateAction
from app.schemas.capabilities import DATA_BINDING_ERROR, DATA_BINDING_SUPPORTED
from app.schemas.component_resource import ComponentCatalog
@@ -39,18 +39,8 @@ def merge_config(defaults: dict, overrides: dict) -> dict:
def complete_add(action: AddAction, catalog: ComponentCatalog) -> AddAction:
"""用物料默认配置补全新增组件,模型显式写出的字段优先,并按物料设置 sample_data"""
material = next(
(item for item in catalog.components if item.key == action.component_key), None
)
if material is None:
raise ValueError(
f"物料 {action.component_key} 不存在,可用物料用 list_components 查看"
)
if material.type != action.node.type:
raise ValueError(
f"物料 {material.key} 的类型是 {material.type},node.type 必须与之一致"
)
"""按 node.type 找物料补全新增组件,模型显式写出的字段优先,并按物料设置 sample_data"""
material = find_material(catalog, action.node.type)
if not DATA_BINDING_SUPPORTED and action.node.model_extra.get("dataId"):
raise ValueError(DATA_BINDING_ERROR)
overrides = action.node.model_dump(mode="json", exclude_unset=True)
+21 -1
View File
@@ -8,6 +8,7 @@
import asyncio
import json
import os
import re
import socket
import subprocess
import sys
@@ -29,18 +30,37 @@ CONFIG_PATH = PROJECT_ROOT / "mcp_servers.json"
# 物料服务返回的目录版本须与主进程一致,否则模型查到的字段和校验依据不同
MATERIAL_SERVER = "materials"
LOCAL_HOSTS = {"localhost", "127.0.0.1"}
# 配置中引用环境变量的写法
ENV_REFERENCE = re.compile(r"\$\{(\w+)\}")
class MCPServiceError(RuntimeError):
"""MCP 配置错误、服务不可用,或物料目录版本不一致"""
def _expand_env(value):
"""只展开字符串值里的 ${NAME};变量未设置时报错,不把字面量当成地址或密钥"""
if isinstance(value, dict):
return {key: _expand_env(item) for key, item in value.items()}
if isinstance(value, list):
return [_expand_env(item) for item in value]
if not isinstance(value, str):
return value
def lookup(match: re.Match) -> str:
if (found := os.environ.get(match[1])) is None:
raise MCPServiceError(f"MCP 服务配置引用的环境变量 {match[1]} 未设置")
return found
return ENV_REFERENCE.sub(lookup, value)
def load_mcp_config(path: Path = CONFIG_PATH) -> dict[str, dict]:
"""读取服务列表并展开环境变量;文件缺失或格式不对时报错"""
if not path.exists():
raise MCPServiceError(f"未找到 MCP 服务配置: {path}")
try:
config = json.loads(os.path.expandvars(path.read_text(encoding="utf-8")))
config = _expand_env(json.loads(path.read_text(encoding="utf-8")))
except json.JSONDecodeError as error:
raise MCPServiceError(f"MCP 服务配置不是合法 JSON: {error}") from error
if not isinstance(config, dict) or not all(
+6
View File
@@ -17,6 +17,7 @@ from app.core.logging import setup_logging
@asynccontextmanager
async def lifespan(application: FastAPI):
setup_logging()
from app.agent.screen_agent import get_screen_agent
from app.clients.decision import close_decision_engine
from app.clients.mcp import start_mcp_servers, stop_mcp_servers
from app.clients.user_center import close_user_center
@@ -35,6 +36,11 @@ async def lifespan(application: FastAPI):
await warm_material_cache(persistence.db)
# 拉起 mcp_servers.json 中由应用管理的服务;单个服务失败只记录,用到它的请求执行时报错
mcp_processes = await start_mcp_servers()
# 提前拉取 MCP 工具并组装 agent,问题在启动时暴露;失败只记录,首个请求会重试
try:
await get_screen_agent()
except Exception as error:
logger.opt(exception=error).error("screen agent 预先组装失败")
application.state.agent_service = AgentService(
build_graph(persistence.checkpointer), persistence.store
)
-20
View File
@@ -55,26 +55,6 @@ def flush_tracing() -> None:
logger.opt(exception=exc).warning("Langfuse flush 失败")
async def ainvoke_screen(
graph: Any,
payload: Any,
*,
thread_id: str,
session_id: str | None = None,
user_id: int | None = None,
) -> Any:
"""每轮调用记录一个根 span,返回、等待用户或异常时结束。
session_id 由调用方在首次与恢复时一致传入(通常为 conversation_id)。
未传时始终使用 thread_id,不从仅首次请求才有的 payload 推断。
user_id 为用户中心的用户 ID,接口调用时传入,本地脚本不传。
"""
with _screen_trace(payload, thread_id, session_id, user_id) as (config, root):
result = await graph.ainvoke(payload, config=config)
_finish_trace(root, result)
return result
async def astream_screen(graph, payload, *, thread_id, session_id=None, user_id=None):
"""同一轮流保持同一个追踪上下文,关闭流时结束追踪"""
from contextlib import aclosing
-4
View File
@@ -81,7 +81,3 @@ def build_graph(checkpointer=None):
if checkpointer is None:
checkpointer = InMemorySaver(serde=build_checkpoint_serde())
return workflow.compile(checkpointer=checkpointer)
# 模块加载时创建共享图实例,复用该实例的调用也共用其检查点存储
query_app = build_graph()
-2
View File
@@ -62,8 +62,6 @@ class ScreenAgentNode(BaseNode):
]
},
context=ScreenAgentContext(screen=context.screen, catalog=catalog),
# 调用次数由 ModelCallLimitMiddleware 限制,这里只防止异常循环
config={"recursion_limit": 4 * self.config.max_agent_steps + 10},
)
if result.get("structured_response") is None:
last = result["messages"][-1]
+1 -1
View File
@@ -105,7 +105,7 @@ def list_components(catalog: ComponentCatalog) -> dict:
def get_materials(catalog: ComponentCatalog, component_types: list[str]) -> dict:
"""指定类型的物料默认配置,同一类型的多个预设全部返回"""
"""指定类型的物料默认配置"""
known = {item.type for item in catalog.components}
if unknown := [item for item in component_types if item not in known]:
raise ValueError(
+2 -6
View File
@@ -5,7 +5,6 @@ from typing import Literal
from pydantic import Field
from app.schemas.actions.base import ActionModel
from app.schemas.base import NonBlankString
from app.schemas.screen import ScreenNode
@@ -13,13 +12,10 @@ class AddAction(ActionModel):
"""按物料新增组件"""
type: Literal["add"] = "add"
component_key: NonBlankString = Field(
description="物料目录编号,即 list_components 返回的 key"
)
node: ScreenNode = Field(
description="新组件。id、type、layout 必填,其余只写需要覆盖物料默认配置的字段;"
"后端按物料默认配置补全:对象逐层覆盖,数组整体替换,null 明确覆盖。"
"id 不能与现有组件重复,同一批里先 delete 的 ID 可以再用;type 必须与物料一致"
"后端按 type 找到物料,用默认配置补全:对象逐层覆盖,数组整体替换,null 明确覆盖。"
"id 不能与现有组件重复,同一批里先 delete 的 ID 可以再用;type 用 list_components 返回的 type"
)
sample_data: bool = Field(
default=False,
View File
-16
View File
@@ -1,16 +0,0 @@
# 这是一个示例 Python 脚本。
# 按 Shift+F10 执行或将其替换为您的代码。
# 按 双击 Shift 在所有地方搜索类、文件、工具窗口、操作和设置。
def print_hi(name):
# 在下面的代码行中使用断点来调试脚本。
print(f'Hi, {name}') # 按 Ctrl+F8 切换断点。
# 按装订区域中的绿色按钮以运行脚本。
if __name__ == '__main__':
print_hi('PyCharm')
# 访问 https://www.jetbrains.com/help/pycharm/ 获取 PyCharm 帮助
+2 -8
View File
@@ -46,10 +46,7 @@ def _require_catalog() -> ComponentCatalog:
@mcp.tool()
async def list_components() -> dict[str, Any]:
"""列出可以新增的组件:key、type、名称、用途和默认宽高
新建大屏、新增组件或更换类型时,先从这里选类型,再用 get_component_materials 查看默认配置
"""
"""列出可以新增的组件:key、type、名称、用途和默认宽高"""
return material_fields.list_components(_require_catalog())
@@ -60,10 +57,7 @@ async def get_component_materials(
Field(description="要使用的物料类型,可一次传多个", min_length=1),
],
) -> dict[str, Any]:
"""获取物料的完整默认配置(schema)和使用说明
新增组件前先查看所选类型的默认配置,只覆盖需要修改的字段,不从零编写 props
"""
"""获取物料的完整默认配置(schema)和使用说明"""
return material_fields.get_materials(_require_catalog(), component_types)
+1 -1
View File
@@ -11,7 +11,7 @@ description: 把已有组件换成另一种组件类型,例如"把折线图换
2. 用 list_components 选出用户要的新类型,用 get_component_materials 查看它的默认配置
3. 在同一批动作里先 delete 原组件,再 add 新组件:
- node.id 沿用原组件 ID,layout 沿用原组件的位置和尺寸(用户另有要求除外)
- component_key 选新类型对应的物料
- node.type 填新类型
4. 用 apply_actions 提交
## 数据迁移
+1 -5
View File
@@ -8,7 +8,7 @@ description: 新建大屏,或在大屏上新增组件、整体重新布局、
## 选组件
1. 用 list_components 查看可用组件,按用户要展示的内容选类型;不编造目录中没有的组件类型
2. 用 get_component_materials 查看所选类型的默认配置(可一次查多个类型),新组件以默认配置为基础,只写需要改的字段,例如标题
2. 用 get_component_materials 查看所选类型的默认配置
3. 新建大屏时,根据主题安排一个标题文本和若干图表;图表标题按要展示的内容写,不沿用物料的示例标题
## 布局
@@ -22,7 +22,3 @@ description: 新建大屏,或在大屏上新增组件、整体重新布局、
- 重新布局:用 move 和 resize 调整已有组件,不删除、不重建
- 统一风格:先用 get_nodes 读取组件配置,用 search_fields 找到颜色等字段,再逐个修改;画布背景用 update_canvas
## 提交
本次的所有新增和调整放在同一批,用 apply_actions 提交
-4
View File
@@ -23,7 +23,3 @@ description: 修改大屏上已有的组件或画布:改标题、文字、颜
- 相对变化("放大一倍""往右移 100")依据大屏摘要中的当前位置和尺寸换算
- "放到正中间"等按画布尺寸计算目标位置
## 提交
本次的所有修改放在同一批,用 apply_actions 提交