Provider 侧:
· 新增 `provider-profile`(纯数据):各家的默认端点 / 结构化出参姿势 / 是否回推理
集中一处,adapter 只按画像行事 —— 新增 provider 只改画像,不写一族 if-else。
DeepSeek:`https://api.deepseek.com`,支持 json_object,v4 系列是推理模型。
· 建实例时 URL 可留空 → 取该 provider 的默认端点(openai-compatible 本就是
「随便哪个兼容端点」,没有默认,必须自填)。
· 推理正文归 `reasoning` 单列:那是**过程**不是结论,不进出参、不参与 schema 校验、
不入录制(录的是语义入参与结论)。
· 两档实例 deepseek-v4-flash / deepseek-v4-pro 已建;`quote-extractor` 是**用途名**,
指向 flash 档 —— 换档只改这一处绑定,活动一行不动。密钥只从环境读、落库即密封,
治理台只回「是否已绑定」(实测:库里是密文,视图无明文字段,三个实例探活全通)。
沿路修掉五处真问题:
① **同一任务被并发推进**:推进器与派发器各推各的,用的不是同一把锁 ——
实测同一步跑两遍、同一张卡发两张、同一行草稿写出两条互相打架的版本。
两者改用同一把 advisory lock(键的推导提取共用)。
② **LLM 步出参从不校验**:声明了 outputSchema,解析不出来就静默当 `{}` 写下去,
毛病要到下游缺字段时才炸、还炸在别处。改为「出参必过 schema」(AM 12.3),
不过即按声明的失败走向,并把模型原文记进留痕,好让人看见它到底回了什么。
③ **出参路径不认点号**:入参能走 `a.b.0.c`,出参只认扁平键 ——
模型按 schema 回嵌套结构(最常见的情形)就写出一条空记录。两边共用一份解析。
④ **归一步拿不到原文**:通道载荷只带指针不带正文、start→A1 边也没把主题正文传下去,
模型只拿到一个单号,于是回空 records。通道载荷带原文,边映射一路带到 A1。
⑤ **出参形状没讲给模型听**:声明了 schema 却只在提示里写「只输出 JSON」,
模型只能猜键名(实测回 `{"items":[]}` 而声明要 `{"records":[...]}`)。
改由网关按 outputSchema 统一附上说明 —— 这是系统的事,不该每个活动作者各写一遍。
另修:连接池没有 error 处理器,空闲连接被服务端掐断会让整个进程退出(一次运维操作
就能把 API 带崩)。
结果:一封 RFQ 入站 → 模型从正文抽出 pn/brand/eau/目标价/交期 →
D1 判定跳过补全 → 查历史价 → DMN 三分类 Type A → 验收放行卡,**全程零人工兜底**。
gates: tsc / eslint / depcruise(319 模块)/ api-report / 409 测试 全绿
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
101 lines
3.4 KiB
TypeScript
101 lines
3.4 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 注入 `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 }>;
|
|
};
|