Files

5204 lines
205 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
export type NodeType =
| 'input' | 'output' | 'llm' | 'agent' | 'supervisor' | 'tool' | 'code' | 'merge'
| 'branch' | 'loop' | 'subgraph' | 'memory' | 'retrieve' | 'transform'
| 'human' | 'validate' | 'metrics' | 'analysis' | 'report' | 'dual_check' | 'challenge'
export interface GraphNode {
id: string
type: NodeType
position: { x: number; y: number }
data: { label?: string; description?: string; config: Record<string, any> }
}
export interface GraphEdge {
id?: string
source: string
target: string
sourceHandle?: string | null
targetHandle?: string | null
label?: string
}
export interface GraphSpec {
nodes: GraphNode[]
edges: GraphEdge[]
viewport?: Record<string, any>
defaults?: Record<string, any>
}
export interface Workflow {
id: string
name: string
description: string
graph: GraphSpec
tags: string[]
version: number
is_template: boolean
status?: 'draft' | 'published' | 'governed'
published_version?: number | null
/** 当前发布版本的级别。画布上改过之后 status 回到 draft,「这是不是受管工作流」看这一项(老服务端不给) */
published_level?: 'published' | 'governed' | null
/** 画布这一版的图和当前发布版内容相同(服务端按图哈希比):撤回改动后保存出来的新版本不算「领先」(老服务端不给) */
same_as_published?: boolean
/** 最近一次发布的署名。发布时没有署名为 null */
published_by?: string | null
run_count?: number
created_at?: string
updated_at?: string
}
/** 工作流的一个版本。列表接口只有前四项,取单个版本时才有 graph 等 */
export interface WorkflowVersion {
id: string
version: number
note: string
created_at?: string
workflow_id?: string
graph?: GraphSpec
graph_hash?: string | null
/** 这一版入口节点声明的字段。正式运行的表单照它填,不能照画布 */
input_fields?: { name: string; required?: boolean; [key: string]: any }[]
/** 是不是当前的已发布版本 */
published?: boolean
/** 发布时记下的数据目录版本,没记过为 null */
catalog_versions?: CatalogVersions | null
/** 发布之后目录有变化的表 */
catalog_changes?: CatalogDriftTable[]
/** 发布时记下的表结构版本 {源名: {表名: 结构版本}}(只记写死的 SQL 直接引用的表);记录之前发布的为 null */
schema_versions?: Record<string, Record<string, number>> | null
/** 发布之后结构版本变了的表 */
schema_changes?: SchemaDriftTable[]
}
/**
* 发布时记下的数据目录版本(服务端 catalog_impact.recorded_versions),两组都是 {源名: {表名: 版本}}:
* - direct:SQL 里写着的表,0 表示当时还没有目录
* - possible:Agent 绑定了查询工具的数据源中所有有目录的表(SQL 运行时才生成);源下为空表示当时一张有目录的
* 表都没有。只记了直接引用的老版本这一组为空
*/
export interface CatalogVersions {
direct: Record<string, Record<string, number>>
possible: Record<string, Record<string, number>>
}
/** 发布之后数据目录有变化的一张表(服务端 catalog_impact.catalog_drift;运行里的 catalog.drift 事件同形) */
export interface CatalogDriftTable {
source: string
source_id: string
table: string
/** 表现在的中文名 */
label: string | null
/** 发布时的版本,0 表示当时还没有目录 */
published: number | null
current: number
/** direct:SQL 里写着这张表;possible:Agent 可能查询的表。老事件没有这一项,按 direct */
impact: 'direct' | 'possible'
}
/**
* 发布之后表结构变了的一张表(服务端 catalog_impact.schema_drift;运行里的 schema.drift 事件同形)。和目录有变化
* (CatalogDriftTable)分开记:这里的版本是表结构版本,不是目录版本
*/
export interface SchemaDriftTable {
source: string
source_id: string
table: string
/** 表现在的中文名 */
label: string | null
/** 发布时的结构版本 */
published: number
/** 现在的结构版本;表已删除时为 null */
current: number | null
removed: boolean
/** 发布之后测到、还没处理的结构变化(最多 5 条,more 是另外还有几条) */
changes: { id: string; kind: string; column: string | null; severity: 'info' | 'warning' | 'action'; summary: string }[]
more: number
}
/**
* 后端写进 runs.status 的值。interrupted 既可能是停在审批上,也可能是服务重启
* 打断后挂起——显示时用 lib/status 的 resolveStatus 配合审批列表区分。
* suspended 目前后端不写,前端推导的 RunPhase 会用到,这里一并收下。
*/
export type RunStatus =
| 'queued' | 'running' | 'interrupted' | 'succeeded' | 'failed' | 'cancelled' | 'suspended'
/**
* 运行的用量与时长。
*
* 三种时长口径不同,不能混用:duration_ms / active_ms 是各段执行时长之和(审批
* 恢复、续跑的每一段都算),wall_ms 是从第一次开始到结束的墙钟,wait_ms 是等人
* 审批的总时长。老数据只有 duration_ms,而且可能只是最后一段。
*/
export interface RunUsage {
duration_ms?: number
wall_ms?: number
active_ms?: number
wait_ms?: number
input_tokens?: number
output_tokens?: number
cost_usd?: number
/** 金额估不出来的调用次数(模型不在价格目录中),cost_usd 只是估得出的那部分。没有这种调用时不给 */
unpriced_calls?: number
[key: string]: any
}
export interface Run {
id: string
workflow_id: string | null
workflow_name: string
status: RunStatus
input: Record<string, any>
output: Record<string, any>
error: string | null
usage: RunUsage
run_class?: 'formal' | 'exploratory'
version?: number | null
version_hash?: string | null
manifest_hash?: string | null
/** 封存到第几条事件为止。有它才说明这条运行的清单封存过 */
manifest_seq?: number | null
/** 失败时能定位到的节点 */
error_node_id?: string | null
started_by?: string | null
/** 发起时实际生效的记忆域和知识库(没指定时取设置里的默认值)。老后端不给 */
memory_scope?: string | null
collection?: string | null
created_at?: string
started_at?: string
finished_at?: string | null
}
export interface RunEvent {
seq: number
type: string
node_id: string | null
ts: number
data: Record<string, any>
replay?: boolean
}
export interface Approval {
id: string
run_id: string
node_id: string
mode: 'approve' | 'input' | 'edit'
title: string
/**
* 工具审批的 payload 是 {kind: 'tool_approval', tool, args, title}。探索运行里 MCP /
* 自定义工具的审批另带 trust_key(同 ToolInfo.trust_key):有它,审批卡才给「始终允许」
*/
payload: Record<string, any>
status: string
response: Record<string, any>
created_at?: string
// 以下是审批卡的上下文,老后端不给
workflow_id?: string | null
workflow_name?: string | null
node_label?: string | null
run_status?: RunStatus | null
run_class?: 'formal' | 'exploratory' | null
/** 谁处理的。null 表示处理时没有署名,显示「未署名」 */
resolved_by?: string | null
resolved_at?: string | null
/**
* 这条审批对应的待办(一次中断对应一条 Approval 和一条 Task,应用平台 D-r)。有它审批卡直接按 id 取待办,
* 不必再按运行取一遍待办列表去对;老服务端不给
*/
task_id?: string | null
}
/**
* 人工审批审的报告的核对概况(human 节点请求里的 payload.reports,服务端 nodes/human._reviewed_reports):
* 上游直接连着的、或者标题和说明里引用了正文的报告撰写节点,各一项。stats 同 report.checked,judge 是裁判摘要
* 的几项(mode:inline 节点里当场判的——交给审批的报告探索运行也当场判 / on_demand 按需判,老服务端的探索运行)。
* 老后端、审的不是报告时没有
*/
export interface ApprovalReport {
node_id: string
label?: string
stats?: EvidenceStats
/** 结论句策略:off / require_citation / judge */
claims?: string
doc_artifact?: string
judge?: { mode?: string; counts?: Record<string, unknown>; complete?: boolean; unjudged?: unknown; limits_hit?: unknown }
}
/**
* 后端记着的最近一次测连接(模型接入、数据源、MCP 的列表接口都平铺这四项)。
* 没测过、或者连接配置改过之后全是 null。用 lib/health 的 healthFromServer 转成
* HealthRecord 给 HealthPill
*/
export interface ServerHealth {
last_checked_at?: string | null
last_check_ok?: boolean | null
last_latency_ms?: number | null
last_error?: string | null
}
export interface Provider extends ServerHealth {
id: string
name: string
kind: string
base_url: string | null
default_model: string | null
models: { id: string; label?: string; context?: number; pricing?: any }[]
enabled: boolean
extra: Record<string, any>
api_key_masked: string
has_key: boolean
}
/**
* MCP / 自定义工具在探索运行里的信任档(节点审批策略为「仅危险工具」时才看它):
* ask 每次等人批(默认);gated 每次先问门控模型,它判可疑的仍交给人;always 直接执行。
* 正式运行不看它,一律按 ask
*/
export type ToolTrust = 'ask' | 'gated' | 'always'
export interface ToolInfo {
id: string
name: string
description: string
category: string
source: 'builtin' | 'custom' | 'mcp'
dangerous: boolean
/**
* 运行时可能停下来等审批。MCP / 自定义工具:ask、gated 为 true,always 为 false;
* 更老的后端一律给 false(那时审批关卡认不出它们)
*/
runtime_approval?: boolean
/** 只有 MCP / 自定义工具有,老后端不给:没有它就不显示信任档控件 */
trust?: ToolTrust
/** 改信任档时用的键(PUT /api/tools/trust),等于 id */
trust_key?: string
schema: Record<string, any>
/**
* 只有自定义工具有:库里存着的参数定义哪里写坏了(一句中文)。绑了它的节点运行时
* 一定失败,列表上要醒目地标出来。null / 缺省表示没问题
*/
problem?: string | null
/**
* 在工具库里执行只演练、不发送:写外部系统的能力(被治理为写入,或绑定端点、视参数而定),和执行接口同一个判断。
* 详情区的提示条照它说「这里只演练」,不说「请求会实际发出」(老服务端不给)
*/
write_rehearsal?: boolean
}
/**
* 数据源(GET /api/datasources 的一行)。tools 是它给模型的两个工具名
* db_query__<name> / db_schema__<name>——/api/tools 里没有它们,挑工具要从这里取
*/
export interface DataSource extends ServerHealth {
id: string
/** 标识,进了工具名,建好不能改 */
name: string
kind: string
host: string | null
port: number | null
database: string | null
username: string | null
options: Record<string, any>
readonly: boolean
description: string
enabled: boolean
password_masked?: string
has_password?: boolean
table_count?: number
schema_synced_at?: string | null
/** 上次探查为什么没拿到表;空串 = 没出错(或还没探查) */
schema_error?: string
available_schemas?: string[]
tools?: string[]
/** 缓存按哪个 schema 探的:'' 默认 schema,null 没有缓存 */
cached_schema?: string | null
/**
* upload:上传的表格(连接信息由系统维护,不能手改、不能重新探查,更新数据靠同名重新上传);
* manual:手工登记的连接。卡片按它分到「表格」「数据库」两个标签,不再按库文件路径猜
*/
origin?: 'upload' | 'manual' | (string & {})
/** 上传表格当前启用的版本。手工登记的源恒为 null */
current_snapshot?: CurrentSnapshot | null
/** simple 简单导入;recipe 按配方导入;手工登记的源为 null。老后端没有这个字段 */
import_mode?: 'simple' | 'recipe' | null
/** 按配方导入的源当前启用的配方 */
current_recipe?: { id: string; seq: number; origin: string; activated_at: string | null; signed_by: string | null } | null
/** 这个源最近一个未结束的导入(按配方导入的暂存区) */
open_staging?: { id: string; kind: ImportStagingKind; status: ImportStagingStatus; created_at: string } | null
}
/** 上传表格当前启用的版本(DataSource.current_snapshot) */
export interface CurrentSnapshot {
id: string
/**
* 这一版导入(发布)的时间。迁移补建的初始版本(raw_state 为 absent、没有文件名)例外:
* 这里是迁移那一刻(升级后服务启动的时间),不是导入时间
*/
created_at: string | null
/** 上传时的文件名。迁移前上传的老版本没有记录,是空串 */
file_name: string
/** 原件:kept 保存在服务端;purged 已清除;absent 没有保存(迁移前上传的老版本) */
raw_state: 'kept' | 'purged' | 'absent' | (string & {})
/** 期 3:导入模式。简单导入、期 3 之前的版本没有或为 null(按每期替换理解) */
mode?: 'replace' | 'accumulate' | (string & {}) | null
/** 期 3:当前版本含几期(按期累积时可以多于 1) */
periods?: number
/** 期 3:各期统计期的最早起点、最晚终点;没有统计期为 null */
period_start?: string | null
period_end?: string | null
/** 期 3:最近一次成为当前版本的时刻(回滚之后卡片写「…启用」,不再写当初的导入时间) */
activated_at?: string | null
}
/**
* 上传表格时解析器要用户先拍板的事(POST /api/datasources/upload 回 422,body 是
* {detail, decision})。mixed:数字列混入非数字;shape:交叉表、多块结构
*/
export type UploadDecision =
| { kind: 'mixed'; details: { columns: UploadMixedColumn[] } }
| {
kind: 'shape'
details: {
reasons: UploadShapeReason[]
/** 预告:选了按原样导入之后还会问的混合列(没有就是 undefined) */
mixed?: UploadMixedColumn[]
/** 预告是不是看完了整张表;为假时之后的混合列可能比这里多 */
mixed_complete?: boolean
}
}
/** 一列以数字为主、混有非数字的值。values 是非数字的取值和个数(最多五种) */
export interface UploadMixedColumn {
sheet: string
table: string
/** SQL 列名 */
column: string
/** 原表头 */
header: string
numeric: number
nonnumeric: number
values: { value: string; count: number }[]
}
/** 这张表不是一行一条记录的一条理由。cells 是 A1 坐标或区域,message 是给人看的一句话(已含坐标) */
export interface UploadShapeReason {
sheet: string
/** date_header / date_row / section_title / table_totals / formula_above */
kind: string
cells: string[]
message: string
}
/** 导入回执里的一张表 */
export interface UploadedTable {
name: string
/** 原工作表名(CSV 是文件名) */
sheet: string
rows: number
/** name 是 SQL 列名,header 是原表头(表头格为空时是空串) */
columns: { name: string; type: string; header?: string | null }[]
/** 导入区域(A1,表头行到最后一行数据) */
region?: string | null
/** 按原样导入、未经规整 */
unshaped?: boolean
blank_rows_skipped?: number
/** 去掉的左右两侧整列为空的列(列字母) */
columns_trimmed?: string[]
}
/** 导入时做过的类型转换:kind 是 thousands_separator / nonnumeric_to_null / kept_as_text */
export interface UploadConversion {
table: string
column: string
kind: string
count: number
examples: unknown[]
}
/** POST /api/datasources/upload 成功时的回执 */
export interface UploadResult {
source: DataSource
/** 同名就地更新(新版本替换了旧版本),不是新建 */
replaced: boolean
import_id?: string
snapshot_id?: string
/** 同一份文件、同样的选项以前导入过,沿用了那次的库 */
build_reused?: boolean
/** 期 3:服务端的同版本数据文件曾被改动,已用本次上传的文件恢复(P3-SPEC 第 8 节遗留项 1) */
build_restored?: boolean
/** reason:hidden 隐藏工作表不导入;empty 没有内容。state:visible / hidden / veryHidden */
skipped_sheets?: { sheet: string; state: string; reason: string }[]
conversions?: UploadConversion[]
warnings?: string[]
tables: UploadedTable[]
}
// ---------------------------------------------------------------------------
// 按配方导入(期 2:/api/datasources/imports/*、/{id}/reupload|redraft|recipe)
// 字段与后端 recipe_types.py 的 dataclass 同名;后端多出来的字段界面不认也不报错
// ---------------------------------------------------------------------------
/** first 首次导入;reupload 上传新一期;redraft 修改配方(不换文件);switch 从简单导入切换 */
export type ImportStagingKind = 'first' | 'reupload' | 'redraft' | 'switch' | (string & {})
export type ImportStagingStatus = 'drafting' | 'trialed' | 'rejected' | 'committed' | 'discarded' | 'expired' | (string & {})
/**
* 配方 JSON(agentlab-recipe/2)。结构由服务端的静态校验把关,界面只按已知字段读写,所以这里是宽松类型:
* 表单改哪个字段就按路径写哪个字段,整份经 PUT recipe 交给服务端
*/
export type Recipe = Record<string, any>
/** 配方静态校验的问题:path 是 JSON Pointer(如 /tables/0/units/客流),message 是整句人话 */
export interface RecipeProblem {
path: string
code: string
message: string
/** 期 3:这条问题对应的修复提议 id(Staging.fixes 里的 FixProposal.id);没有提议为 [] 或缺省 */
fix_ids?: string[]
}
/** structure 结构问题;data_quality 数据质量;confirm 需确认;input 需要录入;recipe 配方不合法 */
export type ImportProblemCategory = 'structure' | 'data_quality' | 'confirm' | 'input' | 'recipe' | (string & {})
/** 干跑、试运行的问题(契约 Problem)。cells 是「工作表!A1」或「工作表!A1:B2」,最多 20 个 */
export interface ImportProblem {
code: string
category: ImportProblemCategory
message: string
cells?: string[]
/** 修复按钮的种类(FixKind);界面不靠它放按钮,只按 fix_ids 放(P3-SPEC 3.1) */
fix?: string | null
/** 期 3:构造修复补丁的参数(只含原文和坐标),界面不直接用:补丁由服务端算 */
fix_args?: Record<string, any> | null
/** 期 3:这条问题对应的修复提议 id(Staging.fixes 里的 FixProposal.id);没有提议为 [] 或缺省 */
fix_ids?: string[]
}
export type CheckStatus = 'passed' | 'mismatch' | 'unverifiable' | 'info' | (string & {})
/** 一条核对的结果(契约 CheckResult)。acceptable:可以写理由接受 */
export interface CheckResult {
id: string
kind: string
title: string
status: CheckStatus
category: 'structure' | 'data_quality' | 'info' | (string & {})
checked?: number
failed?: number
unverifiable?: number
sql?: string | null
params?: unknown[]
details?: string[]
cells?: string[]
acceptable?: boolean
reasons?: Record<string, number>
}
/**
* 确认项的来源(契约 ConfirmItem.source),确认清单按它分组(P3-SPEC 9.5):recipe 配方;edit 修改(fix:*、select:*、
* redraft_adopted);accumulate 累积;outside / sheet / context 本期;diff 差异;switch 切换
*/
export type ConfirmSource = 'recipe' | 'diff' | 'outside' | 'sheet' | 'context' | 'switch' | 'edit' | 'accumulate' | (string & {})
/** 启用前要逐条勾选的一项(契约 ConfirmItem;id 的取值见 P2-SPEC 7.5、P3-SPEC 9.5) */
export interface ConfirmItem {
id: string
label: string
detail?: string
required?: boolean
/** 缺省按 recipe */
source?: ConfirmSource
}
/** 上传新一期与上一期相比的一条差异(契约 DiffItem) */
export interface DiffItem {
kind: string
label: string
detail?: string
requires_confirm?: boolean
confirm_id?: string | null
}
/** 建议卡片:一条建议加一句理由,指向网格上的格子 */
export interface Card {
id: string
title: string
reason: string
cells?: string[]
/** 对应的待确认问题(Question.id) */
question?: string | null
}
export interface QuestionOption {
value: string
label: string
/** 选它时必须写理由(如「不登记」) */
needs_reason?: boolean
}
/** 待确认问题:只有封闭的选项;选项对配方的修改由服务端应用,界面只发选了哪一项 */
export interface Question {
id: string
text: string
options: QuestionOption[]
default?: string | null
}
/** 已选的回答(StagingOut.answers 的值) */
export interface QuestionAnswer {
value: string
reason?: string | null
}
/** 规则或 AI 起草的结果(契约 Draft,不含给模型看的那份原因) */
export interface Draft {
recipe: Recipe | null
complete: boolean
origin: 'rules' | 'ai' | (string & {})
cards?: Card[]
questions?: Question[]
failures?: string[]
}
/** 一次模型调用的用量 */
export interface AiUsage {
model: string
input_tokens: number
output_tokens: number
cost_usd: number
ok: boolean
at: string
attempt?: number
}
/** 一张可见工作表的原始网格预览 */
export interface GridPreview {
sheet: string
bounds: string | null
total_rows?: number
total_cols?: number
/** 预览只取了前面的部分 */
truncated?: boolean
rows?: number[]
cols?: number[]
/** [行, 列, 显示文字, 种类];种类:text / number / date / bool / error / formula / formula_uncached */
cells: [number, number, string, string][]
/** A1 → 公式原文 */
formulas?: Record<string, string>
merges?: string[]
hidden_rows?: number[]
hidden_cols?: number[]
}
/** 格子的去向(契约 Role),按区域给出。期 3 新增角色 ignored(按配方忽略) */
export interface RegionMark {
sheet: string
role: string
ref: string
/** 期 3:所在块的 id(区域外文字、统计期为 null):网格按块描边、框选的重放比对按它过滤 */
block?: string | null
}
/** 格子账:每张工作表的非空格和各去向的个数 */
export interface LedgerSheet {
sheet: string
nonempty_scan: number
nonempty_read: number
roles?: Record<string, number>
unclaimed?: number
}
export interface ReceiptTable {
name: string
sheet: string
kind: 'data' | 'reported_total' | (string & {})
columns: { name: string; type: string; header?: string | null; unit?: string | null; role?: string }[]
grain: string[]
rows: number
sources?: string[]
}
export interface PeriodOut {
start: string
end: string
source: 'cells' | 'human' | (string & {})
cells?: string[]
signed_by?: string | null
texts?: Record<string, string>
annotated?: Record<string, string>
}
export interface OutsideText {
sheet: string
/** 带工作表名的坐标「工作表!A1」(与问题的 cells 同一写法) */
cell: string
text: string
kind: 'text' | 'text_digits' | (string & {})
period_source?: boolean
/**
* 期 4:所在行或列是否被隐藏(执行器写入)。true 在隐藏行列里;false 可见;null 或没有这个键:导入时没有记录
* (期 4 之前的清单)。裁判摘录只送 false 的项,界面照常显示全部区域外文字
*/
hidden?: boolean | null
}
/** 试运行回执(Extraction 去掉问题和核对之后给界面看的部分) */
export interface TrialReceipt {
ledger?: LedgerSheet[]
tables?: ReceiptTable[]
period?: PeriodOut | null
placeholders?: Record<string, number>
outside_text?: OutsideText[]
canonicalized_total?: number
blank_rows_skipped?: number
formula_cells_accepted?: number
full_calc_on_load?: boolean
db_sha256?: string
/** 期 3:排除的行(H2)。期 3 之前的回执没有这个字段:缺省表示「未记录」,不等于「没有排除」。
* 服务端:试运行回执(TrialOut.receipt)只抄 recipe_imports._RECEIPT_VIEW 白名单里的键,WP-5 要把
* rows_excluded 加进去;导入清单的 receipt 也要有。期 3 的试运行一律带这个键(没有排除时是 []) */
rows_excluded?: ExcludedRows[]
[key: string]: any
}
/** passed 通过;needs_input 需要录入统计期;needs_decision 有可接受的核对待写理由;rejected 拒收 */
export type TrialStatus = 'passed' | 'needs_input' | 'needs_decision' | 'rejected' | (string & {})
/** 最近一次试运行(StagingOut.trial) */
export interface TrialOut {
trial_id: string
status: TrialStatus
receipt: TrialReceipt
problems: ImportProblem[]
checks: CheckResult[]
confirm_items: ConfirmItem[]
/** 可以写理由接受、本次未通过的核对 id */
acceptable: string[]
/** 表 → 说明预览 */
notes?: Record<string, { comment: string; columns?: Record<string, string> }>
diff?: DiffItem[] | null
same_as_import?: { id: string; seq: number } | null
base_snapshot_id?: string | null
/** 期 3:累积计划。新配方是按期累积、或现行是按期累积时给出 */
accumulate?: AccumulatePlan | null
/** 期 3:工作配方与现行配方不同时的新旧对照 */
recipe_compare?: RecipeComparison | null
/** 期 3:并集的结构核对 U1–U3(物化了才有) */
union_checks?: CheckResult[] | null
/** 期 3:同一份构建以前被接受过的理由,只读显示,不预填 */
prior_acceptances?: PriorAcceptance[]
}
/** AI 起草的结果摘要 */
export interface AiDraftSummary {
draft: Draft | null
usage?: AiUsage[]
attempts?: number
total_tokens?: number
cost_usd?: number
error?: string | null
}
/** 暂存区(一次未提交的按配方导入):GET /imports/{id} 等接口的返回 */
export interface Staging {
id: string
kind: ImportStagingKind
status: ImportStagingStatus
source: { id: string; name: string; exists: boolean; import_mode: 'simple' | 'recipe' | null }
file: { name: string; size: number; sha256_prefix: string }
sheets?: {
name: string; state: string; bounds: string | null; nonempty: number; merged?: number
formulas?: number; formulas_uncached?: number; hidden_rows?: number; hidden_cols?: number
}[]
skipped_sheets?: { sheet: string; state: string }[]
full_calc_on_load?: boolean
grids: GridPreview[]
draft: Draft | null
ai_draft: AiDraftSummary | null
/** offered:这次导入提供 AI 起草入口;available:模型接入可用 */
ai: { offered: boolean; available: boolean; reason?: string; model?: string; provider?: string }
ai_consents?: { signed_by: string | null; at: string; model: string; compressed_sha256: string; chars: number }[]
recipe: Recipe | null
recipe_origin?: string | null
recipe_problems: RecipeProblem[]
answers: Record<string, QuestionAnswer>
cards: Card[]
questions: Question[]
draft_problems: ImportProblem[]
/** 干跑只检查了每张工作表的前若干行 */
draft_partial: boolean
/** 分段标题原文 → 常量可选的词 */
candidates: Record<string, string[]>
/** 单位词表 */
units: string[]
marks: RegionMark[]
trial: TrialOut | null
context_inputs?: Record<string, { start: string; end: string; signed_by?: string | null }>
/** 最近一次试运行时数据源的当前版本(与 trial.base_snapshot_id 同值;提交时对不上回 409 base_changed)。
* 数据源还没有当前版本(首次导入)或没试运行过时为 null;试运行作废后保留原值。
* 所以不能拿它是不是 null 判断「试运行过没有」,那要看 trial 与 status */
base_snapshot_id?: string | null
/** 提交后生成的导入记录 id;未提交为 null */
committed_import_id?: string | null
/** 期 3:修复提议(按当前问题现算)。问题对象上的 fix_ids 指向这里的 id */
fixes?: FixProposal[]
/** 期 3:当前工作配方的哈希(撤销是否可用、预览是否过期都按它判断) */
recipe_sha256?: string | null
/** 期 3:已应用的修改(修复、框选),按 seq 升序 */
edits?: StagingEdit[]
/** 期 3:改配方后不再成立、被移除的回答 */
answers_dropped?: AnswerDropped[]
created_at?: string
updated_at?: string
expires_at?: string
}
/** GET draft-ai/preview:将要发给模型的压缩表示原文 */
export interface AiPreview {
text: string
chars: number
/** 同意令牌:绑定全文、接入名和模型 id,POST draft-ai 原样带回(三者任一变了服务端回 409 ai_preview_stale) */
sha256: string
/** 全文本身的 sha256(留痕用) */
text_sha256?: string
model: string
provider: string
}
/** POST imports/{id}/commit 的返回 */
export interface CommitOut {
source: DataSource
import_id: string
snapshot_id: string
build_id: string
recipe_id: string
build_reused: boolean
/** 与某次导入完全相同,未新建版本 */
unchanged: boolean
/** 期 3:服务端的同版本数据文件曾被改动,已用本次上传的文件恢复 */
build_restored?: boolean
/** 期 3:启用后当前版本含几期 */
parts?: number
/** 期 3:与此前某个版本内容相同,直接启用了那个版本 */
snapshot_reused?: boolean
}
/** GET /{source_id}/recipe:当前启用的配方(补全了默认值) */
export interface CurrentRecipe {
recipe_id: string
seq: number
origin: string
recipe: Recipe
recipe_sha256: string
confirmations: { id: string; label: string; at: string }[]
signed_by: string | null
activated_at: string | null
}
// ---------------------------------------------------------------------------
// 期 3:修复按钮、框选、按期累积、配方对照、版本页(P3-SPEC 第 9 节)。字段与后端 recipe_types.py 的 dataclass
// 和接口的 JSON 同名(Selection 的 as 除外:后端 dataclass 叫 as_,JSON 里是 as)
// ---------------------------------------------------------------------------
/** 修复按钮的种类(契约 FIX_KINDS) */
export type FixKind =
| 'remove_label' | 'add_label' | 'edit_members' | 'rename_title' | 'declare_total' | 'ignore_cells'
| 'declare_placeholder' | 'declare_hidden' | 'rename_sheet' | 'declare_subtotals' | 'declare_text_measure'
| (string & {})
/** 修复提议的一个封闭选项。效果由服务端算:界面只发 fix_id、value 和理由 */
export interface FixOption {
value: string
label: string
detail?: string
/** 选它必须写理由(1–200 字):理由填好后才预览,理由改了要重新预览 */
needs_reason?: boolean
breaking?: boolean
}
/** 提议对应哪一条问题:problem / recipe_problem 按下标,sheet_renamed 按本期的工作表名 */
export interface FixAnchor {
kind: 'problem' | 'recipe_problem' | 'sheet_renamed' | (string & {})
index?: number | null
sheet?: string | null
}
/** 修复提议(Staging.fixes 的一项,契约 FixProposal) */
export interface FixProposal {
/** fx- 加 12 位十六进制:同样的问题得到同样的 id */
id: string
kind: FixKind
problem_code: string | null
title: string
cells: string[]
target: Record<string, any>
options: FixOption[]
anchor: FixAnchor
}
/** 框选「选它是什么」的封闭取值(契约 SELECTION_AS) */
export type SelectionAs =
| 'list' | 'crosstab' | 'segment' | 'derived' | 'section_title' | 'ignore_rows' | 'ignore_columns' | 'ignore_outside'
/** 框选的参数(按 as 取用:list 用 header_rows、table、bottom;segment 用 role、table;derived 用 keep;
* section_title 用 segment;ignore_* 用 reason) */
export interface SelectionOptions {
/** 1–3,默认 1 */
header_rows?: number
table?: string
/** box 以框的下边为准(默认);auto 下边界按规则推断 */
bottom?: 'box' | 'auto'
role?: 'measures' | 'dimension'
keep?: boolean
segment?: string
reason?: string
[key: string]: unknown
}
/** 一次框选(edits/preview、edits/apply 请求里的 selection) */
export interface SelectionRequest {
sheet: string
/** A1 区域,不带工作表名,列字母大写、不带 $(「C5」或「C5:F8」):别的写法服务端回 422 edit_invalid */
ref: string
as: SelectionAs
options?: SelectionOptions
}
/** edits/preview、edits/apply 的请求:fix 与 selection 二选一。seq 是界面的预览序号,原样回传 */
export type EditRequest =
| { fix: { id: string; option: string; reason?: string }; selection?: never; seq?: number }
| { selection: SelectionRequest; fix?: never; seq?: number }
/** 框选换算出的锚点(契约 Anchor),界面显示「按文字定位」 */
export interface EditAnchor {
kind: 'header' | 'row_label' | 'section_title' | 'total_word' | 'axis' | 'after_title' | 'outside_text' | (string & {})
text: string
cell?: string | null
}
/** 框选的期望区域与重放结果的比对(契约 ReplayCompare) */
export interface ReplayCompare {
/** 列表:{header, data, total};交叉表:{axis, labels, values} */
expected: Record<string, string | null>
actual: Record<string, string | null>
match: boolean
diffs: string[]
/** 部分干跑时只比较前多少行;完整比较为 null */
window_rows?: number | null
}
/** POST edits/preview 的返回(P3-SPEC 9.4 EditPreview) */
export interface EditPreview {
ok: boolean
kind: 'fix' | 'selection' | (string & {})
/** 目标键(remove_label:日间:7-8);确认项 fix:<key> / select:<key> 用它 */
key: string
title: string
summary: string[]
/** 按完整形式写的 JSON Patch:不给用户看,最多收在技术细节里 */
ops: { op: string; path: string; value?: unknown }[]
anchors: EditAnchor[]
notes: string[]
/** ok=false 时的原因(category=recipe),坐标可点 */
problems: ImportProblem[]
recipe_sha256_before: string | null
/** apply 时原样作为 expected_sha256 发回 */
recipe_sha256_after: string | null
recipe_problems: RecipeProblem[]
dry_run: { problems: ImportProblem[]; partial: boolean; marks: RegionMark[] } | null
replay: ReplayCompare | null
/** 对现行配方的破坏性变化:表名 → 人话列表;空对象表示没有。
* 注意:这不是后端 EditResult.breaking(那是服务端内部的布尔)。WP-5 组装预览时必须总是用
* table_changes 算出的字典覆盖它;收到布尔说明服务端漏了覆盖,Object.entries(false) 是 [],
* 破坏性变化会从预览里消失 */
breaking: Record<string, string[]>
accumulate_change: 'same' | 'compatible' | 'retire' | 'semantic' | (string & {}) | null
/** 当前工作配方对比修改后的配方(复用 RecipeCompare 显示) */
compare: RecipeComparison | null
/** 框选:换算出的期望区域 */
expected?: Record<string, string | null> | null
/** 框选:新增或替换的块 id */
block?: string | null
/** 请求里的预览序号,原样回传:不是最新一次的回包丢弃 */
seq?: number | null
}
/**
* 暂存区里已应用的一次修改(Staging.edits 的一项)。服务端 recipe_imports._edit_out 从修改记录里去掉撤销用的起点
* (before、base_sha256_after)和补丁(ops,只进导入清单),其余原样给出
*/
export interface StagingEdit {
seq: number
kind: 'fix' | 'selection' | (string & {})
key: string
title: string
/** 预览时的人话摘要(EditPreview.summary),应用时原样记下 */
summary?: string[]
at: string
signed_by: string | null
recipe_sha256_before: string | null
recipe_sha256_after: string | null
/** 之后整份替换过配方(PUT):不能撤销,也不再出确认项 */
superseded: boolean
/** 只有最后一条、未被覆盖的为 true */
undoable: boolean
/** kind=fix:人选的是哪条提议、哪一项、写的理由(与 edits/apply 请求里的 fix 同形)。WP-5 评审修复之前记下的修改没有 */
fix?: { id: string; option: string; reason: string | null }
/** kind=selection:这次框选(Selection.to_json,键是 as,options 总是对象)。同上,旧记录没有 */
selection?: SelectionRequest & { options: SelectionOptions }
}
/** 改配方后被移除的回答:changed 需要重新回答;gone 问题已不适用。text 是问题文字(界面不露 id) */
export interface AnswerDropped {
id: string
text: string
reason: 'changed' | 'gone' | (string & {})
}
/** 同一份构建以前被接受过的理由(TrialOut.prior_acceptances),只读显示 */
export interface PriorAcceptance {
check_id: string
reason: string
signed_by: string | null
at: string
/** 第几次导入 */
import_seq: number
}
/** 排除的行的原因(契约 ExcludedRows.reason) */
export type ExcludedReason =
| 'hidden_excluded' | 'blank_skipped' | 'ignored_rows' | 'ignored_outside' | 'after_stop' | 'total_not_kept'
| 'subtotal'
| (string & {})
/** 回执里的「排除的行」(契约 ExcludedRows)。按表头忽略的列不在这里(记在回执的 ignored_columns) */
export interface ExcludedRows {
sheet: string
reason: ExcludedReason
/** [[起, 止], …],1 起的行号闭区间 */
rows: [number, number][]
cells: number
anchor?: string | null
block?: string | null
}
/** ReceiptBlocks.tsx 的「排除的行」:向导回执和版本页的清单视图共用。onFocus 收「工作表!A1」 */
export interface RowsExcludedProps {
rows: ExcludedRows[] | null | undefined
onFocus?: (cell: string) => void
}
/** ReceiptBlocks.tsx 的回执摘要:receipt 是试运行回执或导入清单里的 receipt(形状相同) */
export interface ReceiptSummaryProps {
receipt: TrialReceipt | null | undefined
onFocus?: (cell: string) => void
}
/** 累积计划的 action(契约 ACCUMULATE_ACTIONS) */
export type AccumulateAction = 'replace' | 'first' | 'append' | 'replace_period' | 'restart' | 'rejected' | (string & {})
/** 累积计划里的一期。本期 import_id、seq 为 null,new 为 true */
export interface AccumulatePart {
import_id: string | null
seq: number | null
start: string
end: string
file_name: string
new: boolean
/** 表 → 该期行数 */
rows: Record<string, number>
/** 该期配方不满足按期累积的原因(契约 accumulate_blockers) */
blockers: RecipeProblem[]
}
/** 计划里提到的一期(被替换、被移出、部分重叠):至少有起止,其余字段同 AccumulatePart、可能缺省 */
export interface AccumulatePeriodRef {
import_id?: string | null
seq?: number | null
start: string | null
end: string | null
file_name?: string
rows?: Record<string, number>
}
/** 表或列的新增、退役:column 为 null 表示整张表 */
export interface TableColumnRef {
table: string
column: string | null
periods?: string[]
}
/** 各期维度取值的差异(P3-SPEC 2.5) */
export interface LabelSetDiff {
table: string
column: string
segment: string
periods: { start: string; end: string; missing: string[]; extra: string[] }[]
}
/** 累积计划(TrialOut.accumulate,P3-SPEC 9.4 AccumulatePlan) */
export interface AccumulatePlan {
mode: 'replace' | 'accumulate' | (string & {})
action: AccumulateAction
/** 本期统计期。source:cells 取自表内,human 人工录入;每期替换且没有统计期时 start、end 为 null */
period: { start: string | null; end: string | null; source?: 'cells' | 'human' | (string & {}) }
parts: AccumulatePart[]
replaces: AccumulatePeriodRef | null
/** restart、模式切换时移出当前版本的各期:界面标「将不在当前版本中」 */
dropped: AccumulatePeriodRef[]
overlaps: AccumulatePeriodRef[]
gaps: { start: string; end: string }[]
backfill: boolean
change: 'same' | 'compatible' | 'retire' | 'semantic' | (string & {})
added: TableColumnRef[]
retired_new: TableColumnRef[]
retired_existing: TableColumnRef[]
semantic: string[]
label_sets: LabelSetDiff[]
mode_switch: 'replace->accumulate' | 'accumulate->replace' | (string & {}) | null
reason: string | null
/** 物化了的并集:rows 是「启用后当前版本」的行数;没有物化为 null(此时等于本期) */
union: { union_id: string; db_sha256: string; rows: Record<string, number> } | null
}
/** 新旧配方对照里一列的摘要 */
export interface CompareColumnBrief {
type: string
unit?: string | null
source?: string | null
}
export type CompareStatus = 'added' | 'removed' | 'changed' | 'same' | (string & {})
/** 新旧配方对照(P3-SPEC 9.4 RecipeComparison,WP-3 compare_recipes 产出) */
export interface RecipeComparison {
tables: {
name: string
status: CompareStatus
columns: {
name: string
status: CompareStatus
old: CompareColumnBrief | null
new: CompareColumnBrief | null
/** table_changes 的 kind(type / unit / source / store / const_value …) */
changes: string[]
}[]
grain: { old: string[] | null; new: string[] | null; changed: boolean }
kind: { old: string | null; new: string | null }
/** 这张表除单位以外的破坏性变化(table_changes 的原话),是完整清单;单位变化只在 columns[].changes 与 units_changed 里 */
breaking: string[]
}[]
segments: {
id: string
status: CompareStatus
title: { old: string | null; new: string | null }
labels: { added: string[]; removed: string[] }
ignore: { added: string[]; removed: string[] }
}[]
relations: { id: string; status: CompareStatus; old: string | null; new: string | null }[]
sheets: { id: string; name: { old: string | null; new: string | null } }[]
mode: { old: string | null; new: string | null }
breaking: boolean
/** 单位变化的列(界面排在最前) */
units_changed: { table: string; column: string; old?: string | null; new?: string | null }[]
accumulate: {
change: string
added: TableColumnRef[]
retired_new: TableColumnRef[]
retired_existing: TableColumnRef[]
} | null
}
/** POST imports/{id}/redraft-rules 的返回:不改工作配方,「采用」时再 PUT recipe(origin: rules_redraft) */
export interface RedraftRulesOut {
draft: Draft
aligned_recipe: Recipe | null
/** 名字对齐的人话说明 */
alignment: string[]
compare: RecipeComparison | null
}
// ---- 版本页(P3-SPEC 第 7 节)
/** 一期的统计期引用:简单导入或没有统计期的期,start、end 为 null,带 file_name */
export interface PeriodRef {
start: string | null
end: string | null
file_name?: string
}
/** 版本列表里一个版本的一期 */
export interface SnapshotPart {
import_id: string
seq: number
period_start: string | null
period_end: string | null
file_name: string
raw_state: 'kept' | 'purged' | 'absent' | (string & {})
status: string
/** 表 → 该期行数 */
rows: Record<string, number>
/** 接受的条数(数据质量不成立 / 合计无法核对) */
overrides: number
waivers: number
revoked: boolean
}
/** GET /{source_id}/snapshots 的一行(7.1)。当前版本排第一 */
/** 快照不能启用的原因代码(后端 recipe_types.SnapshotReasonCode;VERSIONS_TEXT.notActivatable 的键与它相同) */
export type SnapshotReasonCode = 'current' | 'retired' | 'file_lost' | 'contains_revoked'
export interface SnapshotOut {
id: string
current: boolean
mode: 'replace' | 'accumulate' | (string & {}) | null
created_at: string | null
activated_at: string | null
/** 简单导入为 null */
recipe: { id: string; seq: number } | null
parts: SnapshotPart[]
/** 表 → 启用这个版本后的行数 */
tables: Record<string, number>
db_sha256_prefix: string
/** 数据文件的字节数 */
db_size: number | null
/** 记录没回收、文件在 */
available: boolean
/** 引用它的运行数(任何状态) */
pinned_runs: number
activatable: boolean
/** 不能启用的原因(人话);能启用为 null。服务端取后端契约 SNAPSHOT_NOT_ACTIVATABLE[reason_code],
* 与 VERSIONS_TEXT.notActivatable 逐字相同 */
reason: string | null
/** 不能启用的原因的代码(契约补充:7.1 只给了人话,available=false 同时覆盖「已回收」和「数据文件已丢失」,
* 界面分不出来)。能启用为 null。界面按 VERSIONS_TEXT.notActivatable[reason_code] 显示,缺省或不认识时
* 退回显示 reason,不去匹配 reason 的原文。几种同时成立时服务端取第一个:current > retired > file_lost >
* contains_revoked */
reason_code?: SnapshotReasonCode | null
/** 当前的遮罩列里,这个版本没有同名列的 */
mask_lost: string[]
/** 与当前版本相比,启用后增减的期 */
periods_diff: { added: PeriodRef[]; removed: PeriodRef[] }
}
/** POST …/snapshots/{snapshot_id}/activate 的请求(7.2) */
export interface ActivateSnapshotBody {
confirm: true
/** 界面看到的当前版本 id:对不上回 409 base_changed */
expected_current_snapshot_id: string | null
/** mask_lost 非空时必填,与它相同 */
ack_mask_lost?: string[]
reason?: string
signed_by?: string | null
}
export interface ActivateSnapshotOut {
source: DataSource
snapshot_id: string
previous_snapshot_id: string | null
recipe_id: string | null
}
/** 导入清单、快照清单(7.3)。verified:取回时内容哈希复验通过 */
export interface ManifestOut {
artifact_id: string | null
verified?: boolean
/** import_manifest;简单导入是 build_report(构建回执);snapshot_manifest;单期快照是 snapshot_view(合成的视图) */
kind: 'import_manifest' | 'build_report' | 'snapshot_manifest' | 'snapshot_view' | (string & {})
content: Record<string, any>
}
/** POST …/imports/{import_id}/remove 的请求(7.4,累积模式) */
export interface RemovePeriodBody {
confirm: true
expected_current_snapshot_id: string | null
/** 1–500 字,必填 */
reason: string
signed_by?: string | null
}
export interface RemovePeriodOut {
source: DataSource
snapshot_id: string
removed_import_id: string
/** 复用(或复活)了同内容的已有版本 */
reused: boolean
}
/** POST …/imports/{import_id}/revoke-acceptance 的请求(7.5) */
export interface RevokeAcceptanceBody {
confirm: true
expected_current_snapshot_id: string | null
reason: string
signed_by?: string | null
/** 替换模式:取自 revoke_plan.target_snapshot_id */
expected_target_snapshot_id?: string | null
/** 回滚目标有遮罩列丢失时带上,与 revoke_plan.mask_lost 相同 */
ack_mask_lost?: string[]
}
export interface RevokeAcceptanceOut {
source: DataSource
snapshot_id: string
action: 'rollback' | 'remove_period' | (string & {})
}
/** 一条接受(导入记录的 acceptances) */
export interface ImportAcceptance {
check_id: string
kind: 'override' | 'waiver' | (string & {})
reason: string
signed_by: string | null
at: string | null
}
/** 作废接受的预案(只对在当前版本里、带接受的导入给):确认框照它写 */
export interface RevokePlan {
/** 不能作废时为 null,reason 写原因 */
action: 'rollback' | 'remove_period' | (string & {}) | null
target_snapshot_id: string | null
/** 替换模式的回滚目标 */
target: { parts: PeriodRef[]; recipe_seq: number | null; mode: string | null; simple: boolean } | null
/** 累积模式:移除后的各期 */
result_parts: PeriodRef[]
gaps: { start: string; end: string }[]
mask_lost: string[]
reason: string | null
}
/** GET /{source_id}/imports 的一行(期 1 接口,期 3 补字段,7.6) */
export interface ImportRecord {
id: string
seq: number
build_id: string
file_name: string
file_size: number | null
raw_sha256: string | null
raw_state: 'kept' | 'purged' | 'absent' | (string & {})
status: string
/** 清除记录 {at, reason, signed_by…};没清除为 null */
purged: Record<string, any> | null
/** 在当前版本里(与 in_current 同值) */
current: boolean
created_at: string | null
activated_at: string | null
recipe_id: string | null
period_start: string | null
period_end: string | null
signed_by: string | null
/** 署名是不是认证过的身份:按这一期导入清单里记下的(05-已知遗留 (c)),旧清单记的是 false。老后端不给 */
signed_by_verified?: boolean
/** 接受的条数 */
overrides: number
waivers: number
/** 期 3 */
acceptances?: ImportAcceptance[]
revoked?: { at: string; reason: string; signed_by: string | null; signed_by_verified: boolean } | null
manifest_artifact?: string | null
recipe_seq?: number | null
in_current?: boolean
/** 表 → 该期行数 */
rows?: Record<string, number>
revoke_plan?: RevokePlan | null
/** 同一份原件内容在其他导入记录里的引用,按源汇总(本源也算)。source_name:现在的后端对已删除的源写
* 「已删除的数据源」,老后端或老数据可能是 null——声明成可空,让界面的兜底由类型强制(同 also_purged) */
raw_shared_with?: { source_name: string | null; count: number }[]
/** 引用这份原件的未结束导入个数 */
raw_open_stagings?: number
}
/** POST …/imports/{import_id}/purge-raw 的请求(理由必填) */
export interface PurgeRawBody {
confirm: true
reason: string
signed_by?: string | null
}
export interface PurgeRawOut {
import: ImportRecord
file_deleted: boolean
/** 同一份内容一并清除的其他导入记录 */
also_purged: { id: string; source_id: string; source_name: string | null; seq: number; file_name: string }[]
/** 一并放弃的未完成导入 */
discarded_stagings: string[]
}
/** 自定义工具(GET /api/custom-tools 的一行;POST / PATCH 的返回同形) */
export interface CustomTool {
id: string
name: string
description: string
kind: 'http' | 'python' | (string & {})
parameters: Record<string, any>
config: Record<string, any>
enabled: boolean
/** 同 ToolInfo.problem:参数定义写坏了的那句话,没问题是 null */
problem?: string | null
}
export interface Skill {
id: string
name: string
description: string
instructions: string
examples: { input?: string; output?: string }[]
suggested_tools: string[]
tags: string[]
enabled: boolean
}
export interface MemoryItem {
id: string
scope: string
kind: string
content: string
importance: number
use_count: number
meta: Record<string, any>
/**
* 从哪来。kind=run 时带运行、节点和工作流名;manual 是手动添加的;playground 是
* 在工具库里直接调 remember 写进来的。老后端不给
*/
source?: MemorySource | null
created_at?: string
/** 后端不再给:每次计数的回忆都会写这一行,updated_at 跟着回忆走,不是编辑时间 */
updated_at?: never
last_used_at?: string | null
}
export interface MemorySource {
kind?: 'run' | 'manual' | 'playground' | string
run_id?: string | null
node_id?: string | null
workflow_name?: string | null
node_label?: string | null
/** 来源运行是否还在。删掉之后只剩文字,不再给链接 */
run_exists?: boolean
[key: string]: any
}
export interface KbDocument {
id: string
collection: string
title: string
source: string
mime: string
chunk_count: number
/** ready | processing | failed。大文件的切块和算向量在后台跑 */
status?: string
error?: string
meta?: { progress?: { done: number; total: number } }
created_at?: string
}
export interface ValidationIssue {
/**
* info 是建议,不是问题(比如 evidence.upgrade_available「可以升级为可追溯结构」):store 在校验回来时
* 就把它分到 advice 里,issues 里只有 error / warning——节点卡、检查器、运行按钮都只认这两档
*/
level: 'error' | 'warning' | 'info'
node_id?: string | null
edge_id?: string | null
message: string
/** 出问题的是节点配置里的哪一项:'prompt'、'tools'、'cases[1].condition'。老后端不给 */
field?: string | null
/** 机读编号(governed.agent_approval_never 这类),发布前检查按它认修复。老后端不给 */
code?: string | null
/** 这条问题对应的修复 id:publish-check 的 fixes 里同 id 的那一项。没有修复时为空 */
fix?: string | null
/**
* 发布门禁里的 SQL 检查:这条检查本来的级别。level 说的是拦不拦(已发布档一律 warning),它说的是错误还是提醒。
* 别的问题、老服务端不给
*/
sql_level?: 'error' | 'warning' | null
}
/** 发布等级:发布弹窗、问题面板的发布前检查共用 */
export type PublishLevel = 'published' | 'governed'
/**
* 发布前检查给的一项修复(POST /workflows/{id}/publish-check 的 fixes)。
* auto:答案唯一、不降低要求,预览后一键修;choice:要人拿主意,options 里挑(multiple 可多选);
* assist:结构性的,交给 Copilot 试着改,产出同样只是预览
*/
export interface PublishFix {
id: string
code: string
kind: 'auto' | 'choice' | 'assist'
node_id?: string | null
/** 这条修复落在哪个字段:点名的模型接入一处一条(同一节点上的几处按它区分) */
field?: string | null
label: string
preview?: { field?: string | null; before?: unknown; after?: unknown } | null
/** handoff:选了这一项就是「交给 Copilot」(G4 的「它在做计算」)。只认这个标记,不认值——
* 别的选项拿节点 id 当值,节点 id 恰好叫 copilot 的报告撰写节点照样是普通候选。
* demo:点名的模型接入的候选,修完以后执行时落到演示类接入(不调用真实模型),选项名旁加醒目的标签 */
options?: { value: unknown; label: string; hint?: string | null; handoff?: boolean; demo?: boolean }[]
multiple?: boolean
/** 建议值:界面上标「建议」,仍然要人选 */
default?: unknown
}
export interface PublishCheck {
level: PublishLevel
ok: boolean
issues: ValidationIssue[]
fixes: PublishFix[]
}
/** 自动修复预览里给人看的一项改动:「节点 · 字段:原值 → 新值」 */
export interface AutofixChange {
fix_id: string
node_id?: string | null
node_title?: string | null
field?: string | null
before?: unknown
after?: unknown
label?: string | null
}
/** POST /workflows/{id}/autofix:只给预览,不改库、不发布 */
export interface AutofixResult {
/** 修复后的整张图;人确认后前端走保存接口存成草稿 */
graph: GraphSpec | null
changes: AutofixChange[]
applied: string[]
rejected: { fix_id: string; reason: string }[]
/** 修复后仍然存在的问题(带 code 和 fix) */
remaining: ValidationIssue[]
/** 交给 Copilot 的那一段:它认为要人拿主意的放进 questions,不硬改 */
assist: { ok: boolean; summary?: string; questions?: string[] } | null
/** 修复后是否已经没有 error */
ok: boolean
}
/**
* 一键升级预览里的一项改动。形状同自动修复的 AutofixChange,另认这几种:
* field 为 type 是节点类型的变化(llm → report);field 为 node 是新插入的节点(after 是节点);
* field 为 edge 是连线的改接(before / after 是 {source, target})。rule 是命中的改写规则(R1–R4;Copilot 那一段是 assist)。
* 同一步(同一个 fix_id)的几项改动共用一句 label
*/
export interface UpgradeChange extends AutofixChange {
rule?: string | null
}
/** 升级给的说明:不改、只建议的(R5:代码节点要不要标成 source),原样给人看 */
export interface UpgradeNote {
text: string
node_id?: string | null
rule?: string | null
level?: 'info' | 'warning'
}
/** POST /copilot/upgrade-evidence:只给预览,不自动保存 */
export interface UpgradeResult {
/** 升级后的整张图;人确认后走现有的保存 */
graph: GraphSpec | null
changes: UpgradeChange[]
notes: UpgradeNote[]
/** 升级后的图重跑 validate 和门禁剩下的问题 */
issues: ValidationIssue[]
/** 没有采用的改动(Copilot 的改写冒出新问题、碰了禁止规则),写明原因 */
rejected: { fix_id: string; reason: string }[]
/** assist 时 Copilot 的那一段:非纯算术、保留没改的代码节点在 warnings 里 */
assist: { ok: boolean; summary?: string; questions?: string[]; warnings?: string[] } | null
}
/** 单个节点在一次运行中的实时状态,驱动画布上的高亮。 */
export interface NodeRuntime {
/** cancelled / suspended 是终态清扫时收的:运行被取消或服务重启时还没跑完的节点 */
/** rejected:这一步被人驳回(node.failed 带 reason=rejected,run/trace 的 NodeState 同名) */
status: 'idle' | 'running' | 'done' | 'failed' | 'waiting' | 'skipped' | 'cancelled' | 'suspended' | 'rejected'
durationMs?: number
preview?: any
error?: string
tokens?: string
thinking?: string
toolCalls?: { tool: string; args?: any; result?: string; ok?: boolean; agent?: string }[]
/** supervisor 的协作矩阵:谁被派了、在第几轮、几个人同时在跑。运行中长出来 */
team?: TeamRun
/** loop 走到第几轮(从 1 数)。卡片上那个"第 2/5 轮" */
iteration?: number
/** 分支/循环实际走的出口。命中的那条亮起来、其余压暗——不然一个六出口的
* 分支节点跑完,谁也说不清它到底选了哪条 */
takenHandle?: string
/** 走这条出口的理由:表达式分支命中了哪条条件,或调度者为什么派给这个人 */
reason?: string
}
/**
* 协作团队一轮里的一个人。
*
* ms 是**各自量出来**的耗时,不是拿整轮墙钟摊的——界面要显示"并行省了多少",
* 那个数按 N×墙钟 推算会把快的那个也记成最慢那条,省下的时间就被夸大了。
*/
export interface TeamMember {
agent: string
instruction: string
ms: number
status: 'running' | 'done' | 'failed' | 'waiting' | 'cancelled' | 'suspended'
result?: string
}
export interface TeamRound {
round: number
/** 这一轮同时派了几个人。1 就是串行的一步 */
parallel: number
/** 这一轮实际花的时间:并发时是最慢那个 */
wallMs: number
/** 各人耗时之和:并发时它大于 wallMs,差值就是省下的 */
sumMs: number
reason?: string
members: TeamMember[]
}
export interface TeamRun {
/** 花名册,按第一次出现的顺序——泳道的行顺序 */
members: string[]
rounds: TeamRound[]
/** 并行一共省下多少毫秒。全程串行则为 0 */
savedMs: number
/** 协作是不是已经收尾了 */
finished: boolean
}
/** 一处 {{ }} 引用 */
export interface VarRef {
node_id: string
node_label: string
field: string
expr: string
}
/** 一个可以被 {{ }} 引用的东西。由后端静态分析整张图得出 */
export interface Variable {
path: string
kind: 'input' | 'var' | 'node' | 'builtin' | 'loop'
label: string
produced_by?: string | null
produced_by_label?: string | null
order: number
refs: VarRef[]
description: string
}
export interface VarIssue {
level: 'error' | 'warning' | 'info'
message: string
node_id?: string | null
path: string
}
/**
* 一次对话。
*
* kind 分两种:chat 是问数据页(一轮 = 建图 → 跑图 → 出答案,进左侧列表),
* canvas 是画布右栏的 Copilot(依附某张图,一轮 = 一次改图,不进列表)。
*/
export interface Conversation {
id: string
title: string
/**
* 前端建的只有 chat、canvas;服务端另建两种:app 是应用编辑器的助手对话,blueprint 是蓝图对话(docs/design/normal-user-apps.md
* §2.2,权限跟着蓝图走,经 GET /api/blueprints/{id}/conversation 读)
*/
kind: 'chat' | 'canvas' | 'app' | 'blueprint'
workflow_id?: string | null
archived: boolean
created_at?: string
last_active_at?: string
turn_count: number
/** 列表里的副标题,让人一眼认出是哪次聊天 */
last_question: string
/**
* 最后一轮停在哪。左栏对这次没打开过的会话也能标出运行中、待审批、失败、被驳回(rejected:运行被审批人驳回结束,
* 服务端 cancelled + end_reason=rejected)。没有轮次时为 null;老后端不给
*/
last_status?: 'running' | 'waiting' | 'error' | 'cancelled' | 'rejected' | 'suspended' | 'done' | null
/** 最后一轮的运行。建图阶段就断了的没有 */
last_run_id?: string | null
}
export interface ConversationDetail extends Conversation {
turns: ConversationTurn[]
}
export interface ConversationTurn {
id: string
seq: number
question: string
answer: string
explanation: string
graph?: GraphSpec | null
/** 可能为空:建图阶段就失败时压根没有 run,但这一轮仍然发生过 */
run_id?: string | null
status: 'running' | 'done' | 'error'
error: string
/** 复核结论。null = 这一轮没复核过,和「复核过、没发现问题」不是一回事 */
review?: ReviewResult | null
/**
* 可信度元数据(出具档位、运行类别、耗时、查库次数……),前端整块写、整块读,
* 后端只用它算 last_status。老轮次挂在 review.meta 下,后端读出来时统一放到这里
*/
meta?: Record<string, any> | null
created_at?: string
/**
* 这一轮最后一次落库的时刻(UTC,后端 TurnOut)。后端判「建图断了」按它算
* (conversations.BUILD_STALE),前端判断同一件事时用同一只钟。老后端没有
*/
updated_at?: string
/** 这一轮在服务端的任务(问数据整轮在服务端跑,backend/app/chat/job.job_view)。这一波之前的老轮次没有,为 null */
job?: ChatTurnJob | null
}
/** 问数据一轮在服务端的任务现在怎样。status 用后台任务的状态词;stage 是在哪一步;attempt_seq 是这一次尝试从哪一条事件开始 */
export interface ChatTurnJob {
status: 'queued' | 'running' | 'succeeded' | 'failed' | 'cancelled' | 'interrupted'
stage: 'build' | 'run' | 'waiting' | 'review' | null
mode: string
run_id: string | null
attempt: number
last_seq: number
attempt_seq: number
error: string | null
/** 等不及的那次复核还在后台跑 */
late_review?: boolean
}
/**
* Copilot 改图前后都在、而绑定的工具变了的节点或协作成员。
* 画布上看不出来(节点还在),跑起来才发现查不了库,所以改图回执要明说
*/
export interface ToolChange {
node_id: string
label: string
/** 协作成员的名字;节点本身的工具为 null */
member: string | null
/** 配置里的哪一项:'tools' 或 'agents[2].tools' */
field: string
before: string[]
after: string[]
added: string[]
removed: string[]
}
/**
* Copilot 自查(SSE op='check')里的一条问题。新后端给对象,老会话里存的是
* 「「node_id」message」一行字符串,两种都要认,见 CopilotCheckEntry
*/
export interface CopilotCheckIssue {
level: 'error' | 'warning'
node_id: string | null
edge_id: string | null
/** 不再以「节点 id」开头;要带节点名自己用 node_id 查 */
message: string
/** 出问题的配置项,比如 'tools'、'agents[1].tools',检查器据此定位 */
field?: string | null
/** datasource_out_of_scope、tools_dropped 这类机器码 */
code?: string
}
export type CopilotCheckEntry = string | CopilotCheckIssue
/** SSE 的自查操作。repairing 时带第几轮;warnings 是不挡运行的提醒(tools_dropped) */
export interface CopilotCheckOp {
op: 'check'
status: 'repairing' | 'passed' | 'failed' | 'error'
issues?: CopilotCheckEntry[]
warnings?: CopilotCheckIssue[]
round?: number
repaired?: number
message?: string
detail?: string
}
/**
* 一次运行跑完之后的复核结论(后端 engine/review.py)。
*
* 「跑完了」和「答得对」是两回事:检索降级、工具报错、agent 步数用满这些事
* 原先一件都不会进到答案里,用户拿到一个看起来很完整的结论,却无从知道它是在
* 什么条件下得出的。复核层就是把这段补上。
*/
export interface ReviewSignal {
kind: string
detail: string
/** broken = 答案不可信;degraded = 能用但有缺口 */
severity: 'broken' | 'degraded'
/**
* agent 为什么提前收尾(后端 engine/guards.py):steps / stall / budget_tokens / budget_usd / context。
* 老运行没有。只有 steps(或者没有这个字段的老运行)放宽步数才对症
*/
reason?: string
}
export interface ReviewResult {
/** ok=没问题 / annotated=加了说明 / rewritten=重写过 / reverted=重写里有新数字被退回 */
verdict: 'ok' | 'annotated' | 'rewritten' | 'reverted'
/** 给用户看的异常说明。排在成果上方 */
note: string
/** 重写后的答案。null 表示沿用原答案 */
answer: string | null
/** 改写之前的那份。改写是有损的,得能对照原件 */
original?: string | null
/** 换个配置重跑一遍大概率就好了 */
retry: boolean
severity: 'broken' | 'degraded' | ''
signals: ReviewSignal[]
/**
* 要不要带着原图重来一次、重来时交给助手的话(服务端 backend/app/chat/turn.py 按这一轮试过几次判):要就是那句话,不要
* 是 null。老服务端没有这个字段,问数据页照原来的规则自己判
*/
retry_instruction?: string | null
/** 复核这一次的用量(服务端 providers/usage.py 的汇总,没调模型时全 0):这一轮的花费 = 助手 + 运行 + 复核 */
usage?: {
cost_usd?: number | null; unpriced_calls?: number; calls?: number; input_tokens?: number; output_tokens?: number
by_model?: Record<string, { input_tokens: number; output_tokens: number; calls: number; cost_usd: number
unpriced_calls: number }>
}
}
// -------------------------------------------------------------------------
// 可点击证据(报告撰写节点产出的文档、证据接口)
//
// 形状照后端 engine/evidence.py 和 api/evidence.py。前端对缺字段一律宽容:接口和文档
// 是分期长出来的,老运行、别的版本的后端都可能少几个键,少了就降级显示,不白屏。
// 偏移(span、start/end)按 Unicode 码点算,不是 JS 的 UTF-16 下标。
// -------------------------------------------------------------------------
/** 一处引用解析后的结果 */
export interface EvidenceCitation {
/** 不带前缀的引用,如 gmv、week */
ref?: string
/** 目录里的别名,如 m:gmv、i:week、Q1 */
alias?: string
/** 引文:hit 是检索快照里第几条命中,start / end 是引文在那条原文里的码点位置;分析结论的具名的数:key([[a:A1.top_share]]) */
locator?: { metric?: string; field?: string; row?: number; column?: string; table?: string
hit?: number; start?: number; end?: number; key?: string }
eid?: string
/** metric / input / cell / table / column / quote / query */
kind?: string
role?: string
status?: 'resolved' | 'unresolved' | string
/** 解析不了的原因(人话) */
reason?: string
conv?: string
value?: unknown
rendered?: string
/** 引文的原文出处:检索快照、文档、片段 */
source?: EvidenceQuoteSource
/** [[t:]] / [[c:]] 写了一个哪里都找不到的名字:可能是编造的 */
unknown?: boolean
/** 表结构快照不全,找不到的名字只能说核对不了 */
unverified?: boolean
}
/** 引文出自哪:检索快照工件、知识库里的文档和片段 */
export interface EvidenceQuoteSource {
artifact?: string
document?: string
chunk?: string
title?: string
ordinal?: number
}
/** 可点的最小片段 */
export interface EvidenceSegment {
id: string
/** text / number / value / entity / quote / structural */
kind: string
text: string
span?: [number, number]
/** deterministic / none / neutral,后续期还有 probabilistic、candidate */
state?: string
/** 带前缀的原始引用,如 m:gmv|万 */
ref?: string
cite?: EvidenceCitation
strong?: boolean
/** uncited_number / unresolved_ref / unknown_entity(可能是编造的名字)/ unverified_entity(核对不了) */
issue?: string
/** 实体写在反引号里:text 自带两个反引号,按行内代码画 */
code?: boolean
/** 系统自动链接的名字,不是写作者写的标记(不算句子的依据) */
auto?: boolean
/** 反引号里自动发现的可疑名字(没有 ref 时名字在这里) */
name?: string
/** 旧运行按数值猜的候选(state 为 candidate) */
candidates?: EvidenceCandidate[]
}
/**
* 旧运行(没有契约)按数值猜的一个候选:查询快照的一格,或口径卡的一个指标。
* 只做展示,不是证据
*/
export interface EvidenceCandidate {
kind?: 'cell' | 'metric' | string
/** Q1.r0.gmv / m:gmv */
ref?: string
alias?: string
artifact?: string
locator?: EvidenceLocator & { metric?: string }
eid?: string
value?: unknown
rendered?: string
/** 指标名 */
name?: string
caliber?: string
version?: string
node_id?: string
tool?: string
/** 和答案里那个数差多少 */
diff?: number
}
/** 旧运行的猜测:guess_sources 的输出(agentlab.guess/1) */
export interface EvidenceGuess {
schema?: string
mode?: string
/** 「猜测的来源,不能当证据:…」 */
note?: string
markdown?: string
segments?: EvidenceSegment[]
stats?: { numbers?: number; guessed?: number; unguessed?: number; candidates?: number }
/** 猜的是成果里哪个字段 */
field?: string
}
/** 句子、列表项或表格单元格 */
export interface EvidenceUnit {
id: string
/** claim / connective / heading / code。确定性规则分的,不是裁判结论 */
kind?: string
span?: [number, number]
cites?: string[]
see?: EvidenceCitation[]
segments: EvidenceSegment[]
/** 表格单元格的位置,表头 row = -1 */
loc?: { row: number; col: number }
/** 列表项的缩进层级 */
depth?: number
/**
* 结论句裁判的判定(四期):只有送了裁判的候选句有。正式运行(以及交给人工审批的探索运行)里节点当场判的
* 进封存(post_seal false);其余探索运行里候选句先标 unjudged + reason on_demand,点开再判
*/
verdict?: EvidenceVerdict
}
/**
* 模型对一句结论的判定。是模型的判断,不是证据,不能被引用。
* status 为 unjudged 时 reason 写明为什么没判:四个上限(max_claims / max_cost_usd / daily_max_usd /
* timeout_s,rationale 以「已到上限(…)」开头)、error、format、missing、on_demand(探索运行按需)
*/
export interface EvidenceVerdict {
/**
* contradicted:证据和原句冲突;insufficient:证据里没有相关信息(missing 写缺的是什么)。
* unsupported 是拆档之前的取值:旧文档、旧事件里保留原样,界面上照旧写「证据不支持」
*/
status: 'supported' | 'partial' | 'contradicted' | 'insufficient' | 'unsupported' | 'not_a_claim' | 'unjudged' | string
/** 理由,最多 120 字(后端在句子边界截断,加「…」) */
rationale?: string
/** insufficient 时缺的是什么:「SQL」「字段清单」「取数的查询」…,由裁判给出 */
missing?: string
/** 按哪一版裁判规则判的(新判的都带;拆档之前判的没有) */
rule_version?: number
/** 判定自己带着的模型提醒(有的话盖过报告、答复、证据图里的) */
same_model?: boolean
priced?: boolean | null
/** 裁判模型 id;探索运行还没判的是 null */
judge?: string | null
/** 封存之后按需追加的(evidence.judged 事件) */
post_seal?: boolean
/** 裁判看过、摘录里真有的证据编号 */
used?: string[]
reason?: string
cost_usd?: number
}
/** 文档(和 report.checked、节点产出)里的裁判摘要:键的顺序同后端 judge.SUMMARY_KEYS */
export interface EvidenceJudgeSummary {
/** inline:节点里当场判(正式运行,以及交给人工审批的探索运行);on_demand:其余探索运行按需 */
mode?: 'inline' | 'on_demand' | string
model?: string | null
on_unsupported?: 'degrade' | 'withhold' | string
rewrite_once?: boolean
limits?: { max_claims?: number | null; max_cost_usd?: number | null; timeout_s?: number | null
daily_max_usd?: number | null } | null
candidates?: number
/** 预筛放掉、没送裁判的结论句 */
screened?: string[]
counts?: Partial<Record<'supported' | 'partial' | 'contradicted' | 'insufficient' | 'unsupported' | 'not_a_claim'
| 'unjudged', number>>
unjudged?: Record<string, number>
limits_hit?: string[]
complete?: boolean
priced?: boolean | null
cost_usd?: number
calls?: number
reused?: number
duration_ms?: number
gaps?: string[]
notes?: string[]
same_model?: boolean
/** 撰写这份报告实际用的模型 id(裁判模型和它相同时 same_model 为真) */
writer_model?: string | null
/**
* 不支持的句子交回写作者改写一次(rewrite_once)。units:交回的句子在封存文档里的编号(采用了改写稿时,
* 改掉的句子是 null);sentences:交回的原句;changed:改写稿里新写的句子的编号(没采用时为空);
* reason:没采用的原因
*/
rewrite?: { units?: (string | null)[]; sentences?: string[]; applied?: boolean; reason?: string | null
changed?: string[] } | null
}
/**
* POST /runs/{id}/evidence/judge 的答复:evidence.judged 事件的载荷(NOTES-A4 §8)。前端对缺字段宽容:
* verdicts 也认成 [{unit, …}] 的列表
*/
export interface EvidenceJudgeResult {
/** 判的是哪份报告:{node_id, doc_artifact}(事件载荷里是节点 id 字符串) */
report?: string | { node_id?: string; doc_artifact?: string }
doc_artifact?: string
verdicts?: Record<string, EvidenceVerdict> | (EvidenceVerdict & { unit?: string })[]
model?: string | null
cost_usd?: number
calls?: number
limits_hit?: string[]
gaps?: string[]
notes?: string[]
skipped?: Record<string, string>
post_seal?: boolean
/** 触顶、没跑成、点的不是结论句时的一句话;判成了是 null */
message?: string | null
/** 触顶时怎么调,一个上限一句 */
adjust?: string[]
/** 这一次裁判模型和写作模型相同、模型在不在价格表里、写作模型 id */
same_model?: boolean
priced?: boolean | null
writer_model?: string | null
[key: string]: unknown
}
/** 证据图 reports[].judge_meta:这份报告最近一次按需裁判用的模型 */
export interface EvidenceJudgeMeta {
model?: string | null
same_model?: boolean
priced?: boolean | null
writer_model?: string | null
}
/** 这句能不能「请模型判断」:片段接口按运行的实际情况答(和按需裁判接口同一套条件) */
export interface EvidenceOnDemand {
available?: boolean
/**
* formal / legacy / not_a_claim / judged / unsealed / seal_broken;outdated 是封存后追加的判定早于当前的裁判规则
* (available 为真,照常给按钮)
*/
reason?: string | null
message?: string | null
}
export interface EvidenceBlock {
id: string
/** heading / paragraph / list / table / quote / code / hr */
type: string
level?: number
ordered?: boolean
start?: number
lang?: string
units: EvidenceUnit[]
}
export interface EvidenceViolation {
/**
* uncited_number / unresolved_ref / render_mismatch / eid_mismatch / …;分析能力一轮的说法违规:analysis_claim_uncited
* (判断词未挂分析结论)、analysis_claim_mismatch(判断与分析结论不符)、causal_unsupported(因果说法没有依据)
*/
code: string
/** 说法违规:判断词的类(source、anomaly、causal……)、要挂的算子、所挂的分析结论编号 */
claim?: string
op?: string
analyses?: string[]
message?: string
span?: [number, number]
text?: string
/** 指向的片段。结构片段(列表序号、代码围栏标签)里的数字没有可画线的文字,只能在清单里找 */
segment?: string
unit?: string
ref?: string
context?: string
}
export interface EvidenceStats {
/** 说法违规几处(只有记了分析说法核对的文档有) */
analysis_claims?: number
units?: number
segments?: number
claims?: number
connective?: number
numbers?: number
numbers_cited?: number
values?: number
uncited_numbers?: number
unresolved?: number
see?: number
uncited_claims?: number
violations?: number
/** 三期:有出处的实体、引文片段数;可疑实体、核对不了的名字数。老文档没有这几个键,缺省当 0 */
entities?: number
quotes?: number
unknown_entities?: number
unverified_entities?: number
/** 四期:claims 为 judge 时追加,候选句按判定的句数(unsupported 是拆档之前的取值,旧文档里才有) */
supported?: number
partial?: number
contradicted?: number
insufficient?: number
unsupported?: number
not_a_claim?: number
unjudged?: number
}
/** report_doc 工件的内容 */
export interface EvidenceDocData {
schema?: string
run_id?: string | null
node_id?: string
/** 渲染后的全文(真实数字,不含标记) */
markdown?: string
/** 建文档时的完整目录:alias → 条目 */
catalog?: Record<string, Record<string, any>>
blocks: EvidenceBlock[]
stats?: EvidenceStats
violations?: EvidenceViolation[]
/** 四期:写作时结论句策略是 judge 才有 */
judge?: EvidenceJudgeSummary
/** 分析能力一轮:报告节点核分析说法时记下的处置(check / strict)和说法表版本;老文档没有 */
analysis_claims?: { mode?: string; lexicon?: number }
/** 系统写的「分析范围」:这次有分析结论、报告节点没关掉时才有 */
analysis_scope?: AnalysisScope
}
/**
* 成果上的标注:这几个字段逐字等于某个报告撰写节点的文档,可以逐段点开看证据。
* 契约 report_from 指着的那份在最外层;另有报告也被原样放进成果的,列在 others 里
*/
export interface EvidenceFieldRef {
report_node?: string
doc_artifact?: string
fields?: string[]
others?: EvidenceFieldRef[]
}
/** 封存状态 */
export interface EvidenceSeal {
sealed?: boolean
ok?: boolean | null
/** 这件证据在不在封存范围内 */
covered?: boolean
manifest_seq?: number
legacy?: boolean
}
/** GET /runs/{id}/evidence:整次运行的证据图 */
export interface EvidenceGraph {
run_id?: string
schema?: string
/** cited / legacy_contract / legacy_text / none */
mode?: string
/** 没有报告时为什么(none)、旧版出具怎么认的(legacy_*):一句人话 */
note?: string
/**
* legacy_contract:{note, tier, matched, unmatched, field},按旧契约的位置标出的数字;
* legacy_text:guess_sources 的输出(EvidenceGuess),可能按字段分成几份
*/
legacy?: Record<string, any> | Record<string, any>[] | null
/** legacy_text 另一种放法:猜测单独放在这里 */
guess?: EvidenceGuess | EvidenceGuess[] | Record<string, EvidenceGuess> | null
seal?: EvidenceSeal
reports?: { node_id?: string; doc_artifact?: string; doc_sealed?: boolean; fields?: string[]; stats?: EvidenceStats
/** 文档取回时哈希复验:false 对不上(或不是这次运行这个节点写的),null 文件不在了 */
hash_ok?: boolean | null
/** 写这份报告时的结论句策略(report.checked 里记的;老后端、升级前的运行没有) */
claims?: string | null
/**
* 四期:封存之后按需追加的判定(evidence.judged 叠在一起),{unit: 判定}。后端没给时前端只认
* 点开时片段接口、按需裁判接口答的那几句
*/
post_seal_verdicts?: Record<string, EvidenceVerdict> | null
/** 封存的裁判摘要(文档的 judge) */
judge?: EvidenceJudgeSummary | null
/** 最近一次按需裁判用的模型:和写作模型相同没有、在不在价格表里 */
judge_meta?: EvidenceJudgeMeta | null
/**
* 期 4:被反方审过的句子 → 判断(_challenge_verdicts,和片段接口的 unit.challenge 同一个读法,每句的判断
* 一字不差)。正文据此给被审过的句子挂句末标记、点开整句面板。一句都没审、文档读不出来、老服务端都没有这个键
*/
challenge_verdicts?: EvidenceChallengeVerdicts }[]
evidence?: { alias?: string; eid?: string; kind?: string; label?: string; node_id?: string; artifact?: string
sealed?: boolean; cited_by?: string[]; report?: string }[]
/** cites(片段 → 证据)、supports(句子 → 依据)、derived_from(分析结论 → 它的输入 Q、下钻的上一层 A);report 是哪份报告的边 */
edges?: { from: string; to: string; rel: string; report?: string }[]
}
/**
* 查询快照里的位置:单元格 {row, column};agent 的数组字段按行映射时是 {rows: [首, 尾], column | columns}
* (columns 是「字段 → 列」)。行号从 0 数
*/
export interface EvidenceLocator {
row?: number
column?: string
rows?: [number, number] | number[]
columns?: Record<string, string>
}
/** 指标的一个输入:值从哪来 */
export interface EvidenceInput {
path?: string
value?: unknown
node_id?: string
/**
* 一期:input / transform / agent / code / tool / llm …(产出节点的类型);
* 二期能追到查询快照那一格的两种:agent_field(agent 开了 cite_fields 的字段)、tool_cell(cell() 取数)
*/
via?: string
field?: string
/** 只有 code 节点有:source 取数 / compute 计算(缺省) */
role?: string
/**
* 一期 ok / missing;agent_field、tool_cell 是核对状态:verified 与快照一致、mismatch 对不上、
* unresolved 核对不了、missing 模型照实说没查到(from 为 null)
*/
status?: string
/** agent 字段的出处,agent 节点内的编号(Q1.r0.gmv)。和报告目录的全局编号不是一回事,靠 artifact 对应 */
ref?: string | null
/** 查询快照的工件 id:跳到哪一个查询步骤按它认 */
artifact?: string
locator?: EvidenceLocator
eid?: string
/** agent_field mismatch:模型报的值(value 是快照里的) */
model_value?: unknown
/** tool_cell mismatch:快照里的值(value 是算的时候用的) */
snapshot_value?: unknown
/**
* tool_cell:按位置取的格(vars.q.rows[0][0])穿过了「只是把调用工具节点的结果原样按 JSON 解析」的整形节点,这是那个
* 节点的 id;through_label 是它在图里的名字(证据接口补的)。cell() 取的格没有这两个键
*/
through?: string | null
through_label?: string
/** unresolved 的原因(人话) */
reason?: string
/** 输入对到的查询在报告目录里的编号(Q3):接口按工件补的全局编号 */
query?: string | null
/** 输入落在哪一格,全局编号(Q3.r5.amount) */
cell?: string
/** agent 的数组字段:引用的行段到了截断查询结果的末行,后面还有没取回的行(query 步骤上是快照本身截断了) */
truncated?: boolean | null
/**
* 期 4:取自数据源设置了遮罩的列(_withheld):服务端已把值换成「已遮罩」、拿掉 model_value / snapshot_value,这里写明
* 是哪几列。没遮的没有这个键
*/
masked?: string[]
/**
* 期 4:遮没遮核对不了(查询快照、取值链上的产出读不出来):值同样换成「已遮罩」,这是原因(人话,服务端写好的一句)。
* 不进 masked:面板底下「这些列设置了遮罩」不把它们算进去
*/
mask_unverified?: string
/**
* via 为 metric、转抄另一张口径卡的指标(nodes.<卡>.metrics[i].value)时,运行时跨卡往下追的出处:那个指标的 id、那张卡的
* 指标集工件、转抄了几层、那个指标的输入是不是都追到了原始数据(服务端 engine/lineage.metric_copy)。老运行、别的输入没有
*/
upstream?: EvidenceUpstreamRef | null
/**
* 子工作流(docs/design/orchestration-round/00-总方案.md §2):读子工作流出口的字段、接上子图里那一格的出处时,经过的子工作流
* 节点和出口字段(subgraph、field);node_id 落在子工作流里(「子工作流节点 id/…」)时证据接口另给图上的名字
* (node_label、subgraph_label:「子工作流 › 节点」),画布、记录页认不出这个 id 时照它写。via 为 subgraph_input:停在
* 子工作流的入口(子图的运行输入不是父图的原始数据)
*/
subgraph?: string
node_label?: string
subgraph_label?: string
}
/** 转抄的输入跨卡往下追的出处(EvidenceInput.upstream) */
export interface EvidenceUpstreamRef {
metric?: string
artifact?: string | null
depth?: number
traced?: boolean
}
// ---------------------------------------------------------------------------
// 期 4:证据接口里的核查状态(api/evidence.py)。是核查,不是证据:没核查过的指标、句子不带这些键。形状照服务端的
// 真实输出逐字核对过,前端认的时候仍按「认不出就不画」写(run/EvidencePanel.tsx 的 dualCheckOf / challengeOf)
// ---------------------------------------------------------------------------
/** 双盲复算跳过一个指标的原因(_not_checked):关闭了;这个指标没有来自查询的输入;不在复算范围内(或值不是数) */
export type EvidenceDualCheckSkip = 'disabled' | 'no_inputs' | 'not_targeted'
/**
* 指标步骤上的双盲复算状态(_dual_check_of / _dual_status)。没有双盲复算复核过这份指标集时指标步骤没有 dual_check。
* 产出读不出来、退回台账条目时只有 status、node_id 和 choice
*/
export interface EvidenceDualCheck {
/** agreed 与盲算一致;disagreed 不一致;unchecked 未能完成(盲算触顶、出错,或输入没法单独复算);skipped 没复算这个指标 */
status: 'agreed' | 'disagreed' | 'unchecked' | 'skipped'
/** 双盲复算节点 */
node_id: string
/** 只有 disagreed:审核人的选择(a 采用原流程、b 采用盲算、neither 两边都不对)。没选的没有 */
choice?: DualCheckChoice
/** 只有 agreed / disagreed:盲算值,服务端按口径卡的格式渲染好的 */
blind_rendered?: string
/** unchecked:给人看的原因;skipped:EvidenceDualCheckSkip 里的代码(界面认不出的代码不显示) */
reason?: string
}
/**
* 一份报告里被反方审过的句子 → 判断(证据图 reports[].challenge_verdicts,_challenge_verdicts)。只有被审过的句子;判断和
* 点开这一句时片段接口答的 unit.challenge.verdict 一字不差。正文据此挂句末标记,证据面板在片段接口答回来之前先按它写判断
*/
export type EvidenceChallengeVerdicts = Record<string, ChallengeVerdict | (string & {})>
/** 句子上的反方审稿判断(_challenge_of):片段接口答的 unit.challenge。没被反方审过的句子没有这个键 */
export interface EvidenceUnitChallenge {
/** 这一句的判断(反方模型给的,不是系统核对) */
verdict: ChallengeVerdict | (string & {})
/** 反方审稿节点(几个都审了这一句时取最后跑完的那个) */
node_id: string
/** 审稿记录(challenge_artifact)。服务端保存失败时为 null:判断照样有,理由和查询看不了 */
artifact: string | null
/** 实际生效的处置:degrade 被推翻的计入出具缺口;flag 只标注;fail 节点报错、运行停了 */
on_refuted: 'degrade' | 'flag' | 'fail'
/** 报告撰写节点的名字(画布上的标题,取不到时是节点 id) */
report_label: string
/** 这次审稿四种判断各几句:整份报告的(节点产出里的那一份),不是这一句的 */
counts: ChallengeCounts
/** 没能反查的句数(都记为无法判断,不会多于 counts.inconclusive)。节点产出里有才带 */
unreviewed?: number
}
/** 口径卡钉在另一个已发布工作流某个版本里的口径卡上 */
export interface EvidenceCaliberSource {
workflow_id?: string
workflow_version?: number | string
node_id?: string
/** 接口顺手给的工作流名;没给时前端按工作流目录找 */
workflow_name?: string
}
/** 口径卡上游有新版本时声明的处置(和子工作流的升版处置同一套) */
export interface EvidenceCaliberUpgrade {
node_id?: string
workflow_id?: string
pinned?: number
latest?: number
/** recompute / dual / incomparable */
policy?: string
policy_label?: string
}
/**
* 片段证据链的一步:metric(指标)、input(指标的一个输入,也可能挂在 metric.inputs 里)、
* run_input(报告直接引用的运行输入)、query(查询快照:被引用的行加前后各 2 行)。
* input 步骤同时带着 EvidenceInput 的字段(via、status、model_value、ref、locator、artifact)
*/
/** 合并查询的一个输入(证据面板的 merge.inputs) */
export interface EvidenceMergeInput {
/** 合并 SQL 里的表名 */
alias: string
node_id?: string | null
/** 节点在画布上的名字 */
label?: string | null
/** 这个输入在报告目录里的编号(Q1);不在这份报告的目录里时为空 */
query?: string | null
rows?: number | null
/** 数据源名;输入本身是合并结果时是「合并查询」 */
source?: string | null
artifact?: string | null
sealed?: boolean
/** 这个输入查询对照数据目录查出的问题(形状同查询步骤的 checks)。没查出问题、快照读不出来时没有这个键 */
checks?: SqlCheckItem[]
}
/**
* 出具声明(output._issuance、issuance 事件)里的 sql_checks:没通过 SQL 检查的查询,一条一个。报告页、出具横幅据此
* 在显眼处说「因为 SQL 检查没通过而降档」。gap 是 gaps 里对应的那一句(单列之后不在其余缺口里重复)
*/
export interface IssuanceSqlCheck {
/** 「查询「取数」(Q1)」;给不出时没有 */
query?: string
node_id?: string
/** 每条问题那半句 */
problems?: string[]
/** 受影响的引用:「指标「订单金额」」「Q1 第 1 行「gmv」」 */
refs?: string[]
gap?: string
}
/** 查询用到的一张表、查询当时它的数据目录版本 */
export interface EvidenceCatalogVersion {
/** 表结构里的表名 */
table: string
/** 目录里的中文名 */
label?: string
version?: number | null
}
/** 被引用的一格追到了哪个输入的哪一格;追不到时 input 为空,note 说明只有表级来历 */
export interface EvidenceMergeTrace {
/** 合并结果里的 [行, 列],行号从 0 数 */
cell: [number, string]
input: string | null
query: string | null
row: number | null
column: string | null
note?: string
}
export interface EvidenceMerge {
sql?: string | null
inputs: EvidenceMergeInput[]
warnings: { code?: string | null; message: string }[]
traced: EvidenceMergeTrace[]
}
/** 指标的一个输入追不到原始数据:出处追到哪个节点为止。认不出是哪个节点时 node_id、label 为 null */
/** 查询区间里这张表没有数据的那几列(服务端 data/coverage.result_gaps) */
export interface EvidenceNoData {
columns: string[]
table?: string
column?: string
/** 查的区间(按天,两头都含),没有限制的那一头为 null */
window?: (string | null)[]
/** 表里这一列的范围;表里一个值都没有时为 null */
data_range?: (string | null)[] | null
/** live 现查;profile / observed 现查不成,按剖析、取值观察 */
basis?: string
basis_at?: string
message: string
}
export interface EvidenceUntraced {
path: string
node_id: string | null
/** 封存的图里那个节点的名字 */
label: string | null
/** 节点类型(transform、code、llm……) */
type: string | null
}
// ---------------------------------------------------------------------------
// 分析能力一轮(docs/design/analysis-round):分析结论在证据里的样子。形状照服务端 api/evidence.py 的 _analysis_chain、
// _comparability_view,报告文档的 analysis_scope(analysis.contract.ScopeBlock),出具声明的 analysis_claims / comparability /
// untraced(engine/nodes/io.py)。判定语、判定标准、原因、附带说明、代入式、闭合核对的数都是服务端写好的字,界面照原样显示
// ---------------------------------------------------------------------------
/** 分析结论的判定:判定码、点名的段(一段一个列表,多维时几个值;乘积拆解是因子名)、方向 */
export interface AnalysisVerdict {
code?: string
subjects?: (string | number | null)[][]
direction?: 'up' | 'down' | null
}
/** 附带说明:一种说法,服务端渲染好的一句 */
export interface AnalysisCaveat { code?: string; text?: string }
/** 可比性检查现查的一条:一列的实际范围(table、column、min、max),或按运行当天现算的界(expr、value) */
export interface AnalysisProbe {
source?: string
table?: string
column?: string
min?: string | null
max?: string | null
basis?: 'live' | 'profile' | 'observed' | string
at?: string
expr?: string
value?: string | null
}
/**
* 出处链里分析结论的六步(step 依次是 analysis、analysis_method、analysis_calc、analysis_input(每份输入一步)、
* analysis_recompute、analysis_seal)共用的形状:每一步只带自己那几项
*/
export interface EvidenceAnalysisStep {
step: string
alias?: string
// ---- analysis:判定 ----
op?: string
mode?: string | null
/** 「变化拆解(可加)」:服务端 contract.method_label */
method_label?: string
/** card 分析卡、agent Agent 现场做的、metric 口径卡执行时自动做的可比性检查 */
origin?: 'card' | 'agent' | 'metric' | string
item?: string
call_id?: string
node_id?: string | null
label?: string | null
artifact?: string | null
eid?: string
eid_ok?: boolean
/** 这一处引用的是结论的哪一项:判定语、具名的数(key)、结果表的一格(row、column;column_label 是结果表的列名,给人看的) */
cited?: {
what?: 'phrase' | 'value' | 'cell' | string
key?: string
row?: number
column?: string
column_label?: string | null
raw?: unknown
conv?: string
}
rendered?: string
render_ok?: boolean
hash_ok?: boolean | null
status?: string
reason_code?: string | null
reason?: string | null
phrase?: string
criterion?: string
verdict?: AnalysisVerdict | null
caveats?: AnalysisCaveat[]
hypothesis?: string
/** 「在验证什么」那一行的说法:谁写的照实说,服务端按台账条目给(Agent 写的假设 / 分析卡上写的『在验证什么』) */
hypothesis_label?: string
hypothesis_note?: string
note?: string
// ---- analysis_method:方法、参数、实际用的阈值、现查结果 ----
method?: string
params?: Record<string, unknown>
settings?: { key: string; label?: string; value: unknown }[]
probes?: AnalysisProbe[]
// ---- analysis_calc:具名的数、代入式、结果表 ----
values?: { key: string; raw?: unknown; rendered?: string; cited?: boolean }[]
formulas?: { label?: string; text?: string }[]
table?: {
columns: string[]
labels?: Record<string, string>
total_rows?: number
row_index?: number[]
rows: unknown[][]
masked?: string[]
highlight?: { cells?: [number, string][] }
window_truncated?: boolean
}
// ---- analysis_input:一份输入 ----
/** 这一步是哪份结论的输入 */
of?: string
ref?: string
role?: string
role_label?: string
columns?: string[]
rows?: 'all' | number[]
row?: number
column?: string
value?: string | null
/** 那一格的值按服务端的规则渲染出来的字(render_cell:千分位、NULL 写「—」),和闭合核对里的写法一致 */
value_rendered?: string
/** 输入落到报告目录的哪个 Q */
query?: string | null
sealed?: boolean
/** headline 那一格和结论里总数的闭合核对:mode 可加对各段之和,比率对各段分子之和 ÷ 分母之和 */
closure?: { mode?: string; total_key?: string; total?: string; headline?: string; closed?: boolean }
// ---- analysis_recompute:复算(服务端算好的那一句) ----
text?: string
// ---- analysis_seal:封存 ----
ledger?: boolean
/** metric_set:口径卡指标上的可比性结论,记在指标集里、随卡封存 */
via?: string
}
/** 指标步骤上的两期可比性(api/evidence._comparability_view):判定、判定语、原因,连同那份可比性结论的出处链 */
export interface EvidenceComparability {
verdict?: 'comparable' | 'conditional' | 'incomparable' | string
phrase?: string
reasons?: string[]
artifact?: string | null
hash_ok?: boolean | null
sealed?: boolean
method?: string
criterion?: string
caveats?: AnalysisCaveat[]
probes?: AnalysisProbe[]
/** 判定 → 方法 → 计算 → 输入 → 复算 → 封存(两份输入快照的查询步骤指标链里已经有了,不再给) */
steps?: EvidenceAnalysisStep[]
}
/** 报告文档里系统写的「分析范围」的一条(contract.ScopeItem) */
export interface AnalysisScopeItem {
alias?: string
op?: string
mode?: string | null
/** 拆解的维度(给人看的叫法) */
dims?: string[]
verdict?: string | null
phrase?: string
/** 下钻:上一层的编号 */
parent?: string | null
reason?: string | null
}
/** 报告文档的 analysis_scope(contract.ScopeBlock):不进正文、不过裸数字和说法核对,随文档封存 */
export interface AnalysisScope {
phenomena?: AnalysisScopeItem[]
decomposed?: AnalysisScopeItem[]
excluded?: AnalysisScopeItem[]
refused?: AnalysisScopeItem[]
/** 候选维度里没拆过的叫法;null 是候选维度核对不了(看 note) */
not_decomposed?: string[] | null
note?: string | null
/** 这次做了几次差异检验 */
tests?: number
/** 检验校正过没有:只做了一次为 true,不止一次为 false,没做为 null */
corrected?: boolean | null
}
/** 出具声明的 analysis_claims:处置、几处、记没记缺口、按种类的计数、前几处(同报告文档的说法违规) */
export interface IssuanceAnalysisClaims {
mode?: 'check' | 'strict' | string
count?: number
counts?: Record<string, number>
counted?: boolean
violations?: EvidenceViolation[]
}
/** 出具声明的 comparability:两期有条件可比、不可比的指标,一条一个 */
export interface IssuanceComparability {
metric?: string
name?: string
verdict?: string
phrase?: string
reasons?: string[]
artifact?: string
}
/** 出具声明的 untraced:报告引用的指标里输入追不到原始数据的,counted 是记了缺口(受管级别的正式运行) */
export interface IssuanceUntraced {
metrics?: { metric?: string; name?: string; stops?: { path?: string; node_id?: string | null; label?: string | null; type?: string | null }[] }[]
counted?: boolean
}
/** Agent 分析工具 tool.end 的 analysis(服务端 tools/analysis.event_view):preview 只是结论的前一截,判定语从这里读 */
export interface AnalysisEventView {
/** 这份结论在本节点的编号(「【分析 An】」那个);不记台账的执行没有 */
alias?: string | null
op?: string
mode?: string | null
status?: 'ok' | 'refused' | string
verdict?: string | null
direction?: string | null
phrase?: string
reason_code?: string | null
}
/** 分析卡的一项(节点配置 items[i],engine/schema.analysis_issues 校验) */
export interface AnalysisItem {
id?: string
op?: string
mode?: string | null
/** 要分析的上游查询节点(调用数据库查询工具的「调用工具」节点,或「合并查询」);两份查询时写在 params 的 base_query、target_query */
from?: string
params?: Record<string, any>
hypothesis?: string
}
export interface EvidenceStep extends Omit<EvidenceInput, 'status'> {
step: string
/**
* upstream:转抄另一张口径卡的指标时跨卡接上的那一步(api/evidence._upstream_steps)。of 是接在哪个输入后面(输入的路径),
* label 是那张卡在图里的名字,traced 是那个指标的输入都追到了原始数据,depth 是转抄了几层
*/
of?: string
label?: string | null
traced?: boolean
depth?: number
metric?: string
decimals?: number | null
format?: string
conv?: string | null
eid?: string
/** eid 能由工件和定位重算出来 */
eid_ok?: boolean
/** 工件取回时哈希复验通过、指标在卡里 */
hash_ok?: boolean
/** 卡里的值按同样的格式渲染出来就是报告上的字 */
render_ok?: boolean
/** 这件证据在封存范围内 */
sealed?: boolean
alias?: string
name?: string
value?: unknown
unit?: string
rendered?: string
caliber?: string
version?: string
node_id?: string
artifact?: string
expression?: string
substituted?: string
recompute_ok?: boolean | null
status?: string
/** metric:表达式把截断的查询结果当成整组用了(计数、求和……),值只算到了取回的那部分 */
incomplete?: boolean
/** metric:不完整的原因(人话,「基于被截断的查询结果计算(只取回了前 1000 行),结果不完整」) */
incomplete_reason?: string
/** metric:所依据的查询没通过 SQL 检查(有错误级的问题),值照算,出具已按缺口降档 */
sql_check_failed?: boolean
/** metric:没通过的原因(人话,「所依据的查询未通过 SQL 检查(…),结果不可靠」) */
sql_check_reason?: string
/** metric:没有值,不是缺输入,是输入都齐、计算式自己算不出来(分母为 0)。状态仍是 missing_input */
undefined?: boolean
/**
* metric:输入取自「查询区间里这张表没有数据」的列,值照算,出具按缺口降档(true);
* query:这次查询里这样的几列(服务端 data/coverage.py,查询时记进快照)。没有问题时没有这个键
*/
no_data?: boolean | EvidenceNoData[]
/** metric:没有数据的原因(人话,「所依据的查询区间里这张表没有数据:…」) */
no_data_reason?: string
/** metric(期 4):这个指标的双盲复算状态。没有双盲复算复核过这份指标集时没有这个键 */
dual_check?: EvidenceDualCheck
/**
* metric:输入里追不到原始数据(查询快照里的格、运行输入)的,空数组是都追到了;卡上没有输入、老服务端没有这个键。
* 和「有出处」(口径卡确定性复算一致)是两件事,面板分开说(服务端 engine/lineage.py)
*/
untraced?: EvidenceUntraced[]
/** query:这次查询对照数据目录查出的问题。没查出问题时没有这个键 */
checks?: SqlCheckItem[]
/** query:查询当时冻结的表结构快照(工件 id)。合并查询、老快照没有 */
schema_artifact?: string
/** query:这条查询用到的表在表结构快照里冻结的数据目录版本(SQL 检查对照的那一版)。没有目录时没有这个键 */
catalog?: EvidenceCatalogVersion[]
inputs?: EvidenceInput[]
/** 口径卡钉在哪个工作流的哪一版(metric;方案第 5 节的写法,接口实际给在 source 上) */
caliber_from?: EvidenceCaliberSource | null
/** 上游有新版本时的处置(metric);没有升版是 null */
caliber_upgrade?: EvidenceCaliberUpgrade | null
// ---- query:查询快照的一个窗口 ----
tool?: string
/** query:数据源名;metric:口径卡钉住的来源 {workflow_id, workflow_version, node_id, workflow_name} */
source?: string | EvidenceCaliberSource | null
sql?: string
columns?: string[]
/** 只有被引用的行加前后各 2 行;遮罩列的值已经换成「已遮罩」 */
rows?: unknown[][]
/** rows[0] 在快照里是第几行(从 0 数) */
row_offset?: number
/** rows 里每一行在快照里是第几行:被引用的行隔得远时窗口不连续,按它认 */
row_index?: number[]
/** 快照一共几行 */
total_rows?: number | null
/** 查询撞了行数上限,库里还有更多 */
truncated?: boolean | null
/** 被引用的行太多,窗口只给了前面一截 */
window_truncated?: boolean
/** 被引用的行(快照里的行号)和列;cells 是精确到格的 [行, 列],有它就只高亮这几格 */
highlight?: { rows?: number[]; cols?: string[]; cells?: [number, string][] }
/** 这一步里被遮罩的列 */
masked?: string[]
/** 这一步为什么没有行(快照不在封存范围里、哈希对不上、取不回来) */
note?: string
/** 数据源改名或删掉了、按查询当时记下的遮罩处理时的那句说明。行照样有 */
mask_note?: string
/**
* 期 4:可以请求推断来源(GET …/segments/{sid}/provenance)。只有带文档标记的报告、单元格片段的查询步骤、
* 查询快照来自上传的表格时才有,且恒为 true;其余一个键都不加。前端只在 seg.cite?.kind === 'cell' 且它为 true 时请求
*/
provenance?: boolean
/** 合并查询的结果(快照的 source 是「合并查询」):合并了哪几个输入、合并 SQL、执行时的警告、被引用的格追到哪 */
merge?: EvidenceMerge
/** 这一步是哪次合并查询的输入(合并结果在目录里的编号):排在那个合并步骤后面,高亮的是追到的格 */
merged_into?: string
/** query:这一步是哪份分析结论的输入(结论在目录里的编号 A…):接在那份结论的输入后面 */
input_of?: string
/** metric:跨两份同形查询算的环比、同比,口径卡执行时自动做的可比性检查。没做过检查的指标没有这个键 */
comparability?: EvidenceComparability
// ---- entity:表或字段(三期)----
/** entity:table / column */
kind?: string
table?: string
column?: string
/** 字段类型(表结构快照里记的) */
type?: string
/** 同名字段在好几张表里时,是哪几张 */
tables?: string[]
/** 这个名字的来历:表结构快照、查询 SQL、查询结果列 */
sources?: EvidenceEntitySource[]
/** 出现在哪几次查询里(报告目录的全局编号) */
queries?: string[]
/** 表结构快照什么时候同步的 */
synced_at?: string
/** 可疑实体:最接近的已知名字 */
closest?: (string | { alias?: string; name?: string; table?: string; kind?: string })[]
/** 可疑实体:查过几份表结构快照、几次查询 */
checked?: { schemas?: number; queries?: number }
/** 表结构快照只存了一部分(库里表太多) */
snapshot_truncated?: boolean
/** 好几张表都有的同名字段:各表的类型 */
types?: Record<string, string>
/** 表:字段清单和类型(封存范围内的表结构快照,最多 60 个,遮罩的列不列入);多出来的个数 */
fields?: { name?: string; type?: string | null }[]
fields_more?: number
/** 有列按遮罩没列进字段清单 */
fields_masked?: boolean
/** 表:几个字段、是不是视图;表或字段的注释 */
is_view?: boolean
comment?: string
nullable?: boolean
primary_key?: boolean
// ---- quote:逐字引文(三期)----
/** quote:引文所在那条命中的原文(也可能放在 content 里) */
text?: string
/** quote:引文所在那条命中的全文;快照不在封存范围里、取不回来时是 null */
content?: string | null
quote?: string
match?: { hit?: number; start?: number; end?: number }
/** quote:按记下的位置把原文切出来,和引文逐字比对的结果 */
match_ok?: boolean | null
/** quote:哪个知识库、检索的是什么 */
collection?: string
query?: string | null
}
/** 实体的一处来历 */
export interface EvidenceEntitySource {
/** schema(表结构快照)/ sql(查询 SQL 用到的表)/ result(查询结果列) */
kind?: string
artifact?: string
alias?: string
source?: string
truncated?: boolean
}
/** GET /runs/{id}/evidence/audit 的一行(形状照 api/evidence.py;前端经 lib/evidence 的 auditFromApi 读) */
export interface EvidenceAuditRow {
/** cited / none / suspicious / candidate */
group?: string
report?: string | null
field?: string | null
segment?: string | null
unit?: string | null
/** number / value / entity / quote / claim / violation */
kind?: string
text?: string
state?: string
issue?: string | null
ref?: string | null
alias?: string | null
evidence_kind?: string | null
evidence?: string | null
eid?: string | null
artifact?: string | null
node_id?: string | null
sealed?: boolean | null
span?: [number, number] | null
sentence?: string
note?: string | null
candidates?: EvidenceCandidate[]
}
/** GET /runs/{id}/evidence/audit:记录页的审计表 */
export interface EvidenceAudit {
run_id?: string
schema?: string
mode?: string
seal?: EvidenceSeal & { events?: number; message?: string }
reports?: { node_id?: string; doc_artifact?: string; hash_ok?: boolean | null; doc_sealed?: boolean
fields?: string[]; claims?: string | null; stats?: EvidenceStats }[]
groups?: { key?: string; label?: string; count?: number; rows?: EvidenceAuditRow[] }[]
counts?: Record<string, number>
total?: number
note?: string
legacy_note?: string
}
/** GET /runs/{id}/evidence/segments/{sid}:点开一个片段 */
export interface EvidenceSegmentDetail {
report?: { node_id?: string; doc_artifact?: string }
segment?: Partial<EvidenceSegment> & { unit?: string }
unit?: { id?: string; kind?: string; text?: string; span?: [number, number]; cites?: string[]
/** 这一句的判定:封存的文档里的,或者按最新的 evidence.judged 叠上的(post_seal) */
verdict?: EvidenceVerdict
/** 这一句能不能请模型判断、不能的话为什么 */
on_demand?: EvidenceOnDemand
/** 期 4:反方审稿对这一句的判断。没审到这一句的没有这个键 */
challenge?: EvidenceUnitChallenge }
block?: { id?: string; type?: string }
chain?: EvidenceStep[]
/** 片段为什么是现在这个状态,一句人话 */
note?: string
/** 落在这个片段(或这一句、没有片段的)上的违规 */
violations?: EvidenceViolation[]
seal?: EvidenceSeal
/** 查询步骤里被遮罩的列(数据源 options.mask_columns)。遮罩只减少暴露,不是安全边界 */
redacted?: { columns?: string[] }
/** 可疑实体:最接近的已知名字(最多 3 个),给「是不是想写…」 */
closest?: (string | { alias?: string; name?: string; table?: string; kind?: string })[]
/**
* 口径账本(next-round 02 做法 3):这个数的口径标签和比对的来历。封存后比对,没比对过的运行、不是数的片段没有这个键
* (老运行的返回一字不差)。形状见 typesLedger.ts
*/
ledger?: import('./typesLedger').SegmentLedger
/** 指标接口(/evidence/metrics/{node}/{metric})给的展示值:遮罩的写「已遮罩」 */
rendered?: string
}
// -------------------------------------------------------------------------
// 期 4:推断的来源(GET /runs/{id}/evidence/segments/{sid}/provenance?report=…)
// 契约照 backend/app/data/provenance_types.py(asdict 之后的 JSON,P4-SPEC 2.8.1)。这是期 4 新加的接口,
// 答复里每个键都在(值可以是 null),所以这里不写成可选;老服务端没有这个接口时回 404 且没有 code,三节都不画。
// -------------------------------------------------------------------------
/** inferred 给出格子;table_only 只给表级来历(version 非空);none 不画,或只画一句原因 / 红色提示 */
export type ProvenanceStatus = 'inferred' | 'table_only' | 'none'
/** 不下钻的原因(provenance_types.ReasonCode)。界面不按它拼字,原样显示服务端给的 reason.text */
export type ProvenanceReasonCode =
| 'legacy_doc' | 'not_cell' | 'not_sealed' | 'not_upload' | 'simple_upload' | 'manifest_unreadable'
| 'chain_mismatch' | 'expression' | 'alias' | 'multi_table' | 'unparsed' | 'no_pk' | 'pk_missing' | 'masked'
| 'null_value' | 'null_pk' | 'snapshot_gone' | 'db_tampered' | 'recheck_missing' | 'recheck_multiple'
| 'recheck_mismatch' | 'no_lineage' | 'merge_no_lineage'
/** 标红的提示:出现时 reason.code 与它相同,界面只画提示 */
export type ProvenanceAlertCode = 'db_tampered' | 'chain_mismatch' | 'manifest_unreadable'
/** 附带的格:日期表头格、行标签、分段标题、列表头、合计标签 */
export type ProvenanceFromRole = 'axis_header' | 'row_label' | 'section_title' | 'col_header' | 'total_label'
/** 本期结论(照搬清单里 CheckResult.status) */
export type ProvenancePartStatus = 'passed' | 'mismatch' | 'unverifiable' | 'info'
/** 这一行的关系核对结果(只给关系核对) */
export type ProvenanceRowStatus = 'passed' | 'mismatch' | 'unverifiable'
/** 这一格的合计核对结果(只给 K、G、T) */
export type ProvenanceCellStatus = 'ok' | 'unverifiable' | 'unknown' | 'not_formula'
export interface ProvenanceReportRef {
node_id: string
doc_artifact: string
}
/** 片段本身是单元格引用就给(与 status 无关),不是时为 null */
export interface ProvenanceCellRef {
/** 目录里的查询编号(Q3) */
alias: string
row: number
column: string
/** 查询快照的工件 id */
artifact: string | null
}
export interface ProvenanceReason {
code: ProvenanceReasonCode
/** 细分,给测试和日志用,界面不显示 */
detail: string
/** 界面原样显示 */
text: string
}
export interface ProvenanceAlert {
code: ProvenanceAlertCode
text: string
}
export interface ProvenanceTableRef {
name: string
kind: 'data' | 'reported_total'
}
export interface ProvenancePeriod {
start: string
end: string
source: 'cells' | 'human'
/** 解析出统计期的格子,带工作表名(「客流汇总!B2」) */
cells: string[]
/** 人工录入时的署名(未认证) */
signed_by: string | null
}
/** 一条接受理由(导入清单里的,内容寻址,是当时的事实) */
export interface ProvenanceAcceptance {
check_id: string
title: string | null
kind: 'override' | 'waiver'
reason: string
signed_by: string | null
at: string | null
}
/** 导入记录上的当前状态(清除原件、作废接受) */
export interface ProvenanceStateNote {
at: string | null
signed_by: string | null
reason: string | null
}
/** 数据版本里的一期(按统计期升序)。raw_state、purged、revoked 是当前状态,界面标「当前状态」 */
export interface ProvenancePart {
import_id: string
seq: number
/** 导入清单的工件 id:「查看导入清单」按它经 /api/artifacts/{id} 取 */
manifest: string
/** 没有统计期时 null(「统计期未记录」) */
period: ProvenancePeriod | null
file_name: string | null
raw_sha256: string | null
/** 区域(「客流汇总!B4:AG30」,多张表用「、」连);取不到为 null */
region: string | null
/** 排除的行数之和;期 3 之前的回执没有记录时为 null */
excluded_rows: number | null
recipe_sha256: string | null
/** 配方第几版(当前状态);取不到为 null */
recipe_seq: number | null
/** 导入时间(清单的 created_at) */
committed_at: string | null
/** 署名(未认证) */
signed_by: string | null
acceptances: ProvenanceAcceptance[]
raw_state: 'kept' | 'purged' | 'absent' | null
purged: ProvenanceStateNote | null
revoked: ProvenanceStateNote | null
/** 这一格所在的一期:inferred 时恰好一项为 true */
has_row: boolean
}
/** 数据版本(表级来历),来自封存链上的清单,不加「推断」标注 */
export interface ProvenanceVersion {
source: string
snapshot_id: string
mode: 'replace' | 'accumulate'
union: boolean
tables: ProvenanceTableRef[]
/** false:数据源设有遮罩,不出「查看导入清单」 */
manifest_view: boolean
parts: ProvenancePart[]
}
export interface ProvenanceFromCell {
role: ProvenanceFromRole
/** 这一格给出的是哪一列的值;宽表指标列的行标签(指标名取自哪一格)为 null */
column: string | null
sheet: string
/** 不带工作表名的坐标(「G4」) */
cell: string
/** 格子原文;拿不到为 null */
text: string | null
/** 只给分段标题:配方中的定位文字 */
locate_title: string | null
}
export interface ProvenanceYear {
source: 'period' | 'human'
cells: string[]
signed_by: string | null
/** 这一块的日期表头有的是日期格、有的是文本,无法确定这一格属于哪一种 */
mixed: boolean
}
/**
* 这一行标签列(交叉表的维度、合计表的合计项)的原文与数据库里的规范写法不同时给出。被引用的是值列也给:
* 合计表那一格是「18-22 时合计」→「18-22时合计」,全角标签那一期的值格是「8-9」→「8-9」
*/
export interface ProvenanceCanonical {
raw: string
canonical: string
}
export interface ProvenanceRecheck {
/** 占位符一律是 ?,按主键的顺序与 params 一一对应 */
sql: string
params: unknown[]
ok: boolean
}
/** 推断出的格子(只在 inferred 时有) */
export interface ProvenanceCellSource {
table: string
column: string
column_role: 'axis' | 'dim' | 'derive' | 'const' | 'measure' | 'value' | 'text' | (string & {})
kind: 'data' | 'reported_total'
pk: Record<string, unknown>
/** 快照库的 rowid(累积时是合并后的行号) */
rowid: number
part_seq: number
part_rowid: number
sheet: string
/** 不带工作表名的坐标(「G5」) */
cell: string
header: string | null
unit: string | null
from: ProvenanceFromCell[]
year: ProvenanceYear | null
canonical: ProvenanceCanonical | null
raw_purged: boolean
/** 列表这一块按合并单元格的左上格填充 */
merged_fill: boolean
recheck: ProvenanceRecheck
}
/** 相关核对的一条(只在 inferred 时有) */
export interface ProvenanceCheck {
id: string
kind: string
title: string
part_status: ProvenancePartStatus
row_status: ProvenanceRowStatus | null
cell_status: ProvenanceCellStatus | null
acceptance: ProvenanceAcceptance | null
/** K 无法核对时的原因摘要(可能带数字,系统呈现) */
detail: string | null
}
/** GET /runs/{id}/evidence/segments/{sid}/provenance:推断的来源 */
export interface EvidenceProvenance {
schema: 'agentlab.provenance/1' | (string & {})
report: ProvenanceReportRef
segment: string
cell: ProvenanceCellRef | null
status: ProvenanceStatus
/** status 不是 inferred 时必有 */
reason: ProvenanceReason | null
/** 标红;有它时不再画 reason.text */
alert: ProvenanceAlert | null
/** 运行已封存且封存核对通过;false 时数据版本一节加一句未封存 */
sealed: boolean
/** status 为 none 时为 null */
version: ProvenanceVersion | null
cell_source: ProvenanceCellSource | null
checks: ProvenanceCheck[]
/** 被引用的格在合并查询的结果里时,经过的每一次合并;别的查询为空 */
merge: ProvenanceMergeHop[]
}
/** 经过的一次合并查询:合并结果里被引用的格追到了哪个输入的哪一格;追不到的那一跳后四项为 null */
export interface ProvenanceMergeHop {
/** 合并结果在目录里的编号(Q3) */
alias: string
node_id: string | null
/** 追到的输入别名(合并 SQL 里的表名) */
input: string | null
/** 那个输入在目录里的编号(Q1) */
query: string | null
row: number | null
column: string | null
}
// ===========================================================================
// 业务数据目录(/api/datasources/{id}/catalog,后端 app/data/catalog.py)。
// 每张表一份业务说明:中文名、粒度、业务主键、列的含义和度量类型、表与表的关联关系……每一项都注明来源和状态。
// 目录只记数据事实,不放计算公式(公式在口径卡里)。
// ===========================================================================
/** 项的来源:数据库注释、外键约束、命名推断、数据剖析、模型起草、人工填写 */
export type CatalogSource = 'comment' | 'fk' | 'name' | 'profile' | 'llm' | 'human'
/** 项的状态:推断、已验证(外键约束、数据剖析)、已确认(人工)、已驳回(人工) */
export type CatalogStatus = 'proposed' | 'verified' | 'confirmed' | 'rejected'
/** 表类型:明细(事实)、维度、快照、日志、配置 */
export type CatalogTableKind = 'fact' | 'dimension' | 'snapshot' | 'log' | 'config'
/** 列的度量类型:可累加(流量)、存量、比率、标识、状态、属性 */
export type CatalogMeasure = 'flow' | 'stock' | 'ratio' | 'identifier' | 'status' | 'attribute'
/** 关系的基数(从本表看过去) */
export type CatalogCardinality = 'many_to_one' | 'one_to_one' | 'one_to_many'
/** 目录里的一项:值连同来源、状态 */
export interface CatalogItem<T = unknown> {
value: T
source: CatalogSource
status: CatalogStatus
/** 备注:机器写的说明(模型起草的依据、数据剖析的测量、双盲复算的出处……) */
note?: string
/**
* 确认时的依据:保存修改提案(表助手、助手流里「建议更新数据目录」)时使用者确认的依据,和备注分开存。值改了换成那一次的,
* 撤销审阅回到推断时去掉;只由服务端写,整份提交时带上也不作数
*/
reason?: string
updated_at?: string
}
/** 业务日期:按哪一列算,规则和时区写成文字 */
export interface CatalogBusinessDate {
column: string
rule?: string
timezone?: string
}
/** 一列的目录 */
export interface CatalogColumnNotes {
label?: CatalogItem<string>
meaning?: CatalogItem<string>
unit?: CatalogItem<string>
measure?: CatalogItem<CatalogMeasure>
/** 码值 → 含义 */
codes?: CatalogItem<Record<string, string>>
}
/** 一条关联关系。编号由两端的表和列算出来,改了指向就是另一条 */
export interface CatalogRelation {
id: string
columns: string[]
to_table: string
to_columns: string[]
cardinality: CatalogCardinality | null
/** 0 到 1:本表这几列的值在被指向表里找得到的比例(数据剖析给出) */
coverage: number | null
source: CatalogSource
status: CatalogStatus
note?: string
/** 确认时的依据(同 CatalogItem.reason) */
reason?: string
updated_at?: string
/**
* 基数是数据剖析用数据核实的(子表一侧数过是否唯一),只由剖析写。外键、命名推断的基数没有这个键。
* 一对多关联后重复计算的检查只在它为真、或者关系人工确认过时报「错误」,否则最多是「提醒」
*/
cardinality_checked?: boolean
}
/** 一张表的目录(notes)。字段固定,服务端不认识的一律拒收 */
export interface CatalogNotes {
label?: CatalogItem<string>
description?: CatalogItem<string>
grain?: CatalogItem<string>
keys?: CatalogItem<string[]>
kind?: CatalogItem<CatalogTableKind>
business_date?: CatalogItem<CatalogBusinessDate>
/** SQL 条件片段 */
valid_filter?: CatalogItem<string>
dedup?: CatalogItem<string>
columns?: Record<string, CatalogColumnNotes>
relations?: CatalogRelation[]
}
/** 各状态的项数(表级项、列级项、关系都算),四种状态都有键 */
export type CatalogCounts = Record<CatalogStatus, number>
/** 表清单的一行。表结构里的每张表各一行(没有目录的也列);目录还在、表已不在的 in_schema 为 false,排在最后 */
export interface CatalogTableRow {
table_name: string
qualified: string
is_view: boolean
in_schema: boolean
/** 中文名(被驳回的不算) */
label: string | null
label_status: CatalogStatus | null
kind: CatalogTableKind | null
counts: CatalogCounts
/** 没被驳回的关联关系条数 */
relations: number
/** 被运行查询过的次数(同一次运行里结果相同的重复查询只算一次) */
usage: number
/** 这张表还没有目录时为 0 */
version: number
updated_at: string | null
updated_by: string | null
/** 这张表最近一次起草任务里的那一项(任务还在进行就是进行中的状态);没起草过为 null。老服务端没有这个键 */
last_draft?: CatalogLastDraft | null
/** 这张表最近一次剖析(任务还在进行就是进行中的状态);没剖析过为 null。老服务端没有这个键 */
last_profile?: CatalogLastProfile | null
/** 按「需要剖析」的口径这张表要不要剖析(表结构里、不是视图、有可剖析的项,且满足 profile_due_reason 之一) */
profile_due?: boolean
/**
* 为什么需要剖析:never 从没剖析过;unfinished 最近一次剖析没做完(部分完成、失败、已停止);new_relations 目录里有待核实的
* 关系不在最近一次剖析的检查项里;new_checks 最近一次剖析之后多出了码值、日期检查(某一列被记成了状态类、日期列变了)。
* 不需要时为 null
*/
profile_due_reason?: CatalogProfileDueReason | null
/** 按当前目录和表结构这张表要做几项检查;0 表示没有可剖析的项(视图为 0) */
profile_checks?: number
}
/** GET /datasources/{id}/catalog */
export interface CatalogList {
/** 使用次数多的在前 */
tables: CatalogTableRow[]
/** 导入表格的源:表和列的说明由系统按核对结果生成,只读 */
system_notes: boolean
/** 没有表结构时的原因(「尚未探查结构」「结构探查失败:…」);有表结构时为 null */
schema_note: string | null
/** 探查结构截断了(每个数据源最多取 200 张表):没探查到的表不在清单里,助手也看不到。老后端没有这一项 */
schema_truncated?: boolean
/** 数据库里一共几张表(含视图);没截断时等于探查到的张数 */
schema_total?: number
/** 这个数据源的起草要求(使用者补充给模型的背景和要求);没填为 null。老服务端没有这一项 */
guidance?: string | null
/** 排队或进行中的起草任务(页面据此显示进度);没有为 null。老服务端没有这一项 */
active_job?: CatalogActiveDraftJob | null
/** 排队或进行中的剖析任务(页面据此显示进度);没有为 null。老服务端没有这一项 */
active_profile_job?: CatalogActiveProfileJob | null
}
/** 表结构里的一列 */
export interface CatalogStructureColumn {
name: string
type: string
pk: boolean
not_null: boolean
/** 数据库注释;导入表格的源是系统按核对结果生成的说明 */
comment: string | null
}
export interface CatalogForeignKey {
columns: string[]
to_table: string
to_columns: string[]
}
export interface CatalogStructure {
qualified: string | null
is_view: boolean
/** 表注释;导入表格的源是系统按核对结果生成的说明 */
comment: string | null
columns: CatalogStructureColumn[]
primary_key: string[]
/** 升级前探查的缓存里没有:null 表示不知道,空列表表示读过、没有 */
foreign_keys: CatalogForeignKey[] | null
unique: string[][] | null
}
/** GET /datasources/{id}/catalog/{table};PUT、审阅也返回这个形状 */
export interface CatalogDetail {
table_name: string
in_schema: boolean
notes: CatalogNotes
version: number
updated_at: string | null
updated_by: string | null
/** 表结构里已经没有这张表时为 null */
structure: CatalogStructure | null
system_notes: boolean
usage: number
/** 这张表最近一次剖析的完整结果(含检查清单、发现、发过的查询;任务还在进行就是进行中的状态);没剖析过为 null */
last_profile?: CatalogProfileTaskDetail | null
}
/** 单项审阅:确认、驳回、撤销审阅(回到来源的初始状态;人工填写的项直接删掉) */
export type CatalogReviewAction = 'confirm' | 'reject' | 'reset'
// ---------------------------------------------------------------------------
// 起草任务(/api/datasources/{id}/catalog/draft-jobs):起草在服务端后台进行,逐表起草、每张表一结束就并入目录。
// 关掉对话框不停任务;服务重启时还没结束的任务记 interrupted,不自动续跑,由人点「重试」把没写全的表重新起草一遍
// (重试不跳过已写进目录的段,整张表重新分段起草;已写进目录的内容不会丢)。
// ---------------------------------------------------------------------------
/**
* 起草任务的状态:排队、进行中、完成、部分完成(有失败、部分完成或停止的表)、失败(任务整体出错)、已停止(用户停止)、
* 服务重启中断
*/
export type CatalogDraftJobStatus = 'queued' | 'running' | 'succeeded' | 'partial' | 'failed' | 'cancelled' | 'interrupted'
/**
* 任务里一张表的状态。ok:写入成功(或没有东西要写),用模型时各段都写出来了;partial:用模型,有的段没写出来;
* failed:整张表没起草(error),或用模型时一段都没写出来(model_error),按注释、外键、命名起草的部分照常写入;
* skipped:表结构里没有这张表;cancelled:停止或服务重启时还没开始、没写完
*/
export type CatalogDraftTaskStatus = 'pending' | 'running' | 'ok' | 'partial' | 'failed' | 'skipped' | 'cancelled'
/** 起草任务里的一张表(按起草顺序) */
export interface CatalogDraftTask {
table_name: string
status: CatalogDraftTaskStatus
/** 列数 */
columns: number
/** 模型分几段起草(超过 40 列的宽表拆成表级一段加列分段);不用模型时为 null */
parts_total: number | null
parts_done: number
/**
* 已经写进目录的段数:每写完一段就并入目录,停止、服务重启时已写进的段保留。老服务端没有这个键
*/
parts_written?: number
/**
* 这张表实际分成的块数和已结束的块数:额度用完、超时后段被缩小、拆半,块会比段多。不用模型时为 null;
* 老服务端没有这两个键
*/
chunks_total?: number | null
chunks_done?: number | null
added: number
updated: number
removed: number
version: number
/**
* 模型给了这张表几项可用内容(并入目录之前)。没用模型、模型这部分失败时为 null。
* 大于 0 而新增、更新、删除都是 0,才是「模型给了内容、只是和现有目录一致」
*/
model_items: number | null
/**
* 模型拿不准、留空的列数:模型列出了(或这一段请它写了),中文名和含义却都没给(给了却都没通过校验的也算)。
* 表的状态不因此变(照旧可以是已完成)。老服务端没有这个键
*/
blank_columns?: number
/** 留空的列里的前几个(最多 5 个,列名照表结构原样)。老服务端没有这个键 */
blank_sample?: string[]
/**
* 这张表起草时记下的几句话(服务端整句中文,不算失败),比如模型提议的有效记录条件没有收下的原因。最多 10 句;
* 老服务端没有这个键
*/
log?: string[]
/** 整张表没起草的原因:表结构里没有、写入一直冲突、服务重启中断 */
error: string | null
/** 模型那部分没写出来的原因和办法(第几段、哪些列、为什么) */
model_error: string | null
started_at: string | null
finished_at: string | null
}
/** 起草任务的模型用量 */
export interface CatalogDraftUsage {
/** 调用次数(没写出来的、超时的也算) */
calls: number
/** 超时的调用不计 token(照实记 0:服务方可能已经生成了内容) */
input_tokens: number
output_tokens: number
/** 超时的调用次数。老服务端没有这个键 */
timeouts?: number
/** 没写出来的调用次数:额度用完、只给空壳、超时、出错。老服务端没有这个键 */
failed_calls?: number
}
/** 一次起草任务 */
export interface CatalogDraftJob {
id: string
source_id: string
status: CatalogDraftJobStatus
use_model: boolean
/** 实际用的模型 id;不用模型、还没开始时为 null */
model: string | null
/** 这次用到的起草要求(数据源级那份的快照) */
guidance: string | null
/** 这次的补充说明 */
note: string | null
created_at: string
started_at: string | null
finished_at: string | null
/** 谁建的(X-Actor) */
created_by: string | null
/** 重试建的任务记着原任务的 id,别的为 null。接口约定里没有这一项,服务端多给的;界面不用,老服务端没有这个键 */
retry_of?: string | null
/** 表数 */
total: number
/** 已结束的表数 */
done: number
counts: Record<CatalogDraftTaskStatus, number>
added: number
updated: number
removed: number
/** 各表模型拿不准、留空的列数合计。老服务端没有这个键 */
blank_columns?: number
/** 模型调用的用量;拿不到用量时照实是 0 */
usage: CatalogDraftUsage
/**
* 当前每段最多几列:一次起草里额度用完或超时累计 2 次后,之后新开始的段减半(40 → 20 → 10,最低 10)。
* 不用模型时可能为 null;老服务端没有这个键
*/
part_columns?: number | null
/**
* 调用接连没写出来时的提醒(一句中文:最近几次里几次没写出来、原因,建议换起草模型、缩小范围或停止);
* 好转后服务端清掉,为 null。老服务端没有这个键
*/
notice?: string | null
/** 任务整体出错的原因 */
error: string | null
/**
* 排在同一个数据源的剖析任务后面(起草和剖析同源串行,按建立顺序):剖析结束后自动开始。不在等时为 null;
* 老服务端没有这个键
*/
waiting_for?: 'profile' | 'introspect' | null
tasks: CatalogDraftTask[]
}
/** 目录清单顶上的进行中任务(GET /datasources/{id}/catalog 的 active_job) */
export interface CatalogActiveDraftJob {
id: string
status: CatalogDraftJobStatus
total: number
done: number
/** 排在剖析任务后面(同 CatalogDraftJob.waiting_for) */
waiting_for?: 'profile' | 'introspect' | null
}
/** 清单一行上这张表最近一次起草的结果 */
export interface CatalogLastDraft {
status: CatalogDraftTaskStatus
job_id: string
finished_at: string | null
error: string | null
model_error: string | null
/** 那次起草模型拿不准、留空的列数(同 CatalogDraftTask.blank_columns)。老服务端没有这个键 */
blank_columns?: number
/**
* 最近一次这张表结束于完成、部分完成的起草记下的留空列数(没有这样的起草为 null)。最近一次是停止、服务重启中断、
* 又没有新写入时,界面用它接着提示留空的列。老服务端没有这个键
*/
blank_columns_from?: number | null
/** 那次起草已写进目录的段数(同 CatalogDraftTask.parts_written)。接口约定里没有这一项,服务端给了才用 */
parts_written?: number
}
/**
* 目录修改提案的一项(服务端 catalog.PatchChange)。助手转出的提案和预览接口都是这个形状。
* path 是审阅接口的写法;新增关联关系已规范成 relations.<编号>
*/
export interface CatalogPatchChange {
path: string
/** 当前目录里的值;没有(或被驳回)为 null */
before: unknown
before_status: CatalogStatus | null
/** 保存后的值(码值是补充后的完整对照) */
after: unknown
/** 交回保存的原样取值(码值只有补充的那几个) */
value: unknown
reason: string
/** change 值有变化;confirm 值相同、还不是已确认(保存即确认);same 已经是这个值且已确认 */
state: 'change' | 'confirm' | 'same'
/** 服务端对这一项的说明(目前只有:新增的关联关系没写基数、又推算不出来)。没有时不给 */
note?: string
}
/** 保存、预览提案时交回的一项:路径、原样取值、理由 */
export interface CatalogPatchSubmit {
path: string
value: unknown
reason?: string
}
/** POST /datasources/{id}/catalog/{table}/patch/preview:对着当前目录重算改前、改后(只读) */
export interface CatalogPatchPreview {
table_name: string
version: number
changes: CatalogPatchChange[]
/** 不合法的项的说明(目录刚被改过、列已删除……) */
problems: string[]
}
/** 影响面里模板的一个节点(服务端 catalog_impact.template_refs) */
export interface CatalogImpactNode {
node_id: string
label: string
type: string
/** direct:SQL 里写着这张表;possible:Agent 运行时自己写 SQL,可能用到 */
impact: 'direct' | 'possible'
/** 合并查询:经由哪些输入(别名 → 节点) */
via?: { node_id: string; label: string; alias: string }[]
/** 协作节点:绑定了查询工具的成员 */
member?: string
}
/** 引用这张表的一个已发布或受管模板(按它当前的已发布版本算) */
export interface CatalogImpactTemplate {
workflow_id: string
name: string
/** 当前的已发布版本 */
version: number
level: 'published' | 'governed'
impact: 'direct' | 'possible'
nodes: CatalogImpactNode[]
}
// ---------------------------------------------------------------------------
// 审阅队列(/api/datasources/{id}/catalog/review-queue、review/batch、review-progress、{table}/usage,后端
// data/catalog_review.py):跨表列出推断项,按表的使用次数、这一项对数字的影响和来源排好,可以成批确认、驳回。
// ---------------------------------------------------------------------------
/** 队列的筛选:全部、影响数字的项(有效记录条件、去重、业务日期、关联关系、业务主键、度量类型)、名称和含义 */
export type CatalogReviewKind = 'all' | 'facts' | 'names'
/** 队列里的一项(只列推断状态的项) */
export interface CatalogReviewItem {
table_name: string
/** 审阅接口的路径写法 */
path: string
/** 字段名(valid_filter、label……;关联关系是 relation) */
field: string
/**
* 字段的界面叫法(服务端写好的「中文名」「码值」「有效记录条件」「关联关系」),不含是哪一列:界面上的位置按 path
* 说(「列 status 的码值」,review.ts 的 reviewWhere)
*/
field_label: string
/** 列级的项是哪一列;表级的项、关联关系为 null。接口约定里没有写明,服务端给了才用 */
column?: string | null
value: unknown
/** 值写成一行字(服务端写好的) */
display: string
source: CatalogSource
status: CatalogStatus
note: string | null
/** 越大越靠前:表的使用次数权重 × 风险权重 × 来源权重 */
priority: number
/** 为什么排在这里(中文,服务端写好的) */
reasons: string[]
/** 所在表被运行用过的次数 */
table_usage: number
/** 所在表的目录版本:成批审阅时逐表带上 */
version: number
}
/** GET /catalog/review-queue */
export interface CatalogReviewQueue {
total: number
items: CatalogReviewItem[]
}
/** POST /catalog/review/batch 的一项 */
export interface CatalogReviewBatchItem {
table_name: string
path: string
action: CatalogReviewAction
if_version: number
}
/** 成批审阅逐条的结果:一张表冲突不影响别的表;成功的写明这张表写入后的版本 */
export interface CatalogReviewBatchResult {
table_name: string
path: string
ok: boolean
version: number | null
error: string | null
code: string | null
}
/** 用过这张表的一次运行(新的在前) */
export interface CatalogUsageRun {
run_id: string
workflow_id: string | null
workflow_name: string | null
run_class: 'formal' | 'exploratory' | null
status: string
started_at: string | null
}
/** GET /catalog/{table}/usage */
export interface CatalogTableUsage {
total: number
runs: CatalogUsageRun[]
}
/**
* GET /catalog/review-progress:常用的前 top_n 张表、全部表、影响数字的项各审阅到哪儿。
* 「常用表」只算被运行用过的:tables 可能比 top_n 少,没有运行时是 0。三行的进度都按「已确认 ÷(已确认 + 待确认)」算
* (review.ts 的 confirmProgress):已驳回的、有确证的(外键约束)不用再确认,不计入
*/
export interface CatalogReviewProgress {
top_n: number
/** items_proposed、items_verified:接口约定里没有写明,服务端给了才用(没有待确认数时只写已确认几项) */
top: { tables: number; items_total: number; items_confirmed: number; items_proposed?: number; items_verified?: number }
all: { items_total: number; items_confirmed: number; items_proposed: number; items_rejected: number; items_verified?: number }
facts: { proposed: number; confirmed: number }
}
// ---------------------------------------------------------------------------
// 导出导入(/api/datasources/{id}/catalog/export、import/preview、import,后端 data/catalog_transfer.py):
// 本机整理好的目录带到另一个实例。先预览(只读),确认后逐表写入。
// ---------------------------------------------------------------------------
/** 导出的文件:含全部状态的项 */
export interface CatalogExportFile {
format: 'agentlab-catalog'
version: 1
exported_at: string
source: { name: string; kind: string }
guidance: string | null
tables: Record<string, { notes: CatalogNotes; version: number }>
}
/** 一处冲突:这里已确认、已驳回的项和文件中的不一样,以这里为准 */
export interface CatalogImportConflict {
path: string
label: string
file_value: unknown
target_value: unknown
why: string
}
/** 预览、导入报告里的一张表 */
export interface CatalogImportTable {
/** 文件中的表名 */
table_name: string
/** 对上的这个数据源里的表名(名字不一样时才有意思);对不上为 null */
matched_as: string | null
add: number
update: number
keep: number
conflicts: CatalogImportConflict[]
skipped: { path: string; why: string }[]
/** 导入后这张表的目录版本(只有导入的报告有) */
version?: number | null
/** 这张表没导入的原因(逐表写入,一张表失败不影响别的)。接口约定里没有写明,服务端给了才显示 */
error?: string | null
}
export interface CatalogImportTotals {
tables: number
add: number
update: number
keep: number
conflicts: number
skipped: number
}
/** POST /catalog/import/preview、/catalog/import */
export interface CatalogImportReport {
tables: CatalogImportTable[]
/** 文件中有、这个数据源里对不上的表 */
unmatched_tables: string[]
totals: CatalogImportTotals
/** 起草要求:写入文件中的(这里还没有)、保留这里的、不导入 */
guidance: { file: string | null; target: string | null; action: 'keep' | 'replace' | 'none' }
}
// ---------------------------------------------------------------------------
// 表助手(POST /api/datasources/{id}/catalog/{table}/assistant,后端 data/catalog_assistant.py):对话完善一张表的
// 目录。回答是事件流,每个事件一行 data: <JSON>。对话不存服务端,每一轮都把前面的对话和还没保存的修改带上。
// ---------------------------------------------------------------------------
/** 提案里的一项:和助手流里的 catalog_patch 一样,多一个给人看的位置 */
export interface CatalogAssistantChange extends CatalogPatchChange {
label?: string
}
/**
* 事件流里的一个事件。服务端等模型时另发 SSE 注释行(「: keep-alive」,不是事件),读流时跳过。
* query 的 row_count 是取回的行数;truncated(接口约定里没有写明,服务端给了才用)为真时后面还有行,取回的只是前一截
*/
export type CatalogAssistantEvent =
| { type: 'text'; delta: string }
| { type: 'query'; sql: string; columns: string[]; rows: unknown[][]; row_count: number; truncated?: boolean; error: string | null }
| { type: 'proposal'; source_id: string; table: string; version: number; changes: CatalogAssistantChange[] }
| { type: 'done'; usage: { input_tokens: number; output_tokens: number }; queries: number }
| { type: 'error'; message: string; code?: string | null }
/** 交给表助手的一轮请求 */
export interface CatalogAssistantRequest {
messages: { role: 'user' | 'assistant'; content: string }[]
/** 界面上还没保存的修改:最多 200 项(服务端同一个上限,多了整个请求 422),多出来的只交最近的 200 项 */
pending: CatalogPatchSubmit[]
}
/** GET /datasources/{id}/catalog/{table}/impact */
export interface CatalogImpact {
table: string
/** 直接引用的在前 */
templates: CatalogImpactTemplate[]
}
// ===========================================================================
// 数据剖析(后端 app/data/catalog_profile.py、catalog_profile_jobs.py):对业务库发少量只读查询,核实推断的关联关系、
// 取码值候选、提议业务日期。默认关闭,按数据源在 options.catalog_profile 里开启。
// 剖析是服务端的后台任务(/api/datasources/{id}/catalog/profile-jobs):逐表剖析,每张表有自己的查询次数和时长预算,
// 每张表一结束就写进目录;同时剖析几张表按设置(缺省一张),每张表的查询一条一条发,可以随时停止。和同一个数据源的起草任务
// 按建立顺序排队,一个结束了另一个才开始。
// 服务重启时还没结束的任务记 interrupted,不自动续跑(由人点「重试没做完的表」)。
// ===========================================================================
/** 剖析设置里的数值项 */
export type CatalogProfileNumberKey =
'table_max_queries' | 'table_max_s' | 'query_timeout_s' | 'sample_size' | 'max_scan_rows' | 'table_concurrency'
/** 一个数据源的剖析设置(服务端 ProfileSettings.to_dict);任务里的 settings 是建任务时的快照 */
export interface CatalogProfileSettings {
enabled: boolean
/** 每张表最多发几条查询,用完只停这张表 */
table_max_queries: number
/** 每张表最多剖析几秒,到时只停这张表 */
table_max_s: number
/** 单条查询时限(不超过数据源自身的查询时限,也不超过这张表剩下的时长) */
query_timeout_s: number
sample_size: number
/** 0 表示一律不做整表统计 */
max_scan_rows: number
/** 同时剖析几张表(1–3,缺省 1);每张表的查询仍一条一条发 */
table_concurrency: number
}
/** 为什么需要剖析(GET /catalog 每行的 profile_due_reason) */
export type CatalogProfileDueReason = 'never' | 'unfinished' | 'new_relations' | 'new_checks'
/**
* 剖析的范围:selected 指定的表;needed 需要剖析的表;all 全部可剖析的表(不含视图);observe 观察到期的表(只做取值观察);
* maintenance 定时维护(需要剖析的表 + 观察到期的表)。后两种由定时维护建,界面不发起
*/
export type CatalogProfileScope = 'selected' | 'needed' | 'all' | 'observe' | 'maintenance'
/**
* 剖析任务的状态:排队、进行中、完成、部分完成(有失败、部分完成、已停止的表)、失败(任务整体出错)、已停止(使用者停止)、
* 服务重启中断。和起草任务同一套
*/
export type CatalogProfileJobStatus = CatalogDraftJobStatus
/**
* 任务里一张表的状态。ok:每项检查都有结论;partial:有项因为运行原因没查成(unfinished);failed:写入一直冲突或整张表出错;
* skipped:视图、表结构里没有、空表、没有可剖析的项;cancelled:停止、止损、服务重启时还没开始、没做完
*/
export type CatalogProfileTaskStatus = CatalogDraftTaskStatus
/** 跳过一项的原因(CatalogProfileSkip.reason、检查项的 reason) */
export type CatalogProfileSkipReason = 'budget' | 'deadline' | 'failed' | 'timeout' | 'error' | 'rejected' | 'slow' | 'cancelled'
| 'masked' | 'too_large' | 'view' | 'unsupported' | 'no_data' | 'missing' | 'high_cardinality' | 'nothing'
/** 一类检查:关联关系、码值、日期列;取值观察的四类(行数、码值观察、最新业务日期、按日行数) */
export type CatalogProfileCheckKind = 'relation' | 'codes' | 'date' | 'rows' | 'codes_observe' | 'freshness' | 'daily_volume'
/** 发现的合计(任务里是逐表合计) */
export interface CatalogProfileSummary {
/** 有结论的关系条数 */
relations_checked: number
/** 其中状态为已验证的 */
relations_verified: number
/** 取到码值候选的列数 */
codes: number
/** 这些码值里含义还空着的个数之和 */
codes_pending: number
/** 提议了业务日期的表数(逐表时 0 或 1) */
business_dates: number
}
/** 一张表的行数:stats 数据库的统计信息(估算);count 数到上限为止;unknown 没能得到 */
export interface CatalogProfileSize {
rows: number | null
method: 'stats' | 'count' | 'unknown' | (string & {})
/** 数到上限也没数完:至少这么多行 */
at_least: number | null
}
/** 关系的核对结论:覆盖率、基数,升为已验证(verified)或保持推断(proposed);人工确认过的只补覆盖率和基数 */
export interface CatalogProfileRelationFinding {
kind: 'relation'
path: string
target: string
columns: string[]
to_table: string
to_columns: string[]
status: CatalogStatus
confirmed: boolean
/** 剖析之前目录里这条关系的状态:报告按它说「升为已验证」还是「仍为已验证」。老服务端不给 */
previous_status?: CatalogStatus | null
coverage: number
cardinality: CatalogCardinality | null
/** 基数是不是用数据核实的(子表一侧数过是否唯一)。老服务端不给 */
cardinality_checked?: boolean
/** 抽了几个不同的键值、在被指向表里对上几个 */
sample: number
matched: number
summary: string
}
/** 码值候选:观察到的取值和行数,含义留给人填 */
export interface CatalogProfileCodesFinding {
kind: 'codes'
path: string
column: string
values: { value: string; rows: number }[]
rows: number
status: CatalogStatus
/** 写进目录的码值里还有几个含义空着(别的来源写过的含义会沿用)。老服务端不给 */
pending?: number
summary: string
}
/** 业务日期提议:表里只有一个日期类列 */
export interface CatalogProfileDateFinding {
kind: 'business_date'
path: string
column: string
min: string
max: string
status: CatalogStatus
summary: string
}
export type CatalogProfileFinding = CatalogProfileRelationFinding | CatalogProfileCodesFinding | CatalogProfileDateFinding
/** 没做的一项。detail 是可以直接显示的整句 */
export interface CatalogProfileSkip {
/** relation / codes / date / row_estimate / table */
kind: string
target: string
path: string | null
/**
* 运行原因没查成(计入 unfinished、使表成为部分完成):budget 这张表的查询次数用完、deadline 这张表的时长用完、failed 连续失败
* 止损、timeout、error、rejected 未通过只读守卫、slow 超时比例止损、cancelled 停止或服务重启。
* 按规则不查(不算没做完):masked、too_large、view、unsupported、no_data、missing、high_cardinality、nothing 没有可剖析的项
*/
reason: CatalogProfileSkipReason | (string & {})
detail: string
}
/** 任务里的一张表(按 seq) */
export interface CatalogProfileTaskRow {
seq: number
table_name: string
status: CatalogProfileTaskStatus
checks_total: number
/** 结束了的项(含跳过的) */
checks_done: number
/** 每类检查 [结束了的, 一共];这类没有检查时是 [0, 0]。取值观察的四类合成 observe 一类(老服务端不给) */
checks_by_kind: Partial<Record<'relation' | 'codes' | 'date' | 'observe', [number, number]>>
/** 进行中时正在做哪一项的整句(如「正在核对 orders.customer_id → customers.id」);其余时候 null */
current: string | null
queries: number
elapsed_ms: number
row_estimate: CatalogProfileSize | null
summary: CatalogProfileSummary
/** 跳过的项数 */
skipped_count: number
/** 其中因为运行原因没查成的项数 */
unfinished: number
added: number
updated: number
removed: number
version: number
/** 整张表的原因:表结构里没有、视图、空表、没有可剖析的项、写入一直冲突、停止 / 中断 / 止损 */
error: string | null
started_at: string | null
finished_at: string | null
}
/** 检查清单里的一项 */
export interface CatalogProfileCheck {
kind: CatalogProfileCheckKind
/** 哪一项:关系写「customer_id → customers.id」,码值、日期写列名 */
target: string
/** 审阅路径;没有为 null */
path: string | null
state: 'pending' | 'running' | 'done' | 'skipped'
/** 跳过的原因(同 CatalogProfileSkip.reason);没跳过为 null */
reason: CatalogProfileSkipReason | (string & {}) | null
/** 可以直接显示的整句(结论或跳过的原因);没有为 null */
detail: string | null
queries: number
}
/** 发过的一条查询。sql 的字面量都换成了 ?(IN 列表里抽到的键值不留在 AgentLab 的库里) */
export interface CatalogProfileQuery {
at: string
kind: 'row_estimate' | CatalogProfileCheckKind
target: string
sql: string
ms: number
/** 返回的行数;没返回(超时、出错、守卫拒)时为 null */
rows: number | null
outcome: 'ok' | 'timeout' | 'error' | 'rejected'
/** 超时、出错、守卫拒时的原因 */
detail: string | null
}
/** GET /profile-jobs/{id}/tasks/{seq}:一张表的完整结果;GET /catalog/{table} 的 last_profile 也是这个形状 */
export interface CatalogProfileTaskDetail extends CatalogProfileTaskRow {
job_id: string
job_status: CatalogProfileJobStatus
created_by: string | null
checks: CatalogProfileCheck[]
findings: CatalogProfileFinding[]
skipped: CatalogProfileSkip[]
/** 日期类列的取值范围 */
/** invalid:零日期、占位日期这类不计入最小值和最大值的个数(没有时不给) */
date_ranges: { column: string; min: string; max: string; invalid?: number }[]
query_log: CatalogProfileQuery[]
}
/** 一次剖析任务 */
export interface CatalogProfileJob {
id: string
source_id: string
status: CatalogProfileJobStatus
scope: CatalogProfileScope
/** 建任务时的剖析设置快照 */
settings: CatalogProfileSettings
created_at: string
started_at: string | null
finished_at: string | null
/** 谁建的(X-Actor) */
created_by: string | null
/** 重试建的任务记着原任务的 id */
retry_of: string | null
/** manual 人点的;schedule 定时维护建的(fire_at 是那一次触发的计划时刻)。老服务端不给 */
trigger?: 'manual' | 'schedule'
fire_at?: string | null
/** 排在同一个数据源的起草任务后面:起草结束后自动开始。不在等时为 null */
waiting_for: 'draft' | 'introspect' | null
total: number
/** 已结束的表数 */
done: number
counts: Record<CatalogProfileTaskStatus, number>
/** 整个任务发出的查询数、其中超时的、失败的 */
queries: number
timeouts: number
errors: number
/** 任务级止损停下的原因:failed 连续 3 条查询失败;slow 最近 10 条里 7 条超时。没停为 null */
stop_reason: 'failed' | 'slow' | null
/** 最近查询多数超时时的提醒(一句中文);好转后服务端清掉 */
notice: string | null
/** 任务整体出错或止损的整句原因 */
error: string | null
added: number
updated: number
removed: number
summary: CatalogProfileSummary
tasks: CatalogProfileTaskRow[]
}
/** 清单一行上这张表最近一次剖析 */
export interface CatalogLastProfile {
job_id: string
seq: number
status: CatalogProfileTaskStatus
started_at: string | null
finished_at: string | null
summary: CatalogProfileSummary
skipped_count: number
unfinished: number
error: string | null
row_estimate: CatalogProfileSize | null
}
/** 目录清单顶上的进行中剖析任务(GET /catalog 的 active_profile_job) */
export interface CatalogActiveProfileJob {
id: string
status: CatalogProfileJobStatus
total: number
done: number
waiting_for: 'draft' | 'introspect' | null
}
// ===========================================================================
// 基于数据目录的 SQL 检查(后端 app/data/sqlcheck.py):七条规则,编号稳定
// ===========================================================================
export type SqlCheckCode = 'fanout_sum' | 'stock_summed' | 'join_unconfirmed' | 'ratio_aggregated' | 'missing_valid_filter'
| 'unknown_code' | 'wrong_date_column'
/** error:依据已核实、结果很可能有误;warning:依据有确证、结果可能有误;info:依据只是推断 */
export type SqlCheckLevel = 'error' | 'warning' | 'info'
/** 一条检查结果(证据接口查询步骤里的 checks、助手自查和发布前检查的问题)。交回模型改写用的那句不上界面,这里不收 */
export interface SqlCheckItem {
code: SqlCheckCode | (string & {})
level: SqlCheckLevel | (string & {})
/** 给人看的说明 */
message: string
/** schema_cache 里的表名 */
table: string
column?: string
relation_id?: string
sql_excerpt?: string
}
// ===========================================================================
// 数字推演(后端 app/api/whatif.py、app/engine/recompute.py):在一次运行的口径卡上改输入、排除查询快照里的行,
// 看指标和报告里引用它的地方会变成多少。推演不是出具:不写运行记录、不改报告
// ===========================================================================
/** 推演的改动:输入路径 → 新值;查询快照的工件 id → 要排除的行号(快照里的行号,从 0 数) */
export interface WhatIfOverrides {
inputs: Record<string, number>
exclude_rows: Record<string, number[]>
}
/** POST /runs/{id}/evidence/whatif 的请求体 */
export interface WhatIfRequest {
/** 报告撰写节点 id:只列这份报告里引用的地方 */
report?: string
/** 口径卡节点 id */
card: string
/** 只推演这一个指标 */
metric?: string
overrides: WhatIfOverrides
}
/** 一个输入在推演里的样子 */
export interface WhatIfInput {
path: string
via?: string | null
node_id?: string | null
/** 落在哪份查询快照上(cell() 取的格、agent 字段、整份查询结果):排除这份快照里的行会影响它 */
artifact?: string
locator?: EvidenceLocator
kind: 'number' | 'text' | 'bool' | 'missing' | 'container' | (string & {})
/** 数值(或原本缺失)的标量、不是遮罩列、不决定取哪一格、还原得出:可以改。整个指标推演不了时一律为 false */
editable: boolean
/** 原值、推演用的值。容器写大概(「[12 项]」);遮罩列的一律是 null */
old?: unknown
new?: unknown
changed?: boolean
/** 起了作用的被排除行(快照里的行号,从 0 数) */
excluded_rows?: number[]
/**
* 这个输入实际用到的快照行(含两端,从 0 数):单格一行、数组字段一段、整份查询结果全部。排除别的行影响不到它,
* 界面只让这几行可勾。遮罩的、还原不了的(完整值找不回来、快照读不出来)没有,决定取哪一格的数组字段、整份结果
* 也没有(少了几行,取到哪一格说不准),整个指标推演不了时一律没有
*/
row_span?: [number, number]
masked?: boolean
/** 落在的查询快照读不出来(文件不在、与哈希不一致):遮没遮核对不了,值不给 */
withheld?: boolean
}
/**
* unchanged:没碰到改动,原值原样交回;ok:重算了;excluded:排除行后缺了输入,算不出来;error:新值让计算出错(除以零……);
* unsupported:推演不了(有输入还原不了、口径卡没记输入,或者排除行后说不准受不受影响);masked:用到了遮罩列,
* 不推演。整个指标推演不了的不改动也是 unsupported / masked(unsupported 里另有一条 input 为空的)
*/
export type WhatIfStatus = 'unchanged' | 'ok' | 'excluded' | 'error' | 'unsupported' | 'masked'
export interface WhatIfMetric {
id: string
name: string
unit: string
decimals?: number | null
format?: string
old: unknown
new: unknown
delta: number | null
/** 按口径卡的格式渲染(和报告里的字同一套规则) */
rendered_old: string
/** unsupported、masked 时为 null:不知道会变成什么 */
rendered_new: string | null
/** 带正负号;比率、百分数写「个百分点」。没变化、不是数时为 null */
rendered_delta: string | null
changed: boolean | null
status: WhatIfStatus | (string & {})
reason: string | null
/** 推演用的代入式(重算了才有;没碰到改动的是卡上原来的代入式) */
substituted: string | null
inputs: WhatIfInput[]
}
/** 报告里引用这个指标的一处,推演之后会写成什么 */
export interface WhatIfSegment {
report: string
unit: string
sid: string
metric: string
rendered_old: string
rendered_new: string | null
changed: boolean | null
}
export interface WhatIfUnsupported {
metric: string
/**
* null:整个指标都推演不了(口径卡没记输入、用到了遮罩列、有输入还原不了或所在的快照读不出来),reason 和指标上的
* 一样。不论改什么都推演不了,不改动也有这一条;界面据此只写原因,不画输入框、勾选框
*/
input: string | null
reason: string
}
/** 引用了这张口径卡的下游口径卡:只标可能受影响,不重算 */
export interface WhatIfAffectedCard {
node_id: string
artifact: string
caliber: string
version: string
metrics: string[]
/** 直接引用(false 是经别的卡间接引用) */
direct: boolean
}
/**
* GET /runs/{id}/evidence/whatif/rows:一页查询快照的行(推演时勾行用),形状和证据面板的查询步骤一致,
* 可以直接交给 queryWindow。遮罩列写「已遮罩」;快照读不出来时没有行,note 说明原因
*/
export interface WhatIfRowsPage {
run_id: string
artifact: string
node_id?: string | null
tool?: string | null
source?: string | null
columns: string[]
rows: unknown[][]
row_offset: number
row_index: number[]
total_rows: number | null
truncated: boolean | null
masked: string[]
hash_ok: boolean | null
sealed: boolean
note?: string
}
export interface WhatIfResult {
run_id: string
card: { node_id: string; artifact: string; caliber: string; version: string }
report: string | null
/** 运行已封存且封存核对一致:推演依据的数据不会再变 */
sealed: boolean
seal: { sealed: boolean; ok: boolean | null }
metrics: WhatIfMetric[]
segments: WhatIfSegment[]
unsupported: WhatIfUnsupported[]
affected_cards: WhatIfAffectedCard[]
}
// ---------------------------------------------------------------------------
// 定时任务(/api/schedules,页面在 pages/schedules)。前后端按同一份接口约定,字段名和形状不各自发挥;时间一律是
// ISO 字符串(UTC),按定时任务自己的时区显示
// ---------------------------------------------------------------------------
/**
* 运行列表里的定时来源:发起它的定时任务,手动发起的是 null,老服务端不给。写在这里而不是上面 Run 的定义里:
* 同名接口会合并,定时任务的类型集中在文件末尾这一段,不和别的工作包改同一处
*/
export interface Run {
schedule_id?: string | null
}
export type ScheduleRunClass = 'formal' | 'exploratory'
/** Webhook 消息的格式:通用 JSON、飞书 / 企业微信 / 钉钉的群机器人 */
export type ScheduleWebhookFormat = 'generic' | 'feishu' | 'wecom' | 'dingtalk'
/** 订阅的事件:运行结束、运行失败、停在人工审批上(只在真有待审批时发) */
export type ScheduleEvent = 'finished' | 'failed' | 'awaiting_approval'
/** 触发的来源:到点、手动「立即运行」、错过之后在宽限内补发、由数据事件触发(trigger_kind=event 的任务) */
export type ScheduleFireKind = 'scheduled' | 'manual' | 'misfire' | 'event'
/** 触发的结局:发起了运行、上一次还没结束而跳过、错过时间超出宽限而跳过、发起时被拒 */
export type ScheduleFireStatus = 'started' | 'skipped_overlap' | 'skipped_misfire' | 'failed_to_start'
/**
* 定时的目标种类(next-round X4:定时只有一套):工作流、评测题集、数据源维护。非工作流目标没有工作流、输入模板、运行类别
* 和 Webhook(backend/app/api/schedules.py)
*/
export type ScheduleTargetKind = 'workflow' | 'eval_suite' | 'source_maintenance'
/** 读出来的 Webhook 设置。完整地址服务端加密保存、不再返回,只露主机和末 4 位 */
export interface ScheduleWebhook {
format: ScheduleWebhookFormat
/** 如 open.feishu.cn/…ab12 */
url_hint: string
has_secret: boolean
events: ScheduleEvent[]
}
/** 一次触发发出的一条通知 */
export interface ScheduleFireNotice {
event: ScheduleEvent
status: 'ok' | 'failed'
at: string
}
/**
* 运行成功结束后和同一定时任务上一次成功的运行的自动对比:基准是哪一次、各类指标的个数(同对比接口的 summary)。
* 只有个数,具体的数在对比页上
*/
export interface ScheduleFireCompare {
base_run_id: string
summary: RunCompareSummary
/**
* 基准是不是紧挨着的上一次。上一次失败了、取消了时,基准是再往前成功的那一次,为 false:界面写明比的是哪一次。
* 老服务端不给,按 true 看
*/
previous?: boolean
}
/** 一次触发(触发历史的一行) */
/** 数据源定时维护的步骤(app/data/source_maintenance.STEPS) */
export type ScheduleMaintenanceStep = 'introspect' | 'profile_needed' | 'observe'
/**
* 数据源维护的一次触发建了什么(app/data/source_maintenance 记在触发上的 inputs):要做的步骤、建了的探查 / 剖析任务(step 是
* 这个任务做的那几步,剖析、观察合成一个任务时写成 profile_needed+observe)、没执行的步骤和原因。和数据源页「定时维护」的
* 「最近一次」读的是同一份
*/
export interface ScheduleMaintenanceInputs {
steps?: ScheduleMaintenanceStep[]
jobs?: { step: string; kind: 'introspect' | 'profile'; id: string; tables?: number; scope?: string }[]
skipped?: { step: ScheduleMaintenanceStep; reason: string }[]
}
export interface ScheduleFire {
id: string
/** 计划触发的时间;手动运行是按下的那一刻 */
fire_at: string
kind: ScheduleFireKind
status: ScheduleFireStatus
run_id: string | null
/** 发起的那次运行现在的状态(同 Run.status) */
run_status: RunStatus | null
/** 跳过、发起失败的原因(中文) */
reason: string | null
/** 目标是评测题集时这一次触发建的那次评测(没有 run_id);老服务端不给 */
eval_run_id?: string | null
/** 目标是数据源维护时这一次建了哪些任务、哪几步没执行和原因(触发记录的 inputs);别的目标为 null,老服务端不给 */
inputs?: ScheduleMaintenanceInputs | null
notified: ScheduleFireNotice[]
/**
* 和同一定时任务上一次成功的运行的对比。运行没有成功结束、没有可比的、还没算、比不了、任何一边又在执行(不在终态)
* 或重新封存过时为 null;老服务端不给
*/
compare?: ScheduleFireCompare | null
created_at: string
}
export interface Schedule {
id: string
name: string
/** 目标:种类、id、种类的叫法、目标现在的名字(不在了是保存时记下的)、还在不在。老服务端不给(都是工作流) */
target_kind?: ScheduleTargetKind
target_id?: string
target_label?: string
target_name?: string
target_exists?: boolean
/** 非工作流目标存空串 */
workflow_id: string
workflow_name: string
/** 五段 cron:分 时 日 月 星期 */
cron: string
/** IANA 时区名,如 Asia/Shanghai */
timezone: string
run_class: ScheduleRunClass
/** 字段名 → 模板字符串,变量({{ schedule.yesterday }} 这类)按触发时间计算 */
input_template: Record<string, string>
enabled: boolean
/** 上一次还没结束时怎么办:目前只有跳过 */
overlap: 'skip'
/** 错过触发时间后多少秒内恢复还补发一次 */
misfire_grace_s: number
webhook: ScheduleWebhook | null
/** 暂停中、或者往后不会再触发时为 null */
next_run_at: string | null
last_fire: ScheduleFire | null
/** 频率的中文说法,如「每周一 09:00(Asia/Shanghai)」 */
description_text: string
created_by: string | null
created_at: string
updated_at: string
/** 乐观锁:修改时带上读到的版本,别人在这期间改过时回 409 */
version: number
}
/** 新建、修改时交上去的 Webhook。地址、密钥的明文只在写的时候给 */
export interface ScheduleWebhookIn {
format: ScheduleWebhookFormat
/** 修改时:省略表示不改,空串表示删除整个 Webhook */
url?: string
/** 修改时:省略表示不改,空串表示清除 */
secret?: string
events: ScheduleEvent[]
}
/** POST /api/schedules 的请求体 */
export interface ScheduleCreate {
name: string
workflow_id: string
cron: string
timezone: string
run_class: ScheduleRunClass
input_template: Record<string, string>
enabled: boolean
overlap: 'skip'
misfire_grace_s: number
webhook: ScheduleWebhookIn | null
}
/**
* POST /api/schedules 建非工作流目标(评测题集、数据源维护,总方案 X4)的请求体:写 target_kind、target_id,不写工作流、
* 输入模板和 Webhook(服务端回 422,field 是那一项);运行类别不用。改的时候照样用 SchedulePatch(只带 name、cron、timezone、
* enabled、misfire_grace_s 这几项)
*/
export interface ScheduleTargetCreate {
name: string
target_kind: Exclude<ScheduleTargetKind, 'workflow'>
target_id: string
cron: string
timezone: string
enabled: boolean
overlap: 'skip'
misfire_grace_s: number
}
/** PATCH /api/schedules/{id} 的请求体:只带改了的字段,外加读到的版本 */
export type SchedulePatch = Partial<Omit<ScheduleCreate, 'webhook'>> & {
webhook?: ScheduleWebhookIn
version: number
}
export interface SchedulePreviewRequest {
cron: string
timezone: string
input_template?: Record<string, string>
workflow_id?: string
n?: number
}
/** 预览:频率的中文说法、接下来几次触发时间、按下一次触发时间渲染出的输入。非法时 422 {detail, field} */
export interface SchedulePreview {
description_text: string
next: string[]
inputs: Record<string, string>
}
/** 输入模板里能用的变量 */
export interface ScheduleVariable {
/** 如 schedule.yesterday */
name: string
/** 如「昨天(YYYY-MM-DD)」 */
label: string
example: string
/** 补充说明(ISO 周的跨年规则),没有的是空串;老服务端没有这个键 */
note?: string
}
/** POST /api/schedules/{id}/run:这次手动触发和它发起的运行(没发起时为 null) */
export interface ScheduleRunResult {
fire: ScheduleFire
run_id: string | null
}
// ---------------------------------------------------------------------------
// 数据事件触发(docs/design/orchestration-round/00-总方案.md §4;服务端 scheduler/triggers.py、outbox.py):一个
// trigger_kind=event 的定时任务,目标只能是工作流。同名接口会合并,这里只往上面几个类型上补字段
// ---------------------------------------------------------------------------
/** 触发方式:按 cron 到点、由数据事件触发。老服务端不给,按 cron 看 */
export type ScheduleTriggerKind = 'cron' | 'event'
/** 事件种类:上传表格出了新的一期、上游工作流运行成功结束、数据源有要处理的变化 */
export type ScheduleEventKind = 'table.version' | 'workflow.finished' | 'source.changed'
/** 触发条件(服务端规范化过的那一份)。对象键按事件种类:表格、数据源写 source_id,上游工作流写 workflow_id */
export interface ScheduleTrigger {
event: ScheduleEventKind
source_id?: string
workflow_id?: string
/** 只认新增的一期(不写即新增、替换都认),只给 table.version */
period?: 'new' | 'any'
/** 只认某个应用发起的、某种来源的运行,只给 workflow.finished */
app_id?: string
origin_kind?: string
/**
* 也认途经驳回的(上游走了某个审批的驳回出口、按驳回那条路跑完,状态仍是成功)、也认应用补算的;缺省都不认。只给
* workflow.finished,不写即 false
*/
include_rejected?: boolean
include_backfill?: boolean
/** 去抖:同一个任务这么多秒内的几次事件合成一次 */
debounce_s: number
/** 保存时记下的对象名(对象删掉之后界面写它);读出来才有,交上去不用带 */
subject_name?: string
}
/** 事件触发任务现在有几条事件在等:没派发的(调度关着时一直留着)、在去抖窗口里的、窗口结束的时刻、调度开没开 */
export interface ScheduleTriggerState {
events: number
waiting: number
due_at: string | null
scheduler_on: boolean
}
export interface Schedule {
trigger_kind?: ScheduleTriggerKind
/** 事件触发时的触发条件;cron 任务为 null */
trigger?: ScheduleTrigger | null
/** 给人看的触发说明(事件触发时同 description_text);cron 任务为 null */
trigger_text?: string | null
trigger_state?: ScheduleTriggerState | null
}
/** 事件触发的那一次触发带的事件说明(服务端 outbox.EventFire.brief,不含出口字段的值) */
export interface ScheduleFireEvent {
event: ScheduleEventKind
subject_id: string
subject_name: string
/** 一句说法:「上传表格「门店销售」出了新的一期(2026-09)」 */
text: string
/** 去抖合并了几次事件 */
merged: number
first_at: string | null
last_at: string | null
/** 这次运行在事件触发链上是第几层 */
depth: number
/** 立即运行时按示例事件发起的(不是真来了一次事件);real 为假时是示例值 */
sample: boolean
real: boolean
/** 几个过了补发宽限的去抖窗口合成一条跳过时才有:合了几个窗口、合掉的期次(上传表格);老服务端不给 */
windows?: number | null
periods?: string[] | null
}
export interface ScheduleFire {
/** 事件触发的那几次的事件说明;别的为 null,老服务端不给 */
event?: ScheduleFireEvent | null
}
export interface ScheduleCreate {
/** 不写即 cron(字段和以前一字不差) */
trigger_kind?: ScheduleTriggerKind
trigger?: Omit<ScheduleTrigger, 'debounce_s'> & { debounce_s?: number }
}
export interface SchedulePreviewRequest {
trigger_kind?: ScheduleTriggerKind
trigger?: Omit<ScheduleTrigger, 'debounce_s'> & { debounce_s?: number }
}
export interface SchedulePreview {
/** 事件触发时:按哪一次事件渲染的(real 为假时是示例值,对象还没发生过这种事件) */
event?: { text: string; real: boolean } | null
}
/** GET /api/schedules/trigger-options:每种事件能选的对象(ok 为假时不能选,reason 写为什么,由服务端判)、来源、应用 */
export interface ScheduleTriggerOptions {
events: {
event: ScheduleEventKind
label: string
subject_label: string
objects: { id: string; name: string; ok: boolean; reason: string | null }[]
/** 一页没给完(对象按名字排、一页有上限):界面按关键字再查。老服务端不给,当作给完了 */
more?: boolean
}[]
origins: { kind: string; label: string }[]
apps: { id: string; name: string }[]
default_debounce_s: number
max_debounce_s: number
max_depth: number
}
/** GET /api/schedules/{id}/fires 的一页,新的在前;next_before 是下一页的游标,没有更多时为 null */
export interface ScheduleFirePage {
items: ScheduleFire[]
next_before: string | null
}
/** POST /api/schedules/{id}/webhook/test:用已保存的设置发一条测试消息 */
export interface ScheduleWebhookTest {
ok: boolean
http_status: number | null
message: string
}
// 运行对比(GET /api/runs/{id}/compare/base、/compare?base=):两次运行的指标变化按数据 / 口径 / 模板与 SQL 拆开说。
// 形状照前后端共用的接口约定,字段名一个不改;数值都是服务端算好的,界面只排版,渲染值原样显示
// ---------------------------------------------------------------------------
/** 对比里的一次运行。版本是空的:没从固定版本发起(探索运行);schedule_id 是空的:手动发起 */
export interface RunCompareRef {
id: string
workflow_name: string
version: number | null
run_class: 'formal' | 'exploratory' | null
started_at: string | null
finished_at: string | null
schedule_id: string | null
}
/** 默认基准从哪来:同一定时任务的上一次,没有就同一工作流、同一运行类别的上一次 */
export type RunCompareBaseReason = 'same_schedule' | 'same_workflow'
/** GET /runs/{id}/compare/base。找不到时 404,detail 是一句原因 */
export interface RunCompareBase {
base_run_id: string
reason: RunCompareBaseReason | (string & {})
base: RunCompareRef
/**
* 基准是不是紧挨着的上一次(服务端 pick_base 算的,同触发历史里自动对比的 previous):上一次失败、取消、还没结束时
* 为 false,运行详情「更多」里的入口据此写「与上一次成功的运行对比」。老服务端不给,按 true 算
*/
previous?: boolean
}
/**
* 指标的分类。unchanged:未变;data_only:只数据变;caliber_and_data:口径与数据都变(能严格拆时给口径效应、数据效应);
* sql_changed:所依据的查询 SQL 结构变了(口径不同,不拆);params_only:SQL 只差字面量(定时运行的日期);
* added / removed:只有本次 / 只有基准有;incomparable:缺比较所需的信息
*/
export type RunCompareClass =
| 'unchanged' | 'data_only' | 'caliber_and_data' | 'sql_changed' | 'params_only' | 'added' | 'removed' | 'incomparable'
/** 各类指标的个数 */
export type RunCompareSummary = Record<RunCompareClass, number>
export interface RunCompareTemplateNode {
node_id: string
title: string
change: 'added' | 'removed' | 'config' | (string & {})
/** 改了的配置项(节点配置里的键) */
keys: string[]
/** 约定之外、服务端顺带给的:各配置项在界面上的叫法,和 keys 一一对应。没给(或对不上)时照写键名 */
labels?: string[]
}
export interface RunCompareTemplate {
base_version: number | null
target_version: number | null
/** 两次运行的执行图完全相同 */
same_graph: boolean
nodes: RunCompareTemplateNode[]
}
/**
* 严格拆分:口径效应 = 新口径(本次的数据) − 旧口径(本次的数据),数据效应 = 旧口径(本次的数据) − 旧口径(基准的数据),
* 两者之和正好是总变化。*_rendered 是服务端按口径卡格式写好的(带正负号)
*/
export interface RunCompareDecomposition {
caliber_effect: number
data_effect: number
caliber_rendered: string
data_rendered: string
}
/** 一个输入在两次运行里的值(容器写大概,缺的是 null) */
export interface RunCompareInput {
path: string
base: unknown
target: unknown
/** 约定之外、服务端顺带给的:取自设置了遮罩的列,两边的值都不给 */
masked?: boolean
}
export interface RunCompareAttributionRow {
/** 各维度列上的取值,和 columns 一一对应 */
key: unknown[]
base: number | null
target: number | null
delta: number | null
/** 这一项的变化占总变化的比例(小数,0.4 是 40%)。度量列之和对不上指标的输入时为 null,原因在 note */
share: number | null
}
/** 维度归因:按键对齐两边查询结果的行,变化最大的排在前面 */
export interface RunCompareAttribution {
columns: string[]
measure: string
rows: RunCompareAttributionRow[]
total_delta: number | null
note: string | null
}
export interface RunCompareMetric {
card_id: string
card_title: string
metric_id: string
name: string
unit: string
decimals: number | null
base: number | null
target: number | null
base_rendered: string | null
target_rendered: string | null
delta: number | null
/** 带正负号;比率、百分数写「个百分点」。没变化、不是数时可能为 null */
delta_rendered: string | null
/** 相对基准变化了百分之几(已乘 100,保留两位:12.5 是 +12.5%,和占比的小数写法不同);基准为 0、缺值时为 null */
delta_pct: number | null
classification: RunCompareClass | (string & {})
reasons: string[]
decomposition: RunCompareDecomposition | null
/** 没法严格拆分的原因;能拆、或者用不着拆(只数据变、未变)时为 null */
decomposition_note: string | null
inputs: RunCompareInput[]
attribution: RunCompareAttribution | null
}
/** same:完全相同;params_only:规范化之后相同,只差字面量;changed:结构不同;added / removed:只有一边有 */
export type RunCompareStructure = 'same' | 'params_only' | 'changed' | 'added' | 'removed'
export interface RunCompareQuery {
node_id: string
node_title: string
/** 同一节点里的第几条查询(服务端的序号,只用来排序和区分,界面另按 1 起数) */
ordinal: number
structure: RunCompareStructure | (string & {})
base_sql: string | null
target_sql: string | null
/** 不同的字面量(SQL 写法,字符串带引号),按出现的顺序 */
literals: { base: unknown; target: unknown }[]
base_rows: number | null
target_rows: number | null
base_truncated: boolean | null
target_truncated: boolean | null
/** 约定之外、服务端顺带给的:结构变了时差在哪,一处一句中文(按对结果的影响从大到小) */
differences?: string[]
/** 约定之外、服务端顺带给的:快照读不出来、与哈希不一致时的一句说明 */
problem?: string
}
/** 运行对比里分析卡的一边(analysis.end 那一项):没有这一项为 null */
export interface RunCompareAnalysisSide {
status?: string | null
verdict?: string | null
direction?: string | null
phrase?: string
artifact?: string | null
}
/**
* 运行对比里分析卡的一项(engine/compare._analyses):两次运行的分析卡按节点和项的标识对齐。change:same 判定和判定语都一样、
* verdict 状态或判定变了、phrase 判定一样而判定语里的数变了、added / removed 只在一边有
*/
export interface RunCompareAnalysis {
node_id: string
title?: string
item: string
op?: string | null
mode?: string | null
base: RunCompareAnalysisSide | null
target: RunCompareAnalysisSide | null
change: 'same' | 'verdict' | 'phrase' | 'added' | 'removed' | string
}
/** GET /runs/{id}/compare?base=。任一未封存或封存核对不一致 409;不是同一工作流、是同一次运行 422 */
export interface RunComparison {
target: RunCompareRef
base: RunCompareRef
summary: RunCompareSummary
template: RunCompareTemplate
metrics: RunCompareMetric[]
/** 分析卡按项对齐(分析能力一轮);老服务端没有 */
analyses?: RunCompareAnalysis[]
queries: RunCompareQuery[]
notes: string[]
}
/** 运行接口回的运行带着发起它的定时任务(RunOut.schedule_id);Run 上没写这一项,对比页的运行卡要用 */
export type RunWithSchedule = Run & { schedule_id?: string | null }
// ---------------------------------------------------------------------------
// 双盲复算:审批 payload、完整对照工件、节点配置,以及设置里「核查」一组(期 3 前后端共用的约定)
// ---------------------------------------------------------------------------
/** 审核人对一个指标的裁决:a 采用原流程的结果,b 采用盲算的结果,neither 两边都不对 */
export type DualCheckChoice = 'a' | 'b' | 'neither'
/** 双盲复算挂起时 payload.summary 的一项:审批卡先用它画,完整对照另从 card_artifact 取 */
export interface DualCheckSummaryItem {
metric_id: string
name: string
unit?: string | null
original_rendered: string | null
blind_rendered: string | null
/** (盲算值 − 原流程值) ÷ |原流程值| × 100,保留两位小数(12.5 表示相差 12.5%);原流程值为 0 时为 null */
delta_pct: number | null
/** 具体差异,一条一句中文,最多 3 条 */
diff: string[]
/** 只有兜底说明:两条 SQL 的写法不同,未能归纳成具体差异 */
fallback: boolean
/**
* 约定之外、服务端另带的:采用原流程(a)、采用盲算(b)各会往数据目录里写什么。老服务端没有这一项,卡上按差异的
* 形状有条件地说
*/
catalog?: { a: DualCheckCatalogSide; b: DualCheckCatalogSide }
}
/**
* 采用一边的结果会往数据目录里写什么:服务端按发起审批时的数据目录预告(dual_check_facts.preview,和裁决后真写的
* 是同一套判断)。write 为真时 table 的 item(business_date / valid_filter / dedup)会记为 value(有效记录条件是和
* 目录里已有的条件合起来之后的整句);为假时 reason 说明为什么不写
*/
export type DualCheckCatalogSide =
| { write: true; source?: string | null; table: string; item: string; value: unknown }
| { write: false; reason?: string | null }
/** payload.kind 为 dual_check 的审批 payload(审批的 mode 是 input) */
export interface DualCheckPayload {
kind: 'dual_check'
node_id: string
title: string
/** 完整对照放在这个工件里(GET /api/artifacts/{id}),内容见 DualCheckCard */
card_artifact: string
summary: DualCheckSummaryItem[]
/** 约定之外、服务端另带的:节点实际生效的「未采用原流程时」。没有时卡上从运行的图快照里读 */
on_reject?: 'degrade' | 'fail'
}
/** 审核人的回复:POST /approvals/{id}/decide 的 value */
export interface DualCheckReply {
choices: Record<string, DualCheckChoice>
note: string
}
/** 指标的一个输入:取自哪条查询的哪一格(alias 是查询快照的编号) */
export interface DualCheckInput {
path: string
value: unknown
sql?: string | null
source?: string | null
alias?: string | null
}
/** 盲算那一边的输入多一段盲算模型自己的说明 */
export interface DualCheckBlindInput extends DualCheckInput {
rationale?: string | null
}
export interface DualCheckSide<I extends DualCheckInput = DualCheckInput> {
value: number | null
rendered: string | null
inputs: I[]
}
/** 两条 SQL 差在哪(服务端 sql_semantics.explain_sql_diff)。aspect 为 other 的是兜底说明,parse 是解析不了 */
export interface DualCheckDiff {
equivalent: boolean
items: { aspect: string; text: string }[]
/** 用了兜底说明(一条具体说法都没有,或者具体说法只覆盖了一部分) */
fallback: boolean
/**
* 规范化之后两条 SQL 不同的行:「- 」开头是原流程的,「+ 」开头是盲算的;「 」开头的是两边相同的上级行,只为标出
* 不同的行在哪个子句里(老工件里没有)
*/
text_diff: string[]
}
export interface DualCheckMetric {
metric_id: string
name: string
unit?: string | null
decimals?: number | null
original: DualCheckSide
blind: DualCheckSide<DualCheckBlindInput>
diff: DualCheckDiff
/** 相差比例,口径同 DualCheckSummaryItem.delta_pct(有的话) */
delta_pct?: number | null
}
/** card_artifact 工件的内容 */
export interface DualCheckCard {
metrics: DualCheckMetric[]
/** 盲算用的模型 */
model?: string | null
/** 盲算模型和原流程的模型相同 */
same_model?: boolean
}
/** 双盲复算节点的配置。留空的项:card 取上游的口径卡,metrics 取输入来自查询的全部指标,其余跟随设置 */
export interface DualCheckConfig {
card?: string
metrics?: string[]
definitions?: Record<string, string>
provider?: string
model?: string
tolerance?: number
on_reject?: 'degrade' | 'fail'
/** null 或不写:跟随设置 */
enabled?: boolean | null
}
/**
* 设置里的 analysis 一组(GET /settings 给的是补齐缺省之后的完整一组):判定用的阈值、Agent 每次执行最多分析几次、最多下钻
* 几层、列出前几段。键、叫法、范围见 lib/termsAnalysis(照服务端 analysis/contract.py)
*/
export type AnalysisSettings = Record<string, number>
/** 设置里的 verify 一组(GET /settings 给的是补齐默认之后的完整一组;上限为 null 表示不限) */
export interface VerifySettings {
provider: string | null
model: string | null
dual_check: { enabled: boolean; tolerance: number; max_queries: number | null; timeout_s: number | null }
challenge: { enabled: boolean; max_claims: number | null; max_queries: number | null; timeout_s: number | null }
inquiry: { enabled: boolean; max_queries: number | null; timeout_s: number | null }
daily_max_usd: number | null
}
/** GET /settings/verify/spend:今天(本地日期)核查花了多少。unpriced_calls 是估不出金额、没算进 usd 的调用次数 */
export interface VerifySpend {
date?: string
usd?: number
calls?: number
unpriced_calls?: number
daily_max_usd?: number | null
}
// ---------------------------------------------------------------------------
// 反方审稿(challenge 节点):审批卡上的概况、challenge_artifact 工件的内容。形状照前后端共用的接口约定,字段名一个不改;
// 判断由反方模型给出,界面只排版,理由正文、行数这些都是服务端给的
// ---------------------------------------------------------------------------
/** 一句结论经受住反方的复核没有:经受住 / 被削弱 / 被推翻 / 无法判断 */
export type ChallengeVerdict = 'stands' | 'weakened' | 'refuted' | 'inconclusive'
/** 各种判断的句数 */
export type ChallengeCounts = Record<ChallengeVerdict, number>
/** 审批卡、节点产出里列的一句:unit 是报告文档里这一句的 id,text 是结论原文 */
export interface ChallengeItem {
unit: string
text: string
verdict: ChallengeVerdict | (string & {})
}
/**
* 审批卡上的反方审稿概况(human 节点 payload.reports[i].challenge)。items 最多 10 条,完整内容在 artifact 指的工件里。
* on_refuted 是实际生效的处理方式:degrade 被推翻的结论计入出具缺口;flag 只标注。报错(fail)时节点已经失败,到不了审批。
* 老服务端不给时按 degrade
*/
export interface ApprovalChallenge {
counts: ChallengeCounts
items: ChallengeItem[]
/** challenge_artifact 工件 id。服务端保存审稿记录失败时为 null:判断照样有,理由和查询看不了 */
artifact: string | null
on_refuted?: 'degrade' | 'flag' | (string & {})
/** 期 4(nodes/challenge.challenge_summary):没能反查的句数,都记为无法判断。升级前的产出、老服务端没有 */
unreviewed?: number
/**
* 期 4:被推翻的会不会让出具降档(按降档处置、又接在带出具契约的成果节点上游才为真)。升级前的产出、老服务端没有,
* 审批卡按「没写只标注就降档」的旧规则说
*/
degrades?: boolean
}
/**
* 审批审的报告经过反方审稿时多一个 challenge。和上面的 ApprovalReport 是同一个接口(声明合并):反方审稿的类型集中在
* 这一段,这一项也就近写在这里
*/
export interface ApprovalReport {
challenge?: ApprovalChallenge
}
/** 反方为复核一句结论执行的一次只读查询:编号是它自己的(Q1、Q2…),和报告里的查询编号无关 */
export interface ChallengeQuery {
alias: string
/** 查询快照工件 */
artifact: string
sql: string
row_count: number | null
truncated: boolean
/** 数据源名 */
source: string | null
}
/** 工件里的一句:判断、理由、查询 */
export interface ChallengeClaim extends ChallengeItem {
/** 理由渲染后的正文(数字已换成查询结果里的值) */
rationale: string
/**
* 理由组装成的文档(和报告文档同一种形状,数字带出处、裸数字被标出)。约定没说是工件 id 还是文档本身,两种都认;
* 没有时只显示 rationale
*/
rationale_doc?: string | EvidenceDocData | null
queries: ChallengeQuery[]
usage?: { input_tokens?: number; output_tokens?: number; total_tokens?: number; cost_usd?: number | null; calls?: number }
}
/** challenge_artifact 工件的内容:审的哪份报告、每句的复核,以及反方用的模型 */
export interface ChallengeArtifact {
/** 被审的报告撰写节点 */
report: string
doc_artifact: string
claims: ChallengeClaim[]
model: string | null
}
// ---------------------------------------------------------------------------
// 审批质询:审批人就待审的报告提问,核查模型只读查库作答。形状照前后端共用的接口约定(F5a / F5b 同一份),字段名一个
// 不改;回答正文、行数、触顶原因都是服务端给的,界面只排版
// ---------------------------------------------------------------------------
/** 回答停在哪个上限上:每次最多查询次数、时长、这一次的金额、核查的每日金额 */
export type InquiryLimit = 'max_queries' | 'timeout_s' | 'max_cost_usd' | 'daily_max_usd'
/** 质询的回答 */
export interface InquiryAnswer {
/** 渲染后的正文(数字已按出处换好) */
text: string
/** compose_doc 组装的文档工件:有它,正文里的数字可以点开看出处;没有(组装失败)只显示 text */
doc_artifact: string | null
ok: boolean
limit: InquiryLimit | (string & {}) | null
/** 没答成时的中文说明 */
error: string | null
}
/** 核查模型为回答执行的一次只读查询。编号是这次质询自己的,快照按质询工件接口取(遮罩列已遮掉) */
export interface InquiryQuery {
alias: string
artifact: string
sql: string
row_count: number | null
truncated: boolean
/** 数据源名 */
source: string | null
}
export interface InquiryUsage {
input_tokens?: number
output_tokens?: number
cost_usd?: number | null
calls?: number
unpriced_calls?: number
}
/** 一次质询:问的是哪份报告的哪一句(或划选的哪段原文)、问了什么、怎么答的、查了什么 */
export interface Inquiry {
/** 运行记录里那条 approval.inquiry 事件的序号;没写进运行记录时为 null */
seq: number | null
/** 什么时候问的(ISO 时间或秒级时间戳) */
at: string | number | null
/** 提问人的署名;没署名时为 null */
actor: string | null
/** 报告撰写节点 id */
report: string
/** 句子单元 id(如 u12);就整份报告提问时为 null */
unit: string | null
/** 划选的原文 */
quote: string | null
question: string
answer: InquiryAnswer
queries: InquiryQuery[]
/** 作答的核查模型 */
model: string | null
usage: InquiryUsage
/** 运行封存之后才问的(只有探索运行可以) */
post_seal: boolean
/** 提问时停在的那张审批;封存之后问的为 null */
approval_id: string | null
}
/** POST /runs/{id}/inquiries 的请求体 */
export interface InquiryRequest {
report: string
unit: string | null
quote?: string | null
question: string
/** 运行停在审批上时必填:指明是哪一张 */
approval_id?: string
}
/** POST /runs/{id}/inquiries 的答复 */
export interface InquiryResponse {
inquiry: Inquiry
/** 是否写进了运行记录 */
recorded: boolean
/** 没写进运行记录时的中文说明,否则为 null */
note: string | null
}
/** GET /runs/{id}/inquiries 的答复 */
export interface InquiryList {
items: Inquiry[]
}
/**
* GET /runs/{id}/inquiries/artifacts/{artifact}:质询引用过的查询快照,遮罩列照证据面板的规则遮掉(值写「已遮罩」)。
* 约定只说了是快照:可能是快照本身,也可能包在 {id, content} 里(同工件接口),界面两种都认
*/
export interface InquirySnapshot {
columns: string[]
rows: unknown[][]
sql: string | null
truncated: boolean
/** 结果一共几行(快照的 row_count / total_rows),不给时按 rows 的长度 */
total: number | null
/** 遮掉的列 */
masked: string[]
source: string | null
}
// ---------------------------------------------------------------------------
// 应用平台(docs/design/app-platform-and-look.md):运行来源、版本的发布 / 退役与使用情况、从运行提取的来源。
// 接口约定的主体在 typesApp.ts(冻结);这里只往已有的类型上补字段,同名接口会合并
// ---------------------------------------------------------------------------
/** 运行来源(X3)。老运行四项都为空,界面写「来源未记录」 */
export interface Run {
origin_kind?: import('./typesApp').OriginKind | null
/** run.origin 事件的数据:kind、app_id、entry / view、hook、actor、actor_verified…… */
origin?: Record<string, any> | null
app_id?: string | null
app_version?: number | null
}
/** 每个版本各自记发布、退役(A1、F5);VersionDetailOut 另带入口表单和使用情况 */
export interface WorkflowVersion {
level?: 'published' | 'governed' | null
published_at?: string | null
published_by?: string | null
retired_at?: string | null
retired_by?: string | null
retire_note?: string | null
/** 入口字段按 agentlab.form/1 转好的表单(服务端 input_schema(fields)),发起表单照它画 */
input_schema?: import('./typesApp').FormSchema | null
/** 被哪些应用固定、生效记录数、正式运行数(同 GET /api/ledger/workflow-versions/{wf}/{v}) */
usage?: { apps: { app_id: string; name: string; app_version: number }[]; effects: number; formal_runs: number } | null
}
/** 从运行提取时的来源:run_id、对话、轮次(/copilot/from-run 写) */
export interface Workflow {
origin?: Record<string, any> | null
}
/**
* GET /api/datasources/names(应用平台 D-c):开了登录、允许使用者问数据时,问数据页按它列可选的数据源。
* 只有名字和说明,没有连接信息、表结构
*/
export interface DataSourceName {
id: string
name: string
description?: string
kind?: string
enabled?: boolean
}
// ---------------------------------------------------------------------------
// 驳回 ≠ 失败(docs/design/next-round/05-已知遗留.md (a),总方案 X9)
/** 运行为什么结束在 cancelled(服务端 Run.end_reason):rejected 人工驳回,stopped 手动停止。其余终态、老运行为空 */
export type RunEndReason = 'rejected' | 'stopped'
export interface Run {
end_reason?: RunEndReason | null
}
// ---------------------------------------------------------------------------
// 大库:对象索引、探查范围、后台探查(docs/design/next-round/03-数据接入与目录维护.md A1、A2,第 2 波 W2-a)
/** 探查范围的摘要(服务端 schema_cache.scope):范围内几张、读到结构的、读不到的、范围外的、上限、各原因几张 */
export interface SchemaScope {
in_scope: number
reflected: number
failed: number
out_of_scope: number
cap: number
index_total: number
pinned: number
explicit: number
rule: number
on_demand: number
/** dictionary 字典查询 / inspector 退回 Inspector(没有注释) */
index_method?: string
/** 对象索引超过了 5 万个的上限,只收了按名字排序的前面那些 */
index_capped?: boolean
index_at?: string
}
export type IntrospectJobStatus = 'queued' | 'running' | 'succeeded' | 'partial' | 'failed' | 'cancelled' | 'interrupted'
/** 同源队里排在前面的事(waiting_for):起草、剖析、导入目录、同步起草 */
export type LaneKind = 'draft' | 'profile' | 'import' | 'sync_draft' | 'introspect'
/** 数据源卡片上最近一个探查任务(DataSourceOut.introspect_job) */
export interface IntrospectJobBrief {
id: string
status: IntrospectJobStatus
total: number
done: number
failed: number
cancelled: number
waiting_for: LaneKind | null
finished_at: string | null
error: string | null
}
/** GET /datasources/{id}/introspect-jobs/{job_id} 的 job */
export interface IntrospectJob {
id: string
source_id: string
status: IntrospectJobStatus
/** manual 人点的 | schedule 定时维护 */
trigger: string
schema: string
created_by: string | null
created_at: string | null
started_at: string | null
finished_at: string | null
total: number
done: number
counts: Record<'pending' | 'running' | 'ok' | 'failed' | 'cancelled', number>
/** 正在读的那张表 */
current: string | null
/** 读不到结构的表(最多 50 张)和原因 */
failures: { name: string; error: string | null }[]
error: string | null
/** 建任务那一刻的范围:规则原样、各原因几张、对象索引的情况 */
scope: Partial<SchemaScope> & { rules?: IntrospectScopeRules; hits?: number; added?: number; removed?: number }
waiting_for: LaneKind | null
}
/** 探查范围的规则(数据源 options.introspect_scope) */
export interface IntrospectScopeRules {
mode: 'all' | 'rules'
include: string[]
exclude: string[]
/** 显式列出:一律进范围 */
tables: string[]
/** 显式移出:有数据目录、被已发布模板用到的表也排得掉 */
removed: string[]
views: boolean
max_tables: number
updated_by?: string | null
updated_at?: string | null
}
/** 按规则对着对象索引数的命中 */
export interface IntrospectScopePreview {
in_scope: number
/** 计上限的那部分(规则、一律在范围内、显式列出;按需加入的另算) */
hits: number
cap: number
out_of_scope: number
index_total: number
pinned: number
explicit: number
rule: number
on_demand: number
over: boolean
}
/** 按需加入、目录页手工加入范围的留痕 */
export interface IntrospectScopeTrail {
name: string
scope_reason: string | null
/** look 助手查看 | db_schema 运行时查表结构 | catalog 目录页 */
via: 'look' | 'db_schema' | 'catalog' | null
by: string | null
run_id: string | null
conversation_id: string | null
ref: string | null
need: string | null
at: string | null
}
/** GET / PUT /datasources/{id}/introspect-scope、POST …/preview */
export interface IntrospectScope {
scope: IntrospectScopeRules
defaults: { max_tables: number; max_limit: number; index_limit: number; rules_max: number }
/** 没有对象索引时为 null:先刷新对象索引才数得出命中 */
preview: IntrospectScopePreview | null
index: { ready: boolean; schema: string; method: string | null; capped: boolean; at: string | null; total: number | null }
/** 一律在范围内的表(举前 20 个例子) */
pinned_sample: string[]
on_demand: IntrospectScopeTrail[]
}
export interface DataSource {
/** 探查范围的摘要;升级前探查的源、上传表格为 null */
schema_scope?: SchemaScope | null
/** 最近一个探查任务;没有(或升级前同步探查的)为 null */
introspect_job?: IntrospectJobBrief | null
}
/**
* 表结构里没有这张表时为什么(目录清单、表详情的 absent_reason):removed 表已不在(真删除)、missing 数据源里从来没有、
* out_of_scope 在探查范围外、reflect_failed 在范围里却读不到结构、unknown 说不清(升级前只探查了一部分的老缓存)
*/
export type CatalogAbsentReason = 'removed' | 'missing' | 'out_of_scope' | 'reflect_failed' | 'unknown'
export interface CatalogTableRow {
absent_reason?: CatalogAbsentReason | null
}
export interface CatalogDetail {
absent_reason?: CatalogAbsentReason | null
}
export interface CatalogList {
/** 探查范围的摘要;升级前探查的源、上传表格为 null */
scope?: SchemaScope | null
}
/** GET /datasources/{id}/catalog/search 的一项:对象索引里按名字、注释找到的表(含探查范围外的) */
export interface CatalogSearchItem {
name: string
qualified: string
kind: 'table' | 'view'
comment: string | null
est_rows: number | null
in_scope: boolean
scope_reason: string | null
absent_reason: CatalogAbsentReason | null
}
export interface CatalogSearch {
items: CatalogSearchItem[]
total: number
/** 有对象索引(没有的只在表结构里找) */
indexed: boolean
}
/** POST /datasources/{id}/catalog/scope/add */
export interface CatalogScopeAdd {
added: string[]
already: string[]
failed: { name: string; error: string }[]
refused: { name: string; reason: string }[]
scope: SchemaScope | null
}