mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 01:51:57 +00:00
增加口径目录页与试跑:入口表单、依赖说明和只读试跑
This commit is contained in:
@@ -22,6 +22,7 @@
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from typing import Any
|
||||
|
||||
from datetime import datetime
|
||||
@@ -32,7 +33,7 @@ from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.db.base import SessionLocal, get_session
|
||||
from app.db.models import Workflow
|
||||
from app.db.models import Run, Workflow
|
||||
from app.db.models_process import ActionRecord
|
||||
|
||||
router = APIRouter(prefix="/api/ledger", tags=["ledger"])
|
||||
@@ -283,3 +284,69 @@ async def caliber_descriptor(workflow_id: str, version: int, node_id: str,
|
||||
if made is None:
|
||||
raise HTTPException(404, "没有这份口径:工作流不存在、这一版没发布过或已退役,或者节点不是源头口径卡")
|
||||
return made
|
||||
|
||||
|
||||
class TrialIn(BaseModel):
|
||||
"""试跑只收入口字段的值;没写的按口径里的缺省值补。"""
|
||||
|
||||
inputs: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
@router.post("/calibers/{workflow_id}/{version}/{node_id}/trial")
|
||||
async def trial_caliber(workflow_id: str, version: int, node_id: str, payload: TrialIn,
|
||||
session: AsyncSession = Depends(get_session)) -> dict[str, Any]:
|
||||
"""按固定版本试跑一份口径:派生只读小图(取数 + 口径卡),跑一次探索运行,返回指标值和运行 id。
|
||||
|
||||
试跑不是正式出具:来源记 trial,不进使用统计、不自动出具;参数按入口 Schema 校验,缺的补缺省值。不能派生的口径
|
||||
(输入不是查库结果里的格、取数模板引用了运行时变量……)照实回 409。最多等 30 秒;还没跑完就回 running,前端按
|
||||
run_id 去看运行。"""
|
||||
from app.api.deps import current_principal
|
||||
from app.apps.forms import input_schema, validate_form
|
||||
from app.engine.runner import run_manager
|
||||
from app.ledger.derive import NotDerivable, caliber_probe, load_pinned
|
||||
|
||||
try:
|
||||
pinned = await load_pinned(session, workflow_id, int(version), node_id)
|
||||
except NotDerivable as e:
|
||||
raise HTTPException(409, e.message) from None
|
||||
fields = [dict(f) for f in pinned.entry_fields]
|
||||
known = {str(f.get("name")) for f in fields}
|
||||
unknown = sorted(str(k) for k in payload.inputs if str(k) not in known)
|
||||
if unknown:
|
||||
raise HTTPException(422, f"不认识的入口字段:{'、'.join(unknown)}")
|
||||
values = {str(f["name"]): f.get("default") for f in fields if f.get("default") not in (None, "")}
|
||||
values.update({str(k): v for k, v in payload.inputs.items()})
|
||||
if errors := validate_form(input_schema(fields), values):
|
||||
raise HTTPException(422, ";".join(str(e.get("message")) for e in errors[:5]))
|
||||
|
||||
graph = caliber_probe(pinned).graph
|
||||
principal = current_principal()
|
||||
run = await run_manager.start(
|
||||
graph=graph, input_payload=values, run_class="exploratory", origin_kind="trial",
|
||||
origin={"kind": "caliber_trial", "workflow_id": workflow_id, "version": int(version),
|
||||
"node_id": node_id, "card": pinned.node_id},
|
||||
started_by=principal.username)
|
||||
row = None
|
||||
for _ in range(300):
|
||||
async with SessionLocal() as poll:
|
||||
row = await poll.get(Run, run.id)
|
||||
if row is None or row.status in ("succeeded", "failed", "cancelled"):
|
||||
break
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
out: dict[str, Any] = {"run_id": run.id, "status": row.status if row is not None else "unknown",
|
||||
"output": row.output if row is not None else None,
|
||||
"error": row.error if row is not None else None, "metrics": []}
|
||||
if row is not None and row.status == "succeeded":
|
||||
from app.api.evidence import _sealed, _sealed_cards
|
||||
|
||||
sealed = await _sealed(run.id)
|
||||
cards = {nid: card for nid, _, card in _sealed_cards(sealed)}
|
||||
card = cards.get(pinned.node_id) or next(iter(cards.values()), None)
|
||||
if card is not None:
|
||||
out["metrics"] = [
|
||||
{k: m.get(k) for k in ("id", "name", "unit", "value", "rendered", "decimals", "format")
|
||||
if m.get(k) is not None}
|
||||
for m in card.get("metrics") or [] if isinstance(m, dict) and m.get("id")
|
||||
]
|
||||
return out
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
"""口径试跑(POST /api/ledger/calibers/{wf}/{version}/{node}/trial):按固定版本派生只读图,跑一次探索运行。
|
||||
|
||||
试跑不是正式出具:来源记 trial;参数按入口 Schema 校验,缺的补缺省值;返回指标值和运行 id。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from httpx import ASGITransport, AsyncClient
|
||||
|
||||
from app.db.base import SessionLocal
|
||||
from app.db.models import Run
|
||||
from app.engine.runner import run_manager
|
||||
from app.ledger import match as ledger_match
|
||||
from app.main import app
|
||||
from tests import ledger_samples as S
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
async def engine_up(tmp_path):
|
||||
await run_manager.setup()
|
||||
await S.fresh_source(tmp_path / "shop.db")
|
||||
yield
|
||||
await ledger_match.drain()
|
||||
await run_manager.shutdown()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def client():
|
||||
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as c:
|
||||
yield c
|
||||
|
||||
|
||||
async def test_trial_runs_the_pinned_caliber_and_returns_its_metrics(client):
|
||||
wf = await S.publish()
|
||||
res = await client.post(f"/api/ledger/calibers/{wf}/1/card/trial",
|
||||
json={"inputs": {"start": "2026-09-01", "end": "2026-09-08"}})
|
||||
assert res.status_code == 200, res.text
|
||||
body = res.json()
|
||||
assert body["status"] == "succeeded" and body["run_id"]
|
||||
assert [m["id"] for m in body["metrics"]] == ["gmv"]
|
||||
assert float(body["metrics"][0]["value"]) == 170.5
|
||||
assert body["metrics"][0]["unit"] == "元"
|
||||
async with SessionLocal() as session:
|
||||
run = await session.get(Run, body["run_id"])
|
||||
assert run is not None and run.origin_kind == "trial" and run.run_class == "exploratory"
|
||||
assert run.origin["kind"] == "caliber_trial" and run.origin["workflow_id"] == wf
|
||||
|
||||
|
||||
async def test_trial_refuses_missing_required_inputs_and_unknown_fields(client):
|
||||
wf = await S.publish()
|
||||
missing = await client.post(f"/api/ledger/calibers/{wf}/1/card/trial", json={"inputs": {}})
|
||||
assert missing.status_code == 422 and "start" in missing.text
|
||||
unknown = await client.post(f"/api/ledger/calibers/{wf}/1/card/trial",
|
||||
json={"inputs": {"start": "2026-09-01", "end": "2026-09-08", "nope": 1}})
|
||||
assert unknown.status_code == 422 and "nope" in unknown.text
|
||||
@@ -21,6 +21,7 @@ import { SettingsPage } from './pages/SettingsPage'
|
||||
import { SchedulesPage } from './pages/schedules/SchedulesPage'
|
||||
// 评测(下一轮第 4 波,评测-D8):顶级导航,搭建者以上可见
|
||||
import { EvalsPage } from './pages/evals/EvalsPage'
|
||||
import { CaliberCatalog } from './pages/ledger/CaliberCatalog'
|
||||
import { NotFound } from './pages/NotFound'
|
||||
// 应用平台:使用面(FE-5)、搭建面的应用编辑(FE-6)
|
||||
import { AppsHome } from './pages/apps/AppsHome'
|
||||
@@ -194,6 +195,7 @@ function Shell() {
|
||||
<Route path="/evals" element={<EvalsPage />} />
|
||||
<Route path="/evals/:section" element={<EvalsPage />} />
|
||||
<Route path="/evals/:section/:id" element={<EvalsPage />} />
|
||||
<Route path="/ledger/calibers" element={<CaliberCatalog />} />
|
||||
{/* 应用:固定路径(inbox)写在 /apps/:appId 前面,读的人一眼看出它不是应用 id */}
|
||||
<Route path="/apps" element={<AppsHome />} />
|
||||
{/* 要在 /apps/:appId 之前:create 不是应用 id */}
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
* 请求函数从 ./http 导入,不从 ./client 导入(client.ts 要导入这个文件,反过来就成了循环)。
|
||||
*/
|
||||
import type {
|
||||
CertifyQueue, FreezeOut, LedgerFeedbackOut, LedgerFeedbackVerdict, LedgerIssueOut, LedgerReconcileIn, LedgerReconcileOut, RunLedger,
|
||||
CaliberDescriptor, CaliberTrialOut, CertifyQueue, FreezeOut, LedgerFeedbackOut, LedgerFeedbackVerdict, LedgerIssueOut,
|
||||
LedgerReconcileIn, LedgerReconcileOut, RunLedger,
|
||||
} from '../typesLedger'
|
||||
import { get, post, type RequestOptions } from './http'
|
||||
|
||||
@@ -37,4 +38,12 @@ export const caliberLedger = {
|
||||
...(opts.conversation_id ? { conversation_id: opts.conversation_id } : {}),
|
||||
...(opts.turn_id ? { turn_id: opts.turn_id } : {}),
|
||||
}),
|
||||
/** 口径目录:每份已发布口径卡的入口 Schema、依赖、能否组合/出具(不返回 SQL、参数值) */
|
||||
catalog: (opts?: RequestOptions) => get<{ calibers: CaliberDescriptor[] }>('/ledger/calibers', opts),
|
||||
/** 按固定版本试跑:只读探索运行,返回指标值和 run_id;不是正式出具 */
|
||||
trial: (workflowId: string, version: number, nodeId: string, inputs: Record<string, unknown>) =>
|
||||
post<CaliberTrialOut>(
|
||||
`/ledger/calibers/${encodeURIComponent(workflowId)}/${version}/${encodeURIComponent(nodeId)}/trial`,
|
||||
{ inputs },
|
||||
),
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ import type { NavigateFunction } from 'react-router-dom'
|
||||
import { create } from 'zustand'
|
||||
import clsx from 'clsx'
|
||||
import {
|
||||
Bell, BellOff, BookOpen, CalendarClock, Database, FlaskConical, Gauge, History, Hourglass, Inbox, Keyboard, LayoutGrid,
|
||||
MessageSquare, MessageSquarePlus, Monitor, Plus, Search, Settings, SunMoon, Workflow as WorkflowIcon, Wrench,
|
||||
Bell, BellOff, BookOpen, CalendarClock, Database, FlaskConical, Gauge, History, Hourglass, Inbox, Keyboard, Layers,
|
||||
LayoutGrid, MessageSquare, MessageSquarePlus, Monitor, Plus, Search, Settings, SunMoon, Workflow as WorkflowIcon, Wrench,
|
||||
} from 'lucide-react'
|
||||
import { ApiError, api } from '../api/client'
|
||||
import { hasPendingApproval, useCatalog } from '../store/catalog'
|
||||
@@ -66,6 +66,8 @@ export const PAGES: PageDef[] = [
|
||||
{ to: '/schedules', label: SCHEDULE_TEXT.title, hint: SCHEDULE_TEXT.hint, icon: CalendarClock, shortcut: 'Alt+8', keywords: 'schedules schedule cron 定时 计划 周期 自动运行' },
|
||||
// 评测(评测-D8):搭建者以上可见(使用者的页面表在 visiblePages 里收窄,用不着另判),同样排在设置前面;快捷键用最后一个空着的 ⌥0
|
||||
{ to: '/evals', label: EVAL_TEXT.nav, hint: EVAL_TEXT.hint, icon: Gauge, shortcut: 'Alt+0', keywords: 'evals eval evaluation 评测 回归 题集 准确率 A/B 口径命中 重复度' },
|
||||
// 口径目录:已发布口径的入口、依赖和试跑。后加的页,快捷键用没占用的 Alt+L
|
||||
{ to: '/ledger/calibers', label: '口径', hint: '查看已发布口径的入口、依赖,并按参数试跑', icon: Layers, shortcut: 'Alt+L', keywords: 'caliber ledger 口径 指标 试跑 入口' },
|
||||
{ to: '/settings', label: '设置', hint: '模型接入、偏好、运行环境', icon: Settings, shortcut: 'Alt+7', keywords: 'settings provider 模型 署名 主题 偏好' },
|
||||
]
|
||||
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
/**
|
||||
* 口径目录(/ledger/calibers):已发布口径的入口、指标、依赖和试跑。
|
||||
*
|
||||
* 只读目录 + 参数表单 + 一次只读探索运行。口径的技术身份是「工作流@版本/口径卡」;目录给业务的人看的是名字、指标、
|
||||
* 入口字段和能不能用。试跑按固定版本派生只读图(服务端 ledger/derive.caliber_probe),不是正式出具:来源记 trial,
|
||||
* 不进使用统计。SQL 不在页面上出现——服务端 descriptor 本来就不返回。
|
||||
*/
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { ChevronDown, Layers, Play, RefreshCw, Search } from 'lucide-react'
|
||||
import clsx from 'clsx'
|
||||
import { api } from '../../api/client'
|
||||
import { SchemaForm } from '../../components/SchemaForm'
|
||||
import { EmptyState, ErrorState, Notice, PageHeader, Skeleton, Spinner, toast } from '../../components/ui'
|
||||
import { LEDGER_TEXT } from '../../lib/terms'
|
||||
import type { FormSchema } from '../../typesApp'
|
||||
import type { CaliberDescriptor, CaliberTrialOut } from '../../typesLedger'
|
||||
|
||||
const keyOf = (d: CaliberDescriptor) => `${d.workflow_id}@${d.version}/${d.card.node_id}`
|
||||
|
||||
function defaultsOf(schema: FormSchema): Record<string, unknown> {
|
||||
const out: Record<string, unknown> = {}
|
||||
for (const [name, field] of Object.entries(schema.properties ?? {})) {
|
||||
if (field?.default !== undefined && field.default !== null && field.default !== '') out[name] = field.default
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
export function CaliberCatalog() {
|
||||
const [items, setItems] = useState<CaliberDescriptor[] | null>(null)
|
||||
const [error, setError] = useState<unknown>(null)
|
||||
const [query, setQuery] = useState('')
|
||||
const [open, setOpen] = useState<string | null>(null)
|
||||
const [values, setValues] = useState<Record<string, Record<string, unknown>>>({})
|
||||
const [trials, setTrials] = useState<Record<string, CaliberTrialOut>>({})
|
||||
const [busy, setBusy] = useState<string | null>(null)
|
||||
|
||||
const load = () => {
|
||||
setError(null)
|
||||
setItems(null)
|
||||
api.caliberLedger.catalog().then(
|
||||
(body) => setItems(body.calibers),
|
||||
(e) => setError(e),
|
||||
)
|
||||
}
|
||||
useEffect(load, [])
|
||||
|
||||
const shown = useMemo(() => {
|
||||
const q = query.trim().toLowerCase()
|
||||
if (!items || !q) return items ?? []
|
||||
return items.filter((d) => [d.workflow_name, d.card.caliber, d.card.title,
|
||||
...d.metrics.map((m) => m.name ?? m.id)].some((s) => String(s ?? '').toLowerCase().includes(q)))
|
||||
}, [items, query])
|
||||
|
||||
const toggle = (d: CaliberDescriptor) => {
|
||||
const key = keyOf(d)
|
||||
if (open === key) { setOpen(null); return }
|
||||
setOpen(key)
|
||||
setValues((v) => (v[key] ? v : { ...v, [key]: defaultsOf(d.entry_schema) }))
|
||||
}
|
||||
|
||||
const trial = async (d: CaliberDescriptor) => {
|
||||
const key = keyOf(d)
|
||||
setBusy(key)
|
||||
try {
|
||||
const out = await api.caliberLedger.trial(d.workflow_id, d.version, d.card.node_id, values[key] ?? {})
|
||||
setTrials((t) => ({ ...t, [key]: out }))
|
||||
if (out.status === 'succeeded') toast.ok('试跑完成')
|
||||
else if (out.status === 'running') toast.warn('试跑还在运行,打开运行记录看结果')
|
||||
else toast.error(out.error || '试跑没有成功')
|
||||
} catch (e) {
|
||||
toast.error(e)
|
||||
} finally {
|
||||
setBusy(null)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col" data-caliber-catalog="">
|
||||
<PageHeader icon={<Layers size={14} />} title="口径" subtitle="已发布口径的入口、指标、依赖与试跑"
|
||||
actions={<button type="button" className="btn btn-xs btn-ghost" onClick={load} title="重新加载">
|
||||
<RefreshCw size={12} aria-hidden /> 刷新
|
||||
</button>} />
|
||||
<div className="min-h-0 flex-1 overflow-y-auto">
|
||||
<div className="mx-auto max-w-5xl space-y-3 p-4">
|
||||
<label className="relative block">
|
||||
<Search size={13} className="pointer-events-none absolute left-2.5 top-1/2 -translate-y-1/2 text-faint" aria-hidden />
|
||||
<input className="input w-full pl-8" value={query} onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder="按工作流、口径、指标名查找" aria-label="查找口径" />
|
||||
</label>
|
||||
{error ? <ErrorState error={error} onRetry={load} compact />
|
||||
: !items ? <Skeleton rows={5} height={38} />
|
||||
: !shown.length ? <EmptyState offline={false} icon={<Layers size={22} />} title="还没有可用的口径"
|
||||
body={query ? '换个关键词试试' : '在工作流里发布一份带口径卡的版本后,这里就会出现'} />
|
||||
: shown.map((d) => {
|
||||
const key = keyOf(d)
|
||||
const expanded = open === key
|
||||
const result = trials[key]
|
||||
return (
|
||||
<section key={key} className="overflow-hidden rounded-lg border bg-panel" data-caliber={key}>
|
||||
<button type="button" className="flex w-full items-start gap-3 px-3 py-2.5 text-left hover:bg-hover"
|
||||
onClick={() => toggle(d)} aria-expanded={expanded} data-caliber-toggle="">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex flex-wrap items-baseline gap-x-2 gap-y-0.5">
|
||||
<span className="text-sm font-medium">{d.workflow_name}</span>
|
||||
<span className="text-2xs text-faint">v{d.version}</span>
|
||||
{d.card.caliber && <span className="text-xs text-dim">· {d.card.caliber}</span>}
|
||||
<span className="rounded border px-1.5 py-0.5 text-[10px] text-dim">
|
||||
{(LEDGER_TEXT.level as Record<string, string>)[d.level] ?? d.level}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-0.5 text-2xs text-faint">
|
||||
{d.metrics.map((m) => m.name ?? m.id).join('、')}
|
||||
{d.entry_schema.properties && Object.keys(d.entry_schema.properties).length > 0
|
||||
? ` · ${Object.keys(d.entry_schema.properties).length} 个入口字段` : ' · 没有入口字段'}
|
||||
{d.dependencies.tables.length > 0 ? ` · 读 ${d.dependencies.tables.join('、')}` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-2 text-[10px] text-faint">
|
||||
<span>{d.composable ? '可组合' : '不可组合'}</span>
|
||||
<span>{d.issuance.auto_issue ? '可自动出具' : '只可一键出具'}</span>
|
||||
<ChevronDown size={14} className={clsx('transition-transform', expanded && 'rotate-180')} aria-hidden />
|
||||
</div>
|
||||
</button>
|
||||
{expanded && (
|
||||
<div className="space-y-3 border-t px-3 py-3" data-caliber-detail="">
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
<div>
|
||||
<h3 className="mb-1 text-2xs font-semibold text-dim">指标</h3>
|
||||
<ul className="space-y-1 text-xs">
|
||||
{d.metrics.map((m) => (
|
||||
<li key={m.id} className="flex items-baseline justify-between gap-2 border-b border-hairline pb-1 last:border-0">
|
||||
<span>{m.name ?? m.id}</span>
|
||||
<span className="text-2xs text-faint">{m.unit || '—'}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="mb-1 text-2xs font-semibold text-dim">依赖</h3>
|
||||
<p className="text-2xs leading-relaxed text-dim">
|
||||
数据源:{d.dependencies.sources.join('、') || '—'}<br />
|
||||
表:{d.dependencies.tables.join('、') || '—'}<br />
|
||||
{d.compose_reason ? `不能组合:${d.compose_reason}` : '可以按这一版组合'}
|
||||
</p>
|
||||
{d.issuance.reasons.length > 0 && (
|
||||
<p className="mt-1 text-2xs leading-relaxed text-faint">
|
||||
出具:{d.issuance.reasons.join(';')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="mb-1 text-2xs font-semibold text-dim">入口字段</h3>
|
||||
{d.entry_schema.properties && Object.keys(d.entry_schema.properties).length > 0 ? (
|
||||
<SchemaForm schema={d.entry_schema} value={values[key] ?? {}}
|
||||
onChange={(v) => setValues((all) => ({ ...all, [key]: v }))}
|
||||
idPrefix={`caliber-${key}`} />
|
||||
) : <p className="text-2xs text-faint">这份口径不需要参数。</p>}
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<button type="button" className="btn btn-primary btn-sm" disabled={busy === key}
|
||||
onClick={() => void trial(d)} data-caliber-trial="">
|
||||
{busy === key ? <Spinner size={12} /> : <Play size={12} aria-hidden />} 试跑
|
||||
</button>
|
||||
<Link to={`/studio/${d.workflow_id}`} className="btn btn-sm btn-ghost">打开工作流</Link>
|
||||
<span className="text-2xs text-faint">试跑是只读探索运行,不是正式出具</span>
|
||||
</div>
|
||||
{result && (
|
||||
<div className="rounded-md border bg-elev px-3 py-2 text-xs" data-caliber-trial-result={result.status}>
|
||||
{result.status === 'succeeded' ? (
|
||||
<ul className="space-y-1">
|
||||
{result.metrics.map((m) => (
|
||||
<li key={m.id} className="flex items-baseline justify-between gap-2">
|
||||
<span>{m.name ?? m.id}</span>
|
||||
<span className="tnum font-medium">{m.rendered ?? String(m.value ?? '—')}{m.unit ? ` ${m.unit}` : ''}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<Notice tone={result.status === 'running' ? 'info' : 'err'}>
|
||||
{result.status === 'running' ? '还在运行:' : '试跑没有成功:'}
|
||||
{result.error || result.status}
|
||||
{' '}<Link className="underline" to={`/runs/${result.run_id}`}>看运行</Link>
|
||||
</Notice>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
* 给,界面照着画。比对在封存之后做(不在封存范围内),GET /api/runs/{id}/ledger 在比对还没出结果时答 state=pending,
|
||||
* 页面按退避再取(store/ledger.ts),不靠实时流:ledger.matched 在事件总线关闭之后才追加,打开着的页面收不到。
|
||||
*/
|
||||
import type { FormSchema } from './typesApp'
|
||||
|
||||
/** 绑定的一个参数:值是这次运行查到过的遮罩值时不给值(masked),也不能拿它代填出具 */
|
||||
export interface LedgerBinding {
|
||||
@@ -370,3 +371,54 @@ export interface FreezeOut {
|
||||
metrics: string[]
|
||||
sources: CertifyTurn[]
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- 口径目录 / 试跑
|
||||
|
||||
export interface CaliberMetric {
|
||||
id: string
|
||||
name?: string
|
||||
unit?: string
|
||||
decimals?: number
|
||||
format?: string
|
||||
}
|
||||
|
||||
/** GET /api/ledger/calibers:一份已发布口径卡的可调用描述(不返回 SQL、不返回参数值) */
|
||||
export interface CaliberDescriptor {
|
||||
workflow_id: string
|
||||
workflow_name: string
|
||||
version: number
|
||||
level: string
|
||||
graph_hash?: string | null
|
||||
card: { node_id: string; title: string; caliber?: string | null; caliber_version?: string | null }
|
||||
metrics: CaliberMetric[]
|
||||
entry_schema: FormSchema
|
||||
composable: boolean
|
||||
compose_reason?: string | null
|
||||
dependencies: {
|
||||
sources: string[]
|
||||
tables: string[]
|
||||
catalog_versions?: Record<string, unknown> | null
|
||||
schema_versions?: Record<string, unknown> | null
|
||||
}
|
||||
issuance: {
|
||||
level: string
|
||||
auto_issue: boolean
|
||||
requires_independent_proof: boolean
|
||||
side_effects?: string | null
|
||||
reasons: string[]
|
||||
}
|
||||
}
|
||||
|
||||
export interface CaliberTrialMetric extends CaliberMetric {
|
||||
value?: unknown
|
||||
rendered?: string
|
||||
}
|
||||
|
||||
/** POST /api/ledger/calibers/{wf}/{version}/{node}/trial:按固定版本试跑一次 */
|
||||
export interface CaliberTrialOut {
|
||||
run_id: string
|
||||
status: string
|
||||
output?: Record<string, unknown> | null
|
||||
error?: string | null
|
||||
metrics: CaliberTrialMetric[]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user