Archived
app 装配收尾:18 个模块接上,MVP 判据 1-2 端到端跑通
装配(app 是唯一组装层): - wiring/engines.ts:skill/flow/loop/task/process/trigger 六个模块经端口接起来; 两个范式引擎按统一 ParadigmEngine 契约暴露给 task —— 对上层不可区分 - agent-runner.ts:loop 的 Agent 回路经 ModelGateway(两缝之一,回放审看得见); 出参过 schema 框住三型行动,不合规退化为「等人」而不是乱动世界 - container:interaction / human-tool / activity-registry / release 全部接上; event 的 TriggerMatcher 与 org 的 InFlightWorkProbe 以延迟绑定解环,端口不变 - workers/consumers.ts:派发器的两个消费者(任务监听推进旧单 / 触发器开新单,独立判定) - tools/first-party.ts:查历史价(录制回放替身)+ 三分类打标(纯工具,DMN 确定性求值) - seed-quote.ts:治理台四件事一次做完 —— 绑 LLM / 注册工具两审 / 经安装通道装活动包 / 发布流程包(闭合校验通过)+ 配触发器与邮件通道 已验证(真机): 1. 治理台配置全过;活动包静态审 0 条提示、流程包闭合校验通过 2. 一封 RFQ 邮件入站 → 归属解析 → 触发器命中 1 → 起一条工作流 → A1 任务按岗位唯一人解析落到 BD 冯兵,runtime=running 修复:zod 判别式重复值(周期触发两型展开为 periodic-calendar / periodic-data, triggerKindOf 对回三类分类学);活动构建脚本移出 activities(它要用 fs, 而活动包零环境能力 —— depcruise 拦下后按边界搬家) 358 个测试通过,门禁五道全绿 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
This commit is contained in:
co-authored by
Claude Opus 5
parent
c6cdb4c310
commit
78dc03bb58
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"messageId": "m-2001",
|
||||||
|
"from": "buyer@acme.com",
|
||||||
|
"to": [
|
||||||
|
"rfq@we.com"
|
||||||
|
],
|
||||||
|
"cc": [],
|
||||||
|
"subject": "询价 RFQ-A7X 车规 Analog(客户 C001)",
|
||||||
|
"body": "请报价:LM358DR 5000pcs 交期8周;NE555P 2000pcs;TL072CDR 1000pcs。单号 RFQ-A7X,客户码 C001。",
|
||||||
|
"receivedAt": "2026-07-27T02:00:00.000Z",
|
||||||
|
"attachments": []
|
||||||
|
}
|
||||||
@@ -179,7 +179,7 @@ module.exports = {
|
|||||||
options: {
|
options: {
|
||||||
doNotFollow: { path: 'node_modules' },
|
doNotFollow: { path: 'node_modules' },
|
||||||
exclude: {
|
exclude: {
|
||||||
path: ['(^|/)dist/', '(^|/)node_modules/', '(^|/)api-report/', '\\.d\\.ts$'],
|
path: ['(^|/)dist/', '(^|/)\\.build/', '(^|/)node_modules/', '(^|/)api-report/', '\\.d\\.ts$'],
|
||||||
},
|
},
|
||||||
tsPreCompilationDeps: true,
|
tsPreCompilationDeps: true,
|
||||||
tsConfig: { fileName: 'tsconfig.base.json' },
|
tsConfig: { fileName: 'tsconfig.base.json' },
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
dist/
|
dist/
|
||||||
|
.build/
|
||||||
.api-tmp/
|
.api-tmp/
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
@@ -3,5 +3,7 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": { ".": "./src/index.ts" }
|
"exports": {
|
||||||
|
".": "./src/index.ts"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-11
@@ -38,8 +38,8 @@ export declare const formFieldKindSchema: z.ZodEnum<{
|
|||||||
number: "number";
|
number: "number";
|
||||||
file: "file";
|
file: "file";
|
||||||
text: "text";
|
text: "text";
|
||||||
date: "date";
|
|
||||||
check: "check";
|
check: "check";
|
||||||
|
date: "date";
|
||||||
textarea: "textarea";
|
textarea: "textarea";
|
||||||
radio: "radio";
|
radio: "radio";
|
||||||
select: "select";
|
select: "select";
|
||||||
@@ -54,8 +54,8 @@ export declare const formFieldSchema: z.ZodObject<{
|
|||||||
number: "number";
|
number: "number";
|
||||||
file: "file";
|
file: "file";
|
||||||
text: "text";
|
text: "text";
|
||||||
date: "date";
|
|
||||||
check: "check";
|
check: "check";
|
||||||
|
date: "date";
|
||||||
textarea: "textarea";
|
textarea: "textarea";
|
||||||
radio: "radio";
|
radio: "radio";
|
||||||
select: "select";
|
select: "select";
|
||||||
@@ -191,8 +191,8 @@ export declare const viewBlockSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|||||||
}, z.core.$strip>, z.ZodObject<{
|
}, z.core.$strip>, z.ZodObject<{
|
||||||
t: z.ZodLiteral<"mail">;
|
t: z.ZodLiteral<"mail">;
|
||||||
dir: z.ZodEnum<{
|
dir: z.ZodEnum<{
|
||||||
in: "in";
|
|
||||||
out: "out";
|
out: "out";
|
||||||
|
in: "in";
|
||||||
}>;
|
}>;
|
||||||
statusLabel: z.ZodOptional<z.ZodString>;
|
statusLabel: z.ZodOptional<z.ZodString>;
|
||||||
editable: z.ZodOptional<z.ZodBoolean>;
|
editable: z.ZodOptional<z.ZodBoolean>;
|
||||||
@@ -418,8 +418,8 @@ export declare const deliverableSchema: z.ZodObject<{
|
|||||||
}, z.core.$strip>, z.ZodObject<{
|
}, z.core.$strip>, z.ZodObject<{
|
||||||
t: z.ZodLiteral<"mail">;
|
t: z.ZodLiteral<"mail">;
|
||||||
dir: z.ZodEnum<{
|
dir: z.ZodEnum<{
|
||||||
in: "in";
|
|
||||||
out: "out";
|
out: "out";
|
||||||
|
in: "in";
|
||||||
}>;
|
}>;
|
||||||
statusLabel: z.ZodOptional<z.ZodString>;
|
statusLabel: z.ZodOptional<z.ZodString>;
|
||||||
editable: z.ZodOptional<z.ZodBoolean>;
|
editable: z.ZodOptional<z.ZodBoolean>;
|
||||||
@@ -652,8 +652,8 @@ export declare const executionWorkOrderSchema: z.ZodObject<{
|
|||||||
}, z.core.$strip>, z.ZodObject<{
|
}, z.core.$strip>, z.ZodObject<{
|
||||||
t: z.ZodLiteral<"mail">;
|
t: z.ZodLiteral<"mail">;
|
||||||
dir: z.ZodEnum<{
|
dir: z.ZodEnum<{
|
||||||
in: "in";
|
|
||||||
out: "out";
|
out: "out";
|
||||||
|
in: "in";
|
||||||
}>;
|
}>;
|
||||||
statusLabel: z.ZodOptional<z.ZodString>;
|
statusLabel: z.ZodOptional<z.ZodString>;
|
||||||
editable: z.ZodOptional<z.ZodBoolean>;
|
editable: z.ZodOptional<z.ZodBoolean>;
|
||||||
@@ -773,8 +773,8 @@ export declare const executionWorkOrderSchema: z.ZodObject<{
|
|||||||
number: "number";
|
number: "number";
|
||||||
file: "file";
|
file: "file";
|
||||||
text: "text";
|
text: "text";
|
||||||
date: "date";
|
|
||||||
check: "check";
|
check: "check";
|
||||||
|
date: "date";
|
||||||
textarea: "textarea";
|
textarea: "textarea";
|
||||||
radio: "radio";
|
radio: "radio";
|
||||||
select: "select";
|
select: "select";
|
||||||
@@ -863,8 +863,8 @@ export declare const acceptanceWorkOrderSchema: z.ZodObject<{
|
|||||||
number: "number";
|
number: "number";
|
||||||
file: "file";
|
file: "file";
|
||||||
text: "text";
|
text: "text";
|
||||||
date: "date";
|
|
||||||
check: "check";
|
check: "check";
|
||||||
|
date: "date";
|
||||||
textarea: "textarea";
|
textarea: "textarea";
|
||||||
radio: "radio";
|
radio: "radio";
|
||||||
select: "select";
|
select: "select";
|
||||||
@@ -995,8 +995,8 @@ export declare const workOrderSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|||||||
}, z.core.$strip>, z.ZodObject<{
|
}, z.core.$strip>, z.ZodObject<{
|
||||||
t: z.ZodLiteral<"mail">;
|
t: z.ZodLiteral<"mail">;
|
||||||
dir: z.ZodEnum<{
|
dir: z.ZodEnum<{
|
||||||
in: "in";
|
|
||||||
out: "out";
|
out: "out";
|
||||||
|
in: "in";
|
||||||
}>;
|
}>;
|
||||||
statusLabel: z.ZodOptional<z.ZodString>;
|
statusLabel: z.ZodOptional<z.ZodString>;
|
||||||
editable: z.ZodOptional<z.ZodBoolean>;
|
editable: z.ZodOptional<z.ZodBoolean>;
|
||||||
@@ -1116,8 +1116,8 @@ export declare const workOrderSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|||||||
number: "number";
|
number: "number";
|
||||||
file: "file";
|
file: "file";
|
||||||
text: "text";
|
text: "text";
|
||||||
date: "date";
|
|
||||||
check: "check";
|
check: "check";
|
||||||
|
date: "date";
|
||||||
textarea: "textarea";
|
textarea: "textarea";
|
||||||
radio: "radio";
|
radio: "radio";
|
||||||
select: "select";
|
select: "select";
|
||||||
@@ -1180,8 +1180,8 @@ export declare const workOrderSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|||||||
number: "number";
|
number: "number";
|
||||||
file: "file";
|
file: "file";
|
||||||
text: "text";
|
text: "text";
|
||||||
date: "date";
|
|
||||||
check: "check";
|
check: "check";
|
||||||
|
date: "date";
|
||||||
textarea: "textarea";
|
textarea: "textarea";
|
||||||
radio: "radio";
|
radio: "radio";
|
||||||
select: "select";
|
select: "select";
|
||||||
@@ -1375,9 +1375,9 @@ export declare const chatMessageSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|||||||
}, z.core.$strip>, z.ZodObject<{
|
}, z.core.$strip>, z.ZodObject<{
|
||||||
k: z.ZodLiteral<"system">;
|
k: z.ZodLiteral<"system">;
|
||||||
type: z.ZodEnum<{
|
type: z.ZodEnum<{
|
||||||
time: "time";
|
|
||||||
human: "human";
|
human: "human";
|
||||||
data: "data";
|
data: "data";
|
||||||
|
time: "time";
|
||||||
control: "control";
|
control: "control";
|
||||||
}>;
|
}>;
|
||||||
text: z.ZodString;
|
text: z.ZodString;
|
||||||
|
|||||||
+5
-5
@@ -8,9 +8,9 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
export declare const EVENT_TYPES: readonly ["data", "time", "human", "control"];
|
export declare const EVENT_TYPES: readonly ["data", "time", "human", "control"];
|
||||||
export declare const eventTypeSchema: z.ZodEnum<{
|
export declare const eventTypeSchema: z.ZodEnum<{
|
||||||
time: "time";
|
|
||||||
human: "human";
|
human: "human";
|
||||||
data: "data";
|
data: "data";
|
||||||
|
time: "time";
|
||||||
control: "control";
|
control: "control";
|
||||||
}>;
|
}>;
|
||||||
export type EventType = z.infer<typeof eventTypeSchema>;
|
export type EventType = z.infer<typeof eventTypeSchema>;
|
||||||
@@ -71,9 +71,9 @@ export type EventPayload = z.infer<typeof eventPayloadSchema>;
|
|||||||
export declare const envelopeSchema: z.ZodObject<{
|
export declare const envelopeSchema: z.ZodObject<{
|
||||||
id: z.core.$ZodBranded<z.ZodString, "EventId", "out">;
|
id: z.core.$ZodBranded<z.ZodString, "EventId", "out">;
|
||||||
type: z.ZodEnum<{
|
type: z.ZodEnum<{
|
||||||
time: "time";
|
|
||||||
human: "human";
|
human: "human";
|
||||||
data: "data";
|
data: "data";
|
||||||
|
time: "time";
|
||||||
control: "control";
|
control: "control";
|
||||||
}>;
|
}>;
|
||||||
correlation: z.ZodObject<{
|
correlation: z.ZodObject<{
|
||||||
@@ -134,9 +134,9 @@ export type Envelope = z.infer<typeof envelopeSchema>;
|
|||||||
*/
|
*/
|
||||||
export declare const envelopeDraftSchema: z.ZodObject<{
|
export declare const envelopeDraftSchema: z.ZodObject<{
|
||||||
type: z.ZodEnum<{
|
type: z.ZodEnum<{
|
||||||
time: "time";
|
|
||||||
human: "human";
|
human: "human";
|
||||||
data: "data";
|
data: "data";
|
||||||
|
time: "time";
|
||||||
control: "control";
|
control: "control";
|
||||||
}>;
|
}>;
|
||||||
payload: z.ZodObject<{
|
payload: z.ZodObject<{
|
||||||
@@ -235,9 +235,9 @@ export declare const listenerSpecSchema: z.ZodObject<{
|
|||||||
dynamic: "dynamic";
|
dynamic: "dynamic";
|
||||||
}>;
|
}>;
|
||||||
eventType: z.ZodEnum<{
|
eventType: z.ZodEnum<{
|
||||||
time: "time";
|
|
||||||
human: "human";
|
human: "human";
|
||||||
data: "data";
|
data: "data";
|
||||||
|
time: "time";
|
||||||
control: "control";
|
control: "control";
|
||||||
}>;
|
}>;
|
||||||
keyFilter: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
|
keyFilter: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
|
||||||
@@ -260,9 +260,9 @@ export declare const registeredListenerSchema: z.ZodObject<{
|
|||||||
dynamic: "dynamic";
|
dynamic: "dynamic";
|
||||||
}>;
|
}>;
|
||||||
eventType: z.ZodEnum<{
|
eventType: z.ZodEnum<{
|
||||||
time: "time";
|
|
||||||
human: "human";
|
human: "human";
|
||||||
data: "data";
|
data: "data";
|
||||||
|
time: "time";
|
||||||
control: "control";
|
control: "control";
|
||||||
}>;
|
}>;
|
||||||
keyFilter: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
|
keyFilter: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
|
||||||
|
|||||||
+2
-2
@@ -77,9 +77,9 @@ export declare const manifestDeclarationsSchema: z.ZodObject<{
|
|||||||
dynamic: "dynamic";
|
dynamic: "dynamic";
|
||||||
}>;
|
}>;
|
||||||
eventType: z.ZodEnum<{
|
eventType: z.ZodEnum<{
|
||||||
time: "time";
|
|
||||||
human: "human";
|
human: "human";
|
||||||
data: "data";
|
data: "data";
|
||||||
|
time: "time";
|
||||||
control: "control";
|
control: "control";
|
||||||
}>;
|
}>;
|
||||||
keyFilter: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
|
keyFilter: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
|
||||||
@@ -954,9 +954,9 @@ export declare const activityManifestSchema: z.ZodObject<{
|
|||||||
dynamic: "dynamic";
|
dynamic: "dynamic";
|
||||||
}>;
|
}>;
|
||||||
eventType: z.ZodEnum<{
|
eventType: z.ZodEnum<{
|
||||||
time: "time";
|
|
||||||
human: "human";
|
human: "human";
|
||||||
data: "data";
|
data: "data";
|
||||||
|
time: "time";
|
||||||
control: "control";
|
control: "control";
|
||||||
}>;
|
}>;
|
||||||
keyFilter: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
|
keyFilter: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
|
||||||
|
|||||||
+5
-5
@@ -520,12 +520,10 @@ export declare const triggerConfigSchema: z.ZodObject<{
|
|||||||
}, z.core.$strip>], "from">;
|
}, z.core.$strip>], "from">;
|
||||||
}, z.core.$strip>>>;
|
}, z.core.$strip>>>;
|
||||||
spec: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
spec: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
||||||
kind: z.ZodLiteral<"periodic">;
|
kind: z.ZodLiteral<"periodic-calendar">;
|
||||||
mode: z.ZodLiteral<"calendar">;
|
|
||||||
calendar: z.ZodString;
|
calendar: z.ZodString;
|
||||||
}, z.core.$strip>, z.ZodObject<{
|
}, z.core.$strip>, z.ZodObject<{
|
||||||
kind: z.ZodLiteral<"periodic">;
|
kind: z.ZodLiteral<"periodic-data">;
|
||||||
mode: z.ZodLiteral<"data-calendar">;
|
|
||||||
domain: z.ZodString;
|
domain: z.ZodString;
|
||||||
object: z.ZodString;
|
object: z.ZodString;
|
||||||
dateField: z.ZodString;
|
dateField: z.ZodString;
|
||||||
@@ -535,9 +533,9 @@ export declare const triggerConfigSchema: z.ZodObject<{
|
|||||||
kind: z.ZodLiteral<"event">;
|
kind: z.ZodLiteral<"event">;
|
||||||
rule: z.ZodString;
|
rule: z.ZodString;
|
||||||
eventType: z.ZodDefault<z.ZodEnum<{
|
eventType: z.ZodDefault<z.ZodEnum<{
|
||||||
time: "time";
|
|
||||||
human: "human";
|
human: "human";
|
||||||
data: "data";
|
data: "data";
|
||||||
|
time: "time";
|
||||||
control: "control";
|
control: "control";
|
||||||
}>>;
|
}>>;
|
||||||
}, z.core.$strip>, z.ZodObject<{
|
}, z.core.$strip>, z.ZodObject<{
|
||||||
@@ -546,3 +544,5 @@ export declare const triggerConfigSchema: z.ZodObject<{
|
|||||||
}, z.core.$strip>], "kind">;
|
}, z.core.$strip>], "kind">;
|
||||||
}, z.core.$strip>;
|
}, z.core.$strip>;
|
||||||
export type TriggerConfig = z.infer<typeof triggerConfigSchema>;
|
export type TriggerConfig = z.infer<typeof triggerConfigSchema>;
|
||||||
|
/** 四个判别值 → 三类分类学(OS 3.1) */
|
||||||
|
export declare function triggerKindOf(spec: TriggerConfig['spec']): TriggerKind;
|
||||||
|
|||||||
@@ -0,0 +1,139 @@
|
|||||||
|
/**
|
||||||
|
* loop 的 Agent 回路 —— `AgentPort` 的实现(app 层装配,编码规范 §5.2)。
|
||||||
|
*
|
||||||
|
* 「回合由 usecase 驱动,**不放开自由 maxSteps**,预算在环外由 Harness 强制」——
|
||||||
|
* 所以这里只做**一次**决断:把 Enabled 集与红绿灯喂给模型,要它在三型行动里选一个。
|
||||||
|
*
|
||||||
|
* 一切模型调用经 `ModelGateway`(两缝之一),replay 模式下自动走录制 —— 回放审看得见。
|
||||||
|
* 见 docs/debt.md · M5-2:§6.1 规定这里用 Mastra `Agent`;当前直接经 ModelGateway,
|
||||||
|
* 端口不变,换实现时 usecase 不动。
|
||||||
|
*/
|
||||||
|
import type { AgentAction, EnabledSet } from '@modules/loop-engine';
|
||||||
|
import type { ModelGateway } from '@modules/llm';
|
||||||
|
import type { TaskId } from '@contracts';
|
||||||
|
|
||||||
|
/** Agent 只能选三型行动之一(外加规划 / 弃用 / 评估 / 提交)—— schema 把它框住 */
|
||||||
|
const ACTION_SCHEMA = {
|
||||||
|
type: 'object',
|
||||||
|
required: ['kind'],
|
||||||
|
properties: {
|
||||||
|
kind: { enum: ['start', 'plan', 'disable', 'judge', 'tool', 'human', 'listen', 'submit'] },
|
||||||
|
itemId: { type: 'string' },
|
||||||
|
tool: { type: 'string' },
|
||||||
|
input: { type: 'object' },
|
||||||
|
workOrder: { enum: ['execution', 'acceptance'] },
|
||||||
|
card: { type: 'object' },
|
||||||
|
eventType: { enum: ['data', 'time'] },
|
||||||
|
keyFilter: { type: 'object' },
|
||||||
|
deadline: { type: 'string' },
|
||||||
|
criterion: { type: 'string' },
|
||||||
|
satisfied: { type: 'boolean' },
|
||||||
|
rationale: { type: 'string' },
|
||||||
|
reason: { type: 'string' },
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type AgentRunnerDeps = {
|
||||||
|
readonly gateway: ModelGateway;
|
||||||
|
/** 活动声明的自定义模型名 */
|
||||||
|
readonly modelOf: (taskId: TaskId) => Promise<string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createAgentRunner(deps: AgentRunnerDeps): {
|
||||||
|
decide(input: { taskId: TaskId; narrative: string; enabled: EnabledSet; scope: Record<string, unknown> }): Promise<AgentAction>;
|
||||||
|
} {
|
||||||
|
return {
|
||||||
|
async decide({ taskId, narrative, enabled, scope }) {
|
||||||
|
// 交集为空即断点 —— 引擎已经算好了,这里只把它讲给模型听
|
||||||
|
const breakpoint = enabled.entries.length === 0;
|
||||||
|
|
||||||
|
const prompt = [
|
||||||
|
'你是这一单任务的案例工作者。下面是语义引擎发布的**当下可为**与**目标缺口**。',
|
||||||
|
'你只能选一个行动,且必须落在三型行动之内:调工具 / 调人 / 立监听(外加 start、plan、disable、judge、submit)。',
|
||||||
|
'规矩:①触达世界的动作只能 tool(写类会被登记为动作意向,放行时才落地)',
|
||||||
|
'②缺口只有人知道 → human;缺口在世界侧 → listen(必须给 deadline,ISO-8601)',
|
||||||
|
'③成果已齐才 submit。只输出 JSON。',
|
||||||
|
'',
|
||||||
|
`【案例说明书与活动记忆】\n${narrative}`,
|
||||||
|
'',
|
||||||
|
`【当下可为(Enabled 集)】\n${
|
||||||
|
enabled.entries.map((e) => `- ${e.id} ${e.name}(${e.type}${e.requiresManualStart ? ' · 须你启动' : ''}${e.discretionary ? ' · 裁量项' : ''})`).join('\n') || '(空)'
|
||||||
|
}`,
|
||||||
|
`【目标缺口】\n${enabled.gaps.join('、') || '(无)'}`,
|
||||||
|
enabled.judgements.length === 0
|
||||||
|
? ''
|
||||||
|
: `【待你评估的语义性前提】\n${enabled.judgements.map((j) => `- ${j.criterion}:${j.statement}`).join('\n')}`,
|
||||||
|
breakpoint ? '【注意】当下可为 ∩ 目标所需 = 空 —— 这是依赖断点,请调人或立监听。' : '',
|
||||||
|
`【任务数据】\n${JSON.stringify(scope).slice(0, 4000)}`,
|
||||||
|
]
|
||||||
|
.filter((line) => line !== '')
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
|
const model = await deps.modelOf(taskId);
|
||||||
|
const result = await deps.gateway.generate({
|
||||||
|
model,
|
||||||
|
prompt,
|
||||||
|
outputSchema: ACTION_SCHEMA,
|
||||||
|
taskId,
|
||||||
|
label: 'loop-turn',
|
||||||
|
});
|
||||||
|
|
||||||
|
return normalize(result.object, enabled);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 模型出参归一为 AgentAction;不合规一律退化为「立监听等人」而不是乱动世界 */
|
||||||
|
function normalize(object: unknown, enabled: EnabledSet): AgentAction {
|
||||||
|
const raw = (object ?? {}) as Record<string, unknown>;
|
||||||
|
const kind = String(raw['kind'] ?? '');
|
||||||
|
const itemId = String(raw['itemId'] ?? enabled.entries[0]?.id ?? '');
|
||||||
|
|
||||||
|
switch (kind) {
|
||||||
|
case 'start':
|
||||||
|
case 'plan':
|
||||||
|
return { kind: kind as 'start' | 'plan', itemId };
|
||||||
|
case 'disable':
|
||||||
|
return { kind: 'disable', itemId, reason: String(raw['reason'] ?? '此单不走这条') };
|
||||||
|
case 'judge':
|
||||||
|
return {
|
||||||
|
kind: 'judge',
|
||||||
|
criterion: String(raw['criterion'] ?? ''),
|
||||||
|
satisfied: raw['satisfied'] === true,
|
||||||
|
rationale: String(raw['rationale'] ?? ''),
|
||||||
|
};
|
||||||
|
case 'tool':
|
||||||
|
return {
|
||||||
|
kind: 'tool',
|
||||||
|
itemId,
|
||||||
|
tool: String(raw['tool'] ?? ''),
|
||||||
|
input: (raw['input'] ?? {}) as Record<string, unknown>,
|
||||||
|
};
|
||||||
|
case 'human':
|
||||||
|
return {
|
||||||
|
kind: 'human',
|
||||||
|
itemId,
|
||||||
|
card: (raw['card'] ?? {}) as Record<string, unknown>,
|
||||||
|
workOrder: raw['workOrder'] === 'acceptance' ? 'acceptance' : 'execution',
|
||||||
|
};
|
||||||
|
case 'listen':
|
||||||
|
return {
|
||||||
|
kind: 'listen',
|
||||||
|
itemId,
|
||||||
|
eventType: raw['eventType'] === 'time' ? 'time' : 'data',
|
||||||
|
keyFilter: (raw['keyFilter'] ?? {}) as Record<string, string>,
|
||||||
|
// 时限必配 —— 模型没给就按三天兜底,绝不留幽灵等待
|
||||||
|
deadline: typeof raw['deadline'] === 'string' && /^P/.test(raw['deadline']) ? raw['deadline'] : 'P3D',
|
||||||
|
};
|
||||||
|
case 'submit':
|
||||||
|
return { kind: 'submit', rationale: String(raw['rationale'] ?? '判据已收敛') };
|
||||||
|
default:
|
||||||
|
// 看不懂就等人,不擅自触达世界
|
||||||
|
return {
|
||||||
|
kind: 'human',
|
||||||
|
itemId,
|
||||||
|
card: { title: '需要你判断下一步', reason: '模型未给出合规的行动' },
|
||||||
|
workOrder: 'execution',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
+289
-12
@@ -42,6 +42,19 @@ import {
|
|||||||
} from '@modules/toolbox';
|
} from '@modules/toolbox';
|
||||||
import type { MessageService } from '@modules/message';
|
import type { MessageService } from '@modules/message';
|
||||||
import { createMessageRepository, createMessageService } from '@modules/message';
|
import { createMessageRepository, createMessageService } from '@modules/message';
|
||||||
|
import type { HumanToolService } from '@modules/human-tool';
|
||||||
|
import { createCardStore, createHumanToolService } from '@modules/human-tool';
|
||||||
|
import type { InteractionService } from '@modules/interaction';
|
||||||
|
import { createInteractionService, createInteractionStore, templateIsSafe } from '@modules/interaction';
|
||||||
|
import type { ActivityRegistryService } from '@modules/activity-registry';
|
||||||
|
import { createRegistryService, createRegistryStore } from '@modules/activity-registry';
|
||||||
|
import type { ReleaseService } from '@modules/release';
|
||||||
|
import { createIntentStore, createReleaseService } from '@modules/release';
|
||||||
|
import type { TaskService } from '@modules/task';
|
||||||
|
import type { ProcessService } from '@modules/process';
|
||||||
|
import type { TriggerService } from '@modules/trigger';
|
||||||
|
import { FIRST_PARTY_TOOLS } from './tools/first-party.ts';
|
||||||
|
import { wireEngines } from './wiring/engines.ts';
|
||||||
import type { ChannelService } from '@modules/channel';
|
import type { ChannelService } from '@modules/channel';
|
||||||
import {
|
import {
|
||||||
createChannelRepository,
|
createChannelRepository,
|
||||||
@@ -68,21 +81,45 @@ export type Container = {
|
|||||||
readonly toolInvoker: ToolInvoker;
|
readonly toolInvoker: ToolInvoker;
|
||||||
readonly message: MessageService;
|
readonly message: MessageService;
|
||||||
readonly channel: ChannelService;
|
readonly channel: ChannelService;
|
||||||
|
readonly humanTool: HumanToolService;
|
||||||
|
readonly interaction: InteractionService;
|
||||||
|
readonly registry: ActivityRegistryService;
|
||||||
|
readonly release: ReleaseService;
|
||||||
|
readonly task: TaskService;
|
||||||
|
readonly process: ProcessService;
|
||||||
|
readonly trigger: TriggerService;
|
||||||
close(): Promise<void>;
|
close(): Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 在途工作探针 —— 停用前置检查要问的是 task(L6)与 human-tool(L3),二者随 M3/M4 落地。
|
* 在途工作探针 —— 停用前置检查要问的是 task(L6)与 human-tool(L3),二者层级在 org 之上,
|
||||||
* 占位返回空集;M4 起接真实用例(docs/debt.md · M1-1)。
|
* 故以**延迟绑定**的端口接入:org 先构造,真身就位后回填(§5.2 跨层协作经端口)。
|
||||||
*/
|
*/
|
||||||
const emptyInFlightProbe: InFlightWorkProbe = {
|
function createDeferredInFlightProbe(): InFlightWorkProbe & { bind(real: InFlightWorkProbe): void } {
|
||||||
openTasksOf: async () => [],
|
let real: InFlightWorkProbe | undefined;
|
||||||
openCardsOf: async () => [],
|
return {
|
||||||
openApprovalsOf: async () => [],
|
openTasksOf: async (id) => (real === undefined ? [] : real.openTasksOf(id)),
|
||||||
};
|
openCardsOf: async (id) => (real === undefined ? [] : real.openCardsOf(id)),
|
||||||
|
openApprovalsOf: async (id) => (real === undefined ? [] : real.openApprovalsOf(id)),
|
||||||
|
bind(next) {
|
||||||
|
real = next;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/** 触发器判定 —— trigger(L7)随 M5 落地,当下恒不命中(docs/debt.md · M1-1) */
|
/**
|
||||||
const noTriggerMatcher: TriggerMatcher = { match: async () => [] };
|
* 触发器判定的**延迟绑定**:`event`(L1)在 `trigger`(L7)之前构造,
|
||||||
|
* 用一层可后填的间接层把环解开 —— 端口不变,装配顺序解耦(§5.2)。
|
||||||
|
*/
|
||||||
|
function createDeferredTriggerMatcher(): TriggerMatcher & { bind(real: TriggerMatcher): void } {
|
||||||
|
let real: TriggerMatcher | undefined;
|
||||||
|
return {
|
||||||
|
match: async (envelope) => (real === undefined ? [] : real.match(envelope)),
|
||||||
|
bind(next) {
|
||||||
|
real = next;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 监听内容条件的求值 —— @sfeel 的求值语义**前后端同源**(§8.3)。
|
* 监听内容条件的求值 —— @sfeel 的求值语义**前后端同源**(§8.3)。
|
||||||
@@ -100,7 +137,7 @@ const sfeelEvaluator = (expression: string, scope: Readonly<Record<string, unkno
|
|||||||
};
|
};
|
||||||
|
|
||||||
/** 第一方工具实现 —— 由 app 注册进 ToolRuntime;活动只见调用契约 */
|
/** 第一方工具实现 —— 由 app 注册进 ToolRuntime;活动只见调用契约 */
|
||||||
export const firstPartyToolHandlers = new Map<string, ToolHandler>();
|
export const firstPartyToolHandlers = new Map<string, ToolHandler>(FIRST_PARTY_TOOLS);
|
||||||
|
|
||||||
export async function createContainer(config: AppConfig): Promise<Container> {
|
export async function createContainer(config: AppConfig): Promise<Container> {
|
||||||
const pool = new Pool({ connectionString: config.databaseUrl, max: 10 });
|
const pool = new Pool({ connectionString: config.databaseUrl, max: 10 });
|
||||||
@@ -111,11 +148,13 @@ export async function createContainer(config: AppConfig): Promise<Container> {
|
|||||||
// 草稿层的建表:ledger schema 的唯一写者是 ledger adapter,DDL 在其出口给出
|
// 草稿层的建表:ledger schema 的唯一写者是 ledger adapter,DDL 在其出口给出
|
||||||
await pool.query(DRAFT_DDL);
|
await pool.query(DRAFT_DDL);
|
||||||
|
|
||||||
|
const inFlightProbe = createDeferredInFlightProbe();
|
||||||
|
|
||||||
const org = createOrgService({
|
const org = createOrgService({
|
||||||
positions: createPositionRepository(db),
|
positions: createPositionRepository(db),
|
||||||
staff: createStaffRepository(db),
|
staff: createStaffRepository(db),
|
||||||
grants: createGrantRepository(db),
|
grants: createGrantRepository(db),
|
||||||
inFlight: emptyInFlightProbe,
|
inFlight: inFlightProbe,
|
||||||
ids,
|
ids,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -142,12 +181,14 @@ export async function createContainer(config: AppConfig): Promise<Container> {
|
|||||||
ids,
|
ids,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const triggerMatcher = createDeferredTriggerMatcher();
|
||||||
|
|
||||||
const event = createEventService({
|
const event = createEventService({
|
||||||
store: createEventStore(db, ids),
|
store: createEventStore(db, ids),
|
||||||
clock: systemClock,
|
clock: systemClock,
|
||||||
ids,
|
ids,
|
||||||
evaluate: sfeelEvaluator,
|
evaluate: sfeelEvaluator,
|
||||||
triggers: noTriggerMatcher,
|
triggers: triggerMatcher,
|
||||||
messages: message,
|
messages: message,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -224,6 +265,235 @@ export async function createContainer(config: AppConfig): Promise<Container> {
|
|||||||
ids,
|
ids,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* ── L3/L4:交互、人类工具、活动库、放行门 ─────────────────────── */
|
||||||
|
|
||||||
|
const interaction = createInteractionService({
|
||||||
|
store: createInteractionStore(db),
|
||||||
|
events: {
|
||||||
|
publishHumanEvent: async (input) => {
|
||||||
|
await event.publish({
|
||||||
|
type: 'human',
|
||||||
|
correlation: {
|
||||||
|
target: { kind: 'task', id: input.taskId },
|
||||||
|
cause: { kind: 'card-reply', ref: input.cardId },
|
||||||
|
keys: { cardId: input.cardId },
|
||||||
|
},
|
||||||
|
payload: { data: input.payload },
|
||||||
|
occurredAt: systemClock.now().toISOString(),
|
||||||
|
source: { kind: 'desk' },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
memory: {
|
||||||
|
append: async (input) =>
|
||||||
|
void (await memory.append({
|
||||||
|
...input,
|
||||||
|
stamp: { contractVersion: '1', businessModelVersion: '1', skillVersions: [] },
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
audience: { canSee: async () => true },
|
||||||
|
clock: systemClock,
|
||||||
|
ids,
|
||||||
|
});
|
||||||
|
|
||||||
|
const humanTool = createHumanToolService({
|
||||||
|
cards: createCardStore(db),
|
||||||
|
outbox: { sendElicitation: (input) => message.sendElicitation(input) },
|
||||||
|
memory: {
|
||||||
|
append: async (input) =>
|
||||||
|
void (await memory.append({
|
||||||
|
...input,
|
||||||
|
stamp: { contractVersion: '1', businessModelVersion: '1', skillVersions: [] },
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
events: {
|
||||||
|
publishHumanEvent: async (input) => {
|
||||||
|
await event.publish({
|
||||||
|
type: 'human',
|
||||||
|
correlation: {
|
||||||
|
target: { kind: 'task', id: input.taskId },
|
||||||
|
cause: { kind: 'card-reply', ref: input.cardId },
|
||||||
|
keys: { cardId: input.cardId },
|
||||||
|
},
|
||||||
|
payload: { data: input.payload },
|
||||||
|
occurredAt: systemClock.now().toISOString(),
|
||||||
|
source: { kind: 'desk' },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
clock: systemClock,
|
||||||
|
ids,
|
||||||
|
});
|
||||||
|
|
||||||
|
const registry = createRegistryService({
|
||||||
|
store: createRegistryStore(db),
|
||||||
|
tools: { assertContractsAvailable: (names) => toolbox.assertContractsAvailable(names) },
|
||||||
|
models: { assertNamesBound: (names) => llm.assertNamesBound(names) },
|
||||||
|
ledger: {
|
||||||
|
applyDeclarations: async (declarations) => {
|
||||||
|
// 安装时依声明**自生长**:声明合并 → DDL → 只增不破坏
|
||||||
|
const results = await ledger.applyDeclarations(declarations);
|
||||||
|
return results.map((r) => ({
|
||||||
|
applied: r.applied,
|
||||||
|
conflicts: r.conflicts.map((c) => ({ field: c.field, message: c.message })),
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
templates: { isSafe: templateIsSafe },
|
||||||
|
clock: systemClock,
|
||||||
|
ids,
|
||||||
|
// 首次安装无历史 → 免回放审(OS 11.2)
|
||||||
|
replayCases: async () => [],
|
||||||
|
sdkVersion: '0.1.0-m5',
|
||||||
|
});
|
||||||
|
|
||||||
|
const release = createReleaseService({
|
||||||
|
intents: createIntentStore(db),
|
||||||
|
ledger: {
|
||||||
|
checkConflicts: async (taskId) =>
|
||||||
|
(await ledger.checkConflicts(taskId)).map((c) => ({ field: c.field, key: c.key })),
|
||||||
|
releaseWrite: (taskId) => ledger.releaseWrite(taskId),
|
||||||
|
},
|
||||||
|
tools: {
|
||||||
|
isInvocable: async (tool, activityId) => {
|
||||||
|
const contract = await toolbox.get(tool);
|
||||||
|
if (contract === undefined) return { ok: false, reason: `工具未注册:${tool}` };
|
||||||
|
const allowlist = await toolbox.allowlistOf(activityId);
|
||||||
|
if (contract.pure !== true && !allowlist.includes(tool)) {
|
||||||
|
return { ok: false, reason: `工具不在本活动白名单:${tool}` };
|
||||||
|
}
|
||||||
|
return { ok: true };
|
||||||
|
},
|
||||||
|
validateInput: async (tool, input) => {
|
||||||
|
const contract = await toolbox.get(tool);
|
||||||
|
if (contract === undefined) return { ok: false, errors: [`工具未注册:${tool}`] };
|
||||||
|
return schemas.validate(contract.input, input);
|
||||||
|
},
|
||||||
|
validateTarget: async (input) => {
|
||||||
|
const result = await toolInvoker.validate({ tool: input.tool, input: input.payload, activityId: input.activityId, taskId: input.taskId });
|
||||||
|
if (result === undefined) return undefined;
|
||||||
|
return { ok: result.ok, ...(result.error === undefined ? {} : { error: result.error.message }) };
|
||||||
|
},
|
||||||
|
execute: async (input) => {
|
||||||
|
const result = await toolInvoker.invoke({
|
||||||
|
tool: input.tool,
|
||||||
|
input: input.payload,
|
||||||
|
activityId: input.activityId,
|
||||||
|
taskId: input.taskId,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
id: input.idempotencyKey,
|
||||||
|
ok: result.ok,
|
||||||
|
...(result.error === undefined ? {} : { error: result.error.message }),
|
||||||
|
retryable: result.error?.retryable ?? false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cards: {
|
||||||
|
issueAcceptance: async (input) => {
|
||||||
|
const issued = await humanTool.issueAcceptance(
|
||||||
|
{ taskId: input.taskId, ownerId: input.ownerId, humanCallCap: input.humanCallCap },
|
||||||
|
{
|
||||||
|
title: input.title,
|
||||||
|
basis: [...input.basis],
|
||||||
|
deliverable: input.deliverable as never,
|
||||||
|
criteria: [...input.criteria],
|
||||||
|
actions: input.actions.map((a) => ({ ...a })),
|
||||||
|
fields: [],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return { id: issued.order.id };
|
||||||
|
},
|
||||||
|
issueLandingFailure: async (input) => {
|
||||||
|
const issued = await humanTool.issueLandingFailure(
|
||||||
|
{ taskId: input.taskId, ownerId: input.ownerId, humanCallCap: input.humanCallCap },
|
||||||
|
{ failures: input.failures },
|
||||||
|
);
|
||||||
|
return { id: issued.order.id };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tasks: {
|
||||||
|
activityIdOf: async (taskId) => (await engines.task.get(taskId)).activityId,
|
||||||
|
outputPortsOf: async (taskId) => {
|
||||||
|
const task = await engines.task.get(taskId);
|
||||||
|
const manifest = await registry.manifestOf(task.activityId);
|
||||||
|
return {
|
||||||
|
mandatory: manifest.contract.output.mandatory.map((p) => p.name),
|
||||||
|
selective: manifest.contract.output.selective.map((g) => ({
|
||||||
|
group: g.group,
|
||||||
|
ports: g.ports.map((p) => p.name),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ports: {
|
||||||
|
emit: async (input) => {
|
||||||
|
// 落地收敛才发射 —— 下游只见落地完成的任务
|
||||||
|
await event.publish({
|
||||||
|
type: 'data',
|
||||||
|
correlation: {
|
||||||
|
target: { kind: 'task', id: input.taskId },
|
||||||
|
cause: { kind: 'engine', ref: 'output-port' },
|
||||||
|
keys: { port: input.port },
|
||||||
|
},
|
||||||
|
payload: { data: input.payload },
|
||||||
|
occurredAt: systemClock.now().toISOString(),
|
||||||
|
source: { kind: 'engine', detail: 'release' },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
memory: {
|
||||||
|
append: async (input) =>
|
||||||
|
void (await memory.append({
|
||||||
|
...input,
|
||||||
|
stamp: { contractVersion: '1', businessModelVersion: '1', skillVersions: [] },
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
clock: systemClock,
|
||||||
|
ids,
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ── L5–L7:范式引擎、任务、流程层 ─────────────────────────────── */
|
||||||
|
|
||||||
|
const engines = wireEngines({
|
||||||
|
db,
|
||||||
|
clock: systemClock,
|
||||||
|
ids,
|
||||||
|
event,
|
||||||
|
ledger,
|
||||||
|
memory,
|
||||||
|
toolbox,
|
||||||
|
toolInvoker,
|
||||||
|
humanTool,
|
||||||
|
interaction,
|
||||||
|
message,
|
||||||
|
org,
|
||||||
|
llm,
|
||||||
|
modelGateway,
|
||||||
|
registry,
|
||||||
|
release,
|
||||||
|
schemas,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 停用前置检查的真身就位:在途任务问 task,待答卡问 human-tool
|
||||||
|
inFlightProbe.bind({
|
||||||
|
openTasksOf: (staffId) => engines.task.openTasksOf(staffId),
|
||||||
|
openCardsOf: (staffId) => humanTool.openCardsOf(staffId),
|
||||||
|
openApprovalsOf: async () => [],
|
||||||
|
});
|
||||||
|
|
||||||
|
// 触发器就位后回填 —— event 的三级消费自此真判定
|
||||||
|
triggerMatcher.bind({
|
||||||
|
match: (envelope) =>
|
||||||
|
engines.trigger.match({
|
||||||
|
type: envelope.type,
|
||||||
|
keys: envelope.correlation.keys,
|
||||||
|
payload: envelope.payload.data,
|
||||||
|
source: envelope.source.kind,
|
||||||
|
occurredAt: envelope.occurredAt,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
config,
|
config,
|
||||||
pool,
|
pool,
|
||||||
@@ -238,6 +508,13 @@ export async function createContainer(config: AppConfig): Promise<Container> {
|
|||||||
toolInvoker,
|
toolInvoker,
|
||||||
message,
|
message,
|
||||||
channel,
|
channel,
|
||||||
|
humanTool,
|
||||||
|
interaction,
|
||||||
|
registry,
|
||||||
|
release,
|
||||||
|
task: engines.task,
|
||||||
|
process: engines.process,
|
||||||
|
trigger: engines.trigger,
|
||||||
async close() {
|
async close() {
|
||||||
await pool.end();
|
await pool.end();
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { serve } from '@hono/node-server';
|
|||||||
import { loadConfig } from './config.ts';
|
import { loadConfig } from './config.ts';
|
||||||
import { createContainer } from './container.ts';
|
import { createContainer } from './container.ts';
|
||||||
import { createServer } from './server.ts';
|
import { createServer } from './server.ts';
|
||||||
|
import { createConsumers } from './workers/consumers.ts';
|
||||||
import { startDispatcher } from './workers/dispatcher.ts';
|
import { startDispatcher } from './workers/dispatcher.ts';
|
||||||
|
|
||||||
export async function boot(): Promise<void> {
|
export async function boot(): Promise<void> {
|
||||||
@@ -14,8 +15,7 @@ export async function boot(): Promise<void> {
|
|||||||
await container.org.ensureSeeded();
|
await container.org.ensureSeeded();
|
||||||
|
|
||||||
// 派发器:投递记录即队列,按目标 ID 取 advisory lock 串行消费(ADR-008)
|
// 派发器:投递记录即队列,按目标 ID 取 advisory lock 串行消费(ADR-008)
|
||||||
// 消费者随 M4(任务)/ M5(触发器)注册;此刻空表即「只标记送达」
|
const dispatcher = startDispatcher({ container, consumers: createConsumers() }, config.dispatchIntervalMs);
|
||||||
const dispatcher = startDispatcher({ container, consumers: new Map() }, config.dispatchIntervalMs);
|
|
||||||
|
|
||||||
const app = createServer(container);
|
const app = createServer(container);
|
||||||
serve({ fetch: app.fetch, port: config.port }, (info) => {
|
serve({ fetch: app.fetch, port: config.port }, (info) => {
|
||||||
|
|||||||
@@ -0,0 +1,196 @@
|
|||||||
|
/**
|
||||||
|
* 统一报价的验收种子 —— README《MVP 完成判据》第 1 条的机器形态。
|
||||||
|
*
|
||||||
|
* 治理台要做的四件事,这里一次做完:
|
||||||
|
* ① 绑定 LLM 实例(任一 OpenAI 兼容端点)
|
||||||
|
* ② 注册功能工具并给活动白名单
|
||||||
|
* ③ **经安装通道**装入活动包(读 activities 下各包 dist 的清单与产物 —— 数据,不是 import)
|
||||||
|
* ④ 编排「统一报价」流程包 v1 并发布(边映射闭合校验通过)+ 配置事件触发器与邮件通道实例
|
||||||
|
*/
|
||||||
|
import { readFile } from 'node:fs/promises';
|
||||||
|
import { existsSync } from 'node:fs';
|
||||||
|
import { dirname, join, resolve } from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import { activityManifestSchema, processPackageSchema, triggerConfigSchema } from '@contracts';
|
||||||
|
import type { Container } from './container.ts';
|
||||||
|
|
||||||
|
/** 从仓库根找 activities/ —— 只读文件,不 import(ADR-004:安装通道无代码通道) */
|
||||||
|
function activitiesRoot(): string {
|
||||||
|
let dir = dirname(fileURLToPath(import.meta.url));
|
||||||
|
for (let i = 0; i < 8; i += 1) {
|
||||||
|
const candidate = join(dir, 'activities');
|
||||||
|
if (existsSync(candidate)) return candidate;
|
||||||
|
const parent = resolve(dir, '..');
|
||||||
|
if (parent === dir) break;
|
||||||
|
dir = parent;
|
||||||
|
}
|
||||||
|
throw new Error('找不到 activities/ —— 先跑 pnpm activities:build');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 第一方功能工具:查历史价 —— MVP 用录制回放替身代真 CRM(README:mock 只在外界) */
|
||||||
|
const HISTORY_TOOL = {
|
||||||
|
name: 'crm.history-price-read',
|
||||||
|
version: '1.0.0',
|
||||||
|
description: '读取历史价格情报:某 PN 最近一次报价距今天数、行情是否平稳、生命周期状态',
|
||||||
|
input: { type: 'object', required: ['pn'], properties: { pn: { type: 'string' } } },
|
||||||
|
output: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
historyDays: { type: 'integer' },
|
||||||
|
marketStable: { type: 'boolean' },
|
||||||
|
lifecycle: { type: 'string' },
|
||||||
|
hit: { type: 'boolean' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
effect: 'read' as const,
|
||||||
|
errors: [{ code: 'not-found', retryable: false, message: '无历史价' }],
|
||||||
|
idempotent: true,
|
||||||
|
timeoutMs: 10_000,
|
||||||
|
retries: 2,
|
||||||
|
connectionKeys: ['endpoint'],
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 打标纯工具:三分类规则 P-03 的确定性求值 —— 无外效应,活动私有作用域 */
|
||||||
|
const CLASSIFY_TOOL = {
|
||||||
|
...HISTORY_TOOL,
|
||||||
|
name: 'quote-intake.classify',
|
||||||
|
description: '按三分类规则 P-03 逐条打标:输入历史天数/行情稳定/生命周期,输出 Type 与打法',
|
||||||
|
input: {
|
||||||
|
type: 'object',
|
||||||
|
required: ['historyDays'],
|
||||||
|
properties: { historyDays: {}, marketStable: {}, lifecycle: {} },
|
||||||
|
},
|
||||||
|
output: { type: 'object', properties: { type: { type: 'string' }, play: { type: 'string' } } },
|
||||||
|
connectionKeys: [],
|
||||||
|
pure: true,
|
||||||
|
ownerActivityId: 'quote-intake',
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function seedQuote(container: Container): Promise<void> {
|
||||||
|
function log(line: string): void {
|
||||||
|
process.stdout.write(line + '\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ① LLM 实例:四元绑定(活动只见自定义模型名) */
|
||||||
|
await container.llm.upsert({
|
||||||
|
name: 'quote-extractor',
|
||||||
|
provider: 'openai-compatible',
|
||||||
|
baseUrl: process.env['JCAI_LLM_BASE_URL'] ?? 'https://api.deepseek.com/v1',
|
||||||
|
model: process.env['JCAI_LLM_MODEL'] ?? 'deepseek-chat',
|
||||||
|
apiKey: process.env['JCAI_LLM_API_KEY'] ?? 'sk-placeholder',
|
||||||
|
systemUse: true,
|
||||||
|
enabled: true,
|
||||||
|
});
|
||||||
|
log('① LLM 实例已绑定:quote-extractor');
|
||||||
|
|
||||||
|
/* ② 工具注册两审 + 白名单 */
|
||||||
|
for (const contract of [HISTORY_TOOL, CLASSIFY_TOOL]) {
|
||||||
|
const verdict = await container.toolbox.register({
|
||||||
|
contract,
|
||||||
|
samples: [{ input: { pn: 'LM358DR', historyDays: 30 }, expectShape: { type: 'object' } }],
|
||||||
|
...(contract.connectionKeys.length > 0 ? { connectionValues: { endpoint: 'replay://crm' } } : {}),
|
||||||
|
});
|
||||||
|
if (!verdict.accepted) {
|
||||||
|
log(' 工具 ' + contract.name + ' 未过两审:' + verdict.staticFindings.map((f) => f.message).join('; '));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await container.toolbox.setAllowlist('quote-intake', [HISTORY_TOOL.name, CLASSIFY_TOOL.name]);
|
||||||
|
log('② 功能工具已注册并入白名单');
|
||||||
|
|
||||||
|
/* ③ 经安装通道装入活动包 —— 静态审 + 样例集(首装无历史,免回放审) */
|
||||||
|
const root = activitiesRoot();
|
||||||
|
const manifestRaw = JSON.parse(await readFile(join(root, 'quote-intake/dist/manifest.json'), 'utf8'));
|
||||||
|
const manifest = activityManifestSchema.parse(manifestRaw);
|
||||||
|
const artifact = await readFile(join(root, 'quote-intake/dist/index.js'), 'utf8');
|
||||||
|
const verdict = await container.registry.install(manifest, artifact);
|
||||||
|
log('③ 活动包已安装:' + manifest.identity.name + '(静态审 ' + String(verdict.staticFindings.length) + ' 条提示)');
|
||||||
|
|
||||||
|
/* ④ 编排「统一报价」流程包 v1 并发布 */
|
||||||
|
const pkg = processPackageSchema.parse({
|
||||||
|
id: 'unified-quote',
|
||||||
|
name: '统一报价',
|
||||||
|
version: 1,
|
||||||
|
state: 'draft',
|
||||||
|
callInterface: {
|
||||||
|
input: {
|
||||||
|
type: 'object',
|
||||||
|
required: ['customerCode', 'rfqRef'],
|
||||||
|
properties: { customerCode: { type: 'string' }, rfqRef: { type: 'string' }, subject: { type: 'string' } },
|
||||||
|
},
|
||||||
|
output: { type: 'object', properties: { records: { type: 'array' } } },
|
||||||
|
},
|
||||||
|
nodes: [
|
||||||
|
{ id: 'start', kind: 'start', label: '客户RFQ询价', layout: { row: 0, col: 0 } },
|
||||||
|
{
|
||||||
|
id: 'A1',
|
||||||
|
kind: 'activity',
|
||||||
|
label: 'A1 RFQ需求分析',
|
||||||
|
activityId: 'quote-intake',
|
||||||
|
owner: { position: 'BD', strategy: 'sole' },
|
||||||
|
failure: { kind: 'terminate' },
|
||||||
|
message: '真实 RFQ 五花八门,先归一成台账化的统一受理记录,再逐条打标定车道。',
|
||||||
|
layout: { row: 0, col: 1 },
|
||||||
|
},
|
||||||
|
{ id: 'end', kind: 'end', label: '归档闭环', layout: { row: 0, col: 2 } },
|
||||||
|
],
|
||||||
|
edges: [
|
||||||
|
{
|
||||||
|
id: 'e-start-a1',
|
||||||
|
from: 'start',
|
||||||
|
to: 'A1',
|
||||||
|
mapping: [
|
||||||
|
{ target: 'customerCode', source: { from: 'initiation', path: 'customerCode' } },
|
||||||
|
{ target: 'rfqRef', source: { from: 'initiation', path: 'rfqRef' } },
|
||||||
|
],
|
||||||
|
reopen: false,
|
||||||
|
},
|
||||||
|
{ id: 'e-a1-end', from: 'A1', to: 'end', mapping: [], reopen: false },
|
||||||
|
],
|
||||||
|
variables: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
await container.process.savePackage(pkg);
|
||||||
|
const report = await container.process.validate(pkg);
|
||||||
|
if (!report.publishable) {
|
||||||
|
log(' 闭合校验未过,不得发布:' + report.violations.map((v) => v.message).join('; '));
|
||||||
|
} else {
|
||||||
|
await container.process.publish(pkg.id, pkg.version);
|
||||||
|
log('④ 流程包「统一报价」v1 已发布(边映射闭合校验通过)');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 事件触发器「客户RFQ询价」 */
|
||||||
|
const trigger = triggerConfigSchema.parse({
|
||||||
|
id: 'trg-rfq',
|
||||||
|
name: '客户RFQ询价',
|
||||||
|
enabled: true,
|
||||||
|
processPackageId: 'unified-quote',
|
||||||
|
mapping: [
|
||||||
|
{ target: 'customerCode', source: { from: 'expression', sfeel: 'keys.customerCode' } },
|
||||||
|
{ target: 'rfqRef', source: { from: 'expression', sfeel: 'keys.rfqRef' } },
|
||||||
|
{ target: 'subject', source: { from: 'expression', sfeel: 'subject' } },
|
||||||
|
],
|
||||||
|
spec: { kind: 'event', rule: 'contains(subject, "询价") or contains(subject, "RFQ")', eventType: 'data' },
|
||||||
|
});
|
||||||
|
await container.trigger.upsert(trigger);
|
||||||
|
log(' 事件触发器「客户RFQ询价」已配置');
|
||||||
|
|
||||||
|
/* 邮件通道实例:归属键提取规则 + 载荷映射 + 默认认领岗位 */
|
||||||
|
const { positions } = await container.org.listStaff();
|
||||||
|
const op = positions.find((p) => p.code === 'OP');
|
||||||
|
await container.channel.upsert({
|
||||||
|
id: 'ch-rfq-mail',
|
||||||
|
name: '公司询价邮箱',
|
||||||
|
kind: 'mail',
|
||||||
|
address: 'rfq@we.com',
|
||||||
|
enabled: true,
|
||||||
|
extract: [
|
||||||
|
{ key: 'customerCode', regex: '\\bC\\d{3}\\b', source: 'both' },
|
||||||
|
{ key: 'rfqRef', regex: 'RFQ-[A-Z0-9]+', source: 'both' },
|
||||||
|
{ key: 'quoteNo', regex: 'Q\\d{6}', source: 'both' },
|
||||||
|
],
|
||||||
|
mapping: [],
|
||||||
|
...(op === undefined ? {} : { fallbackPosition: op.id }),
|
||||||
|
addressWindowMs: 7 * 86_400_000,
|
||||||
|
});
|
||||||
|
log(' 邮件通道实例「公司询价邮箱」已配置(默认认领岗位:OP)');
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
import { loadConfig } from './config.ts';
|
import { loadConfig } from './config.ts';
|
||||||
import { createContainer } from './container.ts';
|
import { createContainer } from './container.ts';
|
||||||
|
import { seedQuote } from './seed-quote.ts';
|
||||||
|
|
||||||
export async function seed(): Promise<void> {
|
export async function seed(): Promise<void> {
|
||||||
const container = await createContainer(loadConfig());
|
const container = await createContainer(loadConfig());
|
||||||
@@ -12,6 +13,10 @@ export async function seed(): Promise<void> {
|
|||||||
const { staff, positions } = await container.org.listStaff();
|
const { staff, positions } = await container.org.listStaff();
|
||||||
process.stdout.write(`种子完成:${positions.length} 个岗位 / ${staff.length} 名员工\n`);
|
process.stdout.write(`种子完成:${positions.length} 个岗位 / ${staff.length} 名员工\n`);
|
||||||
for (const s of staff) process.stdout.write(` ${s.account.padEnd(12)} ${s.name}(${s.position.code})\n`);
|
for (const s of staff) process.stdout.write(` ${s.account.padEnd(12)} ${s.name}(${s.position.code})\n`);
|
||||||
|
|
||||||
|
// 统一报价的验收种子(README MVP 完成判据第 1 条)
|
||||||
|
process.stdout.write('\n── 治理台配置 ──\n');
|
||||||
|
await seedQuote(container);
|
||||||
} finally {
|
} finally {
|
||||||
await container.close();
|
await container.close();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
/**
|
||||||
|
* 第一方功能工具与纯工具的实现 —— 「实现对系统」(编码规范 §3;语义出处 OS 7.3)。
|
||||||
|
*
|
||||||
|
* 运行时受限:只拿得到**注册申报的连接目标**,没有其它环境能力 —— 这是 `ToolRuntime` 保证的。
|
||||||
|
*/
|
||||||
|
import { evaluateUnaryTest } from '@sfeel';
|
||||||
|
import type { ToolHandler } from '@modules/toolbox';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查历史价 —— 外部系统(CRM)的**录制回放替身**。
|
||||||
|
* README:mock 只允许出现在外界;系统内部机制一律真跑。
|
||||||
|
*/
|
||||||
|
const historyPriceRead: ToolHandler = async ({ input }) => {
|
||||||
|
const pn = String(input['pn'] ?? '');
|
||||||
|
// 录制样本:演示用的历史价情报(真 CRM 接入时换 endpoint,契约不变)
|
||||||
|
const table: Record<string, { historyDays: number; marketStable: boolean; lifecycle: string }> = {
|
||||||
|
LM358DR: { historyDays: 30, marketStable: true, lifecycle: '在产' },
|
||||||
|
NE555P: { historyDays: 240, marketStable: false, lifecycle: '在产' },
|
||||||
|
TL072CDR: { historyDays: 15, marketStable: true, lifecycle: 'EOL' },
|
||||||
|
};
|
||||||
|
const hit = table[pn];
|
||||||
|
if (hit === undefined) {
|
||||||
|
// 无历史 → 走 B(重估后报);这不是错误
|
||||||
|
return { ok: true, output: { historyDays: 9999, marketStable: false, lifecycle: '未知', hit: false } };
|
||||||
|
}
|
||||||
|
return { ok: true, output: { ...hit, hit: true } };
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 三分类规则 P-03 的决策表 —— 与活动包里的资产同一份规则 */
|
||||||
|
const P03_RULES = [
|
||||||
|
{ id: 'p03-c', when: ['-', '-', '"EOL","缺货","要求替代"'], then: { type: 'C', play: '转案处理' } },
|
||||||
|
{ id: 'p03-a', when: ['<= 90', 'true', '-'], then: { type: 'A', play: '直接快答' } },
|
||||||
|
{ id: 'p03-b', when: ['-', '-', '-'], then: { type: 'B', play: '重估后报' } },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 逐条打标 —— **纯工具**:无外效应、无连接配置、活动私有作用域。
|
||||||
|
* 「能算的不问 LLM」:同输入必同结论,零方差零 token;命中规则行随出参留痕。
|
||||||
|
*/
|
||||||
|
const classify: ToolHandler = async ({ input }) => {
|
||||||
|
const values = [input['historyDays'], input['marketStable'], input['lifecycle']];
|
||||||
|
for (const rule of P03_RULES) {
|
||||||
|
const hit = rule.when.every((cell, i) => evaluateUnaryTest(cell, values[i], {}));
|
||||||
|
if (hit) return { ok: true, output: { ...rule.then, rule: rule.id } };
|
||||||
|
}
|
||||||
|
return { ok: false, error: { code: 'no-match', retryable: false, message: '三分类规则无命中行' } };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FIRST_PARTY_TOOLS: ReadonlyArray<readonly [string, ToolHandler]> = [
|
||||||
|
['crm.history-price-read', historyPriceRead],
|
||||||
|
['quote-intake.classify', classify],
|
||||||
|
];
|
||||||
@@ -0,0 +1,559 @@
|
|||||||
|
/**
|
||||||
|
* 范式引擎与任务运行时的装配 —— app 是唯一组装层(编码规范 §5.2)。
|
||||||
|
*
|
||||||
|
* 这里把 M4/M5 的模块用**端口**接起来:跨层协作一律经端口,模块之间不互相认识。
|
||||||
|
*/
|
||||||
|
import type { Db } from '@shared/db.ts';
|
||||||
|
import type { EventService } from '@modules/event';
|
||||||
|
import type { LedgerService } from '@modules/ledger';
|
||||||
|
import type { MemoryService } from '@modules/memory';
|
||||||
|
import type { ToolboxService, ToolInvoker } from '@modules/toolbox';
|
||||||
|
import type { HumanToolService } from '@modules/human-tool';
|
||||||
|
import type { InteractionService } from '@modules/interaction';
|
||||||
|
import type { MessageService } from '@modules/message';
|
||||||
|
import type { OrgService } from '@modules/org';
|
||||||
|
import type { LlmService, ModelGateway } from '@modules/llm';
|
||||||
|
import type { ActivityRegistryService } from '@modules/activity-registry';
|
||||||
|
import type { ReleaseService } from '@modules/release';
|
||||||
|
|
||||||
|
import { createFlowRunner, createFlowStateStore } from '@modules/flow-engine';
|
||||||
|
import { createLoopStateStore, createTurnRunner } from '@modules/loop-engine';
|
||||||
|
import { createSkillEngineService } from '@modules/skill-engine';
|
||||||
|
import { createTaskService, createTaskStore } from '@modules/task';
|
||||||
|
import type { TaskService } from '@modules/task';
|
||||||
|
import { createProcessService, createProcessStore } from '@modules/process';
|
||||||
|
import type { ProcessService } from '@modules/process';
|
||||||
|
import { createTriggerService, createTriggerStore } from '@modules/trigger';
|
||||||
|
import type { TriggerService } from '@modules/trigger';
|
||||||
|
|
||||||
|
import { evaluate as evalSfeel, evaluateCondition, evaluateUnaryTest } from '@sfeel';
|
||||||
|
import type { StaffId, TaskId } from '@contracts';
|
||||||
|
import { createAgentRunner } from '../agent-runner.ts';
|
||||||
|
import type { Clock, IdGenerator } from '../runtime.ts';
|
||||||
|
|
||||||
|
export type EngineWiringDeps = {
|
||||||
|
readonly db: Db;
|
||||||
|
readonly clock: Clock;
|
||||||
|
readonly ids: IdGenerator;
|
||||||
|
readonly event: EventService;
|
||||||
|
readonly ledger: LedgerService;
|
||||||
|
readonly memory: MemoryService;
|
||||||
|
readonly toolbox: ToolboxService;
|
||||||
|
readonly toolInvoker: ToolInvoker;
|
||||||
|
readonly humanTool: HumanToolService;
|
||||||
|
readonly interaction: InteractionService;
|
||||||
|
readonly message: MessageService;
|
||||||
|
readonly org: OrgService;
|
||||||
|
readonly llm: LlmService;
|
||||||
|
readonly modelGateway: ModelGateway;
|
||||||
|
readonly registry: ActivityRegistryService;
|
||||||
|
readonly release: ReleaseService;
|
||||||
|
readonly schemas: { validate(schema: Record<string, unknown>, value: unknown): { ok: boolean; errors: string[] } };
|
||||||
|
};
|
||||||
|
|
||||||
|
export type EngineWiring = {
|
||||||
|
readonly task: TaskService;
|
||||||
|
readonly process: ProcessService;
|
||||||
|
readonly trigger: TriggerService;
|
||||||
|
};
|
||||||
|
|
||||||
|
function writeLog(line: string): void {
|
||||||
|
globalThis.process.stderr.write(`${line}\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** S-FEEL 三件套 —— 求值异常按「不成立」处置,不让流转停摆 */
|
||||||
|
const sfeel = {
|
||||||
|
condition: (expression: string, scope: Readonly<Record<string, unknown>>): boolean => {
|
||||||
|
try {
|
||||||
|
return evaluateCondition(expression, scope);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
expression: (expression: string, scope: Readonly<Record<string, unknown>>): unknown => {
|
||||||
|
try {
|
||||||
|
return evalSfeel(expression, scope);
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
unaryTest: (cell: string, value: unknown, scope: Readonly<Record<string, unknown>>): boolean => {
|
||||||
|
try {
|
||||||
|
return evaluateUnaryTest(cell, value, scope);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function wireEngines(deps: EngineWiringDeps): EngineWiring {
|
||||||
|
const { db, clock, ids } = deps;
|
||||||
|
|
||||||
|
/* ── 任务上下文:活动 ID / 负责人 / 调人预算 —— 引擎共用 ─────────── */
|
||||||
|
const taskStore = createTaskStore(db);
|
||||||
|
const taskContext = {
|
||||||
|
async of(taskId: TaskId) {
|
||||||
|
const task = await taskStore.get(taskId);
|
||||||
|
if (task === undefined) throw new Error(`任务不存在:${taskId}`);
|
||||||
|
return { activityId: task.activityId, ownerId: task.ownerId, humanCallCap: task.caps.humanCalls };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ── 任务数据:引擎读**任务视图** = 正式版 + 本任务草稿层 ───────── */
|
||||||
|
const taskData = {
|
||||||
|
async view(taskId: TaskId) {
|
||||||
|
// 引擎对任务视图求值,不依赖事件(AM 4.2)
|
||||||
|
const objects = await deps.ledger.listObjects();
|
||||||
|
const scope: Record<string, unknown> = {};
|
||||||
|
for (const address of objects) {
|
||||||
|
const page = await deps.ledger.taskView(taskId, {
|
||||||
|
address,
|
||||||
|
where: {},
|
||||||
|
orderBy: [],
|
||||||
|
limit: 200,
|
||||||
|
offset: 0,
|
||||||
|
});
|
||||||
|
scope[`${address.domain}.${address.object}`] = page.rows;
|
||||||
|
}
|
||||||
|
return scope;
|
||||||
|
},
|
||||||
|
async draftWrite(input: {
|
||||||
|
taskId: TaskId;
|
||||||
|
domain: string;
|
||||||
|
object: string;
|
||||||
|
key: Record<string, string | number>;
|
||||||
|
patch: Record<string, unknown>;
|
||||||
|
}) {
|
||||||
|
// 一切过程写入落草稿层 —— 放行前不落事实
|
||||||
|
await deps.ledger.draftWrite({
|
||||||
|
taskId: input.taskId,
|
||||||
|
address: { domain: input.domain, object: input.object },
|
||||||
|
key: input.key,
|
||||||
|
patch: input.patch,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const memoryRecorder = {
|
||||||
|
append: async (input: { taskId: TaskId; kind: string; title: string; detail?: Record<string, unknown> }) => {
|
||||||
|
await deps.memory.append({
|
||||||
|
taskId: input.taskId,
|
||||||
|
kind: input.kind as never,
|
||||||
|
title: input.title,
|
||||||
|
...(input.detail === undefined ? {} : { detail: input.detail }),
|
||||||
|
stamp: { contractVersion: '1', businessModelVersion: '1', skillVersions: [] },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const listeners = {
|
||||||
|
async register(input: { taskId: TaskId; eventType: 'data' | 'time' | 'human'; keyFilter: Record<string, string>; deadline: string }) {
|
||||||
|
const listener = await deps.event.registerListener(input.taskId, {
|
||||||
|
origin: 'dynamic',
|
||||||
|
eventType: input.eventType,
|
||||||
|
keyFilter: input.keyFilter,
|
||||||
|
semantic: 'complete-step',
|
||||||
|
deadline: input.deadline,
|
||||||
|
escalation: ['nudge', 'escalate'],
|
||||||
|
});
|
||||||
|
return { id: listener.id };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const cards = {
|
||||||
|
async issue(input: {
|
||||||
|
taskId: TaskId;
|
||||||
|
ownerId: StaffId;
|
||||||
|
humanCallCap: number;
|
||||||
|
workOrder: 'execution' | 'acceptance';
|
||||||
|
card: Record<string, unknown>;
|
||||||
|
}) {
|
||||||
|
const context = { taskId: input.taskId, ownerId: input.ownerId, humanCallCap: input.humanCallCap };
|
||||||
|
const title = String(input.card['title'] ?? '请处理');
|
||||||
|
const basis = Object.entries(input.card)
|
||||||
|
.filter(([k]) => k !== 'title' && k !== 'fields')
|
||||||
|
.map(([k, v]) => ({ k, v: String(v) }));
|
||||||
|
if (input.workOrder === 'acceptance') {
|
||||||
|
const issued = await deps.humanTool.issueAcceptance(context, {
|
||||||
|
title,
|
||||||
|
basis: basis.length > 0 ? basis : [{ k: '来源', v: '步序声明' }],
|
||||||
|
deliverable: String(input.card['deliverable'] ?? 'deliverable') as never,
|
||||||
|
criteria: [{ label: '成果齐备', level: 'L5', pass: false }],
|
||||||
|
fields: [],
|
||||||
|
});
|
||||||
|
return { id: issued.order.id };
|
||||||
|
}
|
||||||
|
const fields = Array.isArray(input.card['fields'])
|
||||||
|
? (input.card['fields'] as string[]).map((key) => ({ key, label: key, kind: 'text' as const, required: true }))
|
||||||
|
: [{ key: 'result', label: '结果', kind: 'textarea' as const, required: true }];
|
||||||
|
const issued = await deps.humanTool.issueExecution(context, {
|
||||||
|
title,
|
||||||
|
basis: basis.length > 0 ? basis : [{ k: '来源', v: '步序声明' }],
|
||||||
|
ask: String(input.card['reason'] ?? title),
|
||||||
|
fields,
|
||||||
|
});
|
||||||
|
return { id: issued.order.id };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const toolGateway = {
|
||||||
|
invoke: async (input: { tool: string; input: Record<string, unknown>; activityId: string; taskId: TaskId; stepNo?: number }) => {
|
||||||
|
const result = await deps.toolInvoker.invoke({
|
||||||
|
tool: input.tool,
|
||||||
|
input: input.input,
|
||||||
|
activityId: input.activityId,
|
||||||
|
taskId: input.taskId,
|
||||||
|
...(input.stepNo === undefined ? {} : { stepNo: input.stepNo }),
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
ok: result.ok,
|
||||||
|
...(result.output === undefined ? {} : { output: result.output }),
|
||||||
|
...(result.error === undefined ? {} : { error: { message: result.error.message, retryable: result.error.retryable } }),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
isWriteEffect: async (tool: string) => (await deps.toolbox.get(tool))?.effect === 'write',
|
||||||
|
registerIntent: async (input: { taskId: TaskId; tool: string; input: Record<string, unknown>; label: string }) => {
|
||||||
|
// 写类动作登记为动作意向:入参冻结,放行后由系统执行落地
|
||||||
|
await deps.release.registerIntent(input.taskId, {
|
||||||
|
id: ids.next(),
|
||||||
|
tool: input.tool,
|
||||||
|
input: input.input,
|
||||||
|
idempotencyKey: `${input.taskId}:${input.tool}:${ids.next()}`,
|
||||||
|
label: input.label,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ── skill-engine ─────────────────────────────────────────────── */
|
||||||
|
const skills = createSkillEngineService({
|
||||||
|
catalog: {
|
||||||
|
skillsOf: async (activityId) => (await deps.registry.manifestOf(activityId)).assets.skills,
|
||||||
|
allowlistOf: (activityId) => deps.toolbox.allowlistOf(activityId),
|
||||||
|
},
|
||||||
|
tools: {
|
||||||
|
invoke: (input) =>
|
||||||
|
toolGateway.invoke({ ...input, ...(input.stepNo === undefined ? {} : { stepNo: input.stepNo }) }),
|
||||||
|
},
|
||||||
|
memory: memoryRecorder as never,
|
||||||
|
clock,
|
||||||
|
});
|
||||||
|
void skills;
|
||||||
|
|
||||||
|
/* ── flow-engine ──────────────────────────────────────────────── */
|
||||||
|
const flow = createFlowRunner({
|
||||||
|
state: createFlowStateStore(db),
|
||||||
|
evaluate: sfeel.condition,
|
||||||
|
data: taskData,
|
||||||
|
tools: { ...toolGateway, invoke: (i) => toolGateway.invoke(i) },
|
||||||
|
llm: {
|
||||||
|
async generate(input) {
|
||||||
|
// LLM 在 flow 里只以「被调用的工具」出现,不是调度者
|
||||||
|
try {
|
||||||
|
const result = await deps.modelGateway.generate({
|
||||||
|
model: input.model,
|
||||||
|
prompt: input.prompt,
|
||||||
|
outputSchema: input.outputSchema,
|
||||||
|
taskId: input.taskId,
|
||||||
|
stepNo: input.stepNo,
|
||||||
|
label: 'flow-step',
|
||||||
|
});
|
||||||
|
return { ok: result.object !== undefined, ...(result.object === undefined ? { error: '出参不合 schema' } : { object: result.object }) };
|
||||||
|
} catch (error) {
|
||||||
|
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cards,
|
||||||
|
listeners,
|
||||||
|
memory: memoryRecorder as never,
|
||||||
|
tasks: taskContext,
|
||||||
|
clock,
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ── loop-engine ──────────────────────────────────────────────── */
|
||||||
|
const loop = createTurnRunner({
|
||||||
|
state: createLoopStateStore(db),
|
||||||
|
sfeel,
|
||||||
|
data: taskData,
|
||||||
|
agent: createAgentRunner({
|
||||||
|
gateway: deps.modelGateway,
|
||||||
|
modelOf: async (taskId) => {
|
||||||
|
const { activityId } = await taskContext.of(taskId);
|
||||||
|
const manifest = await deps.registry.manifestOf(activityId);
|
||||||
|
return manifest.declarations.models[0] ?? 'default';
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
tools: toolGateway,
|
||||||
|
cards,
|
||||||
|
listeners,
|
||||||
|
memory: {
|
||||||
|
append: memoryRecorder.append as never,
|
||||||
|
loadActivityMemory: async (activityId: string, limit: number) =>
|
||||||
|
(await deps.memory.loadActivityMemory(activityId, { limit })).map((m) => ({ layer: m.layer, statement: m.statement })),
|
||||||
|
},
|
||||||
|
tasks: taskContext,
|
||||||
|
clock,
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ── task(L6):范式在此汇流,对上层不可区分 ──────────────────── */
|
||||||
|
const task = createTaskService({
|
||||||
|
store: taskStore,
|
||||||
|
activities: {
|
||||||
|
async load(activityId) {
|
||||||
|
const manifest = await deps.registry.manifestOf(activityId);
|
||||||
|
const envelope = await deps.registry.loadEnvelope(activityId);
|
||||||
|
return {
|
||||||
|
paradigm: manifest.identity.paradigm,
|
||||||
|
caps: manifest.contract.resources.budget,
|
||||||
|
envelope: { ...envelope },
|
||||||
|
inputSchema: manifest.contract.input.data,
|
||||||
|
businessModel: manifest.assets.businessModel,
|
||||||
|
stallAfter: manifest.declarations.harness.stallAfter,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
schemas: deps.schemas,
|
||||||
|
// 范式在此汇流:两个引擎按统一的 ParadigmEngine 契约暴露给 task —— 对上层不可区分(§9)
|
||||||
|
engines: {
|
||||||
|
loop: {
|
||||||
|
load: (taskId, model) => loop.load(taskId, model as never).then(() => undefined),
|
||||||
|
advance: async (taskId) => {
|
||||||
|
const outcome = await loop.runTurn(taskId);
|
||||||
|
return { status: outcome.status, ...(outcome.blockedReason === undefined ? {} : { reason: outcome.blockedReason }) };
|
||||||
|
},
|
||||||
|
onEvent: async (taskId, event) => {
|
||||||
|
const outcome = await loop.onEvent(taskId, event);
|
||||||
|
return { status: outcome.status, ...(outcome.blockedReason === undefined ? {} : { reason: outcome.blockedReason }) };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
flow: {
|
||||||
|
load: (taskId, model) => flow.load(taskId, model as never).then(() => undefined),
|
||||||
|
advance: async (taskId) => {
|
||||||
|
const outcome = await flow.advance(taskId);
|
||||||
|
return { status: outcome.status, ...(outcome.blockedReason === undefined ? {} : { reason: outcome.blockedReason }) };
|
||||||
|
},
|
||||||
|
onEvent: async (taskId, event) => {
|
||||||
|
const outcome = await flow.resume(taskId, {
|
||||||
|
kind: event.kind === 'control' ? 'time' : event.kind,
|
||||||
|
ref: event.ref,
|
||||||
|
payload: event.payload,
|
||||||
|
});
|
||||||
|
return { status: outcome.status, ...(outcome.blockedReason === undefined ? {} : { reason: outcome.blockedReason }) };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
memory: {
|
||||||
|
append: memoryRecorder.append as never,
|
||||||
|
seal: (taskId) => deps.memory.seal(taskId),
|
||||||
|
distill: (input) => deps.memory.distill(input),
|
||||||
|
hasHistory: async (taskId) => (await deps.memory.history(taskId)).length > 0,
|
||||||
|
},
|
||||||
|
listeners: {
|
||||||
|
hasArmedListener: async (taskId) =>
|
||||||
|
(await deps.event.listListeners(taskId)).some((l) => l.state === 'armed'),
|
||||||
|
},
|
||||||
|
persistence: {
|
||||||
|
hasDrafts: async (taskId) => (await deps.ledger.checkConflicts(taskId)) !== undefined,
|
||||||
|
hasRuntimeState: async (taskId) =>
|
||||||
|
(await flow.position(taskId)) !== undefined || (await loop.runtime(taskId)) !== undefined,
|
||||||
|
},
|
||||||
|
events: {
|
||||||
|
publishControl: async (taskId, semantic, by, reason) => {
|
||||||
|
await deps.event.publish({
|
||||||
|
type: 'control',
|
||||||
|
correlation: { target: { kind: 'task', id: taskId }, cause: { kind: 'governance', ref: by }, keys: {} },
|
||||||
|
payload: { data: { semantic, issuedBy: by, reason } },
|
||||||
|
occurredAt: clock.now().toISOString(),
|
||||||
|
source: { kind: 'governance' },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
publishTerminal: async (taskId, terminal, reason) => {
|
||||||
|
await deps.event.publish({
|
||||||
|
type: 'data',
|
||||||
|
correlation: { target: { kind: 'task', id: taskId }, cause: { kind: 'engine', ref: 'terminal' }, keys: { terminal } },
|
||||||
|
payload: { data: { terminal, reason } },
|
||||||
|
occurredAt: clock.now().toISOString(),
|
||||||
|
source: { kind: 'engine', detail: 'task' },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
notifier: {
|
||||||
|
notify: async (input) => {
|
||||||
|
// 改派留痕并**通知双方**
|
||||||
|
for (const to of [input.from, input.to]) {
|
||||||
|
await deps.message.notify({
|
||||||
|
staffId: to,
|
||||||
|
title: `任务改派:${input.title}`,
|
||||||
|
body: `任务 ${input.taskId} 的负责人由 ${input.from} 改为 ${input.to}(操作人 ${input.by})`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
interaction: { setPhase: async (taskId, phase) => void (await deps.interaction.setPhase(taskId, phase)) },
|
||||||
|
clock,
|
||||||
|
ids,
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ── process(L7) ─────────────────────────────────────────────── */
|
||||||
|
const process = createProcessService({
|
||||||
|
store: createProcessStore(db),
|
||||||
|
activities: {
|
||||||
|
async faces() {
|
||||||
|
const cards = await deps.registry.list();
|
||||||
|
const map = new Map<string, Awaited<ReturnType<ProcessService['validate']>> extends never ? never : never>();
|
||||||
|
void map;
|
||||||
|
const faces = new Map();
|
||||||
|
for (const card of cards) {
|
||||||
|
const manifest = await deps.registry.manifestOf(card.activityId);
|
||||||
|
const input = manifest.contract.input.data as { required?: string[]; properties?: Record<string, { type?: string }> };
|
||||||
|
const outputs: Record<string, Record<string, string>> = {};
|
||||||
|
for (const port of manifest.contract.output.mandatory) {
|
||||||
|
const payload = port.payload as { properties?: Record<string, { type?: string }> };
|
||||||
|
outputs[port.name] = Object.fromEntries(
|
||||||
|
Object.entries(payload.properties ?? {}).map(([k, v]) => [k, v.type ?? 'string']),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
faces.set(card.activityId, {
|
||||||
|
activityId: card.activityId,
|
||||||
|
requiredInputs: input.required ?? [],
|
||||||
|
inputTypes: Object.fromEntries(
|
||||||
|
Object.entries(input.properties ?? {}).map(([k, v]) => [k, v.type ?? 'string']),
|
||||||
|
),
|
||||||
|
outputs,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return faces;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
org: {
|
||||||
|
positions: async () => new Set((await deps.org.listStaff()).positions.map((p) => p.code)),
|
||||||
|
candidates: async (position) => {
|
||||||
|
const staff = await deps.org.staffOfPosition(position);
|
||||||
|
const withLoad = await Promise.all(
|
||||||
|
staff.map(async (s) => ({
|
||||||
|
staffId: s.id,
|
||||||
|
name: s.name,
|
||||||
|
openTasks: (await task.openTasksOf(s.id)).length,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
return withLoad;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tasks: {
|
||||||
|
start: async (input) =>
|
||||||
|
task.start({
|
||||||
|
activityId: input.activityId,
|
||||||
|
ownerId: input.ownerId,
|
||||||
|
title: input.title,
|
||||||
|
workflowId: input.workflowId,
|
||||||
|
nodeId: input.nodeId,
|
||||||
|
data: input.data,
|
||||||
|
...(input.message === undefined ? {} : { message: input.message }),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
approvals: {
|
||||||
|
async raise(input) {
|
||||||
|
// 审批点:卡到审批人工位 —— 与活动级的放行不同层
|
||||||
|
const issued = await deps.humanTool.issueExecution(
|
||||||
|
{ taskId: input.workflowId as never, ownerId: input.approverId, humanCallCap: 99 },
|
||||||
|
{
|
||||||
|
title: `审批:${input.subject}`,
|
||||||
|
basis: input.basis.length > 0 ? [...input.basis] : [{ k: '节点', v: input.nodeId }],
|
||||||
|
ask: '请裁决:通过 / 驳回(驳回必附意见)',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'verdict',
|
||||||
|
label: '裁决',
|
||||||
|
kind: 'radio',
|
||||||
|
required: true,
|
||||||
|
options: [
|
||||||
|
{ value: 'pass', label: '通过' },
|
||||||
|
{ value: 'reject', label: '驳回', negative: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ key: 'comment', label: '依据 / 意见', kind: 'textarea' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return { id: issued.order.id };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sfeel,
|
||||||
|
events: {
|
||||||
|
publish: async (input) => {
|
||||||
|
await deps.event.publish({
|
||||||
|
type: 'data',
|
||||||
|
correlation: {
|
||||||
|
target: { kind: 'workflow', id: input.workflowId as never },
|
||||||
|
cause: { kind: 'engine', ref: input.kind },
|
||||||
|
keys: {},
|
||||||
|
},
|
||||||
|
payload: { data: input.detail },
|
||||||
|
occurredAt: clock.now().toISOString(),
|
||||||
|
source: { kind: 'engine', detail: 'process' },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
receipts: {
|
||||||
|
notify: async (input) => {
|
||||||
|
await deps.message.notify({
|
||||||
|
staffId: input.to,
|
||||||
|
title: `工作流回执:${input.subject}`,
|
||||||
|
body: `工作流 ${input.workflowId} 状态:${input.state}`,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
clock,
|
||||||
|
ids,
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ── trigger(L7) ────────────────────────────────────────────── */
|
||||||
|
const trigger = createTriggerService({
|
||||||
|
store: createTriggerStore(db),
|
||||||
|
process: {
|
||||||
|
start: async (input) => process.start(input),
|
||||||
|
requiredInputs: async (packageId) => {
|
||||||
|
const pkg = await process.getPackage(packageId);
|
||||||
|
const input = pkg?.callInterface.input as { required?: string[] } | undefined;
|
||||||
|
return input?.required ?? [];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sfeel,
|
||||||
|
deadLetters: {
|
||||||
|
// 起单失败入死信 —— 落日志 + 送治理台的消息要素,不静默丢弃
|
||||||
|
report: async (input) => {
|
||||||
|
writeLog(`[trigger] 起单失败入死信:${input.triggerId} —— ${input.reason}`);
|
||||||
|
const { staff } = await deps.org.listStaff();
|
||||||
|
for (const person of staff) {
|
||||||
|
const governs = await deps.org.authorize(person.id, { kind: 'governance', function: 'trigger' }, 'govern');
|
||||||
|
if (!governs) continue;
|
||||||
|
await deps.message.notify({
|
||||||
|
staffId: person.id,
|
||||||
|
title: `触发器起单失败:${input.triggerId}`,
|
||||||
|
body: input.reason,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ledger: {
|
||||||
|
scan: async (input) => {
|
||||||
|
const page = await deps.ledger.query({
|
||||||
|
address: { domain: input.domain, object: input.object },
|
||||||
|
where: {},
|
||||||
|
orderBy: [],
|
||||||
|
limit: 200,
|
||||||
|
offset: 0,
|
||||||
|
});
|
||||||
|
return page.rows.flatMap((row) => {
|
||||||
|
const date = row[input.dateField];
|
||||||
|
if (typeof date !== 'string') return [];
|
||||||
|
return [{ key: { id: String(row['id'] ?? '') }, date, row }];
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
clock,
|
||||||
|
ids,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { task, process, trigger };
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* 派发器的消费者表 —— 三级消费的两个下游(ADR-008 #2;语义出处 OS 6.3)。
|
||||||
|
*
|
||||||
|
* 「任务监听(推进旧单)与触发器(起单规则,开新单)**相互独立判定**
|
||||||
|
* —— 同一事件可既推进旧单又起新单。」
|
||||||
|
*/
|
||||||
|
import type { Delivery } from '@modules/event';
|
||||||
|
import type { TaskId } from '@contracts';
|
||||||
|
import type { Consumer } from './dispatcher.ts';
|
||||||
|
|
||||||
|
/** ① 任务监听:投递到任务,环续跑 */
|
||||||
|
const taskConsumer: Consumer = async (delivery, container) => {
|
||||||
|
const envelope = await container.event.getEvent(delivery.eventId);
|
||||||
|
if (envelope === undefined) return;
|
||||||
|
const taskId = delivery.target.id as TaskId;
|
||||||
|
const kind = envelope.type === 'control' ? 'control' : envelope.type === 'time' ? 'time' : envelope.type === 'human' ? 'human' : 'data';
|
||||||
|
await container.task.deliver(taskId, {
|
||||||
|
kind,
|
||||||
|
ref: envelope.correlation.cause.ref,
|
||||||
|
payload: envelope.payload.data,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/** ② 触发器:开新单;同一事件对同一触发器至多起一单 */
|
||||||
|
const triggerConsumer: Consumer = async (delivery, container) => {
|
||||||
|
const envelope = await container.event.getEvent(delivery.eventId);
|
||||||
|
if (envelope === undefined) return;
|
||||||
|
await container.trigger.fireByEvent(String(delivery.target.id), envelope.id, {
|
||||||
|
type: envelope.type,
|
||||||
|
keys: envelope.correlation.keys,
|
||||||
|
payload: envelope.payload.data,
|
||||||
|
source: envelope.source.kind,
|
||||||
|
occurredAt: envelope.occurredAt,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createConsumers(): ReadonlyMap<Delivery['target']['kind'], Consumer> {
|
||||||
|
return new Map<Delivery['target']['kind'], Consumer>([
|
||||||
|
['task', taskConsumer],
|
||||||
|
['trigger', triggerConsumer],
|
||||||
|
]);
|
||||||
|
}
|
||||||
@@ -93,9 +93,9 @@ export function createTriggerService(deps: TriggerServiceDeps): TriggerService {
|
|||||||
async sweepPeriodic(now) {
|
async sweepPeriodic(now) {
|
||||||
const outcomes: FireOutcome[] = [];
|
const outcomes: FireOutcome[] = [];
|
||||||
for (const config of await deps.store.list()) {
|
for (const config of await deps.store.list()) {
|
||||||
if (!config.enabled || config.spec.kind !== 'periodic') continue;
|
if (!config.enabled) continue;
|
||||||
|
|
||||||
if (config.spec.mode === 'calendar') {
|
if (config.spec.kind === 'periodic-calendar') {
|
||||||
const last = (await deps.store.lastFiredAt(config.id)) ?? new Date(now.getTime() - 86_400_000);
|
const last = (await deps.store.lastFiredAt(config.id)) ?? new Date(now.getTime() - 86_400_000);
|
||||||
const due = nextCalendarFire(config.spec.calendar, last);
|
const due = nextCalendarFire(config.spec.calendar, last);
|
||||||
if (due === undefined || due.getTime() > now.getTime()) continue;
|
if (due === undefined || due.getTime() > now.getTime()) continue;
|
||||||
@@ -104,6 +104,7 @@ export function createTriggerService(deps: TriggerServiceDeps): TriggerService {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (config.spec.kind !== 'periodic-data') continue;
|
||||||
// 数据日历型:日期字段 + 偏移 + 过滤条件
|
// 数据日历型:日期字段 + 偏移 + 过滤条件
|
||||||
const spec = config.spec;
|
const spec = config.spec;
|
||||||
const rows = await deps.ledger.scan({
|
const rows = await deps.ledger.scan({
|
||||||
|
|||||||
@@ -17,6 +17,16 @@
|
|||||||
|
|
||||||
**当前:358 个测试通过(含 1 组对真实 PG 的集成测试),门禁五道全绿。**
|
**当前:358 个测试通过(含 1 组对真实 PG 的集成测试),门禁五道全绿。**
|
||||||
|
|
||||||
|
### MVP 完成判据的跑通进度(README《MVP 完成判据》)
|
||||||
|
|
||||||
|
| # | 场景 | 状态 |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | 治理台:员工岗位 / LLM 实例 / 流程包发布 / 触发器 / 邮件通道 | ✅ `pnpm db:seed` 一次做完 |
|
||||||
|
| 2 | RFQ 邮件入站 → 归属解析 → 触发器命中起单 → A1 落到冯兵工作台 | ✅ 已跑通 |
|
||||||
|
| 3 | A1 受理归一(flow):步序推进 → LLM 抽取 → 台账草稿 → DMN 打标 → 放行 | 🟡 装配已通,待接真实 LLM 端点验证 |
|
||||||
|
| 4–9 | A3 / A2 / A4 / AP / A6 / A7 | ⬜ 活动包未做 |
|
||||||
|
| 10 | 全程可审计 · 工作台五要素 · 治理台在途监控 | ⬜ 前端未做(M6) |
|
||||||
|
|
||||||
## 常用命令
|
## 常用命令
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -11,5 +11,7 @@
|
|||||||
| M1-2 | 条件求值 `passthroughEvaluator` 恒真 | `@sfeel` 属 M3;M1 的监听只用类型 + 关联键过滤,不用内容条件 | M3 | 进行中 |
|
| M1-2 | 条件求值 `passthroughEvaluator` 恒真 | `@sfeel` 属 M3;M1 的监听只用类型 + 关联键过滤,不用内容条件 | M3 | 进行中 |
|
||||||
| M1-3 | ~~投递记录写入即止,尚无派发 worker~~ | 派发器已于 M2 落地(advisory lock 串行 + 消费幂等);消费者(任务 / 触发器)随 M4 / M5 注册 | M2 | 已清理 |
|
| M1-3 | ~~投递记录写入即止,尚无派发 worker~~ | 派发器已于 M2 落地(advisory lock 串行 + 消费幂等);消费者(任务 / 触发器)随 M4 / M5 注册 | M2 | 已清理 |
|
||||||
| M2-1 | 派发器的消费者表为空 —— 投递只标记已送达 | 任务消费者属 task(M4),触发器消费者属 trigger(M5);投递链路本身已可测 | M4 / M5 | 进行中 |
|
| M2-1 | 派发器的消费者表为空 —— 投递只标记已送达 | 任务消费者属 task(M4),触发器消费者属 trigger(M5);投递链路本身已可测 | M4 / M5 | 进行中 |
|
||||||
|
| M5-3 | 活动 .JS 产物是源码文本,未经 tsup 单文件 ESM 真编译 | 静态审扫描的正是产物文本(import 与全局引用),当前形态已可审;真编译在装载器落地时接入 | M6 前 | 进行中 |
|
||||||
|
| M5-2 | loop 的 Agent 回路直接经 `ModelGateway`,未用 Mastra `Agent` | 编码规范 §6.1 规定用 Mastra Agent;`AgentPort` 端口不变,换实现时 usecase 不动;当前实现仍过两缝(录制回放可见) | M6 前 | 进行中 |
|
||||||
| M5-1 | `process` 未用 Mastra workflow 编译器,流转由自持确定性状态机 + `sys.node_instance` 承担 | 编码规范 §6.1 规定流程包编译为 Mastra 图;二者语义等价(挂起/恢复由节点实例表承担),先落可测的自持实现,Mastra 编译器作为可替换 adapter 后补 —— usecase 不动 | M5 收尾 | 进行中 |
|
| M5-1 | `process` 未用 Mastra workflow 编译器,流转由自持确定性状态机 + `sys.node_instance` 承担 | 编码规范 §6.1 规定流程包编译为 Mastra 图;二者语义等价(挂起/恢复由节点实例表承担),先落可测的自持实现,Mastra 编译器作为可替换 adapter 后补 —— usecase 不动 | M5 收尾 | 进行中 |
|
||||||
| M2-2 | pg-boss 已装未用 | 定时作业(监听时限扫描、周期触发器)随 M5 落地;当下派发器用 setInterval | M5 | 进行中 |
|
| M2-2 | pg-boss 已装未用 | 定时作业(监听时限扫描、周期触发器)随 M5 落地;当下派发器用 setInterval | M5 | 进行中 |
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export default tseslint.config(
|
|||||||
{
|
{
|
||||||
ignores: [
|
ignores: [
|
||||||
'**/dist/**',
|
'**/dist/**',
|
||||||
|
'**/.build/**',
|
||||||
'**/node_modules/**',
|
'**/node_modules/**',
|
||||||
'api-report/**',
|
'api-report/**',
|
||||||
'tools/depcruise-tiers.generated.cjs',
|
'tools/depcruise-tiers.generated.cjs',
|
||||||
|
|||||||
+2
-1
@@ -21,7 +21,8 @@
|
|||||||
"dev:api": "pnpm --filter @app/api dev",
|
"dev:api": "pnpm --filter @app/api dev",
|
||||||
"dev:web": "pnpm --filter @app/web dev",
|
"dev:web": "pnpm --filter @app/web dev",
|
||||||
"db:migrate": "pnpm --filter @app/api build && pnpm --filter @app/api migrate",
|
"db:migrate": "pnpm --filter @app/api build && pnpm --filter @app/api migrate",
|
||||||
"db:generate": "drizzle-kit generate"
|
"db:generate": "drizzle-kit generate",
|
||||||
|
"activities:build": "tsup tools/build-activities.ts --format esm --out-dir .build --tsconfig tsconfig.node.json --silent && node .build/build-activities.js"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.0",
|
"@eslint/js": "^9.39.0",
|
||||||
|
|||||||
@@ -244,18 +244,19 @@ export const triggerConfigSchema = z.object({
|
|||||||
processPackageId: processPackageIdSchema,
|
processPackageId: processPackageIdSchema,
|
||||||
/** 起单载荷装配:事件信封与载荷字段 → 调用接口的 data 字段;缺必填即起单失败入死信 */
|
/** 起单载荷装配:事件信封与载荷字段 → 调用接口的 data 字段;缺必填即起单失败入死信 */
|
||||||
mapping: z.array(fieldMappingSchema).default([]),
|
mapping: z.array(fieldMappingSchema).default([]),
|
||||||
|
/**
|
||||||
|
* 判别式取**四个互斥值**:周期触发的两型(日历 / 数据日历)在此展开 ——
|
||||||
|
* `TRIGGER_KINDS` 仍是三类分类学(周期 / 事件 / 登记),两者以 `triggerKindOf()` 对应。
|
||||||
|
*/
|
||||||
spec: z.discriminatedUnion('kind', [
|
spec: z.discriminatedUnion('kind', [
|
||||||
z.object({
|
z.object({
|
||||||
kind: z.literal('periodic'),
|
kind: z.literal('periodic-calendar'),
|
||||||
/** 日历型:纯日历规则 */
|
|
||||||
mode: z.literal('calendar'),
|
|
||||||
/** cron 风格的日历表达式 */
|
/** cron 风格的日历表达式 */
|
||||||
calendar: z.string().min(1),
|
calendar: z.string().min(1),
|
||||||
}),
|
}),
|
||||||
z.object({
|
z.object({
|
||||||
kind: z.literal('periodic'),
|
kind: z.literal('periodic-data'),
|
||||||
/** 数据日历型:按台账日期字段 + 偏移 + 过滤条件逐行起单 */
|
/** 数据日历型:按台账日期字段 + 偏移 + 过滤条件逐行起单 */
|
||||||
mode: z.literal('data-calendar'),
|
|
||||||
domain: z.string().min(1),
|
domain: z.string().min(1),
|
||||||
object: z.string().min(1),
|
object: z.string().min(1),
|
||||||
dateField: z.string().min(1),
|
dateField: z.string().min(1),
|
||||||
@@ -277,3 +278,8 @@ export const triggerConfigSchema = z.object({
|
|||||||
]),
|
]),
|
||||||
});
|
});
|
||||||
export type TriggerConfig = z.infer<typeof triggerConfigSchema>;
|
export type TriggerConfig = z.infer<typeof triggerConfigSchema>;
|
||||||
|
|
||||||
|
/** 四个判别值 → 三类分类学(OS 3.1) */
|
||||||
|
export function triggerKindOf(spec: TriggerConfig['spec']): TriggerKind {
|
||||||
|
return spec.kind === 'event' ? 'event' : spec.kind === 'registration' ? 'registration' : 'periodic';
|
||||||
|
}
|
||||||
|
|||||||
Generated
+22
@@ -48,6 +48,8 @@ importers:
|
|||||||
specifier: ^3.2.4
|
specifier: ^3.2.4
|
||||||
version: 3.2.7(@types/node@22.20.1)(tsx@4.23.1)(yaml@2.9.0)
|
version: 3.2.7(@types/node@22.20.1)(tsx@4.23.1)(yaml@2.9.0)
|
||||||
|
|
||||||
|
activities/quote-intake: {}
|
||||||
|
|
||||||
apps/api:
|
apps/api:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@ai-sdk/openai-compatible':
|
'@ai-sdk/openai-compatible':
|
||||||
@@ -85,22 +87,42 @@ importers:
|
|||||||
specifier: ^8.20.0
|
specifier: ^8.20.0
|
||||||
version: 8.20.0
|
version: 8.20.0
|
||||||
|
|
||||||
|
apps/api/src/modules/activity-registry: {}
|
||||||
|
|
||||||
apps/api/src/modules/channel: {}
|
apps/api/src/modules/channel: {}
|
||||||
|
|
||||||
apps/api/src/modules/event: {}
|
apps/api/src/modules/event: {}
|
||||||
|
|
||||||
|
apps/api/src/modules/flow-engine: {}
|
||||||
|
|
||||||
|
apps/api/src/modules/human-tool: {}
|
||||||
|
|
||||||
|
apps/api/src/modules/interaction: {}
|
||||||
|
|
||||||
apps/api/src/modules/ledger: {}
|
apps/api/src/modules/ledger: {}
|
||||||
|
|
||||||
apps/api/src/modules/llm: {}
|
apps/api/src/modules/llm: {}
|
||||||
|
|
||||||
|
apps/api/src/modules/loop-engine: {}
|
||||||
|
|
||||||
apps/api/src/modules/memory: {}
|
apps/api/src/modules/memory: {}
|
||||||
|
|
||||||
apps/api/src/modules/message: {}
|
apps/api/src/modules/message: {}
|
||||||
|
|
||||||
apps/api/src/modules/org: {}
|
apps/api/src/modules/org: {}
|
||||||
|
|
||||||
|
apps/api/src/modules/process: {}
|
||||||
|
|
||||||
|
apps/api/src/modules/release: {}
|
||||||
|
|
||||||
|
apps/api/src/modules/skill-engine: {}
|
||||||
|
|
||||||
|
apps/api/src/modules/task: {}
|
||||||
|
|
||||||
apps/api/src/modules/toolbox: {}
|
apps/api/src/modules/toolbox: {}
|
||||||
|
|
||||||
|
apps/api/src/modules/trigger: {}
|
||||||
|
|
||||||
apps/web:
|
apps/web:
|
||||||
dependencies:
|
dependencies:
|
||||||
react:
|
react:
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
/**
|
||||||
|
* 活动包的编译产出 —— **构建工具**,不是活动包的一部分(编码规范 §8.2 / ADR-004)。
|
||||||
|
*
|
||||||
|
* 「活动包 = 包清单 + 代码产物(.JS)+ 资产文件;清单**机器可读**,
|
||||||
|
* 系统不执行代码即知包为何物。」
|
||||||
|
*
|
||||||
|
* 产出 `activities/<pkg>/dist/{manifest.json,index.js}`;
|
||||||
|
* api 侧只经**文件**读取它们 —— 与 activities 之间只有安装通道,没有代码通道。
|
||||||
|
* 放在 tools/ 而非 activities/ 是因为它要用 fs:活动包本身**零环境能力**,构建脚本不受此限。
|
||||||
|
*/
|
||||||
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
||||||
|
import { dirname, join } from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import { manifest as quoteIntake } from '../activities/quote-intake/src/manifest.ts';
|
||||||
|
|
||||||
|
const root = dirname(dirname(fileURLToPath(import.meta.url)));
|
||||||
|
|
||||||
|
const PACKAGES = [{ dir: 'quote-intake', manifest: quoteIntake }] as const;
|
||||||
|
|
||||||
|
for (const pkg of PACKAGES) {
|
||||||
|
const base = join(root, 'activities', pkg.dir);
|
||||||
|
const dist = join(base, 'dist');
|
||||||
|
await mkdir(dist, { recursive: true });
|
||||||
|
await writeFile(join(dist, 'manifest.json'), JSON.stringify(pkg.manifest, null, 2), 'utf8');
|
||||||
|
|
||||||
|
// MVP 阶段直接把源码文本当产物落盘:静态审扫描的正是产物文本(import 与全局引用)。
|
||||||
|
// 真编译(tsup 单文件 ESM)在装载器落地时接入 —— 见 docs/debt.md · M5-3
|
||||||
|
const source = await readFile(join(base, 'src', 'index.ts'), 'utf8');
|
||||||
|
await writeFile(join(dist, 'index.js'), source, 'utf8');
|
||||||
|
process.stdout.write(`活动包已产出:${pkg.dir}\n`);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user