forked from ChaiHao/BPOS-MVP
「flow:按**步序位置**推送声明的模版」(AM 07)。原先这条链只有写面没有读面,
活动的 viewTemplates 是一行占位字符串,工作台视图区永远只显示一份硬编码概览。
补齐整条链:
· 步声明新增 `view`:这一步呈现哪张模版 —— 推什么由声明给定,不由临场决定。
· 新增模版填数的纯函数(interaction/domain):`data-slot` 取值、`data-rows` 逐条展开。
**填数只做取值与转义**,没有表达式、没有分支、没有循环 —— 一旦模版能算,
它就成了代码,容器的「无脚本执行权」也就形同虚设。填充**消费槽位**,
交出去的是干净 HTML,不是带模板属性的半成品。
· interaction 补读面 `viewOf`;desk 路由把推送的 HTML 交给前端;
ViewRegion 经 `@ui` 的 ViewContainer(sandbox="" 的 iframe)渲染 —— feature 不碰 innerHTML。
· A1 写了三张真模版(事态全景 / 打标判据 / 成果预览),内联自带样式
(沙箱页面不继承工作台 CSS),按步序位置挂在 S3 / S5 / S6 上。
沿路两处修:
① 取值路径遇到**本身带点号的键**(台账地址 `域.对象`)会被拆成两级、永远取不到。
改为整键优先、找不到再下行;`view-render` 与 `step-sequence` 两处同修。
② 「本单交付」不能画成台账全表:任务视图是正式版 + 我的草稿的合并结果,
直接拿来列会把**别单**放行过的记录一并列出。本单自己写下的行单独放在
`mine:域.对象` 下(整键,取值不歧义)。
另:视图推送失败原先静默吞掉 —— 会让「视图区一直空着」变成查不出原因的怪事;
改为留痕(仍不影响步序推进,呈现是投影)。
实测:一封 RFQ 入站 → 放行前那一屏列出本单交付 LM358DR / TI / 12000 / 8.2 / 10周,
以及 Type A「直接快答」的分类结论与四项打标判据。
gates: tsc / eslint / depcruise(321 模块)/ api-report / 423 测试 全绿
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
110 lines
3.8 KiB
TypeScript
110 lines
3.8 KiB
TypeScript
/**
|
|
* module: flow-engine —— 本模块要外界提供的能力(编码规范 §10.1 / §5.3)。
|
|
*/
|
|
import type { FlowModel, StaffId, TaskId } from '@contracts';
|
|
import type { FlowPosition } from './internal/domain/step-sequence.ts';
|
|
|
|
export type Clock = { now(): Date };
|
|
export type IdGenerator = { next(): string };
|
|
|
|
/** 运行时实例状态 —— 三份持久状态之一(OS 09) */
|
|
export type FlowStateStore = {
|
|
save(taskId: TaskId, model: FlowModel, position: FlowPosition): Promise<void>;
|
|
load(taskId: TaskId): Promise<{ model: FlowModel; position: FlowPosition } | undefined>;
|
|
};
|
|
|
|
/** 条件谓词求值 —— app 注入 `@sfeel`;零 LLM */
|
|
export type PredicateEvaluator = (expression: string, scope: Readonly<Record<string, unknown>>) => boolean;
|
|
|
|
/** 任务数据 —— app 注入 `ledger`(L1):读任务视图 = 正式版 + 本任务草稿层 */
|
|
export type TaskDataGateway = {
|
|
view(taskId: TaskId): Promise<Record<string, unknown>>;
|
|
draftWrite(input: {
|
|
taskId: TaskId;
|
|
domain: string;
|
|
object: string;
|
|
key: Record<string, string | number>;
|
|
patch: Record<string, unknown>;
|
|
}): Promise<void>;
|
|
};
|
|
|
|
/** 工具步与纯工具步 —— app 注入 `toolbox`(L2) */
|
|
export type ToolStepInvoker = {
|
|
invoke(input: {
|
|
tool: string;
|
|
input: Record<string, unknown>;
|
|
activityId: string;
|
|
taskId: TaskId;
|
|
stepNo: number;
|
|
}): Promise<{ ok: boolean; output?: Record<string, unknown>; error?: { message: string; retryable: boolean } }>;
|
|
/** 写类工具在 flow 里也走「登记动作意向」,除非契约显式预授权 */
|
|
registerIntent(input: {
|
|
taskId: TaskId;
|
|
tool: string;
|
|
input: Record<string, unknown>;
|
|
label: string;
|
|
}): Promise<void>;
|
|
isWriteEffect(tool: string): Promise<boolean>;
|
|
};
|
|
|
|
/** LLM 步 —— app 注入 `llm`(L1)的 ModelGateway;模板与出参 schema 随声明版本冻结 */
|
|
export type LlmStepGateway = {
|
|
generate(input: {
|
|
model: string;
|
|
prompt: string;
|
|
outputSchema: Record<string, unknown>;
|
|
taskId: TaskId;
|
|
stepNo: number;
|
|
/** 出参原文 —— 解析失败时要能说清「模型到底回了什么」 */
|
|
}): Promise<{ ok: boolean; object?: unknown; text?: string; error?: string }>;
|
|
};
|
|
|
|
/** 出参校验 —— app 注入 ajv:LLM 步的出参**必过声明的 schema**(AM 12.3) */
|
|
export type SchemaValidator = {
|
|
validate(schema: Record<string, unknown>, value: unknown): { ok: boolean; errors: string[] };
|
|
};
|
|
|
|
/**
|
|
* 视图流控 —— app 注入 `interaction`(L3)。
|
|
* flow 按步序位置推声明的模版;推的是**填好数的 HTML**,容器只渲染不执行。
|
|
*/
|
|
export type ViewGateway = {
|
|
/** 按模版名取模版、以任务视图填数、推给前端;模版不存在就什么都不做 */
|
|
push(input: { taskId: TaskId; activityId: string; template: string; scope: Record<string, unknown> }): Promise<void>;
|
|
};
|
|
|
|
/** 人类工具步 —— app 注入 `human-tool`(L3) */
|
|
export type CardGateway = {
|
|
issue(input: {
|
|
taskId: TaskId;
|
|
ownerId: StaffId;
|
|
humanCallCap: number;
|
|
workOrder: 'execution' | 'acceptance';
|
|
card: Record<string, unknown>;
|
|
}): Promise<{ id: string }>;
|
|
};
|
|
|
|
/** 监听登记 —— app 注入 `event`(L1):异步步的完成条件 = 监听,时限必配 */
|
|
export type ListenerRegistrar = {
|
|
register(input: {
|
|
taskId: TaskId;
|
|
eventType: 'data' | 'time' | 'human';
|
|
keyFilter: Record<string, string>;
|
|
deadline: string;
|
|
}): Promise<{ id: string }>;
|
|
};
|
|
|
|
export type MemoryRecorder = {
|
|
append(input: {
|
|
taskId: TaskId;
|
|
kind: 'action' | 'result' | 'engine-transition';
|
|
title: string;
|
|
detail?: Record<string, unknown>;
|
|
}): Promise<void>;
|
|
};
|
|
|
|
/** 任务上下文 —— 活动 ID、负责人、预算 */
|
|
export type TaskContext = {
|
|
of(taskId: TaskId): Promise<{ activityId: string; ownerId: StaffId; humanCallCap: number }>;
|
|
};
|