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:
JCAI Main Agent
2026-07-27 02:24:36 +08:00
co-authored by Claude Opus 5
parent c6cdb4c310
commit 78dc03bb58
24 changed files with 1406 additions and 47 deletions
+12
View File
@@ -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": []
}
+1 -1
View File
@@ -179,7 +179,7 @@ module.exports = {
options: {
doNotFollow: { path: 'node_modules' },
exclude: {
path: ['(^|/)dist/', '(^|/)node_modules/', '(^|/)api-report/', '\\.d\\.ts$'],
path: ['(^|/)dist/', '(^|/)\\.build/', '(^|/)node_modules/', '(^|/)api-report/', '\\.d\\.ts$'],
},
tsPreCompilationDeps: true,
tsConfig: { fileName: 'tsconfig.base.json' },
+1
View File
@@ -1,5 +1,6 @@
node_modules/
dist/
.build/
.api-tmp/
*.tsbuildinfo
.DS_Store
+3 -1
View File
@@ -3,5 +3,7 @@
"version": "0.1.0",
"private": true,
"type": "module",
"exports": { ".": "./src/index.ts" }
"exports": {
".": "./src/index.ts"
}
}
+11 -11
View File
@@ -38,8 +38,8 @@ export declare const formFieldKindSchema: z.ZodEnum<{
number: "number";
file: "file";
text: "text";
date: "date";
check: "check";
date: "date";
textarea: "textarea";
radio: "radio";
select: "select";
@@ -54,8 +54,8 @@ export declare const formFieldSchema: z.ZodObject<{
number: "number";
file: "file";
text: "text";
date: "date";
check: "check";
date: "date";
textarea: "textarea";
radio: "radio";
select: "select";
@@ -191,8 +191,8 @@ export declare const viewBlockSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
}, z.core.$strip>, z.ZodObject<{
t: z.ZodLiteral<"mail">;
dir: z.ZodEnum<{
in: "in";
out: "out";
in: "in";
}>;
statusLabel: z.ZodOptional<z.ZodString>;
editable: z.ZodOptional<z.ZodBoolean>;
@@ -418,8 +418,8 @@ export declare const deliverableSchema: z.ZodObject<{
}, z.core.$strip>, z.ZodObject<{
t: z.ZodLiteral<"mail">;
dir: z.ZodEnum<{
in: "in";
out: "out";
in: "in";
}>;
statusLabel: z.ZodOptional<z.ZodString>;
editable: z.ZodOptional<z.ZodBoolean>;
@@ -652,8 +652,8 @@ export declare const executionWorkOrderSchema: z.ZodObject<{
}, z.core.$strip>, z.ZodObject<{
t: z.ZodLiteral<"mail">;
dir: z.ZodEnum<{
in: "in";
out: "out";
in: "in";
}>;
statusLabel: z.ZodOptional<z.ZodString>;
editable: z.ZodOptional<z.ZodBoolean>;
@@ -773,8 +773,8 @@ export declare const executionWorkOrderSchema: z.ZodObject<{
number: "number";
file: "file";
text: "text";
date: "date";
check: "check";
date: "date";
textarea: "textarea";
radio: "radio";
select: "select";
@@ -863,8 +863,8 @@ export declare const acceptanceWorkOrderSchema: z.ZodObject<{
number: "number";
file: "file";
text: "text";
date: "date";
check: "check";
date: "date";
textarea: "textarea";
radio: "radio";
select: "select";
@@ -995,8 +995,8 @@ export declare const workOrderSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
}, z.core.$strip>, z.ZodObject<{
t: z.ZodLiteral<"mail">;
dir: z.ZodEnum<{
in: "in";
out: "out";
in: "in";
}>;
statusLabel: z.ZodOptional<z.ZodString>;
editable: z.ZodOptional<z.ZodBoolean>;
@@ -1116,8 +1116,8 @@ export declare const workOrderSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
number: "number";
file: "file";
text: "text";
date: "date";
check: "check";
date: "date";
textarea: "textarea";
radio: "radio";
select: "select";
@@ -1180,8 +1180,8 @@ export declare const workOrderSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
number: "number";
file: "file";
text: "text";
date: "date";
check: "check";
date: "date";
textarea: "textarea";
radio: "radio";
select: "select";
@@ -1375,9 +1375,9 @@ export declare const chatMessageSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
}, z.core.$strip>, z.ZodObject<{
k: z.ZodLiteral<"system">;
type: z.ZodEnum<{
time: "time";
human: "human";
data: "data";
time: "time";
control: "control";
}>;
text: z.ZodString;
+5 -5
View File
@@ -8,9 +8,9 @@
import { z } from 'zod';
export declare const EVENT_TYPES: readonly ["data", "time", "human", "control"];
export declare const eventTypeSchema: z.ZodEnum<{
time: "time";
human: "human";
data: "data";
time: "time";
control: "control";
}>;
export type EventType = z.infer<typeof eventTypeSchema>;
@@ -71,9 +71,9 @@ export type EventPayload = z.infer<typeof eventPayloadSchema>;
export declare const envelopeSchema: z.ZodObject<{
id: z.core.$ZodBranded<z.ZodString, "EventId", "out">;
type: z.ZodEnum<{
time: "time";
human: "human";
data: "data";
time: "time";
control: "control";
}>;
correlation: z.ZodObject<{
@@ -134,9 +134,9 @@ export type Envelope = z.infer<typeof envelopeSchema>;
*/
export declare const envelopeDraftSchema: z.ZodObject<{
type: z.ZodEnum<{
time: "time";
human: "human";
data: "data";
time: "time";
control: "control";
}>;
payload: z.ZodObject<{
@@ -235,9 +235,9 @@ export declare const listenerSpecSchema: z.ZodObject<{
dynamic: "dynamic";
}>;
eventType: z.ZodEnum<{
time: "time";
human: "human";
data: "data";
time: "time";
control: "control";
}>;
keyFilter: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
@@ -260,9 +260,9 @@ export declare const registeredListenerSchema: z.ZodObject<{
dynamic: "dynamic";
}>;
eventType: z.ZodEnum<{
time: "time";
human: "human";
data: "data";
time: "time";
control: "control";
}>;
keyFilter: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
+2 -2
View File
@@ -77,9 +77,9 @@ export declare const manifestDeclarationsSchema: z.ZodObject<{
dynamic: "dynamic";
}>;
eventType: z.ZodEnum<{
time: "time";
human: "human";
data: "data";
time: "time";
control: "control";
}>;
keyFilter: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
@@ -954,9 +954,9 @@ export declare const activityManifestSchema: z.ZodObject<{
dynamic: "dynamic";
}>;
eventType: z.ZodEnum<{
time: "time";
human: "human";
data: "data";
time: "time";
control: "control";
}>;
keyFilter: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
+5 -5
View File
@@ -520,12 +520,10 @@ export declare const triggerConfigSchema: z.ZodObject<{
}, z.core.$strip>], "from">;
}, z.core.$strip>>>;
spec: z.ZodDiscriminatedUnion<[z.ZodObject<{
kind: z.ZodLiteral<"periodic">;
mode: z.ZodLiteral<"calendar">;
kind: z.ZodLiteral<"periodic-calendar">;
calendar: z.ZodString;
}, z.core.$strip>, z.ZodObject<{
kind: z.ZodLiteral<"periodic">;
mode: z.ZodLiteral<"data-calendar">;
kind: z.ZodLiteral<"periodic-data">;
domain: z.ZodString;
object: z.ZodString;
dateField: z.ZodString;
@@ -535,9 +533,9 @@ export declare const triggerConfigSchema: z.ZodObject<{
kind: z.ZodLiteral<"event">;
rule: z.ZodString;
eventType: z.ZodDefault<z.ZodEnum<{
time: "time";
human: "human";
data: "data";
time: "time";
control: "control";
}>>;
}, z.core.$strip>, z.ZodObject<{
@@ -546,3 +544,5 @@ export declare const triggerConfigSchema: z.ZodObject<{
}, z.core.$strip>], "kind">;
}, z.core.$strip>;
export type TriggerConfig = z.infer<typeof triggerConfigSchema>;
/** 四个判别值 → 三类分类学(OS 3.1) */
export declare function triggerKindOf(spec: TriggerConfig['spec']): TriggerKind;
+139
View File
@@ -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
View File
@@ -42,6 +42,19 @@ import {
} from '@modules/toolbox';
import type { MessageService } 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 {
createChannelRepository,
@@ -68,21 +81,45 @@ export type Container = {
readonly toolInvoker: ToolInvoker;
readonly message: MessageService;
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>;
};
/**
* 在途工作探针 —— 停用前置检查要问的是 task(L6)与 human-tool(L3),二者随 M3/M4 落地。
* 占位返回空集;M4 起接真实用例(docs/debt.md · M1-1)。
* 在途工作探针 —— 停用前置检查要问的是 task(L6)与 human-tool(L3),二者层级在 org 之上,
* 故以**延迟绑定**的端口接入:org 先构造,真身就位后回填(§5.2 跨层协作经端口)。
*/
const emptyInFlightProbe: InFlightWorkProbe = {
openTasksOf: async () => [],
openCardsOf: async () => [],
openApprovalsOf: async () => [],
};
function createDeferredInFlightProbe(): InFlightWorkProbe & { bind(real: InFlightWorkProbe): void } {
let real: InFlightWorkProbe | undefined;
return {
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)。
@@ -100,7 +137,7 @@ const sfeelEvaluator = (expression: string, scope: Readonly<Record<string, unkno
};
/** 第一方工具实现 —— 由 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> {
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 在其出口给出
await pool.query(DRAFT_DDL);
const inFlightProbe = createDeferredInFlightProbe();
const org = createOrgService({
positions: createPositionRepository(db),
staff: createStaffRepository(db),
grants: createGrantRepository(db),
inFlight: emptyInFlightProbe,
inFlight: inFlightProbe,
ids,
});
@@ -142,12 +181,14 @@ export async function createContainer(config: AppConfig): Promise<Container> {
ids,
});
const triggerMatcher = createDeferredTriggerMatcher();
const event = createEventService({
store: createEventStore(db, ids),
clock: systemClock,
ids,
evaluate: sfeelEvaluator,
triggers: noTriggerMatcher,
triggers: triggerMatcher,
messages: message,
});
@@ -224,6 +265,235 @@ export async function createContainer(config: AppConfig): Promise<Container> {
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 {
config,
pool,
@@ -238,6 +508,13 @@ export async function createContainer(config: AppConfig): Promise<Container> {
toolInvoker,
message,
channel,
humanTool,
interaction,
registry,
release,
task: engines.task,
process: engines.process,
trigger: engines.trigger,
async close() {
await pool.end();
},
+2 -2
View File
@@ -6,6 +6,7 @@ import { serve } from '@hono/node-server';
import { loadConfig } from './config.ts';
import { createContainer } from './container.ts';
import { createServer } from './server.ts';
import { createConsumers } from './workers/consumers.ts';
import { startDispatcher } from './workers/dispatcher.ts';
export async function boot(): Promise<void> {
@@ -14,8 +15,7 @@ export async function boot(): Promise<void> {
await container.org.ensureSeeded();
// 派发器:投递记录即队列,按目标 ID 取 advisory lock 串行消费(ADR-008)
// 消费者随 M4(任务)/ M5(触发器)注册;此刻空表即「只标记送达」
const dispatcher = startDispatcher({ container, consumers: new Map() }, config.dispatchIntervalMs);
const dispatcher = startDispatcher({ container, consumers: createConsumers() }, config.dispatchIntervalMs);
const app = createServer(container);
serve({ fetch: app.fetch, port: config.port }, (info) => {
+196
View File
@@ -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)');
}
+5
View File
@@ -4,6 +4,7 @@
*/
import { loadConfig } from './config.ts';
import { createContainer } from './container.ts';
import { seedQuote } from './seed-quote.ts';
export async function seed(): Promise<void> {
const container = await createContainer(loadConfig());
@@ -12,6 +13,10 @@ export async function seed(): Promise<void> {
const { staff, positions } = await container.org.listStaff();
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`);
// 统一报价的验收种子(README MVP 完成判据第 1 条)
process.stdout.write('\n── 治理台配置 ──\n');
await seedQuote(container);
} finally {
await container.close();
}
+52
View File
@@ -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],
];
+559
View File
@@ -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 };
}
+42
View File
@@ -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) {
const outcomes: FireOutcome[] = [];
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 due = nextCalendarFire(config.spec.calendar, last);
if (due === undefined || due.getTime() > now.getTime()) continue;
@@ -104,6 +104,7 @@ export function createTriggerService(deps: TriggerServiceDeps): TriggerService {
continue;
}
if (config.spec.kind !== 'periodic-data') continue;
// 数据日历型:日期字段 + 偏移 + 过滤条件
const spec = config.spec;
const rows = await deps.ledger.scan({
+10
View File
@@ -17,6 +17,16 @@
**当前: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
+2
View File
@@ -11,5 +11,7 @@
| M1-2 | 条件求值 `passthroughEvaluator` 恒真 | `@sfeel` 属 M3;M1 的监听只用类型 + 关联键过滤,不用内容条件 | M3 | 进行中 |
| M1-3 | ~~投递记录写入即止,尚无派发 worker~~ | 派发器已于 M2 落地(advisory lock 串行 + 消费幂等);消费者(任务 / 触发器)随 M4 / M5 注册 | M2 | 已清理 |
| 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 收尾 | 进行中 |
| M2-2 | pg-boss 已装未用 | 定时作业(监听时限扫描、周期触发器)随 M5 落地;当下派发器用 setInterval | M5 | 进行中 |
+1
View File
@@ -13,6 +13,7 @@ export default tseslint.config(
{
ignores: [
'**/dist/**',
'**/.build/**',
'**/node_modules/**',
'api-report/**',
'tools/depcruise-tiers.generated.cjs',
+2 -1
View File
@@ -21,7 +21,8 @@
"dev:api": "pnpm --filter @app/api dev",
"dev:web": "pnpm --filter @app/web dev",
"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": {
"@eslint/js": "^9.39.0",
+11 -5
View File
@@ -244,18 +244,19 @@ export const triggerConfigSchema = z.object({
processPackageId: processPackageIdSchema,
/** 起单载荷装配:事件信封与载荷字段 → 调用接口的 data 字段;缺必填即起单失败入死信 */
mapping: z.array(fieldMappingSchema).default([]),
/**
* 判别式取**四个互斥值**:周期触发的两型(日历 / 数据日历)在此展开 ——
* `TRIGGER_KINDS` 仍是三类分类学(周期 / 事件 / 登记),两者以 `triggerKindOf()` 对应。
*/
spec: z.discriminatedUnion('kind', [
z.object({
kind: z.literal('periodic'),
/** 日历型:纯日历规则 */
mode: z.literal('calendar'),
kind: z.literal('periodic-calendar'),
/** cron 风格的日历表达式 */
calendar: z.string().min(1),
}),
z.object({
kind: z.literal('periodic'),
kind: z.literal('periodic-data'),
/** 数据日历型:按台账日期字段 + 偏移 + 过滤条件逐行起单 */
mode: z.literal('data-calendar'),
domain: z.string().min(1),
object: 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>;
/** 四个判别值 → 三类分类学(OS 3.1) */
export function triggerKindOf(spec: TriggerConfig['spec']): TriggerKind {
return spec.kind === 'event' ? 'event' : spec.kind === 'registration' ? 'registration' : 'periodic';
}
+22
View File
@@ -48,6 +48,8 @@ importers:
specifier: ^3.2.4
version: 3.2.7(@types/node@22.20.1)(tsx@4.23.1)(yaml@2.9.0)
activities/quote-intake: {}
apps/api:
dependencies:
'@ai-sdk/openai-compatible':
@@ -85,22 +87,42 @@ importers:
specifier: ^8.20.0
version: 8.20.0
apps/api/src/modules/activity-registry: {}
apps/api/src/modules/channel: {}
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/llm: {}
apps/api/src/modules/loop-engine: {}
apps/api/src/modules/memory: {}
apps/api/src/modules/message: {}
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/trigger: {}
apps/web:
dependencies:
react:
+31
View File
@@ -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`);
}