From 78dc03bb58f9c960eff7a02ae07ff05bf8ff4dcd Mon Sep 17 00:00:00 2001 From: JCAI Main Agent Date: Mon, 27 Jul 2026 02:24:36 +0800 Subject: [PATCH] =?UTF-8?q?app=20=E8=A3=85=E9=85=8D=E6=94=B6=E5=B0=BE:18?= =?UTF-8?q?=20=E4=B8=AA=E6=A8=A1=E5=9D=97=E6=8E=A5=E4=B8=8A,MVP=20?= =?UTF-8?q?=E5=88=A4=E6=8D=AE=201-2=20=E7=AB=AF=E5=88=B0=E7=AB=AF=E8=B7=91?= =?UTF-8?q?=E9=80=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 装配(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) Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m --- .data/attachments/inbound/m-2001.eml | 12 + .dependency-cruiser.cjs | 2 +- .gitignore | 1 + activities/quote-intake/package.json | 4 +- api-report/packages/contracts/src/card.d.ts | 22 +- .../packages/contracts/src/envelope.d.ts | 10 +- .../packages/contracts/src/manifest.d.ts | 4 +- .../contracts/src/process-package.d.ts | 10 +- apps/api/src/app/agent-runner.ts | 139 +++++ apps/api/src/app/container.ts | 301 +++++++++- apps/api/src/app/main.ts | 4 +- apps/api/src/app/seed-quote.ts | 196 ++++++ apps/api/src/app/seed.ts | 5 + apps/api/src/app/tools/first-party.ts | 52 ++ apps/api/src/app/wiring/engines.ts | 559 ++++++++++++++++++ apps/api/src/app/workers/consumers.ts | 42 ++ .../internal/usecase/trigger-service.ts | 5 +- docs/PROGRESS.md | 10 + docs/debt.md | 2 + eslint.config.js | 1 + package.json | 3 +- packages/contracts/src/process-package.ts | 16 +- pnpm-lock.yaml | 22 + tools/build-activities.ts | 31 + 24 files changed, 1406 insertions(+), 47 deletions(-) create mode 100644 .data/attachments/inbound/m-2001.eml create mode 100644 apps/api/src/app/agent-runner.ts create mode 100644 apps/api/src/app/seed-quote.ts create mode 100644 apps/api/src/app/tools/first-party.ts create mode 100644 apps/api/src/app/wiring/engines.ts create mode 100644 apps/api/src/app/workers/consumers.ts create mode 100644 tools/build-activities.ts diff --git a/.data/attachments/inbound/m-2001.eml b/.data/attachments/inbound/m-2001.eml new file mode 100644 index 0000000..3ea4649 --- /dev/null +++ b/.data/attachments/inbound/m-2001.eml @@ -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": [] +} \ No newline at end of file diff --git a/.dependency-cruiser.cjs b/.dependency-cruiser.cjs index 2f45fc1..2d057f1 100644 --- a/.dependency-cruiser.cjs +++ b/.dependency-cruiser.cjs @@ -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' }, diff --git a/.gitignore b/.gitignore index 77e2b73..df3adf3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ node_modules/ dist/ +.build/ .api-tmp/ *.tsbuildinfo .DS_Store diff --git a/activities/quote-intake/package.json b/activities/quote-intake/package.json index 3567a45..05276d9 100644 --- a/activities/quote-intake/package.json +++ b/activities/quote-intake/package.json @@ -3,5 +3,7 @@ "version": "0.1.0", "private": true, "type": "module", - "exports": { ".": "./src/index.ts" } + "exports": { + ".": "./src/index.ts" + } } diff --git a/api-report/packages/contracts/src/card.d.ts b/api-report/packages/contracts/src/card.d.ts index 74aaf9b..9f19848 100644 --- a/api-report/packages/contracts/src/card.d.ts +++ b/api-report/packages/contracts/src/card.d.ts @@ -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; editable: z.ZodOptional; @@ -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; editable: z.ZodOptional; @@ -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; editable: z.ZodOptional; @@ -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; editable: z.ZodOptional; @@ -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; diff --git a/api-report/packages/contracts/src/envelope.d.ts b/api-report/packages/contracts/src/envelope.d.ts index b117bb7..dc2299b 100644 --- a/api-report/packages/contracts/src/envelope.d.ts +++ b/api-report/packages/contracts/src/envelope.d.ts @@ -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; @@ -71,9 +71,9 @@ export type EventPayload = z.infer; export declare const envelopeSchema: z.ZodObject<{ id: z.core.$ZodBranded; 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; */ 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>; @@ -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>; diff --git a/api-report/packages/contracts/src/manifest.d.ts b/api-report/packages/contracts/src/manifest.d.ts index c972b16..21f8257 100644 --- a/api-report/packages/contracts/src/manifest.d.ts +++ b/api-report/packages/contracts/src/manifest.d.ts @@ -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>; @@ -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>; diff --git a/api-report/packages/contracts/src/process-package.d.ts b/api-report/packages/contracts/src/process-package.d.ts index 98b7379..26f1fa1 100644 --- a/api-report/packages/contracts/src/process-package.d.ts +++ b/api-report/packages/contracts/src/process-package.d.ts @@ -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.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; +/** 四个判别值 → 三类分类学(OS 3.1) */ +export declare function triggerKindOf(spec: TriggerConfig['spec']): TriggerKind; diff --git a/apps/api/src/app/agent-runner.ts b/apps/api/src/app/agent-runner.ts new file mode 100644 index 0000000..4a71712 --- /dev/null +++ b/apps/api/src/app/agent-runner.ts @@ -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; +}; + +export function createAgentRunner(deps: AgentRunnerDeps): { + decide(input: { taskId: TaskId; narrative: string; enabled: EnabledSet; scope: Record }): Promise; +} { + 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; + 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, + }; + case 'human': + return { + kind: 'human', + itemId, + card: (raw['card'] ?? {}) as Record, + workOrder: raw['workOrder'] === 'acceptance' ? 'acceptance' : 'execution', + }; + case 'listen': + return { + kind: 'listen', + itemId, + eventType: raw['eventType'] === 'time' ? 'time' : 'data', + keyFilter: (raw['keyFilter'] ?? {}) as Record, + // 时限必配 —— 模型没给就按三天兜底,绝不留幽灵等待 + 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', + }; + } +} diff --git a/apps/api/src/app/container.ts b/apps/api/src/app/container.ts index 0d6874e..f679987 100644 --- a/apps/api/src/app/container.ts +++ b/apps/api/src/app/container.ts @@ -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; }; /** - * 在途工作探针 —— 停用前置检查要问的是 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(); +export const firstPartyToolHandlers = new Map(FIRST_PARTY_TOOLS); export async function createContainer(config: AppConfig): Promise { const pool = new Pool({ connectionString: config.databaseUrl, max: 10 }); @@ -111,11 +148,13 @@ export async function createContainer(config: AppConfig): Promise { // 草稿层的建表: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 { 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 { 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 { toolInvoker, message, channel, + humanTool, + interaction, + registry, + release, + task: engines.task, + process: engines.process, + trigger: engines.trigger, async close() { await pool.end(); }, diff --git a/apps/api/src/app/main.ts b/apps/api/src/app/main.ts index b8f2a0a..8ae6ece 100644 --- a/apps/api/src/app/main.ts +++ b/apps/api/src/app/main.ts @@ -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 { @@ -14,8 +15,7 @@ export async function boot(): Promise { 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) => { diff --git a/apps/api/src/app/seed-quote.ts b/apps/api/src/app/seed-quote.ts new file mode 100644 index 0000000..5a528d2 --- /dev/null +++ b/apps/api/src/app/seed-quote.ts @@ -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 { + 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)'); +} diff --git a/apps/api/src/app/seed.ts b/apps/api/src/app/seed.ts index 37f0f0e..f85e5dd 100644 --- a/apps/api/src/app/seed.ts +++ b/apps/api/src/app/seed.ts @@ -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 { const container = await createContainer(loadConfig()); @@ -12,6 +13,10 @@ export async function seed(): Promise { 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(); } diff --git a/apps/api/src/app/tools/first-party.ts b/apps/api/src/app/tools/first-party.ts new file mode 100644 index 0000000..22952c3 --- /dev/null +++ b/apps/api/src/app/tools/first-party.ts @@ -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 = { + 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 = [ + ['crm.history-price-read', historyPriceRead], + ['quote-intake.classify', classify], +]; diff --git a/apps/api/src/app/wiring/engines.ts b/apps/api/src/app/wiring/engines.ts new file mode 100644 index 0000000..f6762f9 --- /dev/null +++ b/apps/api/src/app/wiring/engines.ts @@ -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, 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>): boolean => { + try { + return evaluateCondition(expression, scope); + } catch { + return false; + } + }, + expression: (expression: string, scope: Readonly>): unknown => { + try { + return evalSfeel(expression, scope); + } catch { + return undefined; + } + }, + unaryTest: (cell: string, value: unknown, scope: Readonly>): 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 = {}; + 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; + patch: Record; + }) { + // 一切过程写入落草稿层 —— 放行前不落事实 + 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 }) => { + 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; 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; + }) { + 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; 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; 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> 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 }; + const outputs: Record> = {}; + for (const port of manifest.contract.output.mandatory) { + const payload = port.payload as { properties?: Record }; + 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 }; +} diff --git a/apps/api/src/app/workers/consumers.ts b/apps/api/src/app/workers/consumers.ts new file mode 100644 index 0000000..e456ba0 --- /dev/null +++ b/apps/api/src/app/workers/consumers.ts @@ -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 { + return new Map([ + ['task', taskConsumer], + ['trigger', triggerConsumer], + ]); +} diff --git a/apps/api/src/modules/trigger/internal/usecase/trigger-service.ts b/apps/api/src/modules/trigger/internal/usecase/trigger-service.ts index d4e547e..93a7d9d 100644 --- a/apps/api/src/modules/trigger/internal/usecase/trigger-service.ts +++ b/apps/api/src/modules/trigger/internal/usecase/trigger-service.ts @@ -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({ diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index f396e0b..fa361c3 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -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 diff --git a/docs/debt.md b/docs/debt.md index b2d418c..88e1974 100644 --- a/docs/debt.md +++ b/docs/debt.md @@ -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 | 进行中 | diff --git a/eslint.config.js b/eslint.config.js index 34c72df..0b27cc4 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -13,6 +13,7 @@ export default tseslint.config( { ignores: [ '**/dist/**', + '**/.build/**', '**/node_modules/**', 'api-report/**', 'tools/depcruise-tiers.generated.cjs', diff --git a/package.json b/package.json index 429f4b1..71999ab 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/contracts/src/process-package.ts b/packages/contracts/src/process-package.ts index f2351c2..b57b825 100644 --- a/packages/contracts/src/process-package.ts +++ b/packages/contracts/src/process-package.ts @@ -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; + +/** 四个判别值 → 三类分类学(OS 3.1) */ +export function triggerKindOf(spec: TriggerConfig['spec']): TriggerKind { + return spec.kind === 'event' ? 'event' : spec.kind === 'registration' ? 'registration' : 'periodic'; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a03d92f..081fce1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: diff --git a/tools/build-activities.ts b/tools/build-activities.ts new file mode 100644 index 0000000..8e1658a --- /dev/null +++ b/tools/build-activities.ts @@ -0,0 +1,31 @@ +/** + * 活动包的编译产出 —— **构建工具**,不是活动包的一部分(编码规范 §8.2 / ADR-004)。 + * + * 「活动包 = 包清单 + 代码产物(.JS)+ 资产文件;清单**机器可读**, + * 系统不执行代码即知包为何物。」 + * + * 产出 `activities//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`); +}