mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 01:01:59 +00:00
普通用户建应用的契约骨架(docs/design/normal-user-apps.md 第五部分):蓝图、蓝图留痕、发布单、文件、记录表的模型和迁移,应用的 scope / owner / blueprint_id;运行来源加 trial、personal_app;应用描述的固定版本可以没有版本号(试运行派生的草稿)或改钉蓝图(个人应用),视图来源加 exploratory,组件加 hidden,表单加文件控件;agentlab.blueprint/1 与 bp_* 操作、agentlab.template/1 与关节类型的定义;实例化、试运行、发布单、个人应用、记录表、文件、白话层的接口签名;蓝图、模板、文件、个人应用、蓝图助手的路由桩(501);前端约定 typesBlueprint.ts、接口桩、新建应用与蓝图页的占位路由、termsBlueprint.ts
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
"""蓝图、试运行、发布单的接口(docs/design/normal-user-apps.md §3.5、§3.6、§3.10)。
|
||||
|
||||
权限在接口里判:共享蓝图要 builder(受管级发布单再要 admin),个人蓝图要拥有者且管理员开了 apps.user_personal_apps。
|
||||
骨架阶段全部 501,包 B2 实现(CRUD 调 blueprints/ops,试运行调 blueprints/trial,发布单调 blueprints/publish_plan)。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
|
||||
router = APIRouter(prefix="/api", tags=["blueprints"])
|
||||
|
||||
_TODO = "尚未实现"
|
||||
|
||||
|
||||
@router.post("/blueprints", status_code=201)
|
||||
async def create_blueprint() -> dict[str, Any]:
|
||||
"""起点:{source: {kind: conversation|template|run|workflow_version, id?, version?}, scope: shared|personal, name?}"""
|
||||
raise HTTPException(501, _TODO)
|
||||
|
||||
|
||||
@router.get("/blueprints")
|
||||
async def list_blueprints() -> dict[str, Any]:
|
||||
raise HTTPException(501, _TODO)
|
||||
|
||||
|
||||
@router.get("/blueprints/{blueprint_id}")
|
||||
async def get_blueprint(blueprint_id: str) -> dict[str, Any]:
|
||||
raise HTTPException(501, _TODO)
|
||||
|
||||
|
||||
@router.patch("/blueprints/{blueprint_id}")
|
||||
async def patch_blueprint(blueprint_id: str) -> dict[str, Any]:
|
||||
"""{ops: [bp_* 操作], name?}:用户在卡片上的修改,来源记成 user。"""
|
||||
raise HTTPException(501, _TODO)
|
||||
|
||||
|
||||
@router.post("/blueprints/{blueprint_id}/instantiate")
|
||||
async def instantiate_blueprint(blueprint_id: str) -> dict[str, Any]:
|
||||
raise HTTPException(501, _TODO)
|
||||
|
||||
|
||||
@router.post("/blueprints/{blueprint_id}/trial")
|
||||
async def trial(blueprint_id: str) -> dict[str, Any]:
|
||||
raise HTTPException(501, _TODO)
|
||||
|
||||
|
||||
@router.get("/blueprints/{blueprint_id}/preview")
|
||||
async def preview(blueprint_id: str) -> dict[str, Any]:
|
||||
raise HTTPException(501, _TODO)
|
||||
|
||||
|
||||
@router.post("/blueprints/{blueprint_id}/publish-plan")
|
||||
async def publish_plan(blueprint_id: str) -> dict[str, Any]:
|
||||
raise HTTPException(501, _TODO)
|
||||
|
||||
|
||||
@router.get("/publish-plans/{plan_id}")
|
||||
async def get_plan(plan_id: str) -> dict[str, Any]:
|
||||
raise HTTPException(501, _TODO)
|
||||
|
||||
|
||||
@router.post("/publish-plans/{plan_id}/run")
|
||||
async def run_plan(plan_id: str) -> dict[str, Any]:
|
||||
raise HTTPException(501, _TODO)
|
||||
|
||||
|
||||
@router.post("/publish-plans/{plan_id}/resume")
|
||||
async def resume_plan(plan_id: str) -> dict[str, Any]:
|
||||
raise HTTPException(501, _TODO)
|
||||
@@ -0,0 +1,25 @@
|
||||
"""蓝图助手和填关节的流式接口(docs/design/normal-user-apps.md §2.2、§2.3、§2.11)。
|
||||
|
||||
- POST /api/copilot/blueprint-stream:蓝图对话。模型输出 bp_* 操作(BlueprintApplier 落地)、look、bp_ask(这一轮就此结束);
|
||||
- POST /api/copilot/fill-joints-stream:按模板填生成关节(sql、metric_expr)。TemplateApplier 只收 fill_joint、只收模板声明过的
|
||||
关节;不同 group 的关节并行填。
|
||||
|
||||
都复用 api/copilot_gen.Generation。包 B1 实现。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
|
||||
router = APIRouter(prefix="/api/copilot", tags=["copilot-blueprint"])
|
||||
|
||||
|
||||
@router.post("/blueprint-stream")
|
||||
async def blueprint_stream() -> Any:
|
||||
raise HTTPException(501, "尚未实现")
|
||||
|
||||
|
||||
@router.post("/fill-joints-stream")
|
||||
async def fill_joints_stream() -> Any:
|
||||
raise HTTPException(501, "尚未实现")
|
||||
@@ -0,0 +1,24 @@
|
||||
"""文件的上传、下载、删除(docs/design/normal-user-apps.md §3.8)。包 B3 实现。"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
|
||||
router = APIRouter(prefix="/api/files", tags=["files"])
|
||||
|
||||
|
||||
@router.post("", status_code=201)
|
||||
async def upload() -> dict[str, Any]:
|
||||
"""multipart:file + context({app_id, entry} | {task_id} | {blueprint_id}),交回引用 {file_id, name, size, sha256, type}。"""
|
||||
raise HTTPException(501, "尚未实现")
|
||||
|
||||
|
||||
@router.get("/{file_id}")
|
||||
async def download(file_id: str) -> Any:
|
||||
raise HTTPException(501, "尚未实现")
|
||||
|
||||
|
||||
@router.delete("/{file_id}", status_code=204)
|
||||
async def delete(file_id: str) -> None:
|
||||
raise HTTPException(501, "尚未实现")
|
||||
@@ -0,0 +1,43 @@
|
||||
"""个人应用与审核(docs/design/normal-user-apps.md §3.9)、页面编辑的候选(§2.6),以及记录的逐行查看(§3.7 records_list)。
|
||||
|
||||
要在 apps.router 之前登记(共用 /api/apps 前缀,路径不冲突)。包 B4 实现个人应用和候选,记录列表由包 B3 实现。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
|
||||
router = APIRouter(prefix="/api", tags=["personal"])
|
||||
|
||||
_TODO = "尚未实现"
|
||||
|
||||
|
||||
@router.post("/blueprints/{blueprint_id}/save-personal")
|
||||
async def save_personal(blueprint_id: str) -> dict[str, Any]:
|
||||
raise HTTPException(501, _TODO)
|
||||
|
||||
|
||||
@router.post("/blueprints/{blueprint_id}/copy", status_code=201)
|
||||
async def copy_for_review(blueprint_id: str) -> dict[str, Any]:
|
||||
raise HTTPException(501, _TODO)
|
||||
|
||||
|
||||
@router.post("/apps/{app_id}/refresh")
|
||||
async def refresh(app_id: str) -> dict[str, Any]:
|
||||
raise HTTPException(501, _TODO)
|
||||
|
||||
|
||||
@router.post("/apps/{app_id}/submit-review")
|
||||
async def submit_review(app_id: str) -> dict[str, Any]:
|
||||
raise HTTPException(501, _TODO)
|
||||
|
||||
|
||||
@router.get("/apps/{app_id}/candidates")
|
||||
async def candidates(app_id: str) -> dict[str, Any]:
|
||||
raise HTTPException(501, _TODO)
|
||||
|
||||
|
||||
@router.get("/apps/{app_id}/records")
|
||||
async def records(app_id: str) -> dict[str, Any]:
|
||||
raise HTTPException(501, _TODO)
|
||||
@@ -0,0 +1,13 @@
|
||||
"""场景模板列表(docs/design/normal-user-apps.md §3.3):GET /api/templates。包 B1 实现。"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
|
||||
router = APIRouter(prefix="/api/templates", tags=["templates"])
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def list_templates() -> dict[str, Any]:
|
||||
raise HTTPException(501, "尚未实现")
|
||||
@@ -10,7 +10,7 @@ from __future__ import annotations
|
||||
|
||||
from typing import Any, Literal
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
||||
|
||||
APP_FORMAT = "agentlab.app/1"
|
||||
FORM_FORMAT = "agentlab.form/1"
|
||||
@@ -35,9 +35,16 @@ DISPOSITIONS = ("recompute", "dual", "incomparable")
|
||||
FORM_WIDGETS = (
|
||||
"text", "textarea", "number", "integer", "date", "datetime", "select", "radio", "multiselect", "checkbox",
|
||||
"principal", "note", "json",
|
||||
# 文件(docs/design/normal-user-apps.md N9):值是上传后拿回的引用 {file_id, name, size, sha256, type},
|
||||
# 字段 type 为 object,只有这个控件能用 object
|
||||
"file",
|
||||
)
|
||||
#: 表单字段能用的 type
|
||||
FORM_TYPES = ("string", "number", "integer", "boolean", "array")
|
||||
#: 文件控件(x-widget: file)的字段类型:只有这个控件能用 object(apps/forms.py 按控件放行)
|
||||
FILE_FIELD_TYPE = "object"
|
||||
#: 文件引用的形状:上传后拿回、提交时原样带回
|
||||
FILE_REF_KEYS = ("file_id", "name", "size", "sha256", "type")
|
||||
#: 表单字段能用的 format。不支持 pattern:JS 和 Python 的正则语义不同,会出现前端放行、后端拒收
|
||||
FORM_FORMATS = ("date", "date-time", "email", "uri")
|
||||
#: 表单校验错误的 code
|
||||
@@ -49,12 +56,23 @@ class _Strict(BaseModel):
|
||||
|
||||
|
||||
class Pin(_Strict):
|
||||
"""一个钉住项。共享应用钉工作流版本;还没发布的(蓝图试运行时派生的草稿)version 为空,应用检查按
|
||||
app.pin_unpublished 拦发布。个人应用(N7)没有工作流:写 blueprint(蓝图 id),version 为空,图在蓝图上。
|
||||
workflow_id 和 blueprint 恰好写一个。"""
|
||||
|
||||
alias: str
|
||||
workflow_id: str
|
||||
version: int
|
||||
workflow_id: str | None = None
|
||||
version: int | None = None
|
||||
graph_hash: str
|
||||
level: str | None = None
|
||||
use: Literal["view", "entry", "process"] = "view"
|
||||
blueprint: str | None = None
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _one_target(self) -> Pin:
|
||||
if (self.workflow_id is None) == (self.blueprint is None):
|
||||
raise ValueError("固定版本要写 workflow_id 或 blueprint 其中一个")
|
||||
return self
|
||||
|
||||
|
||||
class Slot(_Strict):
|
||||
@@ -80,7 +98,8 @@ class Entry(_Strict):
|
||||
|
||||
|
||||
class ViewSource(_Strict):
|
||||
kind: Literal["latest_sealed", "on_demand", "instance"]
|
||||
# exploratory 只在个人应用里合法(N7):取这个应用、这个拥有者、这张图下最近一次成功封存的探索运行
|
||||
kind: Literal["latest_sealed", "on_demand", "instance", "exploratory"]
|
||||
pin: str | None = None
|
||||
# latest_sealed:schedule:<id> | formal
|
||||
from_: str | None = Field(default=None, alias="from")
|
||||
@@ -104,6 +123,8 @@ class Component(_Strict):
|
||||
visible: str | None = None
|
||||
# 可见性表达式里 run / metric / input 指的是哪个视图
|
||||
scope: str | None = None
|
||||
# 作者隐藏了它(N6):解析时跳过,编辑状态下照常画成半透明。和可见性表达式不是一回事
|
||||
hidden: bool = False
|
||||
children: list[Component] = Field(default_factory=list)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
"""普通用户建应用(docs/design/normal-user-apps.md):蓝图、场景模板、实例化、试运行、发布单、个人应用。
|
||||
|
||||
一条链路两种终点(N1):说需求 → 蓝图 → 模板填关节 → 试运行 →(搭建者)发布单 /(使用者)个人应用。
|
||||
"""
|
||||
@@ -0,0 +1,32 @@
|
||||
"""按模板实例化蓝图(N3、§3.3–§3.4):确定性关节按卡片填,生成关节留给模型(TemplateApplier 的 fill_joint),
|
||||
实例化后的图和骨架逐节点比对,只许关节位置不同。模型失败时生成关节留空、标「待填」(P3)。
|
||||
|
||||
包 B1 实现。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
|
||||
@dataclass
|
||||
class Instantiated:
|
||||
#: 实例化出来的图(GraphSpec 的 JSON)。生成关节没填的位置是占位
|
||||
graph: dict[str, Any]
|
||||
#: {joint_id: {"status": filled|pending|refused, "by": template|assistant|user, "note"?: …}}
|
||||
joints: dict[str, Any]
|
||||
#: 按模板排布的应用草稿(agentlab.app/1),钉住项的 version 为空(还没发布)
|
||||
app_spec: dict[str, Any]
|
||||
#: 定时任务参数(看板类)或 None
|
||||
schedule: dict[str, Any] | None
|
||||
problems: list[dict[str, Any]] = field(default_factory=list)
|
||||
|
||||
|
||||
def instantiate(blueprint: dict[str, Any], *, filled: dict[str, Any] | None = None) -> Instantiated:
|
||||
"""蓝图 + 已填的生成关节({joint_id: value})→ 图、关节状态、应用草稿、定时任务参数。纯函数,不落库。"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def skeleton_changes(template_graph: dict[str, Any], graph: dict[str, Any], joints: list[Any]) -> list[dict[str, Any]]:
|
||||
"""骨架比对:除关节位置外的任何差异都交回(tpl.skeleton_changed)。"""
|
||||
raise NotImplementedError
|
||||
@@ -0,0 +1,28 @@
|
||||
"""bp_* 操作的确定性落地(N2、§3.2)。模型(BlueprintApplier)和用户(PATCH /api/blueprints/{id})用同一组操作,
|
||||
只是来源记成 assistant 或 user。每张卡片有白名单(例如 people[].policy.kind 只能是 any / all / quorum),越界的拒绝并照实交回。
|
||||
|
||||
包 B1 实现。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
class BpRefused(Exception):
|
||||
"""操作被拒:message 是写给人看的一句话,code 是 bp.* 机器码。"""
|
||||
|
||||
def __init__(self, message: str, code: str = "bp.refused") -> None:
|
||||
super().__init__(message)
|
||||
self.message = message
|
||||
self.code = code
|
||||
|
||||
|
||||
def apply_bp_op(spec: dict[str, Any], op: dict[str, Any], *, source: str) -> dict[str, Any]:
|
||||
"""把一个 bp_* 操作落到蓝图上,交回新的蓝图(不改入参)。source 是 user / assistant。不合规时抛 BpRefused。"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def blueprint_issues(spec: dict[str, Any], *, for_: str) -> list[dict[str, Any]]:
|
||||
"""蓝图检查(§3.13 bp.*):for_ = trial(缺项挡试运行)/ publish(待确认项挡发布)/ personal。
|
||||
每条 {code, level, path, message}。"""
|
||||
raise NotImplementedError
|
||||
@@ -0,0 +1,31 @@
|
||||
"""个人应用(N7、§3.9):使用者自己的应用。图存在蓝图上(不进工作流表)、只有拥有者看得见、永远跑探索运行
|
||||
(origin_kind=personal_app,强加 MASK_RESULTS / READ_ONLY)、受每日上限约束;想给别人用就提交审核,搭建者在共享副本上发布。
|
||||
|
||||
包 B4 实现。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
#: 管理员开关:使用者能不能建个人应用(apps 设置组里,默认关)
|
||||
SETTING_KEY = "user_personal_apps"
|
||||
|
||||
|
||||
async def save_personal(session: Any, blueprint: Any, *, principal: Any) -> Any:
|
||||
"""校验图满足 chat_policy(节点、工具、无人工、无写入),建或更新 scope=personal 的 App。"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
async def refresh(session: Any, app: Any, *, principal: Any) -> str:
|
||||
"""发起一次个人应用的刷新(探索运行),交回 run_id;受每日上限约束。"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
async def submit_review(session: Any, app: Any, *, to: list[dict[str, str]], principal: Any) -> Any:
|
||||
"""开一条 review 待办派给搭建者。"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
async def copy_for_review(session: Any, blueprint: Any, *, principal: Any) -> Any:
|
||||
"""搭建者处理审核时得到这个蓝图的共享副本(copied_from 指向原蓝图),原个人应用不动。"""
|
||||
raise NotImplementedError
|
||||
@@ -0,0 +1,39 @@
|
||||
"""一次确认发布:发布单(N5、§3.6)。先算出每一步和白话摘要(只读),确认后按序执行、每步留痕,停下后从那一步接着做。
|
||||
|
||||
步骤的顺序让中途失败的状态无害:定时任务先停用、应用发布后才启用;授权放最后。
|
||||
|
||||
包 B2 实现。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
#: 发布单的步骤(顺序即执行顺序)
|
||||
STEP_KINDS = (
|
||||
"save_version", # 保存版本(按图哈希复用已有的)
|
||||
"preflight", # 全量预检(只读):工作流发布前检查 + 应用检查 + 定时任务预览 + 名录核对
|
||||
"publish_workflow", # 发布工作流(受管级只有管理员)
|
||||
"create_schedule", # 建定时任务(停用)
|
||||
"create_app", # 建应用、派生、写岗位绑定
|
||||
"publish_app", # 发布应用(钉住项升版时按蓝图里的处置)
|
||||
"enable_schedule", # 启用定时任务
|
||||
"grant", # 授权
|
||||
"first_run", # 跑第一次(看板类默认勾选)
|
||||
)
|
||||
#: 问题的三类(N10 的 who)
|
||||
PROBLEM_WHO = ("you", "assistant", "admin")
|
||||
|
||||
|
||||
async def plan(session: Any, blueprint: Any, *, level: str, principal: Any, options: dict[str, Any] | None = None) -> Any:
|
||||
"""出发布单(只读预检),交回 PublishPlan(已落库,status=planned)。"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
async def run(session: Any, plan_id: str, *, principal: Any) -> Any:
|
||||
"""按序执行;某步失败时 status=stopped 并写明原因。"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
async def resume(session: Any, plan_id: str, *, principal: Any) -> Any:
|
||||
"""从停下的那一步接着做;已完成的步骤按结果里的 id 核对,不重做。"""
|
||||
raise NotImplementedError
|
||||
@@ -0,0 +1,129 @@
|
||||
"""应用蓝图 agentlab.blueprint/1(docs/design/normal-user-apps.md §2.2、§3.2)。
|
||||
|
||||
蓝图由几张「卡片」组成,每一项都是 {value, source, status}:
|
||||
- source:user(用户填的)/ assistant(助手提的)/ template(模板默认)/ catalog(从数据目录对到的)/ caliber(复用已发布口径卡);
|
||||
- status:confirmed(已确认)/ proposed(待确认)/ missing(缺)。试运行前「缺」都要补齐,发布前「待确认」都要确认。
|
||||
|
||||
模型和用户都只能用 bp_* 操作改蓝图(blueprints/ops.py),落地是确定性的。这里只定形状,不做业务校验。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Literal
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
BLUEPRINT_FORMAT = "agentlab.blueprint/1"
|
||||
ITEM_SOURCES = ("user", "assistant", "template", "catalog", "caliber")
|
||||
ITEM_STATUSES = ("confirmed", "proposed", "missing")
|
||||
#: 卡片(界面上一张卡一个),顺序就是界面上的顺序
|
||||
CARDS = ("purpose", "template", "data", "cadence", "people", "fields", "writeback", "access")
|
||||
#: bp_* 操作。路径是 JSON 指针(/data/0/table)
|
||||
BP_OPS = ("bp_set", "bp_add", "bp_remove", "bp_ask", "bp_template")
|
||||
|
||||
|
||||
class _Strict(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid", populate_by_name=True)
|
||||
|
||||
|
||||
class Item(_Strict):
|
||||
"""卡片上的一项:值、来源、状态,可选一句「为什么」(例如为什么对到这张表)。"""
|
||||
|
||||
value: Any = None
|
||||
source: Literal["user", "assistant", "template", "catalog", "caliber"] = "assistant"
|
||||
status: Literal["confirmed", "proposed", "missing"] = "missing"
|
||||
why: str | None = None
|
||||
|
||||
|
||||
class TemplateRef(_Strict):
|
||||
id: str
|
||||
version: int = 1
|
||||
|
||||
|
||||
class DataItem(_Strict):
|
||||
"""「看哪些数」的一项。table 对到 {source, table};reuse 对到已发布口径卡 {workflow_id, version, node_id, metric_id}
|
||||
时实例化成 caliber_from;measure 是白话或简单聚合的提议,不是最终表达式(最终的在关节里)。"""
|
||||
|
||||
id: str
|
||||
label: Item = Field(default_factory=Item)
|
||||
unit: Item = Field(default_factory=Item)
|
||||
table: Item = Field(default_factory=Item)
|
||||
measure: Item = Field(default_factory=Item)
|
||||
date: Item = Field(default_factory=Item)
|
||||
filter: Item = Field(default_factory=Item)
|
||||
grain: Item = Field(default_factory=Item)
|
||||
reuse: dict[str, Any] | None = None
|
||||
|
||||
|
||||
class Step(_Strict):
|
||||
"""「谁来做」的一步:处理人([{kind: user|group|slot|initiator, name}])、几个人同意算通过、时限、超时给谁。"""
|
||||
|
||||
id: str
|
||||
label: Item = Field(default_factory=Item)
|
||||
who: Item = Field(default_factory=Item)
|
||||
policy: Item = Field(default_factory=Item) # {"kind": any|all|quorum, "m"?: int}
|
||||
due: Item = Field(default_factory=Item) # ISO 8601 时长,由卡片上的「2 天」「48 小时」换算
|
||||
on_timeout: Item = Field(default_factory=Item) # {"action": remind|escalate|reject, "to"?: [...]}
|
||||
threshold: Item = Field(default_factory=Item) # {"field", "op", "value"}:什么条件下走这一步(金额线)
|
||||
|
||||
|
||||
class FieldItem(_Strict):
|
||||
"""「填什么」的一个字段:落到入口表单、待办表单、记录表的列。spec 是 agentlab.form/1 的一个属性。"""
|
||||
|
||||
name: str
|
||||
spec: Item = Field(default_factory=Item)
|
||||
sensitive: bool = False
|
||||
|
||||
|
||||
class Writeback(_Strict):
|
||||
capability: Item = Field(default_factory=Item)
|
||||
mapping: Item = Field(default_factory=Item) # {参数: "form.x" | "metric.y" | 常量}
|
||||
confirm: Item = Field(default_factory=Item) # 要不要人确认(布尔)
|
||||
|
||||
|
||||
class Access(_Strict):
|
||||
use: list[dict[str, str]] = Field(default_factory=list)
|
||||
observe: list[dict[str, str]] = Field(default_factory=list)
|
||||
manage: list[dict[str, str]] = Field(default_factory=list)
|
||||
|
||||
|
||||
class Ask(_Strict):
|
||||
"""助手的反问:一次问一件事,带候选。answered 之后留着做历史。"""
|
||||
|
||||
id: str
|
||||
question: str
|
||||
options: list[str] = Field(default_factory=list)
|
||||
path: str | None = None
|
||||
answer: str | None = None
|
||||
|
||||
|
||||
class BlueprintSpec(_Strict):
|
||||
format: Literal["agentlab.blueprint/1"] = BLUEPRINT_FORMAT
|
||||
purpose: Item = Field(default_factory=Item)
|
||||
template: TemplateRef | None = None
|
||||
data: list[DataItem] = Field(default_factory=list)
|
||||
cadence: Item = Field(default_factory=Item) # {preset: daily|weekly|monthly, weekday?, day?, time, tz, period}
|
||||
people: list[Step] = Field(default_factory=list)
|
||||
fields: list[FieldItem] = Field(default_factory=list)
|
||||
writeback: Writeback | None = None
|
||||
access: Access = Field(default_factory=Access)
|
||||
asks: list[Ask] = Field(default_factory=list)
|
||||
|
||||
|
||||
class BpOp(_Strict):
|
||||
"""一个 bp_* 操作。bp_set / bp_add 带 value;bp_remove 只要 path;bp_ask 带 question / options;
|
||||
bp_template 带 template({id, version})。"""
|
||||
|
||||
op: Literal["bp_set", "bp_add", "bp_remove", "bp_ask", "bp_template"]
|
||||
path: str | None = None
|
||||
value: Any = None
|
||||
question: str | None = None
|
||||
options: list[str] = Field(default_factory=list)
|
||||
template: TemplateRef | None = None
|
||||
|
||||
|
||||
def parse_blueprint(data: Any) -> BlueprintSpec:
|
||||
return BlueprintSpec.model_validate(data)
|
||||
|
||||
|
||||
def empty_blueprint() -> dict[str, Any]:
|
||||
return BlueprintSpec().model_dump(mode="json", by_alias=True)
|
||||
@@ -0,0 +1,82 @@
|
||||
"""场景模板 agentlab.template/1(docs/design/normal-user-apps.md §2.3、§3.3、§3.4):骨架锁住,关节可填。
|
||||
|
||||
一个模板是一份登记(和组件目录同一种做法):骨架(带占位的图)、关节、应用骨架、定时任务、卡片约束、说明。
|
||||
关节分两种:确定性关节由蓝图直接填,不经模型;生成关节只有 sql 和 metric_expr 两类,模型在 TemplateApplier 下
|
||||
只能用 fill_joint 填模板声明过的关节。实例化后的图和骨架逐节点比对,只许关节位置不同(tpl.skeleton_changed)。
|
||||
|
||||
开发者扩展时,模板定义、实例化、测试三样都要给;check_registry 在启动时核对。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
TEMPLATE_FORMAT = "agentlab.template/1"
|
||||
#: 首批五个,外加「自由形」(从已有运行或工作流版本导入,没有关节)
|
||||
TEMPLATE_IDS = ("periodic_board", "approval", "exception_handling", "records", "reconcile_writeback", "freeform")
|
||||
#: 关节类型(§3.4)
|
||||
JOINT_KINDS = ("sql", "metric_expr", "fields", "assignee", "policy", "due", "on_timeout", "threshold", "capability",
|
||||
"contract")
|
||||
#: 交给模型填的关节类型;其余由蓝图确定性地填
|
||||
GENERATED_JOINT_KINDS = ("sql", "metric_expr")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Joint:
|
||||
"""骨架里允许填的一个位置:node_id 节点的 field(配置里的 JSON 指针,如 /args/sql、/metrics/0/expression)。
|
||||
from_card 是确定性关节取自蓝图的哪里(JSON 指针);生成关节为 None。group 相同的生成关节一起填,不同的并行填。"""
|
||||
|
||||
id: str
|
||||
kind: str
|
||||
node_id: str
|
||||
field: str
|
||||
from_card: str | None = None
|
||||
group: str | None = None
|
||||
label: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class TemplateDef:
|
||||
id: str
|
||||
version: int
|
||||
label: str
|
||||
description: str
|
||||
#: 给助手看的适用条件(一两句话)
|
||||
applies_when: str
|
||||
#: 这个模板用到哪些卡片(blueprints/spec.CARDS 的子集)和各自的约束说明
|
||||
cards: dict[str, str]
|
||||
#: 个人应用(使用者)能不能用、能用哪一部分(None = 不能用)
|
||||
personal: str | None
|
||||
#: skeleton(bp) -> (GraphSpec 的 JSON, [Joint]):按蓝图出骨架和关节(关节里的值还没填)
|
||||
skeleton: Callable[[dict[str, Any]], tuple[dict[str, Any], list[Joint]]]
|
||||
#: app_skeleton(bp, graph) -> agentlab.app/1:在 derive_app 的结果上按模板排布
|
||||
app_skeleton: Callable[[dict[str, Any], dict[str, Any]], dict[str, Any]] | None = None
|
||||
#: schedule(bp) -> 定时任务参数 | None
|
||||
schedule: Callable[[dict[str, Any]], dict[str, Any] | None] | None = None
|
||||
tags: list[str] = field(default_factory=list)
|
||||
|
||||
|
||||
_REGISTRY: dict[str, TemplateDef] = {}
|
||||
|
||||
|
||||
def register(template: TemplateDef) -> TemplateDef:
|
||||
if template.id in _REGISTRY:
|
||||
raise ValueError(f"模板「{template.id}」重复登记")
|
||||
_REGISTRY[template.id] = template
|
||||
return template
|
||||
|
||||
|
||||
def get_template(template_id: str) -> TemplateDef | None:
|
||||
return _REGISTRY.get(template_id)
|
||||
|
||||
|
||||
def list_templates() -> list[TemplateDef]:
|
||||
return [_REGISTRY[t] for t in TEMPLATE_IDS if t in _REGISTRY] + [
|
||||
t for k, t in _REGISTRY.items() if k not in TEMPLATE_IDS]
|
||||
|
||||
|
||||
def check_registry() -> list[str]:
|
||||
"""启动时核对:首批模板都登记了、关节类型都认得。交回问题清单(空 = 没问题)。"""
|
||||
problems = [f"模板「{t}」没有登记" for t in TEMPLATE_IDS if t not in _REGISTRY]
|
||||
return problems
|
||||
@@ -0,0 +1,19 @@
|
||||
"""试运行(N4、§3.5):跑一次还没发布的图(探索运行,origin_kind=trial),草稿应用对着这次运行解析
|
||||
(apps/resolve.resolve_page 的 trial 旁路),印章写「试运行」。流程类的人工步骤派给试运行的人自己,写入走测试环境或演练,
|
||||
调度不对 trial 的待办做超时升级。
|
||||
|
||||
包 B2 实现。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
async def start_trial(session: Any, blueprint: Any, *, inputs: dict[str, Any] | None, actor: Any) -> str:
|
||||
"""实例化 → 保存图到蓝图 → 发起探索运行,交回 run_id。蓝图状态记 trialed、last_trial_run_id。"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
async def preview_page(session: Any, blueprint: Any, *, page_id: str, run_id: str | None, principal: Any) -> dict[str, Any]:
|
||||
"""按这次试运行解析应用草稿的一页(agentlab.view/1),印章 from=试运行、trial=true。"""
|
||||
raise NotImplementedError
|
||||
@@ -235,6 +235,10 @@ _COLUMN_MIGRATIONS: list[tuple[str, str, str]] = [
|
||||
("mcp_servers", "endpoint_id", "VARCHAR(32)"),
|
||||
("conversations", "app_id", "VARCHAR(32)"),
|
||||
("conversations", "created_by", "VARCHAR(100)"),
|
||||
# 普通用户建应用(docs/design/normal-user-apps.md §3.1):个人应用(scope=personal、owner)和出自哪个蓝图
|
||||
("apps", "scope", "VARCHAR(20) DEFAULT 'shared'"),
|
||||
("apps", "owner", "VARCHAR(100)"),
|
||||
("apps", "blueprint_id", "VARCHAR(32)"),
|
||||
]
|
||||
|
||||
#: 迁移补上的列要建的索引:(索引名, 表, 列或多列)。create_all 不碰已有的表,表上新列的索引也就不会建。
|
||||
|
||||
@@ -977,3 +977,6 @@ from app.db.models_identity import AuditEvent, AuthSession, Group, GroupMember,
|
||||
from app.db.models_process import ( # noqa: E402,F401
|
||||
ActionRecord, NotifyChannel, NotifyDelivery, Task, TaskAssignment,
|
||||
)
|
||||
from app.db.models_blueprints import ( # noqa: E402,F401
|
||||
Blueprint, BlueprintEvent, PublishPlan, RecordsTable, StoredFile,
|
||||
)
|
||||
|
||||
@@ -33,6 +33,11 @@ class App(Base, TimestampMixin):
|
||||
status: Mapped[str] = mapped_column(String(20), default="active") # active | archived
|
||||
created_by: Mapped[str | None] = mapped_column(String(100), default=None)
|
||||
updated_by: Mapped[str | None] = mapped_column(String(100), default=None)
|
||||
# 普通用户建应用(N7):personal 是使用者自己的个人应用,只有拥有者看得见、永远跑探索运行、没有应用版本
|
||||
scope: Mapped[str] = mapped_column(String(20), default="shared") # shared | personal
|
||||
owner: Mapped[str | None] = mapped_column(String(100), default=None)
|
||||
# 出自哪个蓝图(共享应用经发布单生成、个人应用存蓝图时生成)
|
||||
blueprint_id: Mapped[str | None] = mapped_column(String(32), default=None)
|
||||
|
||||
|
||||
class AppVersion(Base):
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
"""普通用户建应用(docs/design/normal-user-apps.md §3.1):蓝图、蓝图留痕、发布单、文件、记录表。
|
||||
|
||||
蓝图(agentlab.blueprint/1)是用户的话和工作流、应用之间的中间件:卡片上每一项都带来源和状态,模型和用户都只能用
|
||||
bp_* 操作改它(N2)。发布单把「保存版本 → 发布工作流 → 建应用 → 定时任务 → 授权 → 跑第一次」编成可续做的步骤(N5)。
|
||||
文件按 sha256 内容寻址存放、提交时绑到一次运行(N9);记录表是平台托管、只许追加的表(N8)。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import ForeignKey, Index, Integer, String, Text, UniqueConstraint
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.db.base import Base, TimestampMixin, UTCDateTime, new_id, utcnow
|
||||
|
||||
#: 蓝图的可见范围:个人(使用者自己的,终点是个人应用)、共享(搭建者的,终点是发布单)
|
||||
BLUEPRINT_SCOPES = ("personal", "shared")
|
||||
#: 蓝图的状态:起草中 → 试运行过 → 已发布(共享)/ 个人应用(个人);个人应用提交审核时为审核中
|
||||
BLUEPRINT_STATUSES = ("drafting", "trialed", "published", "personal", "in_review")
|
||||
#: 蓝图从哪里来:一段对话、一个模板、一次运行(问数据的一轮回答)、一个已发布的工作流版本
|
||||
BLUEPRINT_SOURCES = ("conversation", "template", "run", "workflow_version")
|
||||
#: 发布单的状态
|
||||
PLAN_STATUSES = ("planned", "running", "stopped", "done")
|
||||
|
||||
|
||||
class Blueprint(Base, TimestampMixin):
|
||||
__tablename__ = "blueprints"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(32), primary_key=True, default=new_id)
|
||||
name: Mapped[str] = mapped_column(String(200), default="")
|
||||
owner: Mapped[str | None] = mapped_column(String(100), default=None, index=True)
|
||||
scope: Mapped[str] = mapped_column(String(20), default="shared")
|
||||
status: Mapped[str] = mapped_column(String(20), default="drafting")
|
||||
# agentlab.blueprint/1(blueprints/spec.py)
|
||||
spec: Mapped[dict[str, Any]] = mapped_column(default=dict)
|
||||
template_id: Mapped[str | None] = mapped_column(String(64), default=None)
|
||||
template_version: Mapped[int | None] = mapped_column(Integer, default=None)
|
||||
# {"kind": conversation|template|run|workflow_version, "id": …, "version"?: …}
|
||||
source: Mapped[dict[str, Any]] = mapped_column(default=dict)
|
||||
# 蓝图对话(kind=blueprint 的 Conversation)
|
||||
conversation_id: Mapped[str | None] = mapped_column(String(32), default=None)
|
||||
# 共享蓝图:实例化出来的工作流和发布出来的应用;个人蓝图:个人应用
|
||||
workflow_id: Mapped[str | None] = mapped_column(String(32), default=None, index=True)
|
||||
app_id: Mapped[str | None] = mapped_column(String(32), default=None, index=True)
|
||||
last_trial_run_id: Mapped[str | None] = mapped_column(String(32), default=None)
|
||||
# 最近一次实例化的图(GraphSpec 的 JSON)和它的哈希。个人应用的图只存在这里(使用者没有工作流)
|
||||
graph: Mapped[dict[str, Any]] = mapped_column(default=dict)
|
||||
graph_hash: Mapped[str | None] = mapped_column(String(64), default=None)
|
||||
# 各关节的状态 {joint_id: {"status": filled|pending|refused, "by": assistant|template|user, "note"?: …}}
|
||||
joints: Mapped[dict[str, Any]] = mapped_column(default=dict)
|
||||
# 审核副本指向原蓝图
|
||||
copied_from: Mapped[str | None] = mapped_column(String(32), default=None)
|
||||
updated_by: Mapped[str | None] = mapped_column(String(100), default=None)
|
||||
|
||||
|
||||
class BlueprintEvent(Base):
|
||||
"""蓝图的留痕:每一次 bp_* 操作(模型提的、用户改的)、每一次实例化和试运行。"""
|
||||
|
||||
__tablename__ = "blueprint_events"
|
||||
__table_args__ = (UniqueConstraint("blueprint_id", "seq"),)
|
||||
|
||||
id: Mapped[str] = mapped_column(String(32), primary_key=True, default=new_id)
|
||||
blueprint_id: Mapped[str] = mapped_column(ForeignKey("blueprints.id", ondelete="CASCADE"), index=True)
|
||||
seq: Mapped[int] = mapped_column(Integer)
|
||||
actor: Mapped[str | None] = mapped_column(String(100), default=None)
|
||||
verified: Mapped[bool] = mapped_column(default=False)
|
||||
# bp_set | bp_add | bp_remove | bp_ask | bp_template | instantiate | fill_joint | trial | publish | save_personal | copy
|
||||
op: Mapped[str] = mapped_column(String(40))
|
||||
data: Mapped[dict[str, Any]] = mapped_column(default=dict)
|
||||
at: Mapped[datetime] = mapped_column(UTCDateTime, default=utcnow)
|
||||
|
||||
|
||||
class PublishPlan(Base, TimestampMixin):
|
||||
"""一次确认发布(N5)。steps 是 [{kind, status: pending|done|failed|skipped, result, error_code, message, at}],
|
||||
summary 是白话摘要 [{step, text}];停在某一步时 status=stopped,修好后从那一步接着做。"""
|
||||
|
||||
__tablename__ = "publish_plans"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(32), primary_key=True, default=new_id)
|
||||
blueprint_id: Mapped[str] = mapped_column(ForeignKey("blueprints.id", ondelete="CASCADE"), index=True)
|
||||
actor: Mapped[str | None] = mapped_column(String(100), default=None)
|
||||
level: Mapped[str] = mapped_column(String(20), default="published")
|
||||
# 发布单针对的那张图(实例化结果的哈希):执行时图变了就拒绝,要求重新出单
|
||||
graph_hash: Mapped[str | None] = mapped_column(String(64), default=None)
|
||||
options: Mapped[dict[str, Any]] = mapped_column(default=dict)
|
||||
steps: Mapped[list[Any]] = mapped_column(default=list)
|
||||
summary: Mapped[list[Any]] = mapped_column(default=list)
|
||||
problems: Mapped[list[Any]] = mapped_column(default=list)
|
||||
status: Mapped[str] = mapped_column(String(20), default="planned")
|
||||
|
||||
|
||||
class StoredFile(Base):
|
||||
"""上传的文件(N9):内容按 sha256 存在 data/files/<前 2 位>/<sha>(只读),这里记元数据和绑定。
|
||||
提交表单时绑到一次运行(一次性)或一行记录;没绑定的上传按时清理。"""
|
||||
|
||||
__tablename__ = "files"
|
||||
__table_args__ = (Index("ix_files_unbound", "bound_run_id", "uploaded_at"),)
|
||||
|
||||
id: Mapped[str] = mapped_column(String(32), primary_key=True, default=new_id)
|
||||
sha256: Mapped[str] = mapped_column(String(64), index=True)
|
||||
name: Mapped[str] = mapped_column(String(300))
|
||||
size: Mapped[int] = mapped_column(Integer)
|
||||
mime: Mapped[str] = mapped_column(String(120), default="application/octet-stream")
|
||||
uploaded_by: Mapped[str | None] = mapped_column(String(100), default=None)
|
||||
uploaded_at: Mapped[datetime] = mapped_column(UTCDateTime, default=utcnow)
|
||||
# 上传时的上下文:{"app_id", "entry"} 或 {"task_id"} 或 {"blueprint_id"}(个人应用、试运行)
|
||||
context: Mapped[dict[str, Any]] = mapped_column(default=dict)
|
||||
bound_run_id: Mapped[str | None] = mapped_column(String(32), default=None)
|
||||
# 附在记录上时:{"table_id", "row_id"}
|
||||
bound_record: Mapped[dict[str, Any] | None] = mapped_column(default=None)
|
||||
purpose: Mapped[str] = mapped_column(String(40), default="input")
|
||||
|
||||
|
||||
class RecordsTable(Base, TimestampMixin):
|
||||
"""记录表(N8)的一个版本:平台托管的 SQLite 文件 data/records/<table_key>.v<version>.db,只许追加。
|
||||
兼容的表单改动(只加可选字段)在同一版本上 ADD COLUMN;不兼容的出新版本,旧版本只读保留。"""
|
||||
|
||||
__tablename__ = "records_tables"
|
||||
__table_args__ = (UniqueConstraint("table_key", "version"),)
|
||||
|
||||
id: Mapped[str] = mapped_column(String(32), primary_key=True, default=new_id)
|
||||
# 同一张记录表各版本共用的键
|
||||
table_key: Mapped[str] = mapped_column(String(64), index=True)
|
||||
version: Mapped[int] = mapped_column(Integer, default=1)
|
||||
name: Mapped[str] = mapped_column(String(200))
|
||||
app_id: Mapped[str | None] = mapped_column(String(32), default=None, index=True)
|
||||
blueprint_id: Mapped[str | None] = mapped_column(String(32), default=None)
|
||||
# 登记成 origin=records 的只读数据源(看板用 db_query__<名字> 查)
|
||||
source_id: Mapped[str | None] = mapped_column(String(32), default=None)
|
||||
# [{name, label, type, required, sensitive}]
|
||||
columns: Mapped[list[Any]] = mapped_column(default=list)
|
||||
retired_at: Mapped[datetime | None] = mapped_column(UTCDateTime, default=None)
|
||||
note: Mapped[str] = mapped_column(Text, default="")
|
||||
@@ -22,6 +22,8 @@ ORIGIN_KINDS = (
|
||||
"app_backfill", # 钉住项按 recompute 升版后,按历次触发的输入补算
|
||||
"hook", # 外部系统经入站钩子触发
|
||||
"api", # 直接调接口,没说是哪
|
||||
"trial", # 蓝图的试运行(探索运行,docs/design/normal-user-apps.md N4)
|
||||
"personal_app", # 个人应用的刷新(探索运行,N7)
|
||||
)
|
||||
|
||||
#: 客户端在 POST /api/runs 里能声明的来源
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
"""文件(docs/design/normal-user-apps.md N9、§3.8):按 sha256 内容寻址存放(data/files/<前 2 位>/<sha>,只读),
|
||||
两步上传(先拿引用再提交表单),提交时绑到一次运行(一次性),引用连同 sha256 进运行输入、随封存固定。
|
||||
读文件:模板函数 file_text(ref);导入:平台内置能力 table_import_period(按上传源已有的配方导入一期,效果写入)。
|
||||
下载按绑定的运行或记录核对读权限。没绑定的上传 24 小时后清理。
|
||||
|
||||
包 B3 实现。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
#: 平台内置能力:把一个文件按上传源已有的配方导入成一期
|
||||
TABLE_IMPORT_PERIOD = "table_import_period"
|
||||
#: 文件类型白名单(待拍板 Q5 按推荐)
|
||||
ALLOWED_MIME_PREFIXES = (
|
||||
"application/pdf",
|
||||
"application/msword",
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
"application/vnd.ms-excel",
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
"text/csv",
|
||||
"text/plain",
|
||||
"image/png",
|
||||
"image/jpeg",
|
||||
)
|
||||
#: 没绑定的上传多久后清理(秒)
|
||||
UNBOUND_TTL_S = 24 * 3600
|
||||
@@ -18,6 +18,11 @@ from app.api import (
|
||||
actions as actions_api, apps as apps_api, auth as auth_api, capabilities as capabilities_api, hooks as hooks_api,
|
||||
identity as identity_api, ledger as ledger_api, notify as notify_api, tasks as tasks_api,
|
||||
)
|
||||
# 普通用户建应用(docs/design/normal-user-apps.md §3.10):蓝图、模板、文件、个人应用。权限在接口里按蓝图的范围和拥有者判
|
||||
from app.api import (
|
||||
blueprints as blueprints_api, copilot_blueprint, files as files_api, personal as personal_api,
|
||||
templates as templates_api,
|
||||
)
|
||||
from app.api.coded import CodedHTTPException, coded_handler
|
||||
from app.api.deps import chat_scope, evidence_scope, require_role, role_by_method, run_scope
|
||||
from app.auth.middleware import AuthMiddleware
|
||||
@@ -221,6 +226,13 @@ for router, guard in (
|
||||
(identity_api.directory_router, _BUILDER),
|
||||
# 能力登记、端点:搭建者只读,改动在接口里要管理员
|
||||
(capabilities_api.router, _BUILDER),
|
||||
# 普通用户建应用:个人应用的接口共用 /api/apps 前缀,要在 apps_api 之前登记;蓝图助手共用 /api/copilot 前缀,
|
||||
# 在接口里按蓝图的范围判(共享要 builder,个人要拥有者且开了 apps.user_personal_apps)
|
||||
(personal_api.router, _USER),
|
||||
(blueprints_api.router, _USER),
|
||||
(templates_api.router, _USER),
|
||||
(files_api.router, _USER),
|
||||
(copilot_blueprint.router, _USER),
|
||||
# 应用、待办、生效记录、通知:各自按应用授权、派给谁再判断
|
||||
(apps_api.router, _USER),
|
||||
(tasks_api.router, _USER),
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
"""白话层(docs/design/normal-user-apps.md N10):问题编号 → 一句话、为什么要紧、谁来改、怎么办。
|
||||
|
||||
覆盖工作流发布门禁(contract.* / governed.* / action.* / process.*)、应用检查(app.*)、蓝图(bp.*)、模板(tpl.*)、
|
||||
个人应用(personal.*)、发布单步骤失败(plan.*)。没登记白话的照旧显示技术说法;检查脚本报「有问题编号没有白话」。
|
||||
who:you(你来定)/ assistant(助手可以修)/ admin(要找管理员),按自动修复的 kind、APP_HUMAN_ONLY、要管理员的编号判。
|
||||
|
||||
框架在这里;内容由包 B2 补齐(B1、B3、B4 新增的编号各自补进来)。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
WHO = ("you", "assistant", "admin")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Plain:
|
||||
title: str
|
||||
why: str = ""
|
||||
who: str = "you"
|
||||
action: str = ""
|
||||
|
||||
|
||||
#: 问题编号 → 白话。前缀匹配用 PLAIN_PREFIX(如 "subgraph_version_" 一类)
|
||||
PLAIN: dict[str, Plain] = {}
|
||||
PLAIN_PREFIX: dict[str, Plain] = {}
|
||||
|
||||
|
||||
def plain_of(code: str | None) -> Plain | None:
|
||||
if not code:
|
||||
return None
|
||||
if code in PLAIN:
|
||||
return PLAIN[code]
|
||||
for prefix, plain in PLAIN_PREFIX.items():
|
||||
if code.startswith(prefix):
|
||||
return plain
|
||||
return None
|
||||
|
||||
|
||||
def with_plain(issue: dict[str, Any]) -> dict[str, Any]:
|
||||
"""给一条问题补上 plain = {title, why, who, action};没登记的不补。"""
|
||||
plain = plain_of(issue.get("code"))
|
||||
if plain is None:
|
||||
return issue
|
||||
return {**issue, "plain": {"title": plain.title, "why": plain.why, "who": plain.who, "action": plain.action}}
|
||||
@@ -0,0 +1,19 @@
|
||||
"""记录表(docs/design/normal-user-apps.md N8、§3.7):平台托管的 SQLite、只许追加。
|
||||
|
||||
- 每行带系统列 _seq / _id / _submitted_by / _submitted_at / _run_id / _supersedes / _schema_version;
|
||||
- 登记成 origin=records 的只读数据源,看板照常用 db_query__<名字> 查;
|
||||
- 写入只经动作:平台内置能力 records_append(效果写入、允许无人值守、幂等键即动作键),查询能力 records_lookup;
|
||||
- 钉版本:运行开始时 data_versions 记下高水位 _seq;查询连接上建同名 TEMP VIEW,只露出 _seq ≤ 高水位、没被更正过的行;
|
||||
- 行级可见:同一套临时视图按人过滤(use 只看自己的,observe / manage 看全部);正式运行以服务身份跑、不过滤。
|
||||
|
||||
包 B3 实现。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
#: 数据源的 origin
|
||||
RECORDS_ORIGIN = "records"
|
||||
#: 平台内置能力(工具名,只能用字母、数字、下划线)
|
||||
RECORDS_APPEND = "records_append"
|
||||
RECORDS_LOOKUP = "records_lookup"
|
||||
#: 系统列
|
||||
SYSTEM_COLUMNS = ("_seq", "_id", "_submitted_by", "_submitted_at", "_run_id", "_supersedes", "_schema_version")
|
||||
@@ -27,6 +27,9 @@ import { EntryPage } from './pages/apps/EntryPage'
|
||||
import { InstancePage } from './pages/apps/InstancePage'
|
||||
import { TaskPage } from './pages/apps/TaskPage'
|
||||
import { AppEditor } from './pages/apps/edit/AppEditor'
|
||||
// 普通用户建应用:新建应用的起点、蓝图页(F5)
|
||||
import { NewAppStart } from './pages/blueprints/NewAppStart'
|
||||
import { BlueprintPage } from './pages/blueprints/BlueprintPage'
|
||||
import { LoginPage } from './pages/LoginPage'
|
||||
import { Logo } from './components/Logo'
|
||||
import {
|
||||
@@ -175,6 +178,10 @@ export default function App() {
|
||||
<Route path="/schedules/:id" element={<SchedulesPage />} />
|
||||
{/* 应用:固定路径(inbox)写在 /apps/:appId 前面,读的人一眼看出它不是应用 id */}
|
||||
<Route path="/apps" element={<AppsHome />} />
|
||||
{/* 要在 /apps/:appId 之前:create 不是应用 id */}
|
||||
<Route path="/apps/create" element={<NewAppStart />} />
|
||||
<Route path="/blueprints/:blueprintId" element={<BlueprintPage />} />
|
||||
<Route path="/blueprints/:blueprintId/:section" element={<BlueprintPage />} />
|
||||
<Route path="/apps/inbox/:taskId" element={<TaskPage />} />
|
||||
<Route path="/apps/:appId" element={<AppPage />} />
|
||||
<Route path="/apps/:appId/p/:pageId" element={<AppPage />} />
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* 普通用户建应用的接口(docs/design/normal-user-apps.md §3.10):蓝图、试运行、发布单、模板、文件、个人应用、页面编辑候选、记录。
|
||||
* 约定在 typesBlueprint.ts。流式的蓝图助手、填关节走 store 里的 SSE(同 copilot),不在这里。
|
||||
*/
|
||||
import type { AppSpec } from '../typesApp'
|
||||
import type {
|
||||
AppCandidate, Blueprint, BlueprintScope, BlueprintSourceKind, BpOp, FileRef, PlainIssue, PublishPlanRow, RecordsPage,
|
||||
TemplateInfo, TrialPage, TrialStart,
|
||||
} from '../typesBlueprint'
|
||||
import { del, get, patch, post, qs, upload, type UploadOptions } from './http'
|
||||
|
||||
export const blueprints = {
|
||||
create: (body: { source: { kind: BlueprintSourceKind; id?: string; version?: number }; scope: BlueprintScope; name?: string;
|
||||
template?: string }) => post<Blueprint>('/blueprints', body),
|
||||
list: (q: { scope?: BlueprintScope } = {}) => get<{ items: Blueprint[] }>(`/blueprints${qs(q)}`),
|
||||
get: (id: string) => get<Blueprint>(`/blueprints/${encodeURIComponent(id)}`),
|
||||
/** 卡片上的修改:bp_* 操作,来源记成 user */
|
||||
patch: (id: string, body: { ops?: BpOp[]; name?: string }) => patch<Blueprint>(`/blueprints/${encodeURIComponent(id)}`, body),
|
||||
instantiate: (id: string) =>
|
||||
post<{ blueprint: Blueprint; app_spec: AppSpec; problems: PlainIssue[] }>(`/blueprints/${encodeURIComponent(id)}/instantiate`, {}),
|
||||
trial: (id: string, body: { inputs?: Record<string, unknown> } = {}) =>
|
||||
post<TrialStart>(`/blueprints/${encodeURIComponent(id)}/trial`, body),
|
||||
preview: (id: string, q: { page?: string; run?: string } = {}) =>
|
||||
get<TrialPage>(`/blueprints/${encodeURIComponent(id)}/preview${qs(q)}`),
|
||||
publishPlan: (id: string, body: { level: 'published' | 'governed'; first_run?: boolean }) =>
|
||||
post<PublishPlanRow>(`/blueprints/${encodeURIComponent(id)}/publish-plan`, body),
|
||||
plan: (planId: string) => get<PublishPlanRow>(`/publish-plans/${encodeURIComponent(planId)}`),
|
||||
runPlan: (planId: string) => post<PublishPlanRow>(`/publish-plans/${encodeURIComponent(planId)}/run`, {}),
|
||||
resumePlan: (planId: string) => post<PublishPlanRow>(`/publish-plans/${encodeURIComponent(planId)}/resume`, {}),
|
||||
// ---- 个人应用(N7)
|
||||
savePersonal: (id: string) => post<{ app_id: string; blueprint: Blueprint }>(`/blueprints/${encodeURIComponent(id)}/save-personal`, {}),
|
||||
copyForReview: (id: string) => post<Blueprint>(`/blueprints/${encodeURIComponent(id)}/copy`, {}),
|
||||
}
|
||||
|
||||
export const templates = {
|
||||
list: () => get<{ items: TemplateInfo[] }>('/templates'),
|
||||
}
|
||||
|
||||
export const files = {
|
||||
/** 先上传拿引用,再把引用作为表单值提交(N9) */
|
||||
upload: (file: File, context: Record<string, string>, opts?: UploadOptions) => {
|
||||
const form = new FormData()
|
||||
form.append('file', file)
|
||||
form.append('context', JSON.stringify(context))
|
||||
return upload<FileRef>('/files', form, opts)
|
||||
},
|
||||
url: (fileId: string) => `/api/files/${encodeURIComponent(fileId)}`,
|
||||
remove: (fileId: string) => del(`/files/${encodeURIComponent(fileId)}`),
|
||||
}
|
||||
|
||||
export const personal = {
|
||||
refresh: (appId: string) => post<{ run_id: string; remaining: number }>(`/apps/${encodeURIComponent(appId)}/refresh`, {}),
|
||||
submitReview: (appId: string, body: { to: { kind: 'user' | 'group'; name: string }[]; note?: string }) =>
|
||||
post<{ task_id: string }>(`/apps/${encodeURIComponent(appId)}/submit-review`, body),
|
||||
candidates: (appId: string) => get<{ items: AppCandidate[] }>(`/apps/${encodeURIComponent(appId)}/candidates`),
|
||||
records: (appId: string, q: { table?: string; cursor?: string; history?: boolean } = {}) =>
|
||||
get<RecordsPage>(`/apps/${encodeURIComponent(appId)}/records${qs(q)}`),
|
||||
}
|
||||
@@ -28,6 +28,8 @@ import { apps, ledger } from './apps'
|
||||
import { actions, tasks } from './tasks'
|
||||
import { auth, identity } from './auth'
|
||||
import { capabilities, endpoints, notify } from './capabilities'
|
||||
// 普通用户建应用(typesBlueprint.ts 是接口约定)
|
||||
import { blueprints, files, personal, templates } from './blueprints'
|
||||
|
||||
// 请求层搬到了 http.ts(见那边的说明)。原先从这里导出的照旧从这里导出,调用方不用改;
|
||||
// 请求函数也一并转出,给不在 api/ 下、又想直接发请求的地方用。api/ 下的命名空间文件请直接从 http.ts 导入
|
||||
@@ -865,6 +867,12 @@ export const api = {
|
||||
capabilities,
|
||||
endpoints,
|
||||
notify,
|
||||
|
||||
// ---- 普通用户建应用(typesBlueprint.ts 是接口约定) ----
|
||||
blueprints,
|
||||
templates,
|
||||
files,
|
||||
personal,
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -5736,3 +5736,4 @@ export const CAPABILITY_REVIEW_TEXT: typeof CATALOG_REVIEW_TEXT = {
|
||||
|
||||
// 应用平台使用面的文案(FE-5 维护)。调用方照旧从 lib/terms 导入
|
||||
export * from './termsApp'
|
||||
export * from './termsBlueprint'
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* 普通用户建应用的文案(docs/design/normal-user-apps.md 第四部分):新建应用的起点、蓝图卡片、试运行、发布摘要。
|
||||
* 包 F5 独占这个文件;页面编辑、文件控件、记录列表、个人应用的文案在 termsApp.ts(包 F6)。由 terms.ts 统一再导出。
|
||||
*/
|
||||
|
||||
export const NEW_APP_TEXT = {
|
||||
title: '新建应用',
|
||||
}
|
||||
|
||||
export const BLUEPRINT_TEXT = {
|
||||
title: '应用蓝图',
|
||||
}
|
||||
@@ -60,7 +60,8 @@ export function backfillTargets(spec: AppSpec, current: AppVersionRow, previous:
|
||||
if (!alias || v.source.kind === 'instance' || current.dispositions?.[alias] !== 'recompute') continue
|
||||
const now = current.pins.find((p) => p.alias === alias)
|
||||
const before = previous.pins.find((p) => p.alias === alias)
|
||||
if (!now || !before || before.version === now.version) continue
|
||||
// 已发布的应用版本里钉住项都有版本号;没有的(个人应用)谈不上升版
|
||||
if (!now || !before || now.version == null || before.version == null || before.version === now.version) continue
|
||||
const max = seriesDepth(spec, view)
|
||||
if (max > 0) out.push({ view, alias, from: before.version, to: now.version, max })
|
||||
}
|
||||
|
||||
@@ -361,11 +361,13 @@ function useViewNodes(view: string): { id: string; label: string }[] {
|
||||
const pin = draft.pins.find((p) => p.alias === source?.pin) ?? (draft.pins.length === 1 ? draft.pins[0] : undefined)
|
||||
const [nodes, setNodes] = useState<{ id: string; label: string }[]>([])
|
||||
useEffect(() => {
|
||||
if (!pin) { setNodes([]); return }
|
||||
const key = `${pin.workflow_id}@${pin.version}`
|
||||
// 还没发布的(试运行派生的草稿)、个人应用的钉住项没有工作流版本可取
|
||||
if (!pin || !pin.workflow_id || pin.version == null) { setNodes([]); return }
|
||||
const { workflow_id: wid, version } = pin
|
||||
const key = `${wid}@${version}`
|
||||
let p = graphCache.get(key)
|
||||
if (!p) {
|
||||
p = api.workflows.version(pin.workflow_id, pin.version)
|
||||
p = api.workflows.version(wid, version)
|
||||
.then((v) => (v.graph?.nodes ?? []).map((n) => ({ id: n.id, label: String(n.data?.label ?? n.id) })))
|
||||
p.catch(() => graphCache.delete(key))
|
||||
graphCache.set(key, p)
|
||||
@@ -450,7 +452,7 @@ function ViewsPanel() {
|
||||
<select className="field h-7 py-0 text-xs" value={v.source.pin ?? ''} aria-label={T.viewPin} title={APP_TERM_TEXT.pin}
|
||||
onChange={(e) => set(key, { source: { ...v.source, pin: e.target.value || null } })}>
|
||||
<option value="">{T.viewPin}</option>
|
||||
{draft.pins.map((p) => <option key={p.alias} value={p.alias}>{p.alias} · v{p.version}</option>)}
|
||||
{draft.pins.map((p) => <option key={p.alias} value={p.alias}>{p.alias}{p.version != null ? ` · v${p.version}` : ''}</option>)}
|
||||
</select>
|
||||
)}
|
||||
{v.source.kind === 'latest_sealed' && (
|
||||
|
||||
@@ -76,9 +76,12 @@ export function PinsSection() {
|
||||
setDraft((s) => ({ ...s, pins: s.pins.map((p, j) => (j === i ? { ...p, ...patch } : p)) }))
|
||||
|
||||
const rederive = async (pin: AppPin) => {
|
||||
// 还没发布的(试运行派生的草稿)、个人应用的钉住项没有工作流版本可派生
|
||||
if (!pin.workflow_id || pin.version == null) return
|
||||
const { workflow_id: wid, version } = pin
|
||||
if (!(await confirmDialog({ title: T.rederiveTitle, body: T.rederiveBody, confirmLabel: T.rederive, danger: true }))) return
|
||||
try {
|
||||
const { spec } = await api.apps.derive({ workflow_id: pin.workflow_id, version: pin.version, name: draft.name })
|
||||
const { spec } = await api.apps.derive({ workflow_id: wid, version, name: draft.name })
|
||||
setDraft(() => ({ ...spec, name: draft.name, description: draft.description || spec.description }))
|
||||
toast.ok(T.rederiveDone)
|
||||
} catch (e) {
|
||||
@@ -107,12 +110,12 @@ export function PinsSection() {
|
||||
{(p) => <input {...p} className="field mono text-xs" value={pin.alias} onChange={(e) => update(i, { alias: e.target.value })} />}
|
||||
</Field>
|
||||
<div className="min-w-0 pb-1.5 text-xs">
|
||||
<div className="truncate font-medium">{nameOf(pin.workflow_id)}</div>
|
||||
<div className="truncate font-medium">{pin.workflow_id ? nameOf(pin.workflow_id) : '—'}</div>
|
||||
<div className="mono text-2xs text-faint">{T.pinHash(pin.graph_hash || '')}</div>
|
||||
</div>
|
||||
<Field label={T.pickVersion}>
|
||||
{() => (
|
||||
<PublishedVersionPicker compact workflowId={pin.workflow_id} value={pin.version}
|
||||
<PublishedVersionPicker compact workflowId={pin.workflow_id ?? ''} value={pin.version}
|
||||
onChange={(v) => v && update(i, { version: v.version, graph_hash: v.graph_hash ?? '',
|
||||
level: v.level ?? null })} />
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { EmptyState } from '../../components/ui'
|
||||
import { BLUEPRINT_TEXT } from '../../lib/terms'
|
||||
|
||||
/**
|
||||
* 蓝图页(docs/design/normal-user-apps.md §4.2–§4.4):左边对话、右边卡片;试运行;发布摘要(搭建者)/ 存成我的应用(使用者)。
|
||||
* 骨架阶段是占位,包 F5 实现。
|
||||
*/
|
||||
export function BlueprintPage() {
|
||||
const { blueprintId } = useParams()
|
||||
return <EmptyState title={BLUEPRINT_TEXT.title} body={blueprintId ?? ''} />
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { EmptyState } from '../../components/ui'
|
||||
import { NEW_APP_TEXT } from '../../lib/terms'
|
||||
|
||||
/**
|
||||
* 新建应用的三个起点(docs/design/normal-user-apps.md §4.1):说说你要什么、从场景模板开始、从已有的开始。
|
||||
* 骨架阶段是占位,包 F5 实现。
|
||||
*/
|
||||
export function NewAppStart() {
|
||||
return <EmptyState title={NEW_APP_TEXT.title} body="" />
|
||||
}
|
||||
@@ -67,6 +67,8 @@ export interface AuditRow {
|
||||
export type FormWidget =
|
||||
| 'text' | 'textarea' | 'number' | 'integer' | 'date' | 'datetime' | 'select' | 'radio' | 'multiselect'
|
||||
| 'checkbox' | 'principal' | 'note' | 'json'
|
||||
/** 文件(N9):字段 type 为 object,值是上传后拿回的引用 FileRef(typesBlueprint.ts) */
|
||||
| 'file'
|
||||
|
||||
export interface FormField {
|
||||
type: 'string' | 'number' | 'integer' | 'boolean' | 'array'
|
||||
@@ -111,11 +113,14 @@ export type Disposition = 'recompute' | 'dual' | 'incomparable'
|
||||
|
||||
export interface AppPin {
|
||||
alias: string
|
||||
workflow_id: string
|
||||
version: number
|
||||
/** 共享应用钉工作流版本;个人应用(docs/design/normal-user-apps.md N7)写 blueprint,workflow_id 为空 */
|
||||
workflow_id: string | null
|
||||
/** 还没发布(蓝图试运行时派生的草稿)、或个人应用时为空 */
|
||||
version: number | null
|
||||
graph_hash: string
|
||||
level?: string | null
|
||||
use: PinUse
|
||||
blueprint?: string | null
|
||||
}
|
||||
|
||||
export interface AppEntry {
|
||||
@@ -128,7 +133,8 @@ export interface AppEntry {
|
||||
}
|
||||
|
||||
export interface AppViewSource {
|
||||
kind: 'latest_sealed' | 'on_demand' | 'instance'
|
||||
/** exploratory 只在个人应用里合法:最近一次成功封存的探索运行(N7) */
|
||||
kind: 'latest_sealed' | 'on_demand' | 'instance' | 'exploratory'
|
||||
pin?: string | null
|
||||
from?: string | null
|
||||
params?: FormSchema | { $ref: string } | null
|
||||
@@ -146,6 +152,8 @@ export interface AppComponent {
|
||||
bind?: AppBinding | null
|
||||
visible?: string | null
|
||||
scope?: string | null
|
||||
/** 作者隐藏了它(N6):解析时跳过,编辑状态下画成半透明 */
|
||||
hidden?: boolean
|
||||
children?: AppComponent[]
|
||||
}
|
||||
|
||||
@@ -216,6 +224,10 @@ export interface AppSummary {
|
||||
/** 当前用户的授权(关登录时为 manage) */
|
||||
my_role: GrantRole | null
|
||||
updated_at: string
|
||||
/** 个人应用(N7):只有拥有者看得见、跑探索运行;老服务端不给时按 shared */
|
||||
scope?: 'shared' | 'personal'
|
||||
owner?: string | null
|
||||
blueprint_id?: string | null
|
||||
}
|
||||
|
||||
export interface AppDetail extends AppSummary {
|
||||
@@ -306,8 +318,8 @@ export interface Stamp {
|
||||
}
|
||||
|
||||
export interface ViewSourceState {
|
||||
/** 来源的种类:按需视图(on_demand)才有「刷新」 */
|
||||
kind?: 'latest_sealed' | 'on_demand' | 'instance'
|
||||
/** 来源的种类:按需视图(on_demand)才有「刷新」;试运行解析时为 trial,个人应用为 exploratory */
|
||||
kind?: 'latest_sealed' | 'on_demand' | 'instance' | 'exploratory' | 'trial'
|
||||
state: 'ok' | 'empty' | 'running' | 'stale' | 'error'
|
||||
run_id: string | null
|
||||
stamp: Stamp | null
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
/**
|
||||
* 普通用户建应用的接口约定(docs/design/normal-user-apps.md 第三部分):蓝图、场景模板、试运行、发布单、文件、记录。
|
||||
* 和后端 app/blueprints/spec.py、app/db/models_blueprints.py 一一对应。改这里要同时改后端(骨架阶段冻结)。
|
||||
*/
|
||||
import type { AppSpec, LintIssue, ViewModel } from './typesApp'
|
||||
|
||||
// ---------------------------------------------------------------- 蓝图 agentlab.blueprint/1
|
||||
|
||||
export type ItemSource = 'user' | 'assistant' | 'template' | 'catalog' | 'caliber'
|
||||
export type ItemStatus = 'confirmed' | 'proposed' | 'missing'
|
||||
|
||||
/** 卡片上的一项:值、来源、状态,可选一句「为什么」 */
|
||||
export interface BpItem<T = unknown> {
|
||||
value: T | null
|
||||
source: ItemSource
|
||||
status: ItemStatus
|
||||
why?: string | null
|
||||
}
|
||||
|
||||
export interface BpDataItem {
|
||||
id: string
|
||||
label: BpItem<string>
|
||||
unit: BpItem<string>
|
||||
/** 对到 {source, table} */
|
||||
table: BpItem<{ source: string; table: string }>
|
||||
measure: BpItem<string>
|
||||
date: BpItem<{ column: string; rule?: string }>
|
||||
filter: BpItem<string>
|
||||
grain: BpItem<string>
|
||||
/** 复用已发布的口径卡 */
|
||||
reuse?: { workflow_id: string; version: number; node_id: string; metric_id: string } | null
|
||||
}
|
||||
|
||||
export interface BpPrincipal { kind: 'user' | 'group' | 'slot' | 'initiator'; name: string }
|
||||
|
||||
export interface BpStep {
|
||||
id: string
|
||||
label: BpItem<string>
|
||||
who: BpItem<BpPrincipal[]>
|
||||
policy: BpItem<{ kind: 'any' | 'all' | 'quorum'; m?: number }>
|
||||
due: BpItem<string>
|
||||
on_timeout: BpItem<{ action: 'remind' | 'escalate' | 'reject'; to?: BpPrincipal[] }>
|
||||
threshold: BpItem<{ field: string; op: string; value: number | string }>
|
||||
}
|
||||
|
||||
export interface BpField { name: string; spec: BpItem<Record<string, any>>; sensitive: boolean }
|
||||
|
||||
export interface BpWriteback {
|
||||
capability: BpItem<string>
|
||||
mapping: BpItem<Record<string, string>>
|
||||
confirm: BpItem<boolean>
|
||||
}
|
||||
|
||||
export interface BpAccess {
|
||||
use: { kind: string; name: string }[]
|
||||
observe: { kind: string; name: string }[]
|
||||
manage: { kind: string; name: string }[]
|
||||
}
|
||||
|
||||
export interface BpAsk { id: string; question: string; options: string[]; path?: string | null; answer?: string | null }
|
||||
|
||||
export interface BlueprintSpec {
|
||||
format: 'agentlab.blueprint/1'
|
||||
purpose: BpItem<string>
|
||||
template: { id: string; version: number } | null
|
||||
data: BpDataItem[]
|
||||
cadence: BpItem<{ preset: 'daily' | 'weekly' | 'monthly'; weekday?: number; day?: number; time: string; tz: string; period?: string }>
|
||||
people: BpStep[]
|
||||
fields: BpField[]
|
||||
writeback: BpWriteback | null
|
||||
access: BpAccess
|
||||
asks: BpAsk[]
|
||||
}
|
||||
|
||||
export type BpOpName = 'bp_set' | 'bp_add' | 'bp_remove' | 'bp_ask' | 'bp_template'
|
||||
export interface BpOp {
|
||||
op: BpOpName
|
||||
/** JSON 指针(/data/0/table) */
|
||||
path?: string | null
|
||||
value?: unknown
|
||||
question?: string | null
|
||||
options?: string[]
|
||||
template?: { id: string; version: number } | null
|
||||
}
|
||||
|
||||
export type BlueprintScope = 'personal' | 'shared'
|
||||
export type BlueprintStatus = 'drafting' | 'trialed' | 'published' | 'personal' | 'in_review'
|
||||
export type BlueprintSourceKind = 'conversation' | 'template' | 'run' | 'workflow_version'
|
||||
|
||||
export interface JointState { status: 'filled' | 'pending' | 'refused'; by: 'template' | 'assistant' | 'user'; note?: string | null }
|
||||
|
||||
export interface Blueprint {
|
||||
id: string
|
||||
name: string
|
||||
owner: string | null
|
||||
scope: BlueprintScope
|
||||
status: BlueprintStatus
|
||||
spec: BlueprintSpec
|
||||
template_id: string | null
|
||||
template_version: number | null
|
||||
source: { kind: BlueprintSourceKind; id?: string | null; version?: number | null }
|
||||
conversation_id: string | null
|
||||
workflow_id: string | null
|
||||
app_id: string | null
|
||||
last_trial_run_id: string | null
|
||||
graph_hash: string | null
|
||||
joints: Record<string, JointState>
|
||||
copied_from: string | null
|
||||
/** 蓝图检查(bp.* / tpl.*),带白话 */
|
||||
issues?: PlainIssue[]
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- 白话层
|
||||
|
||||
export type ProblemWho = 'you' | 'assistant' | 'admin'
|
||||
export interface Plain { title: string; why: string; who: ProblemWho; action: string }
|
||||
/** 检查问题加上白话(后端 app/plain.with_plain);没登记白话的 plain 为空,照旧显示 message */
|
||||
export type PlainIssue = LintIssue & { plain?: Plain | null }
|
||||
|
||||
// ---------------------------------------------------------------- 场景模板
|
||||
|
||||
export interface TemplateCard { card: string; note: string }
|
||||
export interface TemplateInfo {
|
||||
id: string
|
||||
version: number
|
||||
label: string
|
||||
description: string
|
||||
applies_when: string
|
||||
cards: TemplateCard[]
|
||||
/** 个人应用能用哪一部分;不能用为 null */
|
||||
personal: string | null
|
||||
tags: string[]
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- 试运行
|
||||
|
||||
export interface TrialStart { run_id: string; blueprint: Blueprint }
|
||||
/** 试运行解析出来的一页:和应用页同一个视图模型,印章 trial=true */
|
||||
export type TrialPage = ViewModel
|
||||
|
||||
// ---------------------------------------------------------------- 发布单
|
||||
|
||||
export type PlanStepKind =
|
||||
| 'save_version' | 'preflight' | 'publish_workflow' | 'create_schedule' | 'create_app' | 'publish_app'
|
||||
| 'enable_schedule' | 'grant' | 'first_run'
|
||||
export interface PlanStep {
|
||||
kind: PlanStepKind
|
||||
status: 'pending' | 'done' | 'failed' | 'skipped'
|
||||
/** 这一步产出的对象 id(工作流版本、定时任务、应用……) */
|
||||
result?: Record<string, unknown> | null
|
||||
error_code?: string | null
|
||||
message?: string | null
|
||||
at?: string | null
|
||||
}
|
||||
export interface PublishPlanRow {
|
||||
id: string
|
||||
blueprint_id: string
|
||||
level: 'published' | 'governed'
|
||||
graph_hash: string | null
|
||||
status: 'planned' | 'running' | 'stopped' | 'done'
|
||||
steps: PlanStep[]
|
||||
/** 白话摘要:每一步一句 */
|
||||
summary: { step: PlanStepKind; text: string }[]
|
||||
problems: PlainIssue[]
|
||||
options: { first_run?: boolean }
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- 文件、记录
|
||||
|
||||
/** 上传后拿回的引用,作为表单值提交 */
|
||||
export interface FileRef { file_id: string; name: string; size: number; sha256: string; type: string }
|
||||
|
||||
export interface RecordRow {
|
||||
_id: string
|
||||
_seq: number
|
||||
_submitted_by: string | null
|
||||
_submitted_at: string
|
||||
_supersedes: string | null
|
||||
/** 被更正过(有更新的一行指向它) */
|
||||
superseded?: boolean
|
||||
values: Record<string, unknown>
|
||||
}
|
||||
export interface RecordsPage { table: string; version: number; columns: { name: string; label: string; type: string }[]; items: RecordRow[]; mine_only: boolean }
|
||||
|
||||
// ---------------------------------------------------------------- 页面编辑
|
||||
|
||||
/** 「加一张数据卡片」的候选(GET /api/apps/{id}/candidates) */
|
||||
export interface AppCandidate { kind: 'metric' | 'report' | 'table' | 'series'; label: string; group: string; component: AppSpec['pages'][number]['body'][number] }
|
||||
Reference in New Issue
Block a user