mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 19:32:01 +00:00
7739340607
- 问数据和个人应用的图放行绑定只读查询等合规工具的调用工具节点,只读和遮罩在这条路径上生效;被治理成写入的工具在只读运行里直接拒;知识库范围在工具层固定(图级默认值、参数名纠错、Agent 都改不了) - 个人应用的刷新和个人蓝图的试运行只有拥有者能读:运行、事件、证据、工件、文件、运行与审批列表、对比、可验证报告、质询、假设分析、提取工作流一并收紧;个人蓝图的试运行不论谁发起都按个人应用约束 - 共享蓝图只给拥有者、对应应用的管理者、管理员,看不到的一律 404;审核副本归审核人;管理员看别人的个人应用只有元数据、可以归档 - 记录表跨版本汇总(只读挂载旧版本、按当前列 UNION ALL、各版本各自固定高水位,缺的版本照实写进印章);停用前发起的运行可以继续写旧版本;更正沿用原提交人、更正人记审计 - 约定收口:待办种类 review、事件 file.extracted、表单错误 file、模板定义带示例与预置;前端类型一次补齐、去掉临时的局部类型;审核待办卡不再给打不开的链接;读附件事件有中文说法;检查脚本补断言
146 lines
9.0 KiB
Python
146 lines
9.0 KiB
Python
"""流程:待办、派给谁、生效记录、通知(应用平台 A4、A5,docs/design/app-platform-and-look.md §2.4、§2.5、§3.7、§3.8)。
|
||
|
||
一次中断对应一条 Approval(给引擎)和一条 Task(给人)。Task 下挂若干条 TaskAssignment:派给具体的人或组,
|
||
按 policy(any / all / quorum)汇总后才恢复运行。核实待办(kind=reconcile)没有 Approval,approval_id 为空。
|
||
|
||
ActionRecord 是写外部系统的权威记录:先写记录再发送;节点每次执行都先按 action_key 查,已生效的不再发,
|
||
结果未知的永不自动重发。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
from datetime import datetime
|
||
from typing import Any
|
||
|
||
from sqlalchemy import ForeignKey, Index, Integer, String, Text
|
||
from sqlalchemy.orm import Mapped, mapped_column
|
||
|
||
from app.db.base import Base, TimestampMixin, UTCDateTime, new_id, utcnow
|
||
|
||
#: 待办的种类。人工节点三种 + 现有的几种中断 + 动作确认、核实、等外部系统 + 个人应用提交审核(review,
|
||
#: docs/design/normal-user-apps.md N7,app/process/tasks.REVIEW_KIND)
|
||
TASK_KINDS = ("approve", "input", "edit", "tool", "code", "dual_check", "action_confirm", "reconcile", "signal",
|
||
"review")
|
||
#: pending 等人;resolving 已凑齐、正在恢复运行(条件 UPDATE 抢到的那个请求负责);resolved / cancelled / expired 终态
|
||
TASK_STATUSES = ("pending", "resolving", "resolved", "cancelled", "expired")
|
||
#: 动作状态。simulated:探索运行里端点没有 test 环境时的演练,不发送
|
||
ACTION_STATES = ("pending", "sent", "confirmed", "failed", "unknown", "simulated")
|
||
|
||
|
||
class Task(Base, TimestampMixin):
|
||
__tablename__ = "tasks"
|
||
__table_args__ = (Index("ix_tasks_status_due", "status", "due_at"),)
|
||
|
||
id: Mapped[str] = mapped_column(String(32), primary_key=True, default=new_id)
|
||
approval_id: Mapped[str | None] = mapped_column(String(32), default=None, unique=True)
|
||
run_id: Mapped[str] = mapped_column(ForeignKey("runs.id", ondelete="CASCADE"), index=True)
|
||
node_id: Mapped[str | None] = mapped_column(String(64), default=None)
|
||
app_id: Mapped[str | None] = mapped_column(String(32), default=None, index=True)
|
||
app_version: Mapped[int | None] = mapped_column(Integer, default=None)
|
||
kind: Mapped[str] = mapped_column(String(20), default="approve")
|
||
title: Mapped[str] = mapped_column(String(300), default="")
|
||
# agentlab.form/1;没有表单的(批准 / 驳回)为空
|
||
form: Mapped[dict[str, Any] | None] = mapped_column(default=None)
|
||
# {"kind": "any"} | {"kind": "all"} | {"kind": "quorum", "m": 2}
|
||
policy: Mapped[dict[str, Any]] = mapped_column(default=lambda: {"kind": "any"})
|
||
status: Mapped[str] = mapped_column(String(20), default="pending", index=True)
|
||
due_at: Mapped[datetime | None] = mapped_column(default=None)
|
||
remind_at: Mapped[datetime | None] = mapped_column(default=None)
|
||
# {"action": "remind" | "escalate" | "reject", "every"?: ISO 时长, "to_slot"?: 岗位}。没有「自动批准」
|
||
on_timeout: Mapped[dict[str, Any] | None] = mapped_column(default=None)
|
||
escalation_level: Mapped[int] = mapped_column(Integer, default=0)
|
||
# 等外部系统(kind=signal)时,入站 signal 按它找到这条待办
|
||
correlation_key: Mapped[str | None] = mapped_column(String(200), default=None, index=True)
|
||
# 判定结果:{"decision": ..., "values": ..., "signoffs": [...]}
|
||
outcome: Mapped[dict[str, Any] | None] = mapped_column(default=None)
|
||
resolved_by: Mapped[str | None] = mapped_column(String(100), default=None)
|
||
resolved_at: Mapped[datetime | None] = mapped_column(default=None)
|
||
|
||
|
||
class TaskAssignment(Base):
|
||
__tablename__ = "task_assignments"
|
||
__table_args__ = (Index("ix_assign_principal", "principal_kind", "principal", "status"),)
|
||
|
||
id: Mapped[str] = mapped_column(String(32), primary_key=True, default=new_id)
|
||
task_id: Mapped[str] = mapped_column(ForeignKey("tasks.id", ondelete="CASCADE"), index=True)
|
||
principal_kind: Mapped[str] = mapped_column(String(20)) # user | group | anyone | hook
|
||
principal: Mapped[str] = mapped_column(String(100), default="")
|
||
# 从哪来:slot:<岗位> | initiator | expression | escalation | reassign
|
||
source: Mapped[str] = mapped_column(String(40), default="")
|
||
status: Mapped[str] = mapped_column(String(20), default="pending") # pending | responded | withdrawn
|
||
response: Mapped[dict[str, Any] | None] = mapped_column(default=None)
|
||
responded_by: Mapped[str | None] = mapped_column(String(100), default=None)
|
||
responded_at: Mapped[datetime | None] = mapped_column(default=None)
|
||
# 回复人是不是认证过的身份(off 模式下是自报署名,记 False)
|
||
verified: Mapped[bool] = mapped_column(default=False)
|
||
created_at: Mapped[datetime] = mapped_column(UTCDateTime, default=utcnow)
|
||
|
||
|
||
class ActionRecord(Base, TimestampMixin):
|
||
__tablename__ = "action_records"
|
||
|
||
id: Mapped[str] = mapped_column(String(32), primary_key=True, default=new_id)
|
||
# sha256(run_id, node_id, exec);同一次执行第 n 次发送的幂等键是 f"{action_key}:{attempt}"
|
||
action_key: Mapped[str] = mapped_column(String(64), index=True)
|
||
attempt: Mapped[int] = mapped_column(Integer, default=1)
|
||
idem_key: Mapped[str] = mapped_column(String(80), unique=True)
|
||
run_id: Mapped[str] = mapped_column(ForeignKey("runs.id", ondelete="CASCADE"), index=True)
|
||
node_id: Mapped[str] = mapped_column(String(64))
|
||
exec_no: Mapped[int] = mapped_column(Integer, default=1)
|
||
app_id: Mapped[str | None] = mapped_column(String(32), default=None, index=True)
|
||
workflow_id: Mapped[str | None] = mapped_column(String(32), default=None, index=True)
|
||
workflow_version: Mapped[int | None] = mapped_column(Integer, default=None)
|
||
graph_hash: Mapped[str | None] = mapped_column(String(64), default=None)
|
||
capability: Mapped[str] = mapped_column(String(200))
|
||
capability_version: Mapped[int | None] = mapped_column(Integer, default=None)
|
||
endpoint_id: Mapped[str | None] = mapped_column(String(32), default=None)
|
||
environment: Mapped[str] = mapped_column(String(20), default="prod") # prod | test | simulated
|
||
payload_hash: Mapped[str] = mapped_column(String(64))
|
||
# 遮罩后的预览(确认卡上给人看的那一份)
|
||
preview: Mapped[dict[str, Any] | None] = mapped_column(default=None)
|
||
state: Mapped[str] = mapped_column(String(20), default="pending", index=True)
|
||
confirm_task_id: Mapped[str | None] = mapped_column(String(32), default=None)
|
||
request_at: Mapped[datetime | None] = mapped_column(default=None)
|
||
response_status: Mapped[int | None] = mapped_column(Integer, default=None)
|
||
# 回执:外部单号等,原文落工件,这里存 {artifact, summary}
|
||
receipt: Mapped[dict[str, Any] | None] = mapped_column(default=None)
|
||
error: Mapped[str | None] = mapped_column(Text, default=None)
|
||
lookup_attempts: Mapped[int] = mapped_column(Integer, default=0)
|
||
next_lookup_at: Mapped[datetime | None] = mapped_column(default=None)
|
||
# 核实结论:{"by": ..., "how": "lookup" | "manual", "outcome": "confirmed" | "not_applied" | "resend" | "abandon", "external_id"?}
|
||
resolution: Mapped[dict[str, Any] | None] = mapped_column(default=None)
|
||
|
||
|
||
class NotifyChannel(Base, TimestampMixin):
|
||
"""通知渠道。格式、签名、发送同定时任务的 Webhook(四种:generic / feishu / wecom / dingtalk)。"""
|
||
|
||
__tablename__ = "notify_channels"
|
||
|
||
id: Mapped[str] = mapped_column(String(32), primary_key=True, default=new_id)
|
||
name: Mapped[str] = mapped_column(String(100))
|
||
scope: Mapped[str] = mapped_column(String(20), default="global") # global | app | user
|
||
owner_id: Mapped[str | None] = mapped_column(String(32), default=None)
|
||
format: Mapped[str] = mapped_column(String(20), default="generic")
|
||
url: Mapped[str] = mapped_column(Text) # 密文
|
||
secret: Mapped[str | None] = mapped_column(Text, default=None) # 密文
|
||
events: Mapped[list[Any]] = mapped_column(default=list)
|
||
enabled: Mapped[bool] = mapped_column(default=True)
|
||
|
||
|
||
class NotifyDelivery(Base):
|
||
"""通用投递记录。定时任务的投递仍写 webhook_deliveries(schedule_id 非空,SQLite 改不了可空性)。"""
|
||
|
||
__tablename__ = "notify_deliveries"
|
||
|
||
id: Mapped[str] = mapped_column(String(32), primary_key=True, default=new_id)
|
||
channel_id: Mapped[str] = mapped_column(ForeignKey("notify_channels.id", ondelete="CASCADE"), index=True)
|
||
subject_kind: Mapped[str] = mapped_column(String(20)) # task | run | action | channel(渠道的测试消息)
|
||
subject_id: Mapped[str] = mapped_column(String(32))
|
||
event: Mapped[str] = mapped_column(String(40))
|
||
run_id: Mapped[str | None] = mapped_column(String(32), default=None)
|
||
task_id: Mapped[str | None] = mapped_column(String(32), default=None)
|
||
status: Mapped[str] = mapped_column(String(20))
|
||
http_status: Mapped[int | None] = mapped_column(Integer, default=None)
|
||
attempts: Mapped[int] = mapped_column(Integer, default=0)
|
||
error: Mapped[str | None] = mapped_column(Text, default=None)
|
||
created_at: Mapped[datetime] = mapped_column(UTCDateTime, default=utcnow)
|