mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 04:01:58 +00:00
941723b17c
- 身份与工具:登录页建第一个管理员(认人接口的 401 说明能不能建);搭建者有只读的人和组名录,选人给候选并核对名字;授权三行带标题;端点请求头名校验(空名、非法名存不进、发不出时说清原因);写入能力在工具页试运行一律演练、不发送,HTTP 状态照实说成败;写记忆不算只读、查询能力只能选同一端点上的只读能力;审批与遮罩的说法和实际规则一致;开登录时不要署名、导入和发布记登录的人;使用者不显示推演等只给搭建者的操作,403 按是否开登录说话;登录页只认一次人 - 流程与实例:图里用到的岗位(派给谁、超时升级给谁、发送确认派给谁)全部派生、没绑人的发布时报错;写外部系统的参数按能力声明的类型收拢后再算哈希和发送,收不成就不发;发起人的实例页只看这一步的状态,处理人才给待办卡;会签进度带登录核实和时间,升级后照实写规则;实例状态写流程的结论(已退回、未确认、等待核实写入结果),核实生效后不再挂失败原因;流程应用首页不派生最近封存运行的结果块,看板没数据时是安静的空态;样例⑩的金额带类型和下限、待办标题带金额,表单上下限前后端一起生效 - 看板与证据:定时任务模板加 ISO 周、上一 ISO 周、本月、上月;出处链穿过原样解析查询结果的整形节点,追不到原始数据的照实说停在哪里,样例⑨改用 cell();应用首页和版本历史都能做成应用;指标块单位不重复;版本列表带退役说明;定时任务页头跟着刷新;删视图要确认、新视图能起名;编辑器属性用中文名、术语带解释 - 指标出具(用户 2026-10-15 拍板):契约只写「指标来自」、上游没有报告撰写节点、成果里没有模型写的话时按指标出具——必需指标齐、指标输入追到原始数据、成果字段里的数回指得上才完整出具;受管级要求每个指标静态地追到原始数据(governed.metric_untraced);出具横幅和应用印章写明按指标 - 受管工作流降为已发布只给管理员(Q8 顺延,服务端同样拦);工作流接口带当前发布版的级别,画布上改过之后也认得出受管工作流
137 lines
6.0 KiB
Python
137 lines
6.0 KiB
Python
"""钉住项落到库里:每个钉住项对应哪一版工作流、那一版的级别、能不能用。检查、发布、解析、入口共用。"""
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from typing import Any
|
|
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.apps.versions import is_published
|
|
from app.core.artifact_store import graph_hash
|
|
from app.db.models import DataSource, Workflow, WorkflowVersion
|
|
|
|
|
|
@dataclass
|
|
class PinState:
|
|
alias: str
|
|
pin: dict[str, Any]
|
|
workflow: Any | None
|
|
row: Any | None
|
|
|
|
@property
|
|
def name(self) -> str:
|
|
return self.workflow.name if self.workflow is not None else str(self.pin.get("workflow_id") or "")
|
|
|
|
@property
|
|
def label(self) -> str:
|
|
"""「周销售 v7」。"""
|
|
return f"{self.name} v{self.pin.get('version')}"
|
|
|
|
@property
|
|
def graph(self) -> dict[str, Any]:
|
|
return (self.row.graph if self.row is not None else None) or {}
|
|
|
|
@property
|
|
def hash(self) -> str | None:
|
|
if self.row is None:
|
|
return None
|
|
return self.row.graph_hash or graph_hash(self.row.graph or {})
|
|
|
|
@property
|
|
def retired(self) -> bool:
|
|
return self.row is not None and self.row.retired_at is not None
|
|
|
|
@property
|
|
def published(self) -> bool:
|
|
return self.row is not None and self.workflow is not None and is_published(
|
|
self.row, current=self.workflow.published_version)
|
|
|
|
@property
|
|
def usable(self) -> bool:
|
|
"""能从它发起正式运行:发布过、没退役、指纹和应用记下的一样。"""
|
|
return self.published and self.hash == self.pin.get("graph_hash")
|
|
|
|
@property
|
|
def level(self) -> str | None:
|
|
"""这一版的发布级别。记级别之前发布的老版本没有,照旧看工作流现在的 status(同 launch_run)。"""
|
|
if self.row is None:
|
|
return None
|
|
if self.row.level:
|
|
return self.row.level
|
|
if self.published and self.workflow is not None:
|
|
return "governed" if self.workflow.status == "governed" else "published"
|
|
return None
|
|
|
|
|
|
async def load_pins(session: AsyncSession, spec: dict[str, Any]) -> dict[str, PinState]:
|
|
out: dict[str, PinState] = {}
|
|
for pin in spec.get("pins") or []:
|
|
if not isinstance(pin, dict) or not pin.get("alias"):
|
|
continue
|
|
wf_id, version = pin.get("workflow_id"), pin.get("version")
|
|
workflow = await session.get(Workflow, wf_id) if isinstance(wf_id, str) else None
|
|
row = None
|
|
if workflow is not None and isinstance(version, int):
|
|
row = (await session.execute(select(WorkflowVersion).where(
|
|
WorkflowVersion.workflow_id == wf_id, WorkflowVersion.version == version))).scalar_one_or_none()
|
|
out[str(pin["alias"])] = PinState(alias=str(pin["alias"]), pin=pin, workflow=workflow, row=row)
|
|
return out
|
|
|
|
|
|
def needs_upgrade(pins: dict[str, PinState]) -> bool:
|
|
"""钉住的版本有退役的(或者已经不能用了):入口拒绝发起新实例,视图照实写明(A1)。"""
|
|
return any(not p.usable for p in pins.values())
|
|
|
|
|
|
#: 结果只由参数决定的内置工具:同样的输入算出同样的东西。别的(联网、读文件、知识库、记忆、当前时间、执行代码)都算活的
|
|
_FIXED_TOOLS = frozenset({"calculator", "json_query"})
|
|
|
|
|
|
async def reads_live(session: AsyncSession, graph: dict[str, Any]) -> bool:
|
|
"""这张图会不会查活库:绑了不是上传表格的数据源、MCP、自定义工具,或者工具名是模板拼的(发起时说不准)。
|
|
|
|
上传表格按数据版本固定(runner._data_versions),同一版本查到的就是同一份;其余的源每次查都可能不一样,按需视图的
|
|
缓存必须有时效(app.on_demand_ttl)。"""
|
|
from app.engine.launch import tool_bindings
|
|
from app.engine.schema import GraphSpec
|
|
from app.tools.datasource import source_of_tool
|
|
|
|
try:
|
|
spec = GraphSpec.model_validate(graph)
|
|
except Exception: # noqa: BLE001 - 图不成形交给发起时报
|
|
return False
|
|
sources: set[str] = set()
|
|
for _, name in tool_bindings(spec):
|
|
if "{{" in name:
|
|
return True
|
|
if (source := source_of_tool(name)) is not None:
|
|
sources.add(source)
|
|
elif name not in _FIXED_TOOLS:
|
|
return True
|
|
if not sources:
|
|
return False
|
|
origins = dict((await session.execute(select(DataSource.name, DataSource.origin)
|
|
.where(DataSource.name.in_(sorted(sources))))).all())
|
|
return any(origins.get(s) != "upload" for s in sources)
|
|
|
|
|
|
async def write_nodes(session: AsyncSession, graph: dict[str, Any]) -> dict[str, Any]:
|
|
"""图里写外部系统的调用工具节点:{节点 id: 能力登记},按节点出现的顺序。认法和运行时同一个(调用工具节点
|
|
engine/nodes/tools._write_capability → capabilities.registry.writes_externally:治理后效果为 write,或绑定端点、视参数
|
|
而定):只有它们走动作流程——开发送确认卡、留生效记录、结果未知时要人核实。派生(动作状态、核实岗位)和检查(发送
|
|
确认派给的岗位、核实岗位)共用这一份。只读的调用工具节点从不开确认卡,它写的(或取图级默认的)「派给谁」用不到岗位。
|
|
登记里没有的不算:运行时同样不按写入走。"""
|
|
from app.apps.tree import node_map
|
|
from app.capabilities.registry import effective_effect, get_capability, writes_externally
|
|
|
|
out: dict[str, Any] = {}
|
|
for node_id, node in node_map(graph).items():
|
|
name = node["config"].get("tool") if node["type"] == "tool" else None
|
|
if not isinstance(name, str) or not name:
|
|
continue
|
|
cap = await get_capability(session, name)
|
|
if cap is not None and writes_externally(name, effective_effect(cap), getattr(cap, "endpoint_id", None)):
|
|
out[node_id] = cap
|
|
return out
|